diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000000..17a60ad125f7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: + if typing.TYPE_CHECKING: diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..c58290283665 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,30 @@ +'is: documentation': +- changed-files: + - all-globs-to-all-files: '{**/docs/**,**/README.md}' + +'affects: webhost': +- changed-files: + - all-globs-to-any-file: 'WebHost.py' + - all-globs-to-any-file: 'WebHostLib/**/*' + +'affects: core': +- changed-files: + - all-globs-to-any-file: + - '!*Client.py' + - '!README.md' + - '!LICENSE' + - '!*.yml' + - '!.gitignore' + - '!**/docs/**' + - '!typings/kivy/**' + - '!test/**' + - '!data/**' + - '!.run/**' + - '!.github/**' + - '!worlds_disabled/**' + - '!worlds/**' + - '!WebHost.py' + - '!WebHostLib/**' + - any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core" + - 'worlds/generic/**/*.py' + - 'CommonClient.py' diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index ba2660809aaa..d01365745c96 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -71,7 +71,7 @@ jobs: continue-on-error: true if: env.diff != '' && matrix.task == 'flake8' run: | - flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }} + flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }} - name: "mypy: Type check modified files" continue-on-error: true diff --git a/.github/workflows/label-pull-requests.yml b/.github/workflows/label-pull-requests.yml new file mode 100644 index 000000000000..42881aa49d9b --- /dev/null +++ b/.github/workflows/label-pull-requests.yml @@ -0,0 +1,44 @@ +name: Label Pull Request +on: + pull_request_target: + types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed'] + branches: ['main'] +permissions: + contents: read + pull-requests: write + +jobs: + labeler: + name: 'Apply content-based labels' + if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + sync-labels: true + peer_review: + name: 'Apply peer review label' + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened' || + github.event.action == 'ready_for_review') && !github.event.pull_request.draft + runs-on: ubuntu-latest + steps: + - name: 'Add label' + run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + unblock_draft_prs: + name: 'Remove waiting-on labels' + if: github.event.action == 'converted_to_draft' || github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: 'Remove labels' + run: |- + gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \ + --remove-label 'waiting-on: core-review' \ + --remove-label 'waiting-on: world-maintainer' \ + --remove-label 'waiting-on: author' + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.run/Archipelago Unittests.run.xml b/.run/Archipelago Unittests.run.xml new file mode 100644 index 000000000000..24fea0f73fec --- /dev/null +++ b/.run/Archipelago Unittests.run.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/BaseClasses.py b/BaseClasses.py index 855e69c5d48c..25e4e70741a1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -18,11 +18,14 @@ import Options import Utils +if typing.TYPE_CHECKING: + from worlds import AutoWorld + class Group(TypedDict, total=False): name: str game: str - world: auto_world + world: "AutoWorld.World" players: Set[int] item_pool: Set[str] replacement_items: Dict[int, Optional[str]] @@ -55,7 +58,7 @@ class MultiWorld(): plando_texts: List[Dict[str, str]] plando_items: List[List[Dict[str, Any]]] plando_connections: List - worlds: Dict[int, auto_world] + worlds: Dict[int, "AutoWorld.World"] groups: Dict[int, Group] regions: RegionManager itempool: List[Item] @@ -156,11 +159,11 @@ def __init__(self, players: int): self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') self.fix_skullwoods_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) self.fix_palaceofdarkness_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) self.fix_trock_exit = self.AttributeProxy( - lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) + lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']) for player in range(1, players + 1): def set_player_attr(attr, val): @@ -219,6 +222,8 @@ def get_all_ids(self) -> Tuple[int, ...]: def add_group(self, name: str, game: str, players: Set[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 + for group_id, group in self.groups.items(): if group["name"] == name: group["players"] |= players @@ -253,6 +258,8 @@ def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optio def set_options(self, args: Namespace) -> None: # TODO - remove this section once all worlds use options dataclasses + from worlds import AutoWorld + all_keys: Set[str] = {key for player in self.player_ids for key in AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} for option_key in all_keys: @@ -270,6 +277,8 @@ def set_options(self, args: Namespace) -> None: for option_key in options_dataclass.type_hints}) def set_item_links(self): + from worlds import AutoWorld + item_links = {} replacement_prio = [False, True, None] for player in self.player_ids: @@ -572,9 +581,10 @@ def fulfills_accessibility(self, state: Optional[CollectionState] = None): def location_condition(location: Location): """Determine if this location has to be accessible, location is already filtered by location_relevant""" - if location.player in players["minimal"]: - return False - return True + if location.player in players["locations"] or (location.item and location.item.player not in + players["minimal"]): + return True + return False def location_relevant(location: Location): """Determine if this location is relevant to sweep.""" @@ -823,8 +833,8 @@ def __repr__(self): return self.__str__() def __str__(self): - world = self.parent_region.multiworld if self.parent_region else None - return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' + 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})' class Region: @@ -1040,8 +1050,8 @@ def __repr__(self): return self.__str__() def __str__(self): - world = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None - return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' + 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})' def __hash__(self): return hash((self.name, self.player)) @@ -1056,9 +1066,6 @@ def native_item(self) -> bool: @property def hint_text(self) -> str: - hint_text = getattr(self, "_hint_text", None) - if hint_text: - return hint_text return "at " + self.name.replace("_", " ").replace("-", " ") @@ -1178,7 +1185,7 @@ def set_entrance(self, entrance: str, exit_: str, direction: str, player: int) - {"player": player, "entrance": entrance, "exit": exit_, "direction": direction} def create_playthrough(self, create_paths: bool = True) -> None: - """Destructive to the world while it is run, damage gets repaired afterwards.""" + """Destructive to the multiworld while it is run, damage gets repaired afterwards.""" from itertools import chain # get locations containing progress items multiworld = self.multiworld @@ -1329,6 +1336,8 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) def to_file(self, filename: str) -> None: + from worlds import AutoWorld + def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: res = getattr(self.multiworld.worlds[player].options, option_key) display_name = getattr(option_obj, "display_name", option_key) @@ -1452,8 +1461,3 @@ def get_seed(seed: Optional[int] = None) -> int: random.seed(None) return random.randint(0, pow(10, seeddigits) - 1) return seed - - -from worlds import AutoWorld - -auto_world = AutoWorld.World diff --git a/CommonClient.py b/CommonClient.py index c4d80f341611..736cf4922f40 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -460,7 +460,7 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], else: self.update_game(cached_game) if needed_updates: - await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}]) + await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) def update_game(self, game_package: dict): for item_name, item_id in game_package["item_name_to_id"].items(): @@ -477,6 +477,7 @@ def consume_network_data_package(self, data_package: dict): current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {}) current_cache.update(data_package["games"]) Utils.persistent_store("datapackage", "games", current_cache) + logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}") for game, game_data in data_package["games"].items(): Utils.store_data_package_for_checksum(game, game_data) @@ -727,7 +728,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict): await ctx.server_auth(args['password']) elif cmd == 'DataPackage': - logger.info("Got new ID/Name DataPackage") ctx.consume_network_data_package(args['data']) elif cmd == 'ConnectionRefused': diff --git a/Fill.py b/Fill.py index 525d27d3388e..ae44710469e4 100644 --- a/Fill.py +++ b/Fill.py @@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] return new_state -def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], +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: """ - :param world: Multiworld to be filled. + :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. :param locations: Locations to be filled with item_pool :param item_pool: Items to fill into the locations @@ -68,7 +68,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: maximum_exploration_state = sweep_from_pool( base_state, item_pool + unplaced_items) - has_beaten_game = world.has_beaten_game(maximum_exploration_state) + has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state) while items_to_place: # if we have run out of locations to fill,break out of this loop @@ -80,8 +80,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: spot_to_fill: typing.Optional[Location] = None # if minimal accessibility, only check whether location is reachable if game not beatable - if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal: - perform_access_check = not world.has_beaten_game(maximum_exploration_state, + if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal: + perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state, item_to_place.player) \ if single_player_placement else not has_beaten_game else: @@ -122,11 +122,11 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: # Verify placing this item won't reduce available locations, which would be a useless swap. prev_state = swap_state.copy() prev_loc_count = len( - world.get_reachable_locations(prev_state)) + multiworld.get_reachable_locations(prev_state)) swap_state.collect(item_to_place, True) new_loc_count = len( - world.get_reachable_locations(swap_state)) + multiworld.get_reachable_locations(swap_state)) if new_loc_count >= prev_loc_count: # Add this item to the existing placement, and @@ -156,7 +156,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: else: unplaced_items.append(item_to_place) continue - world.push_item(spot_to_fill, item_to_place, False) + multiworld.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement @@ -173,7 +173,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: # validate all placements and remove invalid ones state = sweep_from_pool(base_state, []) for placement in placements: - if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state): + if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state): placement.item.location = None unplaced_items.append(placement.item) placement.item = None @@ -188,7 +188,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: if excluded_locations: for location in excluded_locations: location.progress_type = location.progress_type.DEFAULT - fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock, + fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock, swap, on_place, allow_partial, False) for location in excluded_locations: if not location.item: @@ -196,7 +196,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0: # There are leftover unplaceable items and locations that won't accept them - if world.can_beat_game(): + if multiworld.can_beat_game(): logging.warning( f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})') else: @@ -206,7 +206,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: item_pool.extend(unplaced_items) -def remaining_fill(world: MultiWorld, +def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item]) -> None: unplaced_items: typing.List[Item] = [] @@ -261,7 +261,7 @@ def remaining_fill(world: MultiWorld, unplaced_items.append(item_to_place) continue - world.push_item(spot_to_fill, item_to_place, False) + multiworld.push_item(spot_to_fill, item_to_place, False) placements.append(spot_to_fill) placed += 1 if not placed % 1000: @@ -278,19 +278,19 @@ def remaining_fill(world: MultiWorld, itempool.extend(unplaced_items) -def fast_fill(world: MultiWorld, +def fast_fill(multiworld: MultiWorld, item_pool: typing.List[Item], fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: placing = min(len(item_pool), len(fill_locations)) for item, location in zip(item_pool, fill_locations): - world.push_item(location, item, False) + multiworld.push_item(location, item, False) return item_pool[placing:], fill_locations[placing:] -def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]): +def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]): maximum_exploration_state = sweep_from_pool(state, pool) - minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"} - unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and + minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"} + unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and not location.can_reach(maximum_exploration_state)] for location in unreachable_locations: if (location.item is not None and location.item.advancement and location.address is not None and not @@ -304,36 +304,36 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) - fill_restrictive(world, state, locations, pool, name="Accessibility Corrections") + fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections") -def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): +def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations): maximum_exploration_state = sweep_from_pool(state) unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)] if unreachable_locations: def forbid_important_item_rule(item: Item): - return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal') + return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal') for location in unreachable_locations: add_item_rule(location, forbid_important_item_rule) -def distribute_early_items(world: MultiWorld, +def distribute_early_items(multiworld: MultiWorld, fill_locations: typing.List[Location], itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]: """ returns new fill_locations and itempool """ early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {} - for player in world.player_ids: - items = itertools.chain(world.early_items[player], world.local_early_items[player]) + for player in multiworld.player_ids: + items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player]) for item in items: - early_items_count[item, player] = [world.early_items[player].get(item, 0), - world.local_early_items[player].get(item, 0)] + early_items_count[item, player] = [multiworld.early_items[player].get(item, 0), + multiworld.local_early_items[player].get(item, 0)] if early_items_count: early_locations: typing.List[Location] = [] early_priority_locations: typing.List[Location] = [] loc_indexes_to_remove: typing.Set[int] = set() - base_state = world.state.copy() - base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None)) + base_state = multiworld.state.copy() + base_state.sweep_for_events(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: @@ -345,8 +345,8 @@ def distribute_early_items(world: MultiWorld, early_prog_items: typing.List[Item] = [] early_rest_items: typing.List[Item] = [] - early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids} - early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids} + early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids} + early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids} item_indexes_to_remove: typing.Set[int] = set() for i, item in enumerate(itempool): if (item.name, item.player) in early_items_count: @@ -370,28 +370,28 @@ def distribute_early_items(world: MultiWorld, if len(early_items_count) == 0: break itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove] - for player in world.player_ids: + for player in multiworld.player_ids: player_local = early_local_rest_items[player] - fill_restrictive(world, base_state, + fill_restrictive(multiworld, base_state, [loc for loc in early_locations if loc.player == player], player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_rest_items.extend(early_local_rest_items[player]) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True, + fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True, name="Early Items") early_locations += early_priority_locations - for player in world.player_ids: + for player in multiworld.player_ids: player_local = early_local_prog_items[player] - fill_restrictive(world, base_state, + fill_restrictive(multiworld, base_state, [loc for loc in early_locations if loc.player == player], player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_prog_items.extend(player_local) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True, + fill_restrictive(multiworld, base_state, early_locations, early_prog_items, lock=True, allow_partial=True, name="Early Progression") unplaced_early_items = early_rest_items + early_prog_items if unplaced_early_items: @@ -400,18 +400,18 @@ def distribute_early_items(world: MultiWorld, itempool += unplaced_early_items fill_locations.extend(early_locations) - world.random.shuffle(fill_locations) + multiworld.random.shuffle(fill_locations) return fill_locations, itempool -def distribute_items_restrictive(world: MultiWorld) -> None: - fill_locations = sorted(world.get_unfilled_locations()) - world.random.shuffle(fill_locations) +def distribute_items_restrictive(multiworld: MultiWorld) -> None: + fill_locations = sorted(multiworld.get_unfilled_locations()) + multiworld.random.shuffle(fill_locations) # get items to distribute - itempool = sorted(world.itempool) - world.random.shuffle(itempool) + itempool = sorted(multiworld.itempool) + multiworld.random.shuffle(itempool) - fill_locations, itempool = distribute_early_items(world, fill_locations, itempool) + fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool) progitempool: typing.List[Item] = [] usefulitempool: typing.List[Item] = [] @@ -425,7 +425,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None: else: filleritempool.append(item) - call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations) + call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations) locations: typing.Dict[LocationProgressType, typing.List[Location]] = { loc_type: [] for loc_type in LocationProgressType} @@ -446,34 +446,34 @@ def mark_for_locking(location: Location): if prioritylocations: # "priority fill" - fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, name="Priority") - accessibility_corrections(world, world.state, prioritylocations, progitempool) + accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: # "advancement/progression fill" - fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression") + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression") if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') - accessibility_corrections(world, world.state, defaultlocations) + accessibility_corrections(multiworld, multiworld.state, defaultlocations) for location in lock_later: if location.item: location.locked = True del mark_for_locking, lock_later - inaccessible_location_rules(world, world.state, defaultlocations) + inaccessible_location_rules(multiworld, multiworld.state, defaultlocations) - remaining_fill(world, excludedlocations, filleritempool) + remaining_fill(multiworld, excludedlocations, filleritempool) if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") restitempool = filleritempool + usefulitempool - remaining_fill(world, defaultlocations, restitempool) + remaining_fill(multiworld, defaultlocations, restitempool) unplaced = restitempool unfilled = defaultlocations @@ -481,40 +481,40 @@ def mark_for_locking(location: Location): if unplaced or unfilled: logging.warning( f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') - items_counter = Counter(location.item.player for location in world.get_locations() if location.item) - locations_counter = Counter(location.player for location in world.get_locations()) + items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item) + locations_counter = Counter(location.player for location in multiworld.get_locations()) items_counter.update(item.player for item in unplaced) locations_counter.update(location.player for location in unfilled) print_data = {"items": items_counter, "locations": locations_counter} logging.info(f'Per-Player counts: {print_data})') -def flood_items(world: MultiWorld) -> None: +def flood_items(multiworld: MultiWorld) -> None: # get items to distribute - world.random.shuffle(world.itempool) - itempool = world.itempool + multiworld.random.shuffle(multiworld.itempool) + itempool = multiworld.itempool progress_done = False # sweep once to pick up preplaced items - world.state.sweep_for_events() + multiworld.state.sweep_for_events() - # fill world from top of itempool while we can + # fill multiworld from top of itempool while we can while not progress_done: - location_list = world.get_unfilled_locations() - world.random.shuffle(location_list) + location_list = multiworld.get_unfilled_locations() + multiworld.random.shuffle(location_list) spot_to_fill = None for location in location_list: - if location.can_fill(world.state, itempool[0]): + if location.can_fill(multiworld.state, itempool[0]): spot_to_fill = location break if spot_to_fill: item = itempool.pop(0) - world.push_item(spot_to_fill, item, True) + multiworld.push_item(spot_to_fill, item, True) continue # ran out of spots, check if we need to step in and correct things - if len(world.get_reachable_locations()) == len(world.get_locations()): + if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()): progress_done = True continue @@ -524,7 +524,7 @@ def flood_items(world: MultiWorld) -> None: for item in itempool: if item.advancement: candidate_item_to_place = item - if world.unlocks_new_location(item): + if multiworld.unlocks_new_location(item): item_to_place = item break @@ -537,15 +537,15 @@ def flood_items(world: MultiWorld) -> None: raise FillError('No more progress items left to place.') # find item to replace with progress item - location_list = world.get_reachable_locations() - world.random.shuffle(location_list) + location_list = multiworld.get_reachable_locations() + multiworld.random.shuffle(location_list) for location in location_list: if location.item is not None and not location.item.advancement: # safe to replace replace_item = location.item replace_item.location = None itempool.append(replace_item) - world.push_item(location, item_to_place, True) + multiworld.push_item(location, item_to_place, True) itempool.remove(item_to_place) break @@ -755,7 +755,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked: location_1.event, location_2.event = location_2.event, location_1.event -def distribute_planned(world: MultiWorld) -> None: +def distribute_planned(multiworld: MultiWorld) -> None: def warn(warning: str, force: typing.Union[bool, str]) -> None: if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']: logging.warning(f'{warning}') @@ -768,24 +768,24 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: else: warn(warning, force) - swept_state = world.state.copy() + swept_state = multiworld.state.copy() swept_state.sweep_for_events() - reachable = frozenset(world.get_reachable_locations(swept_state)) + 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) - for loc in world.get_unfilled_locations(): + for loc in multiworld.get_unfilled_locations(): if loc in reachable: early_locations[loc.player].append(loc.name) else: # not reachable with swept state non_early_locations[loc.player].append(loc.name) - world_name_lookup = world.world_name_lookup + world_name_lookup = multiworld.world_name_lookup block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] plando_blocks: typing.List[typing.Dict[str, typing.Any]] = [] - player_ids = set(world.player_ids) + player_ids = set(multiworld.player_ids) for player in player_ids: - for block in world.plando_items[player]: + for block in multiworld.plando_items[player]: block['player'] = player if 'force' not in block: block['force'] = 'silent' @@ -799,12 +799,12 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: else: target_world = block['world'] - if target_world is False or world.players == 1: # target own world + if target_world is False or multiworld.players == 1: # target own world worlds: typing.Set[int] = {player} elif target_world is True: # target any worlds besides own - worlds = set(world.player_ids) - {player} + worlds = set(multiworld.player_ids) - {player} elif target_world is None: # target all worlds - worlds = set(world.player_ids) + worlds = set(multiworld.player_ids) elif type(target_world) == list: # list of target worlds worlds = set() for listed_world in target_world: @@ -814,9 +814,9 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: continue worlds.add(world_name_lookup[listed_world]) elif type(target_world) == int: # target world by slot number - if target_world not in range(1, world.players + 1): + if target_world not in range(1, multiworld.players + 1): failed( - f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})", + f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})", block['force']) continue worlds = {target_world} @@ -844,7 +844,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: item_list: typing.List[str] = [] for key, value in items.items(): if value is True: - value = world.itempool.count(world.worlds[player].create_item(key)) + value = multiworld.itempool.count(multiworld.worlds[player].create_item(key)) item_list += [key] * value items = item_list if isinstance(items, str): @@ -894,17 +894,17 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: count = block['count'] failed(f"Plando count {count} greater than locations specified", block['force']) block['count'] = len(block['locations']) - block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max']) + block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max']) if block['count']['target'] > 0: plando_blocks.append(block) # shuffle, but then sort blocks by number of locations minus number of items, # so less-flexible blocks get priority - world.random.shuffle(plando_blocks) + multiworld.random.shuffle(plando_blocks) plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target'] if len(block['locations']) > 0 - else len(world.get_unfilled_locations(player)) - block['count']['target'])) + else len(multiworld.get_unfilled_locations(player)) - block['count']['target'])) for placement in plando_blocks: player = placement['player'] @@ -915,19 +915,19 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: maxcount = placement['count']['target'] from_pool = placement['from_pool'] - candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds))) - world.random.shuffle(candidates) - world.random.shuffle(items) + candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds))) + multiworld.random.shuffle(candidates) + multiworld.random.shuffle(items) count = 0 err: typing.List[str] = [] successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] for item_name in items: - item = world.worlds[player].create_item(item_name) + 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(world.state, item, False): + if location.can_fill(multiworld.state, item, False): successful_pairs.append((item, location)) candidates.remove(location) count = count + 1 @@ -945,21 +945,21 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: if count < placement['count']['min']: m = placement['count']['min'] failed( - f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}", + 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: - world.push_item(location, item, collect=False) + multiworld.push_item(location, item, collect=False) location.event = True # flag location to be checked during fill location.locked = True logging.debug(f"Plando placed {item} at {location}") if from_pool: try: - world.itempool.remove(item) + multiworld.itempool.remove(item) except ValueError: warn( - f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.", + f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", placement['force']) except Exception as e: raise Exception( - f"Error running plando for player {player} ({world.player_name[player]})") from e + f"Error running plando for player {player} ({multiworld.player_name[player]})") from e diff --git a/Generate.py b/Generate.py index e19a7a973f23..fd4a5a7e1930 100644 --- a/Generate.py +++ b/Generate.py @@ -315,20 +315,6 @@ def prefer_int(input_data: str) -> Union[str, int]: return input_data -goals = { - 'ganon': 'ganon', - 'crystals': 'crystals', - 'bosses': 'bosses', - 'pedestal': 'pedestal', - 'ganon_pedestal': 'ganonpedestal', - 'triforce_hunt': 'triforcehunt', - 'local_triforce_hunt': 'localtriforcehunt', - 'ganon_triforce_hunt': 'ganontriforcehunt', - 'local_ganon_triforce_hunt': 'localganontriforcehunt', - 'ice_rod_hunt': 'icerodhunt', -} - - def roll_percentage(percentage: Union[int, float]) -> bool: """Roll a percentage chance. percentage is expected to be in range [0, 100]""" @@ -357,15 +343,6 @@ def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: if options[option_key].supports_weighting: return get_choice(option_key, category_dict) return category_dict[option_key] - if game == "A Link to the Past": # TODO wow i hate this - if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode", - "triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra", - "triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality", - "boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time", - "red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes", - "misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite", - "random_sprite_on_event"}: - return get_choice(option_key, category_dict) raise Exception(f"Error generating meta option {option_key} for {game}.") @@ -504,101 +481,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): - if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none": - raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.") - glitches_required = get_choice_legacy('glitches_required', weights) - if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']: - logging.warning("Only NMG, OWG, HMG and No Logic supported") - glitches_required = 'none' - ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches', - 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[ - glitches_required] - - ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp") - if not ret.dark_room_logic: # None/False - ret.dark_room_logic = "none" - if ret.dark_room_logic == "sconces": - ret.dark_room_logic = "torches" - if ret.dark_room_logic not in {"lamp", "torches", "none"}: - raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"") - - entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla') - if entrance_shuffle.startswith('none-'): - ret.shuffle = 'vanilla' - else: - ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' - - goal = get_choice_legacy('goals', weights, 'ganon') - - ret.goal = goals[goal] - - - extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available') - - ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20)) - - # sum a percentage to required - if extra_pieces == 'percentage': - percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100 - ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0)) - # vanilla mode (specify how many pieces are) - elif extra_pieces == 'available': - ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any( - get_choice_legacy('triforce_pieces_available', weights, 30)) - # required pieces + fixed extra - elif extra_pieces == 'extra': - extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10))) - ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces - - # change minimum to required pieces to avoid problems - ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) - - ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '') - if not ret.shop_shuffle: - ret.shop_shuffle = '' - - ret.mode = get_choice_legacy("mode", weights) - - ret.difficulty = get_choice_legacy('item_pool', weights) - - ret.item_functionality = get_choice_legacy('item_functionality', weights) - - - ret.enemy_damage = {None: 'default', - 'default': 'default', - 'shuffled': 'shuffled', - 'random': 'chaos', # to be removed - 'chaos': 'chaos', - }[get_choice_legacy('enemy_damage', weights)] - - ret.enemy_health = get_choice_legacy('enemy_health', weights) - - ret.timer = {'none': False, - None: False, - False: False, - 'timed': 'timed', - 'timed_ohko': 'timed-ohko', - 'ohko': 'ohko', - 'timed_countdown': 'timed-countdown', - 'display': 'display'}[get_choice_legacy('timer', weights, False)] - - ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10)) - ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2)) - ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2)) - ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4)) - - ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default') - - ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g") - - ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"), - get_choice_legacy("turtle_rock_medallion", weights, "random")] - - for index, medallion in enumerate(ret.required_medallions): - ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \ - .get(medallion.lower(), None) - if not ret.required_medallions[index]: - raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}") ret.plando_texts = {} if PlandoOptions.texts in plando_options: diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index f3fc9d2cdb72..a51645feac92 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -348,7 +348,8 @@ async def wait_for_retroarch_connection(self): await asyncio.sleep(1.0) continue self.stop_bizhawk_spam = False - logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}") + logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} " + f"running {rom_name.decode('ascii', errors='replace')}") return except (BlockingIOError, TimeoutError, ConnectionResetError): await asyncio.sleep(1.0) diff --git a/Main.py b/Main.py index 8dac8f7d20eb..f1d2f63692d6 100644 --- a/Main.py +++ b/Main.py @@ -30,49 +30,49 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No output_path.cached_path = args.outputpath start = time.perf_counter() - # initialize the world - world = MultiWorld(args.multi) + # initialize the multiworld + multiworld = MultiWorld(args.multi) logger = logging.getLogger() - world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None) - world.plando_options = args.plando_options - - world.shuffle = args.shuffle.copy() - world.logic = args.logic.copy() - world.mode = args.mode.copy() - world.difficulty = args.difficulty.copy() - world.item_functionality = args.item_functionality.copy() - world.timer = args.timer.copy() - world.goal = args.goal.copy() - world.boss_shuffle = args.shufflebosses.copy() - world.enemy_health = args.enemy_health.copy() - world.enemy_damage = args.enemy_damage.copy() - world.beemizer_total_chance = args.beemizer_total_chance.copy() - world.beemizer_trap_chance = args.beemizer_trap_chance.copy() - world.countdown_start_time = args.countdown_start_time.copy() - world.red_clock_time = args.red_clock_time.copy() - world.blue_clock_time = args.blue_clock_time.copy() - world.green_clock_time = args.green_clock_time.copy() - world.dungeon_counters = args.dungeon_counters.copy() - world.triforce_pieces_available = args.triforce_pieces_available.copy() - world.triforce_pieces_required = args.triforce_pieces_required.copy() - world.shop_shuffle = args.shop_shuffle.copy() - world.shuffle_prizes = args.shuffle_prizes.copy() - world.sprite_pool = args.sprite_pool.copy() - world.dark_room_logic = args.dark_room_logic.copy() - world.plando_items = args.plando_items.copy() - world.plando_texts = args.plando_texts.copy() - world.plando_connections = args.plando_connections.copy() - world.required_medallions = args.required_medallions.copy() - world.game = args.game.copy() - world.player_name = args.name.copy() - world.sprite = args.sprite.copy() - world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. - - world.set_options(args) - world.set_item_links() - world.state = CollectionState(world) - logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) + multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None) + multiworld.plando_options = args.plando_options + + multiworld.shuffle = args.shuffle.copy() + multiworld.logic = args.logic.copy() + multiworld.mode = args.mode.copy() + multiworld.difficulty = args.difficulty.copy() + multiworld.item_functionality = args.item_functionality.copy() + multiworld.timer = args.timer.copy() + multiworld.goal = args.goal.copy() + multiworld.boss_shuffle = args.shufflebosses.copy() + multiworld.enemy_health = args.enemy_health.copy() + multiworld.enemy_damage = args.enemy_damage.copy() + multiworld.beemizer_total_chance = args.beemizer_total_chance.copy() + multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy() + multiworld.countdown_start_time = args.countdown_start_time.copy() + multiworld.red_clock_time = args.red_clock_time.copy() + multiworld.blue_clock_time = args.blue_clock_time.copy() + multiworld.green_clock_time = args.green_clock_time.copy() + multiworld.dungeon_counters = args.dungeon_counters.copy() + multiworld.triforce_pieces_available = args.triforce_pieces_available.copy() + multiworld.triforce_pieces_required = args.triforce_pieces_required.copy() + multiworld.shop_shuffle = args.shop_shuffle.copy() + multiworld.shuffle_prizes = args.shuffle_prizes.copy() + multiworld.sprite_pool = args.sprite_pool.copy() + multiworld.dark_room_logic = args.dark_room_logic.copy() + multiworld.plando_items = args.plando_items.copy() + multiworld.plando_texts = args.plando_texts.copy() + multiworld.plando_connections = args.plando_connections.copy() + multiworld.required_medallions = args.required_medallions.copy() + multiworld.game = args.game.copy() + multiworld.player_name = args.name.copy() + multiworld.sprite = args.sprite.copy() + multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. + + multiworld.set_options(args) + multiworld.set_item_links() + multiworld.state = CollectionState(multiworld) + logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:") longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) @@ -103,87 +103,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # This assertion method should not be necessary to run if we are not outputting any multidata. if not args.skip_output: - AutoWorld.call_stage(world, "assert_generate") + AutoWorld.call_stage(multiworld, "assert_generate") - AutoWorld.call_all(world, "generate_early") + AutoWorld.call_all(multiworld, "generate_early") logger.info('') - for player in world.player_ids: - for item_name, count in world.worlds[player].options.start_inventory.value.items(): + for player in multiworld.player_ids: + for item_name, count in multiworld.worlds[player].options.start_inventory.value.items(): for _ in range(count): - world.push_precollected(world.create_item(item_name, player)) + multiworld.push_precollected(multiworld.create_item(item_name, player)) - for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): + for item_name, count in getattr(multiworld.worlds[player].options, + "start_inventory_from_pool", + StartInventoryPool({})).value.items(): for _ in range(count): - world.push_precollected(world.create_item(item_name, player)) + multiworld.push_precollected(multiworld.create_item(item_name, player)) # remove from_pool items also from early items handling, as starting is plenty early. - early = world.early_items[player].get(item_name, 0) + early = multiworld.early_items[player].get(item_name, 0) if early: - world.early_items[player][item_name] = max(0, early-count) + multiworld.early_items[player][item_name] = max(0, early-count) remaining_count = count-early if remaining_count > 0: - local_early = world.early_local_items[player].get(item_name, 0) + local_early = multiworld.early_local_items[player].get(item_name, 0) if local_early: - world.early_items[player][item_name] = max(0, local_early - remaining_count) + multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) del local_early del early - logger.info('Creating World.') - AutoWorld.call_all(world, "create_regions") + logger.info('Creating MultiWorld.') + AutoWorld.call_all(multiworld, "create_regions") logger.info('Creating Items.') - AutoWorld.call_all(world, "create_items") + AutoWorld.call_all(multiworld, "create_items") logger.info('Calculating Access Rules.') - for player in world.player_ids: + for player in multiworld.player_ids: # items can't be both local and non-local, prefer local - world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value - world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player]) + multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value + multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player]) - AutoWorld.call_all(world, "set_rules") + AutoWorld.call_all(multiworld, "set_rules") - for player in world.player_ids: - exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value) - world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value - for location_name in world.worlds[player].options.priority_locations.value: + 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 + for location_name in multiworld.worlds[player].options.priority_locations.value: try: - location = world.get_location(location_name, player) + 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 world.worlds[player].location_name_to_id: + 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: location.progress_type = LocationProgressType.PRIORITY # Set local and non-local item rules. - if world.players > 1: - locality_rules(world) + if multiworld.players > 1: + locality_rules(multiworld) else: - world.worlds[1].options.non_local_items.value = set() - world.worlds[1].options.local_items.value = set() + multiworld.worlds[1].options.non_local_items.value = set() + multiworld.worlds[1].options.local_items.value = set() - AutoWorld.call_all(world, "generate_basic") + AutoWorld.call_all(multiworld, "generate_basic") # 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(world.start_inventory_from_pool[player].value for player in world.player_ids): + 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: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids} + 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 = world.worlds[player] + 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(world.itempool): + for i, item in enumerate(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(world.itempool[i+1:]) + new_items.extend(multiworld.itempool[i+1:]) break else: new_items.append(item) @@ -193,19 +199,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No 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"{world.get_player_name(player)}" + 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(world.itempool) == len(new_items), "Item Pool amounts should not change." - world.itempool[:] = new_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 world.groups.items(): + 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 world.itempool: + 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 @@ -240,13 +246,13 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ new_item.classification |= classifications[item_name] new_itempool.append(new_item) - region = Region("Menu", group_id, world, "ItemLink") - world.regions.append(region) + region = Region("Menu", group_id, multiworld, "ItemLink") + multiworld.regions.append(region) locations = region.locations - for item in world.itempool: + 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} -> {world.player_name[item.player]} {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_) @@ -257,10 +263,10 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ else: new_itempool.append(item) - itemcount = len(world.itempool) - world.itempool = new_itempool + itemcount = len(multiworld.itempool) + multiworld.itempool = new_itempool - while itemcount > len(world.itempool): + while itemcount > len(multiworld.itempool): items_to_add = [] for player in group["players"]: if group["link_replacement"]: @@ -268,64 +274,64 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ else: item_player = player if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(world, "create_item", item_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(world, "create_filler", item_player)) - world.random.shuffle(items_to_add) - world.itempool.extend(items_to_add[:itemcount - len(world.itempool)]) + 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)]) - if any(world.item_links.values()): - world._all_state = None + if any(multiworld.item_links.values()): + multiworld._all_state = None logger.info("Running Item Plando.") - distribute_planned(world) + distribute_planned(multiworld) logger.info('Running Pre Main Fill.') - AutoWorld.call_all(world, "pre_fill") + AutoWorld.call_all(multiworld, "pre_fill") - logger.info(f'Filling the world with {len(world.itempool)} items.') + logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.') - if world.algorithm == 'flood': - flood_items(world) # different algo, biased towards early game progress items - elif world.algorithm == 'balanced': - distribute_items_restrictive(world) + if multiworld.algorithm == 'flood': + flood_items(multiworld) # different algo, biased towards early game progress items + elif multiworld.algorithm == 'balanced': + distribute_items_restrictive(multiworld) - AutoWorld.call_all(world, 'post_fill') + AutoWorld.call_all(multiworld, 'post_fill') - if world.players > 1 and not args.skip_prog_balancing: - balance_multiworld_progression(world) + if multiworld.players > 1 and not args.skip_prog_balancing: + balance_multiworld_progression(multiworld) else: logger.info("Progression balancing skipped.") # we're about to output using multithreading, so we're removing the global random state to prevent accidental use - world.random.passthrough = False + multiworld.random.passthrough = False if args.skip_output: logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start) - return world + return multiworld logger.info(f'Beginning output...') - outfilebase = 'AP_' + world.seed_name + outfilebase = 'AP_' + multiworld.seed_name output = tempfile.TemporaryDirectory() with output as temp_dir: - output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__ - is not world.worlds[player].generate_output.__code__] + output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__ + is not multiworld.worlds[player].generate_output.__code__] with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool: - check_accessibility_task = pool.submit(world.fulfills_accessibility) + check_accessibility_task = pool.submit(multiworld.fulfills_accessibility) - output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)] + output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)] for player in output_players: # skip starting a thread for methods that say "pass". output_file_futures.append( - pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) + pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir)) # collect ER hint info er_hint_data: Dict[int, Dict[int, str]] = {} - AutoWorld.call_all(world, 'extend_hint_information', er_hint_data) + AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data) def write_multidata(): import NetUtils @@ -334,38 +340,38 @@ def write_multidata(): games = {} minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions} slot_info = {} - names = [[name for player, name in sorted(world.player_name.items())]] - for slot in world.player_ids: - player_world: AutoWorld.World = world.worlds[slot] + names = [[name for player, name in sorted(multiworld.player_name.items())]] + for slot in multiworld.player_ids: + player_world: AutoWorld.World = multiworld.worlds[slot] minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version) client_versions[slot] = player_world.required_client_version - games[slot] = world.game[slot] - slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot], - world.player_types[slot]) - for slot, group in world.groups.items(): - games[slot] = world.game[slot] - slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot], + games[slot] = multiworld.game[slot] + slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot], + multiworld.player_types[slot]) + for slot, group in multiworld.groups.items(): + games[slot] = multiworld.game[slot] + slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot], group_members=sorted(group["players"])) precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int] - for player, world_precollected in world.precollected_items.items()} - precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} + for player, world_precollected in multiworld.precollected_items.items()} + precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))} - for slot in world.player_ids: - slot_data[slot] = world.worlds[slot].fill_slot_data() + for slot in multiworld.player_ids: + slot_data[slot] = multiworld.worlds[slot].fill_slot_data() def precollect_hint(location): 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) precollected_hints[location.player].add(hint) - if location.item.player not in world.groups: + if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) else: - for player in world.groups[location.item.player]["players"]: + for player in multiworld.groups[location.item.player]["players"]: precollected_hints[player].add(hint) - locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids} - for location in world.get_filled_locations(): + locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids} + for location in multiworld.get_filled_locations(): 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: " \ @@ -375,18 +381,18 @@ def precollect_hint(location): f"{locations_data[location.player][location.address]}") locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags - if location.name in world.worlds[location.player].options.start_location_hints: + if location.name in multiworld.worlds[location.player].options.start_location_hints: precollect_hint(location) - elif location.item.name in world.worlds[location.item.player].options.start_hints: + elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: precollect_hint(location) - elif any([location.item.name in world.worlds[player].options.start_hints - for player in world.groups.get(location.item.player, {}).get("players", [])]): + 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) # embedded data package data_package = { game_world.game: worlds.network_data_package["games"][game_world.game] - for game_world in world.worlds.values() + for game_world in multiworld.worlds.values() } checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} @@ -394,7 +400,7 @@ def precollect_hint(location): multidata = { "slot_data": slot_data, "slot_info": slot_info, - "connect_names": {name: (0, player) for player, name in world.player_name.items()}, + "connect_names": {name: (0, player) for player, name in multiworld.player_name.items()}, "locations": locations_data, "checks_in_area": checks_in_area, "server_options": baked_server_options, @@ -404,10 +410,10 @@ def precollect_hint(location): "version": tuple(version_tuple), "tags": ["AP"], "minimum_versions": minimum_versions, - "seed_name": world.seed_name, + "seed_name": multiworld.seed_name, "datapackage": data_package, } - AutoWorld.call_all(world, "modify_multidata", multidata) + AutoWorld.call_all(multiworld, "modify_multidata", multidata) multidata = zlib.compress(pickle.dumps(multidata), 9) @@ -417,7 +423,7 @@ def precollect_hint(location): output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): - if not world.can_beat_game(): + if not multiworld.can_beat_game(): raise Exception("Game appears as unbeatable. Aborting.") else: logger.warning("Location Accessibility requirements not fulfilled.") @@ -430,12 +436,12 @@ def precollect_hint(location): if args.spoiler > 1: logger.info('Calculating playthrough.') - world.spoiler.create_playthrough(create_paths=args.spoiler > 2) + multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2) if args.spoiler: - world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) + multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) - zipfilename = output_path(f"AP_{world.seed_name}.zip") + zipfilename = output_path(f"AP_{multiworld.seed_name}.zip") logger.info(f"Creating final archive at {zipfilename}") with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: @@ -443,4 +449,4 @@ def precollect_hint(location): zf.write(file.path, arcname=file.name) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) - return world + return multiworld diff --git a/ModuleUpdate.py b/ModuleUpdate.py index c33e894e8b5f..c3dc8c8a87b2 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -4,14 +4,29 @@ import multiprocessing import warnings -local_dir = os.path.dirname(__file__) -requirements_files = {os.path.join(local_dir, 'requirements.txt')} if sys.version_info < (3, 8, 6): raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) -update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process() +_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) +update_ran = _skip_update + + +class RequirementsSet(set): + def add(self, e): + global update_ran + update_ran &= _skip_update + super().add(e) + + def update(self, *s): + global update_ran + update_ran &= _skip_update + super().update(*s) + + +local_dir = os.path.dirname(__file__) +requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),)) if not update_ran: for entry in os.scandir(os.path.join(local_dir, "worlds")): diff --git a/MultiServer.py b/MultiServer.py index 2b97b2dad0bc..5a7ae63a697f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2279,25 +2279,24 @@ def parse_args() -> argparse.Namespace: async def auto_shutdown(ctx, to_cancel=None): await asyncio.sleep(ctx.auto_shutdown) + + def inactivity_shutdown(): + ctx.server.ws_server.close() + ctx.exit_event.set() + if to_cancel: + for task in to_cancel: + task.cancel() + logging.info("Shutting down due to inactivity.") + while not ctx.exit_event.is_set(): if not ctx.client_activity_timers.values(): - ctx.server.ws_server.close() - ctx.exit_event.set() - if to_cancel: - for task in to_cancel: - task.cancel() - logging.info("Shutting down due to inactivity.") + inactivity_shutdown() else: newest_activity = max(ctx.client_activity_timers.values()) delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity seconds = ctx.auto_shutdown - delta.total_seconds() if seconds < 0: - ctx.server.ws_server.close() - ctx.exit_event.set() - if to_cancel: - for task in to_cancel: - task.cancel() - logging.info("Shutting down due to inactivity.") + inactivity_shutdown() else: await asyncio.sleep(seconds) diff --git a/OoTAdjuster.py b/OoTAdjuster.py index 38ebe62e2ae1..9519b191e704 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -195,10 +195,10 @@ def set_icon(window): window.tk.call('wm', 'iconphoto', window._w, logo) def adjust(args): - # Create a fake world and OOTWorld to use as a base - world = MultiWorld(1) - world.per_slot_randoms = {1: random} - ootworld = OOTWorld(world, 1) + # Create a fake multiworld and OOTWorld to use as a base + multiworld = MultiWorld(1) + multiworld.per_slot_randoms = {1: random} + ootworld = OOTWorld(multiworld, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): result = getattr(args, name, None) diff --git a/Patch.py b/Patch.py index 113d0658c6b7..091545700059 100644 --- a/Patch.py +++ b/Patch.py @@ -8,7 +8,7 @@ import ModuleUpdate ModuleUpdate.update() -from worlds.Files import AutoPatchRegister, APDeltaPatch +from worlds.Files import AutoPatchRegister, APPatch class RomMeta(TypedDict): @@ -20,7 +20,7 @@ class RomMeta(TypedDict): def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]: auto_handler = AutoPatchRegister.get_handler(patch_file) if auto_handler: - handler: APDeltaPatch = auto_handler(patch_file) + handler: APPatch = auto_handler(patch_file) target = os.path.splitext(patch_file)[0]+handler.result_file_ending handler.patch(target) return {"server": handler.server, diff --git a/README.md b/README.md index a1e03293d587..ce2130ce8e7c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Currently, the following games are supported: * Heretic * Landstalker: The Treasures of King Nole * Final Fantasy Mystic Quest +* TUNIC 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/Utils.py b/Utils.py index f6e4a9ab6052..8b91226bed9f 100644 --- a/Utils.py +++ b/Utils.py @@ -871,8 +871,8 @@ def visualize_regions(root_region: Region, file_name: str, *, Example usage in Main code: from Utils import visualize_regions - for player in world.player_ids: - visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml") + for player in multiworld.player_ids: + visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml") """ assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index 61e9164e2652..5a66d1e69331 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -20,8 +20,8 @@ def generate_api(): race = False meta_options_source = {} if 'file' in request.files: - file = request.files['file'] - options = get_yaml_data(file) + files = request.files.getlist('file') + options = get_yaml_data(files) if isinstance(options, Markup): return {"text": options.striptags()}, 400 if isinstance(options, str): diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 33f8dbc09e6c..53d98dfae6ba 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -69,8 +69,8 @@

Generate Game{% if race %} (Race Mode){% endif %}

@@ -185,12 +185,12 @@

Generate Game{% if race %} (Race Mode){% endif %}

+ +
+
- -
-
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 0722ee317466..9cb48009a427 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} - {{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.data %} diff --git a/data/basepatch.bsdiff4 b/data/basepatch.bsdiff4 index a578b248f577..aa8f1375c522 100644 Binary files a/data/basepatch.bsdiff4 and b/data/basepatch.bsdiff4 differ diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index 47af6e003d8e..00021b241f9a 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -22,6 +22,10 @@ SOFTWARE. local SCRIPT_VERSION = 1 +-- Set to log incoming requests +-- Will cause lag due to large console output +local DEBUG = false + --[[ This script expects to receive JSON and will send JSON back. A message should be a list of 1 or more requests which will be executed in order. Each request @@ -271,10 +275,6 @@ local base64 = require("base64") local socket = require("socket") local json = require("json") --- Set to log incoming requests --- Will cause lag due to large console output -local DEBUG = false - local SOCKET_PORT_FIRST = 43055 local SOCKET_PORT_RANGE_SIZE = 5 local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE @@ -330,18 +330,28 @@ function unlock () client_socket:settimeout(0) end -function process_request (req) - local res = {} +request_handlers = { + ["PING"] = function (req) + local res = {} - if req["type"] == "PING" then res["type"] = "PONG" - elseif req["type"] == "SYSTEM" then + return res + end, + + ["SYSTEM"] = function (req) + local res = {} + res["type"] = "SYSTEM_RESPONSE" res["value"] = emu.getsystemid() - elseif req["type"] == "PREFERRED_CORES" then + return res + end, + + ["PREFERRED_CORES"] = function (req) + local res = {} local preferred_cores = client.getconfig().PreferredCores + res["type"] = "PREFERRED_CORES_RESPONSE" res["value"] = {} res["value"]["NES"] = preferred_cores.NES @@ -354,14 +364,21 @@ function process_request (req) res["value"]["PCECD"] = preferred_cores.PCECD res["value"]["SGX"] = preferred_cores.SGX - elseif req["type"] == "HASH" then + return res + end, + + ["HASH"] = function (req) + local res = {} + res["type"] = "HASH_RESPONSE" res["value"] = rom_hash - elseif req["type"] == "GUARD" then - res["type"] = "GUARD_RESPONSE" - local expected_data = base64.decode(req["expected_data"]) + return res + end, + ["GUARD"] = function (req) + local res = {} + local expected_data = base64.decode(req["expected_data"]) local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"]) local data_is_validated = true @@ -372,39 +389,83 @@ function process_request (req) end end + res["type"] = "GUARD_RESPONSE" res["value"] = data_is_validated res["address"] = req["address"] - elseif req["type"] == "LOCK" then + return res + end, + + ["LOCK"] = function (req) + local res = {} + res["type"] = "LOCKED" lock() - elseif req["type"] == "UNLOCK" then + return res + end, + + ["UNLOCK"] = function (req) + local res = {} + res["type"] = "UNLOCKED" unlock() - elseif req["type"] == "READ" then + return res + end, + + ["READ"] = function (req) + local res = {} + res["type"] = "READ_RESPONSE" res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"])) - elseif req["type"] == "WRITE" then + return res + end, + + ["WRITE"] = function (req) + local res = {} + res["type"] = "WRITE_RESPONSE" memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"]) - elseif req["type"] == "DISPLAY_MESSAGE" then + return res + end, + + ["DISPLAY_MESSAGE"] = function (req) + local res = {} + res["type"] = "DISPLAY_MESSAGE_RESPONSE" message_queue:push(req["message"]) - elseif req["type"] == "SET_MESSAGE_INTERVAL" then + return res + end, + + ["SET_MESSAGE_INTERVAL"] = function (req) + local res = {} + res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE" message_interval = req["value"] - else + return res + end, + + ["default"] = function (req) + local res = {} + res["type"] = "ERROR" res["err"] = "Unknown command: "..req["type"] - end - return res + return res + end, +} + +function process_request (req) + if request_handlers[req["type"]] then + return request_handlers[req["type"]](req) + else + return request_handlers["default"](req) + end end -- Receive data from AP client and send message back diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index e221371b2417..95c0baea3a1f 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -164,6 +164,9 @@ # The Legend of Zelda (1) /worlds/tloz/ @Rosalie-A @t3hf1gm3nt +# TUNIC +/worlds/tunic/ @silent-destroyer + # Undertale /worlds/undertale/ @jonloveslegos diff --git a/docs/network protocol.md b/docs/network protocol.md index 274b6e3716bc..338db55299b6 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -27,6 +27,8 @@ There are also a number of community-supported libraries available that implemen | Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | | Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | | | Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | | +| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older | +| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | | ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. @@ -675,8 +677,8 @@ Tags are represented as a list of strings, the common Client tags follow: ### DeathLink A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data: -| Name | Type | Notes | -| ---- | ---- | ---- | -| time | float | Unix Time Stamp of time of death. | -| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." | -| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. | +| Name | Type | Notes | +|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| time | float | Unix Time Stamp of time of death. | +| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." | +| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. | diff --git a/docs/options api.md b/docs/options api.md index 48a3f763fa92..bfab0096bbaf 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -27,14 +27,15 @@ Choice, and defining `alias_true = option_full`. - All options support `random` as a generic option. `random` chooses from any of the available values for that option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. -As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's -create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass: +As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option +to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our +option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass: ```python # options.py from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions +from Options import Toggle, Range, Choice, PerGameCommonOptions class StartingSword(Toggle): @@ -42,13 +43,33 @@ class StartingSword(Toggle): display_name = "Start With Sword" +class Difficulty(Choice): + """Sets overall game difficulty.""" + display_name = "Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options + alias_expert = 2 # same as hard + default = 1 # default to normal + + +class FinalBossHP(Range): + """Sets the HP of the final boss""" + display_name = "Final Boss HP" + range_start = 100 + range_end = 10000 + default = 2000 + + @dataclass class ExampleGameOptions(PerGameCommonOptions): starting_sword: StartingSword + difficulty: Difficulty + final_boss_health: FinalBossHP ``` -This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it -to our world's `__init__.py`: +To then submit this to the multiworld, we add it to our world's `__init__.py`: ```python from worlds.AutoWorld import World diff --git a/docs/style.md b/docs/style.md index 4cc8111425e3..fbf681f28e97 100644 --- a/docs/style.md +++ b/docs/style.md @@ -6,7 +6,6 @@ * 120 character per line for all source files. * Avoid white space errors like trailing spaces. - ## Python Code * We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences. @@ -18,9 +17,10 @@ * Use type annotations where possible for function signatures and class members. * Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls. +* New classes, attributes, and methods in core code should have docstrings that follow + [reST style](https://peps.python.org/pep-0287/). * Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier. - ## Markdown * We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html). @@ -30,20 +30,17 @@ * One space between bullet/number and text. * No lazy numbering. - ## HTML * Indent with 2 spaces for new code. * kebab-case for ids and classes. - ## CSS * Indent with 2 spaces for new code. * `{` on the same line as the selector. * No space between selector and `{`. - ## JS * Indent with 2 spaces. diff --git a/docs/world api.md b/docs/world api.md index 0ab06da65603..72a67bca9de3 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -1,95 +1,95 @@ # Archipelago API -This document tries to explain some internals required to implement a game for -Archipelago's generation and server. Once a seed is generated, a client or mod is -required to send and receive items between the game and server. +This document tries to explain some aspects of the Archipelago World API used when implementing the generation logic of +a game. -Client implementation is out of scope of this document. Please refer to an -existing game that provides a similar API to yours. -Refer to the following documents as well: -- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) -- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) +Client implementation is out of scope of this document. Please refer to an existing game that provides a similar API to +yours, and the following documents: -Archipelago will be abbreviated as "AP" from now on. +* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) +* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) +Archipelago will be abbreviated as "AP" from now on. ## Language AP worlds are written in python3. -Clients that connect to the server to sync items can be in any language that -allows using WebSockets. - +Clients that connect to the server to sync items can be in any language that allows using WebSockets. ## Coding style -AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). -When in doubt use an IDE with coding style linter, for example PyCharm Community Edition. - +AP follows a [style guide](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). +When in doubt, use an IDE with a code-style linter, for example PyCharm Community Edition. ## Docstrings -Docstrings are strings attached to an object in Python that describe what the -object is supposed to be. Certain docstrings will be picked up and used by AP. -They are assigned by writing a string without any assignment right below a -definition. The string must be a triple-quoted string. +Docstrings are strings attached to an object in Python that describe what the object is supposed to be. Certain +docstrings will be picked up and used by AP. They are assigned by writing a string without any assignment right below a +definition. The string must be a triple-quoted string, and should +follow [reST style](https://peps.python.org/pep-0287/). + Example: + ```python from worlds.AutoWorld import World + + class MyGameWorld(World): - """This is the description of My Game that will be displayed on the AP - website.""" + """This is the description of My Game that will be displayed on the AP website.""" ``` - ## Definitions -This section will cover various classes and objects you can use for your world. -While some of the attributes and methods are mentioned here, not all of them are, -but you can find them in `BaseClasses.py`. +This section covers various classes and objects you can use for your world. While some of the attributes and methods +are mentioned here, not all of them are, but you can find them in +[`BaseClasses.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py). ### World Class -A `World` class is the class with all the specifics of a certain game to be -included. It will be instantiated for each player that rolls a seed for that -game. +A `World` is the class with all the specifics of a certain game that is to be included. A new instance will be created +for each player of the game for any given generated multiworld. ### WebWorld Class -A `WebWorld` class contains specific attributes and methods that can be modified -for your world specifically on the webhost: +A `WebWorld` class contains specific attributes and methods that can be modified for your world specifically on the +webhost: -`settings_page`, which can be changed to a link instead of an AP generated settings page. +* `options_page` can be changed to a link instead of an AP-generated options page. -`theme` to be used for your game specific AP pages. Available themes: +* `theme` to be used for your game-specific AP pages. Available themes: -| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | -|---|---|---|---|---|---|---|---| -| | | | | | | | | + | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | + |--------------------------------------------|---------------------------------------------|----------------------------------------------------|-------------------------------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------------|---------------------------------------------| + | | | | | | | | | -`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs. +* `bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be + placed by the site to help users report bugs. -`tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. +* `tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. -`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be -prefixed with the same string as defined here. Default already has 'en'. +* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The + documents must be prefixed with the same string as defined here. Default already has 'en'. -`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values -are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of -the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. +* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values + are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names + of the options and the values are the values to be set for that option. These presets will be available for users to + select from on the game's options page. Note: The values must be a non-aliased value for the option type and can only include the following option types: - - If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` - values. - - If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the +* If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + * If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the `special_range_names` keys. - - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. - - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. - - `random` is also a valid value for any of these option types. +* If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. +* If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. +* `random` is also a valid value for any of these option types. -`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time. +`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on +the webhost at this time. Here is an example of a defined preset: + ```python # presets.py options_presets = { @@ -114,6 +114,7 @@ options_presets = { } } + # __init__.py class RLWeb(WebWorld): options_presets = options_presets @@ -122,47 +123,55 @@ class RLWeb(WebWorld): ### MultiWorld Object -The `MultiWorld` object references the whole multiworld (all items and locations -for all players) and is accessible through `self.multiworld` inside a `World` object. +The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible +through `self.multiworld` from your `World` object. ### Player -The player is just an integer in AP and is accessible through `self.player` -inside a `World` object. +The player is just an `int` in AP and is accessible through `self.player` from your `World` object. ### Player Options -Players provide customized settings for their World in the form of yamls. -A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`. -(It must be a subclass of `PerGameCommonOptions`.) -Option results are automatically added to the `World` object for easy access. -Those are accessible through `self.options.`, and you can get a dictionary of the option values via -`self.options.as_dict()`, passing the desired options as strings. +Options are provided by the user as part of the generation process, intended to affect how their randomizer experience +should play out. These can control aspects such as what locations should be shuffled, what items are in the itempool, +etc. Players provide the customized options for their World in the form of yamls. + +By convention, options are defined in `options.py` and will be used when parsing the players' yaml files. Each option +has its own class, which inherits from a base option type, a docstring to describe it, and a `display_name` property +shown on the website and in spoiler logs. + +The available options are defined by creating a `dataclass`, which must be a subclass of `PerGameCommonOptions`. It has +defined fields for the option names used in the player yamls and used for options access, with their types matching the +appropriate Option class. By convention, the strings that define your option names should be in `snake_case`. The +`dataclass` is then assigned to your `World` by defining its `options_dataclass`. Option results are then automatically +added to the `World` object for easy access, between `World` creation and `generate_early`. These are accessible through +`self.options.`, and you can get a dictionary with option values +via `self.options.as_dict()`, +passing the desired option names as strings. + +Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, and `Range`. +For more information, see the [options api doc](options%20api.md). ### World Settings -Any AP installation can provide settings for a world, for example a ROM file, accessible through -`self.settings.` or `cls.settings.` (new API) -or `Utils.get_options()["_options"][""]` (deprecated). +Settings are set by the user outside the generation process. They can be used for those settings that may affect +generation or client behavior, but should remain static between generations, such as the path to a ROM file. +These settings are accessible through `self.settings.` or `cls.settings.`. -Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. +Users can set these in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. -Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) -for details. +Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) for details. ### Locations -Locations are places where items can be located in your game. This may be chests -or boss drops for RPG-like games but could also be progress in a research tree. +Locations are places where items can be located in your game. This may be chests or boss drops for RPG-like games, but +could also be progress in a research tree, or even something more abstract like a level up. -Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed -in a Region, has access rules and a classification. -The name needs to be unique in each game and must not be numeric (has to -contain least 1 letter or symbol). The ID needs to be unique across all games -and is best in the same range as the item IDs. -World-specific IDs are 1 to 253-1, IDs ≤ 0 are global and reserved. +Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, +and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 +letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs. -Special locations with ID `None` can hold events. +World-specific IDs must be in the range 1 to 253-1; IDs ≤ 0 are global and reserved. Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`. The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being @@ -170,22 +179,21 @@ required, and will prevent progression and useful items from being placed at exc #### Documenting Locations -Worlds can optionally provide a `location_descriptions` map which contains -human-friendly descriptions of locations or location groups. These descriptions -will show up in location-selection options in the Weighted Options page. Extra +Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and +location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra indentation and single newlines will be collapsed into spaces. ```python -# Locations.py +# locations.py location_descriptions = { "Red Potion #6": "In a secret destructible block under the second stairway", - "L2 Spaceship": """ - The group of all items in the spaceship in Level 2. + "L2 Spaceship": + """ + The group of all items in the spaceship in Level 2. - This doesn't include the item on the spaceship door, since it can be - accessed without the Spaeship Key. - """ + This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. + """ } ``` @@ -193,7 +201,7 @@ location_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Locations import location_descriptions +from .locations import location_descriptions class MyGameWorld(World): @@ -202,47 +210,45 @@ class MyGameWorld(World): ### Items -Items are all things that can "drop" for your game. This may be RPG items like -weapons, could as well be technologies you normally research in a research tree. +Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally +research in a research tree. -Each item has a `name`, an `id` (can be known as "code"), and a classification. -The most important classification is `progression` (formerly advancement). -Progression items are items which a player may require to progress in -their world. Progression items will be assigned to locations with higher -priority and moved around to meet defined rules and accomplish progression -balancing. +Each item has a `name`, a `code` (hereafter referred to as `id`), and a classification. +The most important classification is `progression`. Progression items are items which a player *may* require to progress +in their world. If an item can possibly be considered for logic (it's referenced in a location's rules) it *must* be +progression. Progression items will be assigned to locations with higher priority, and moved around to meet defined rules +and satisfy progression balancing. -The name needs to be unique in each game, meaning a duplicate item has the -same ID. Name must not be numeric (has to contain at least 1 letter or symbol). +The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they +will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). -Special items with ID `None` can mark events (read below). +Other classifications include: -Other classifications include * `filler`: a regular item or trash item -* `useful`: generally quite useful, but not required for anything logical +* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations * `trap`: negative impact on the player * `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be combined with `progression`; see below) * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that - will not be moved around by progression balancing; used, e.g., for currency or tokens + will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres #### Documenting Items -Worlds can optionally provide an `item_descriptions` map which contains -human-friendly descriptions of items or item groups. These descriptions will -show up in item-selection options in the Weighted Options page. Extra -indentation and single newlines will be collapsed into spaces. +Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item +groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and +single newlines will be collapsed into spaces. ```python -# Items.py +# items.py item_descriptions = { "Red Potion": "A standard health potion", - "Spaceship Key": """ - The key to the spaceship in Level 2. + "Spaceship Key": + """ + The key to the spaceship in Level 2. - This is necessary to get to the Star Realm. - """ + This is necessary to get to the Star Realm. + """ } ``` @@ -250,7 +256,7 @@ item_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Items import item_descriptions +from .items import item_descriptions class MyGameWorld(World): @@ -259,215 +265,128 @@ class MyGameWorld(World): ### Events -Events will mark some progress. You define an event location, an -event item, strap some rules to the location (i.e. hold certain -items) and manually place the event item at the event location. +An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to +track certain logic interactions, with the Event Item being required for access in other locations or regions, but not +being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is +never made aware of them and these locations can never be checked, nor can the items be received during play. +They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that +is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last +relevant Item. Events function just like any other Location, and can still have their own access rules, etc. +By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement. +They must not exist in the `name_to_id` lookups, as they have no ID. + +The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created: -Events can be used to either simplify the logic or to get better spoiler logs. -Events will show up in the spoiler playthrough but they do not represent actual -items or locations within the game. +```python +from worlds.AutoWorld import World +from BaseClasses import ItemClassification +from .subclasses import MyGameLocation, MyGameItem -There is one special case for events: Victory. To get the win condition to show -up in the spoiler log, you create an event item and place it at an event -location with the `access_rules` for game completion. Once that's done, the -world's win condition can be as simple as checking for that item. -By convention the victory event is called `"Victory"`. It can be placed at one -or more event locations based on player options. +class MyGameWorld(World): + victory_loc = MyGameLocation(self.player, "Victory", None) + victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player)) +``` ### Regions -Regions are logical groups of locations that share some common access rules. If -location logic is written from scratch, using regions greatly simplifies the -definition and allows to somewhat easily implement things like entrance -randomizer in logic. +Regions are logical containers that typically hold locations that share some common access rules. If location logic is +written from scratch, using regions greatly simplifies the requirements and can help with implementing things +like entrance randomization in logic. -Regions have a list called `exits`, which are `Entrance` objects representing -transitions to other regions. +Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions. -There has to be one special region "Menu" from which the logic unfolds. AP -assumes that a player will always be able to return to the "Menu" region by -resetting the game ("Save and quit"). +There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to +return to the "Menu" region by resetting the game ("Save and quit"). ### Entrances -An `Entrance` connects to a region, is assigned to region's exits and has rules -to define if it and thus the connected region is accessible. -They can be static (regular logic) or be defined/connected during generation -(entrance randomizer). +An `Entrance` has a `parent_region` and `connected_region`, where it is in the `exits` of its parent, and the +`entrances` of its connected region. The `Entrance` then has rules assigned to it to determine if it can be passed +through, making the connected region accessible. They can be static (regular logic) or be defined/connected during +generation (entrance randomization). ### Access Rules -An access rule is a function that returns `True` or `False` for a `Location` or -`Entrance` based on the current `state` (items that can be collected). +An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state` +(items that have been collected). ### Item Rules -An item rule is a function that returns `True` or `False` for a `Location` based -on a single item. It can be used to reject placement of an item there. - +An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to +reject the placement of an item there. ## Implementation ### Your World -All code for your world implementation should be placed in a python package in -the `/worlds` directory. The starting point for the package is `__init__.py`. -Conventionally, your world class is placed in that file. +All code for your world implementation should be placed in a python package in the `/worlds` directory. The starting +point for the package is `__init__.py`. Conventionally, your `World` class is placed in that file. -World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, -which can be imported as `from worlds.AutoWorld import World` from your package. +World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, which can be imported as +`from worlds.AutoWorld import World` from your package. AP will pick up your world automatically due to the `AutoWorld` implementation. ### Requirements -If your world needs specific python packages, they can be listed in -`worlds//requirements.txt`. ModuleUpdate.py will automatically -pick up and install them. +If your world needs specific python packages, they can be listed in `worlds//requirements.txt`. +ModuleUpdate.py will automatically pick up and install them. See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format). ### Relative Imports -AP will only import the `__init__.py`. Depending on code size it makes sense to -use multiple files and use relative imports to access them. +AP will only import the `__init__.py`. Depending on code size, it may make sense to use multiple files and use relative +imports to access them. -e.g. `from .options import MyGameOptions` from your `__init__.py` will load -`world/[world_name]/options.py` and make its `MyGameOptions` accessible. +e.g. `from .options import MyGameOptions` from your `__init__.py` will load `world/[world_name]/options.py` and make +its `MyGameOptions` accessible. -When imported names pile up it may be easier to use `from . import options` -and access the variable as `options.MyGameOptions`. +When imported names pile up, it may be easier to use `from . import options` and access the variable as +`options.MyGameOptions`. -Imports from directories outside your world should use absolute imports. -Correct use of relative / absolute imports is required for zipped worlds to -function, see [apworld specification.md](apworld%20specification.md). +Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is +required for zipped worlds to function, see [apworld specification.md](apworld%20specification.md). ### Your Item Type -Each world uses its own subclass of `BaseClasses.Item`. The constructor can be -overridden to attach additional data to it, e.g. "price in shop". -Since the constructor is only ever called from your code, you can add whatever -arguments you like to the constructor. +Each world uses its own subclass of `BaseClasses.Item`. The constructor can be overridden to attach additional data to +it, e.g. "price in shop". Since the constructor is only ever called from your code, you can add whatever arguments you +like to the constructor. + +In its simplest form, we only set the game name and use the default constructor: -In its simplest form we only set the game name and use the default constructor ```python from BaseClasses import Item + class MyGameItem(Item): game: str = "My Game" ``` -By convention this class definition will either be placed in your `__init__.py` -or your `items.py`. For a more elaborate example see `worlds/oot/Items.py`. -### Your location type +By convention, this class definition will either be placed in your `__init__.py` or your `items.py`. For a more +elaborate example see +[`worlds/oot/Items.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py). + +### Your Location Type + +The same thing we did for items above, we will now do for locations: -The same we have done for items above, we will do for locations ```python from BaseClasses import Location + class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = "", code = None, parent = None) -> None: + def __init__(self, player: int, name="", code=None, parent=None) -> None: super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` -in your `__init__.py` or your `locations.py`. - -### Options - -By convention options are defined in `options.py` and will be used when parsing -the players' yaml files. - -Each option has its own class, inherits from a base option type, has a docstring -to describe it and a `display_name` property for display on the website and in -spoiler logs. - -The actual name as used in the yaml is defined via the field names of a `dataclass` that is -assigned to the world under `self.options_dataclass`. By convention, the strings -that define your option names should be in `snake_case`. - -Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. -For more see `Options.py` in AP's base directory. - -#### Toggle, DefaultOnToggle - -These don't need any additional properties defined. After parsing the option, -its `value` will either be True or False. - -#### Range - -Define properties `range_start`, `range_end` and `default`. Ranges will be -displayed as sliders on the website and can be set to random in the yaml. - -#### Choice - -Choices are like toggles, but have more options than just True and False. -Define a property `option_ = ` per selectable value and -`default = ` to set the default selection. Aliases can be set by -defining a property `alias_ = `. - -```python -option_off = 0 -option_on = 1 -option_some = 2 -alias_disabled = 0 -alias_enabled = 1 -default = 0 -``` - -#### Sample -```python -# options.py - -from dataclasses import dataclass -from Options import Toggle, Range, Choice, PerGameCommonOptions - -class Difficulty(Choice): - """Sets overall game difficulty.""" - display_name = "Difficulty" - option_easy = 0 - option_normal = 1 - option_hard = 2 - alias_beginner = 0 # same as easy - alias_expert = 2 # same as hard - default = 1 # default to normal - -class FinalBossHP(Range): - """Sets the HP of the final boss""" - display_name = "Final Boss HP" - range_start = 100 - range_end = 10000 - default = 2000 - -class FixXYZGlitch(Toggle): - """Fixes ABC when you do XYZ""" - display_name = "Fix XYZ Glitch" - -# By convention, we call the options dataclass `Options`. -# It has to be derived from 'PerGameCommonOptions'. -@dataclass -class MyGameOptions(PerGameCommonOptions): - difficulty: Difficulty - final_boss_hp: FinalBossHP - fix_xyz_glitch: FixXYZGlitch -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .options import MyGameOptions # import the options dataclass - -class MyGameWorld(World): - # ... - options_dataclass = MyGameOptions # assign the options dataclass to the world - options: MyGameOptions # typing for option results - # ... -``` +in your `__init__.py` or your `locations.py`. ### A World Class Skeleton @@ -483,7 +402,6 @@ from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification - class MyGameItem(Item): # or from Items import MyGameItem game = "My Game" # name of the game/world this item is from @@ -492,7 +410,6 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation game = "My Game" # name of the game/world this location is in - class MyGameSettings(settings.Group): class RomFile(settings.SNESRomPath): """Insert help text for host.yaml here.""" @@ -511,7 +428,7 @@ class MyGameWorld(World): # ID of first item and location, could be hard-coded but code may be easier # to read with this as a property. base_id = 1234 - # Instead of dynamic numbering, IDs could be part of data. + # instead of dynamic numbering, IDs could be part of data # The following two dicts are required for the generation to know which # items exist. They could be generated from json or something else. They can @@ -530,74 +447,106 @@ class MyGameWorld(World): ### Generation -The world has to provide the following things for generation +The world has to provide the following things for generation: -* the properties mentioned above +* the properties mentioned above * additions to the item pool * additions to the regions list: at least one called "Menu" * locations placed inside those regions * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand -* applying `self.multiworld.push_precollected` for world defined start inventory -* `required_client_version: Tuple[int, int, int]` - Optional client version as tuple of 3 ints to make sure the client is compatible to - this world (e.g. implements all required features) when connecting. +* applying `self.multiworld.push_precollected` for world-defined start inventory -In addition, the following methods can be implemented and are called in this order during generation +In addition, the following methods can be implemented and are called in this order during generation: -* `stage_assert_generate(cls, multiworld)` is a class method called at the start of - generation to check the existence of prerequisite files, usually a ROM for +* `stage_assert_generate(cls, multiworld: MultiWorld)` + a class method called at the start of generation to check for the existence of prerequisite files, usually a ROM for games which require one. * `generate_early(self)` - called per player before any items or locations are created. You can set properties on your world here. Already has - access to player options and RNG. This is the earliest step where the world should start setting up for the current - multiworld as any steps before this, the multiworld itself is still getting set up + called per player before any items or locations are created. You can set properties on your + world here. Already has access to player options and RNG. This is the earliest step where the world should start + setting up for the current multiworld, as the multiworld itself is still setting up before this point. * `create_regions(self)` - called to place player's regions and their locations into the MultiWorld's regions list. If it's - hard to separate, this can be done during `generate_early` or `create_items` as well. + called to place player's regions and their locations into the MultiWorld's regions list. + If it's hard to separate, this can be done during `generate_early` or `create_items` as well. * `create_items(self)` - called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in - the MultiWorld's regions and itempool, and these lists should not be modified afterwards. + called to place player's items into the MultiWorld's itempool. After this step all regions + and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward. * `set_rules(self)` - called to set access and item rules on locations and entrances. - Locations have to be defined before this, or rule application can miss them. + called to set access and item rules on locations and entrances. * `generate_basic(self)` - called after the previous steps. Some placement and player specific - randomizations can be done here. -* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` are called to modify item placement - before, during and after the regular fill process, before `generate_output`. - If items need to be placed during pre_fill, these items can be determined - and created using `get_prefill_items` -* `generate_output(self, output_directory: str)` that creates the output - files if there is output to be generated. When this is - called, `self.multiworld.get_locations(self.player)` has all locations for the player, with - attribute `item` pointing to the item. - `location.item.player` can be used to see if it's a local item. + player-specific randomization that does not affect logic can be done here. +* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` + called to modify item placement before, during, and after the regular fill process; all finishing before + `generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there + are any items that need to be filled this way, but need to be in state while you fill other items, they can be + returned from `get_prefill_items`. +* `generate_output(self, output_directory: str)` + creates the output files if there is output to be generated. When this is called, + `self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the + item. `location.item.player` can be used to see if it's a local item. * `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that will be used by the server to host the MultiWorld. +All instance methods can, optionally, have a class method defined which will be called after all instance methods are +finished running, by defining a method with `stage_` in front of the method name. These class methods will have the +args `(cls, multiworld: MultiWorld)`, followed by any other args that the relevant instance method has. #### generate_early ```python def generate_early(self) -> None: - # read player settings to world instance + # read player options to world instance self.final_boss_hp = self.options.final_boss_hp.value ``` +#### create_regions + +```python +def create_regions(self) -> None: + # Add regions to the multiworld. "Menu" is the required starting point. + # Arguments to Region() are name, player, multiworld, and optionally hint_text + menu_region = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu_region) # or use += [menu_region...] + + main_region = Region("Main Area", self.player, self.multiworld) + # add main area's locations to main area (all but final boss) + main_region.add_locations(main_region_locations, MyGameLocation) + # or + # main_region.locations = \ + # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] + self.multiworld.regions.append(main_region) + + boss_region = Region("Boss Room", self.player, self.multiworld) + # add event to Boss Room + boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) + + # if entrances are not randomized, they should be connected here, otherwise they can also be connected at a later stage + # create Entrances and connect the Regions + menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule + # or + main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) + # connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse + + # if setting location access rules from data is easier here, set_rules can possibly be omitted +``` + #### create_item ```python -# we need a way to know if an item provides progress in the game ("key item") -# this can be part of the items definition, or depend on recipe randomization +# we need a way to know if an item provides progress in the game ("key item") this can be part of the items definition, +# or depend on recipe randomization from .items import is_progression # this is just a dummy + def create_item(self, item: str) -> MyGameItem: - # This is called when AP wants to create an item by name (for plando) or - # when you call it from your own code. - classification = ItemClassification.progression if is_progression(item) else \ - ItemClassification.filler - return MyGameItem(item, classification, self.item_name_to_id[item], - self.player) + # this is called when AP wants to create an item by name (for plando) or when you call it from your own code + classification = ItemClassification.progression if is_progression(item) else + ItemClassification.filler + + +return MyGameItem(item, classification, self.item_name_to_id[item], + self.player) + def create_event(self, event: str) -> MyGameItem: # while we are at it, we can also add a helper to create events @@ -610,8 +559,7 @@ def create_event(self, event: str) -> MyGameItem: def create_items(self) -> None: # Add items to the Multiworld. # If there are two of the same item, the item has to be twice in the pool. - # Which items are added to the pool may depend on player settings, - # e.g. custom win condition like triforce hunt. + # Which items are added to the pool may depend on player options, e.g. custom win condition like triforce hunt. # Having an item in the start inventory won't remove it from the pool. # If an item can't have duplicates it has to be excluded manually. @@ -627,67 +575,10 @@ def create_items(self) -> None: # itempool and number of locations should match up. # If this is not the case we want to fill the itempool with junk. - junk = 0 # calculate this based on player settings + junk = 0 # calculate this based on player options self.multiworld.itempool += [self.create_item("nothing") for _ in range(junk)] ``` -#### create_regions - -```python -def create_regions(self) -> None: - # Add regions to the multiworld. "Menu" is the required starting point. - # Arguments to Region() are name, player, world, and optionally hint_text - menu_region = Region("Menu", self.player, self.multiworld) - self.multiworld.regions.append(menu_region) # or use += [menu_region...] - - main_region = Region("Main Area", self.player, self.multiworld) - # Add main area's locations to main area (all but final boss) - main_region.add_locations(main_region_locations, MyGameLocation) - # or - # main_region.locations = \ - # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] - self.multiworld.regions.append(main_region) - - boss_region = Region("Boss Room", self.player, self.multiworld) - # Add event to Boss Room - boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) - - # If entrances are not randomized, they should be connected here, - # otherwise they can also be connected at a later stage. - # Create Entrances and connect the Regions - menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule - # or - main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) - # Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse - - # If setting location access rules from data is easier here, set_rules can - # possibly omitted. -``` - -#### generate_basic - -```python -def generate_basic(self) -> None: - # place "Victory" at "Final Boss" and set collection as win condition - self.multiworld.get_location("Final Boss", self.player) - .place_locked_item(self.create_event("Victory")) - self.multiworld.completion_condition[self.player] = - lambda state: state.has("Victory", self.player) - - # place item Herb into location Chest1 for some reason - item = self.create_item("Herb") - self.multiworld.get_location("Chest1", self.player).place_locked_item(item) - # in most cases it's better to do this at the same time the itempool is - # filled to avoid accidental duplicates: - # manually placed and still in the itempool - - # for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to - # write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations - # are connected and placed as desired - # from Utils import visualize_regions - # visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") -``` - ### Setting Rules ```python @@ -703,6 +594,7 @@ def set_rules(self) -> None: # set a simple rule for an region set_rule(self.multiworld.get_entrance("Boss Door", self.player), lambda state: state.has("Boss Key", self.player)) + # location.access_rule = ... is likely to be a bit faster # combine rules to require two items add_rule(self.multiworld.get_location("Chest2", self.player), lambda state: state.has("Sword", self.player)) @@ -730,59 +622,80 @@ def set_rules(self) -> None: # get_item_type needs to take player/world into account # if MyGameItem has a type property, a more direct implementation would be add_item_rule(self.multiworld.get_location("Chest5", self.player), - lambda item: item.player != self.player or\ + lambda item: item.player != self.player or item.my_type == "weapon") # location.item_rule = ... is likely to be a bit faster -``` -### Logic Mixin + # place "Victory" at "Final Boss" and set collection as win condition + self.multiworld.get_location("Final Boss", self.player).place_locked_item(self.create_event("Victory")) -While lambdas and events could do pretty much anything, by convention we -implement more complex logic in logic mixins, even if there is no need to add -properties to the `BaseClasses.CollectionState` state object. - -When importing a file that defines a class that inherits from -`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by -the mixin's members. These members should be prefixed with underscore following -the name of the implementing world. This is due to sharing a namespace with all -other logic mixins. - -Typical uses are defining methods that are used instead of `state.has` -in lambdas, e.g.`state.mygame_has(custom, player)` or recurring checks -like `state.mygame_can_do_something(player)` to simplify lambdas. -Private members, only accessible from mixins, should start with `_mygame_`, -public members with `mygame_`. - -More advanced uses could be to add additional variables to the state object, -override `World.collect(self, state, item)` and `remove(self, state, item)` -to update the state object, and check those added variables in added methods. -Please do this with caution and only when necessary. + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + +# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to +# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations +# are connected and placed as desired +# from Utils import visualize_regions +# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") +``` -#### Sample +### Custom Logic Rules + +Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or +Entrance should be +a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9). +Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other +functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly. +For an example, see [The Messenger](/worlds/messenger/rules.py). ```python # logic.py -from worlds.AutoWorld import LogicMixin +from BaseClasses import CollectionState + -class MyGameLogic(LogicMixin): - def mygame_has_key(self, player: int) -> bool: - # Arguments above are free to choose - # MultiWorld can be accessed through self.multiworld, explicitly passing in - # MyGameWorld instance for easy options access is also a valid approach - return self.has("key", player) # or whatever +def mygame_has_key(self, state: CollectionState, player: int) -> bool: + # More arguments above are free to choose, since you can expect this is only called in your world + # MultiWorld can be accessed through state.multiworld. + # Explicitly passing in MyGameWorld instance for easy options access is also a valid approach, but it's generally + # better to check options before rule assignment since the individual functions can be called thousands of times + return state.has("key", player) # or whatever ``` + ```python # __init__.py from worlds.generic.Rules import set_rule -import .logic # apply the mixin by importing its file +from . import logic + class MyGameWorld(World): # ... def set_rules(self) -> None: set_rule(self.multiworld.get_location("A Door", self.player), - lambda state: state.mygame_has_key(self.player)) + lambda state: logic.mygame_has_key(state, self.player)) +``` + +### Logic Mixin + +While lambdas and events can do pretty much anything, more complex logic can be handled in logic mixins. + +When importing a file that defines a class that inherits from `worlds.AutoWorld.LogicMixin`, the `CollectionState` class +is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing +world since the namespace is shared with all other logic mixins. + +Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified +with the state. +Please do this with caution and only when necessary. + +#### pre_fill + +```python +def pre_fill(self) -> None: + # place item Herb into location Chest1 for some reason + item = self.create_item("Herb") + self.multiworld.get_location("Chest1", self.player).place_locked_item(item) + # in most cases it's better to do this at the same time the itempool is + # filled to avoid accidental duplicates, such as manually placed and still in the itempool ``` ### Generate Output @@ -792,9 +705,9 @@ from .mod import generate_mod def generate_output(self, output_directory: str) -> None: - # How to generate the mod or ROM highly depends on the game - # if the mod is written in Lua, Jinja can be used to fill a template - # if the mod reads a json file, `json.dump()` can be used to generate that + # How to generate the mod or ROM highly depends on the game. + # If the mod is written in Lua, Jinja can be used to fill a template. + # If the mod reads a json file, `json.dump()` can be used to generate that. # code below is a dummy data = { "seed": self.multiworld.seed_name, # to verify the server's multiworld @@ -804,8 +717,7 @@ def generate_output(self, output_directory: str) -> None: for location in self.multiworld.get_filled_locations(self.player)}, # store start_inventory from player's .yaml # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod - "starter_items": [item.name for item - in self.multiworld.precollected_items[self.player]], + "starter_items": [item.name for item in self.multiworld.precollected_items[self.player]], } # add needed option results to the dictionary @@ -824,20 +736,20 @@ def generate_output(self, output_directory: str) -> None: ### Slot Data If the game client needs to know information about the generated seed, a preferred method of transferring the data -is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`, -but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once -it has successfully [connected](network%20protocol.md#connected). -If you need to know information about locations in your world, instead -of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that -data already exists on the server. The most common usage of slot data is to send option results that the client needs -to be aware of. +is through the slot data. This is filled with the `fill_slot_data` method of your world by returning +a `Dict[str, Any]`, but, to not waste resources, should be limited to data that is absolutely necessary. Slot data is +sent to your client once it has successfully [connected](network%20protocol.md#connected). +If you need to know information about locations in your world, instead of propagating the slot data, it is preferable +to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most +common usage of slot data is sending option results that the client needs to be aware of. ```python def fill_slot_data(self) -> Dict[str, Any]: - # in order for our game client to handle the generated seed correctly we need to know what the user selected - # for their difficulty and final boss HP - # a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting - # the options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the option's value + # In order for our game client to handle the generated seed correctly we need to know what the user selected + # for their difficulty and final boss HP. + # A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting. + # The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant + # option's value. return self.options.as_dict("difficulty", "final_boss_hp") ``` @@ -847,15 +759,17 @@ Each world implementation should have a tutorial and a game info page. These are the `.md` files in your world's `/docs` directory. #### Game Info + The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional information that may be useful to the player when learning your randomizer should also go here. The file name format is `_.md`. While you can write these docs for multiple languages, currently only the english version is displayed on the website. #### Tutorials + Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial` -defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what -the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, +defined in the `WebWorld`. The file name you use isn't particularly important, but it should be descriptive of what +the tutorial covers, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where `game/document/lang` is the provided URL. @@ -874,12 +788,13 @@ from test.bases import WorldTestBase class MyGameTestBase(WorldTestBase): - game = "My Game" + game = "My Game" ``` -Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. +Next, using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. Example `test_chest_access.py` + ```python from . import MyGameTestBase @@ -889,15 +804,15 @@ class TestChestAccess(MyGameTestBase): """Test locations that require a sword""" locations = ["Chest1", "Chest2"] items = [["Sword"]] - # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained. + # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained self.assertAccessDependency(locations, items) def test_any_weapon_chests(self) -> None: """Test locations that require any weapon""" locations = [f"Chest{i}" for i in range(3, 6)] items = [["Sword"], ["Axe"], ["Spear"]] - # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them. + # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them self.assertAccessDependency(locations, items) ``` -For more information on tests check the [tests doc](tests.md). +For more information on tests, check the [tests doc](tests.md). diff --git a/inno_setup.iss b/inno_setup.iss index be5de320a1c6..b122cdc00b18 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -197,7 +197,7 @@ begin begin // Is the installed version at least the packaged one ? Log('VC Redist x64 Version : found ' + strVersion); - Result := (CompareStr(strVersion, 'v14.32.31332') < 0); + Result := (CompareStr(strVersion, 'v14.38.33130') < 0); end else begin diff --git a/playerSettings.yaml b/playerSettings.yaml index f9585da246b8..b6b474a9fffa 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc game: # Pick a game to play A Link to the Past: 1 requires: - version: 0.4.3 # Version of Archipelago required for this yaml to work as expected. + version: 0.4.4 # Version of Archipelago required for this yaml to work as expected. A Link to the Past: progression_balancing: # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. diff --git a/requirements.txt b/requirements.txt index 263f242dd8a8..5c8a64b215da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ -colorama>=0.4.5 +colorama>=0.4.6 websockets>=12.0 PyYAML>=6.0.1 jellyfish>=1.0.3 -jinja2>=3.1.2 +jinja2>=3.1.3 schema>=0.7.5 -kivy>=2.2.1 +kivy>=2.3.0 bsdiff4>=1.2.4 -platformdirs>=4.0.0 +platformdirs>=4.1.0 certifi>=2023.11.17 -cython>=3.0.6 +cython>=3.0.8 cymem>=2.0.8 orjson>=3.9.10 -discord-webhook>=1.3.0 \ No newline at end of file +discord-webhook>=1.3.0 diff --git a/settings.py b/settings.py index acae86095cda..c58eadf155d7 100644 --- a/settings.py +++ b/settings.py @@ -597,8 +597,8 @@ class LogNetwork(IntEnum): disable_item_cheat: Union[DisableItemCheat, bool] = False location_check_points: LocationCheckPoints = LocationCheckPoints(1) hint_cost: HintCost = HintCost(10) - release_mode: ReleaseMode = ReleaseMode("goal") - collect_mode: CollectMode = CollectMode("goal") + release_mode: ReleaseMode = ReleaseMode("auto") + collect_mode: CollectMode = CollectMode("auto") remaining_mode: RemainingMode = RemainingMode("goal") auto_shutdown: AutoShutdown = AutoShutdown(0) compatibility: Compatibility = Compatibility(2) @@ -673,7 +673,7 @@ class Race(IntEnum): spoiler: Spoiler = Spoiler(3) glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here? race: Race = Race(0) - plando_options: PlandoOptions = PlandoOptions("bosses") + plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts") class SNIOptions(Group): diff --git a/setup.py b/setup.py index c864a8cc9d39..272e6de0be27 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,6 @@ # TODO: move stuff to not require this import ModuleUpdate ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) - ModuleUpdate.update_ran = False # restore for later from worlds.LauncherComponents import components, icon_paths from Utils import version_tuple, is_windows, is_linux @@ -76,7 +75,6 @@ "Ocarina of Time", "Overcooked! 2", "Raft", - "Secret of Evermore", "Slay the Spire", "Sudoku", "Super Mario 64", @@ -305,7 +303,6 @@ def run(self): print(f"Outputting to: {self.buildfolder}") os.makedirs(self.buildfolder, exist_ok=True) import ModuleUpdate - ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) ModuleUpdate.update(yes=self.yes) # auto-build cython modules @@ -352,6 +349,18 @@ def run(self): for folder in sdl2.dep_bins + glew.dep_bins: shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) print(f"copying {folder} -> {self.libfolder}") + # windows needs Visual Studio C++ Redistributable + # Installer works for x64 and arm64 + print("Downloading VC Redist") + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe", + context=context) as download: + vc_redist = download.read() + print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", ) + with open("VC_redist.x64.exe", "wb") as vc_file: + vc_file.write(vc_redist) for data in self.extra_data: self.installfile(Path(data)) @@ -387,8 +396,6 @@ def run(self): folders_to_remove.append(file_name) shutil.rmtree(world_directory) shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") - # TODO: fix LttP options one day - shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") try: from maseya import z3pr except ImportError: diff --git a/test/bases.py b/test/bases.py index d6a43c598ffb..2d4111d19356 100644 --- a/test/bases.py +++ b/test/bases.py @@ -7,7 +7,7 @@ from Generate import get_seed_name from test.general import gen_steps from worlds import AutoWorld -from worlds.AutoWorld import call_all +from worlds.AutoWorld import World, call_all from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item from worlds.alttp.Items import ItemFactory @@ -105,9 +105,15 @@ def _get_items_partial(self, item_pool, missing_item): class WorldTestBase(unittest.TestCase): options: typing.Dict[str, typing.Any] = {} + """Define options that should be used when setting up this TestBase.""" multiworld: MultiWorld + """The constructed MultiWorld instance after setup.""" + world: World + """The constructed World instance after setup.""" + player: typing.ClassVar[int] = 1 - game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" + game: typing.ClassVar[str] + """Define game name in subclass, example "Secret of Evermore".""" auto_construct: typing.ClassVar[bool] = True """ automatically set up a world for each test in this class """ memory_leak_tested: typing.ClassVar[bool] = False @@ -150,8 +156,8 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") self.multiworld = MultiWorld(1) - self.multiworld.game[1] = self.game - self.multiworld.player_name = {1: "Tester"} + self.multiworld.game[self.player] = self.game + self.multiworld.player_name = {self.player: "Tester"} self.multiworld.set_seed(seed) self.multiworld.state = CollectionState(self.multiworld) random.seed(self.multiworld.seed) @@ -159,9 +165,10 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: args = Namespace() for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): setattr(args, name, { - 1: option.from_any(self.options.get(name, getattr(option, "default"))) + 1: option.from_any(self.options.get(name, option.default)) }) self.multiworld.set_options(args) + self.world = self.multiworld.worlds[self.player] for step in gen_steps: call_all(self.multiworld, step) @@ -220,19 +227,19 @@ def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: def can_reach_location(self, location: str) -> bool: """Determines if the current state can reach the provided location name""" - return self.multiworld.state.can_reach(location, "Location", 1) + return self.multiworld.state.can_reach(location, "Location", self.player) def can_reach_entrance(self, entrance: str) -> bool: """Determines if the current state can reach the provided entrance name""" - return self.multiworld.state.can_reach(entrance, "Entrance", 1) + return self.multiworld.state.can_reach(entrance, "Entrance", self.player) def can_reach_region(self, region: str) -> bool: """Determines if the current state can reach the provided region name""" - return self.multiworld.state.can_reach(region, "Region", 1) + return self.multiworld.state.can_reach(region, "Region", self.player) def count(self, item_name: str) -> int: """Returns the amount of an item currently in state""" - return self.multiworld.state.count(item_name, 1) + return self.multiworld.state.count(item_name, self.player) def assertAccessDependency(self, locations: typing.List[str], @@ -246,10 +253,11 @@ def assertAccessDependency(self, self.collect_all_but(all_items, state) if only_check_listed: for location in locations: - self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}") + self.assertFalse(state.can_reach(location, "Location", self.player), + f"{location} is reachable without {all_items}") else: for location in self.multiworld.get_locations(): - loc_reachable = state.can_reach(location, "Location", 1) + loc_reachable = state.can_reach(location, "Location", self.player) self.assertEqual(loc_reachable, location.name not in locations, f"{location.name} is reachable without {all_items}" if loc_reachable else f"{location.name} is not reachable without {all_items}") @@ -258,7 +266,7 @@ def assertAccessDependency(self, for item in items: state.collect(item) for location in locations: - self.assertTrue(state.can_reach(location, "Location", 1), + self.assertTrue(state.can_reach(location, "Location", self.player), f"{location} not reachable with {item_names}") for item in items: state.remove(item) @@ -285,7 +293,7 @@ def test_all_state_can_reach_everything(self): if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game): - excluded = self.multiworld.exclude_locations[1].value + excluded = self.multiworld.worlds[self.player].options.exclude_locations.value state = self.multiworld.get_all_state(False) for location in self.multiworld.get_locations(): if location.name not in excluded: @@ -302,7 +310,7 @@ def test_empty_state_can_reach_something(self): return with self.subTest("Game", game=self.game): state = CollectionState(self.multiworld) - locations = self.multiworld.get_reachable_locations(state, 1) + locations = self.multiworld.get_reachable_locations(state, self.player) self.assertGreater(len(locations), 0, "Need to be able to reach at least one location to get started.") @@ -328,7 +336,7 @@ def fulfills_accessibility() -> bool: for location in sphere: if location.item: state.collect(location.item, True, location) - return self.multiworld.has_beaten_game(state, 1) + return self.multiworld.has_beaten_game(state, self.player) with self.subTest("Game", game=self.game, seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) diff --git a/test/benchmark/__init__.py b/test/benchmark/__init__.py new file mode 100644 index 000000000000..6c80c60b89d7 --- /dev/null +++ b/test/benchmark/__init__.py @@ -0,0 +1,7 @@ +if __name__ == "__main__": + import path_change + path_change.change_home() + import load_worlds + load_worlds.run_load_worlds_benchmark() + import locations + locations.run_locations_benchmark() diff --git a/test/benchmark/load_worlds.py b/test/benchmark/load_worlds.py new file mode 100644 index 000000000000..3b001699f4cb --- /dev/null +++ b/test/benchmark/load_worlds.py @@ -0,0 +1,27 @@ +def run_load_worlds_benchmark(): + """List worlds and their load time. + Note that any first-time imports will be attributed to that world, as it is cached afterwards. + Likely best used with isolated worlds to measure their time alone.""" + import logging + + from Utils import init_logging + + # get some general imports cached, to prevent it from being attributed to one world. + import orjson + orjson.loads("{}") # orjson runs initialization on first use + + import BaseClasses, Launcher, Fill + + from worlds import world_sources + + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + for module in world_sources: + logger.info(f"{module} took {module.time_taken:.4f} seconds.") + + +if __name__ == "__main__": + from path_change import change_home + change_home() + run_load_worlds_benchmark() diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py new file mode 100644 index 000000000000..f2209eb689e1 --- /dev/null +++ b/test/benchmark/locations.py @@ -0,0 +1,101 @@ +def run_locations_benchmark(): + import argparse + import logging + import gc + import collections + import typing + import sys + + from time_it import TimeIt + + from Utils import init_logging + from BaseClasses import MultiWorld, CollectionState, Location + from worlds import AutoWorld + from worlds.AutoWorld import call_all + + 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 + + if sys.version_info >= (3, 9): + @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)) + else: + @staticmethod + def format_times_from_counter(counter: collections.Counter, 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): + for game in sorted(AutoWorld.AutoWorldRegister.world_types): + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + multiworld = MultiWorld(1) + multiworld.game[1] = game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed(0) + multiworld.state = CollectionState(multiworld) + args = argparse.Namespace() + for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(getattr(option, "default")) + }) + multiworld.set_options(args) + + gc.collect() + for step in self.gen_steps: + with TimeIt(f"{game} step {step}", logger): + call_all(multiworld, step) + gc.collect() + + locations = sorted(multiworld.get_unfilled_locations()) + if not locations: + continue + + all_state = multiworld.get_all_state(False) + for location in locations: + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + 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'])}") + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() + + +if __name__ == "__main__": + from path_change import change_home + change_home() + run_locations_benchmark() diff --git a/test/benchmark/path_change.py b/test/benchmark/path_change.py new file mode 100644 index 000000000000..2baa6273e11e --- /dev/null +++ b/test/benchmark/path_change.py @@ -0,0 +1,16 @@ +import sys +import os + + +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 diff --git a/test/benchmark/time_it.py b/test/benchmark/time_it.py new file mode 100644 index 000000000000..95c0314682f6 --- /dev/null +++ b/test/benchmark/time_it.py @@ -0,0 +1,23 @@ +import time + + +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}.") diff --git a/test/general/test_fill.py b/test/general/test_fill.py index e454b3e61d7a..489417771d2a 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -11,30 +11,30 @@ from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule -def generate_multi_world(players: int = 1) -> MultiWorld: - multi_world = MultiWorld(players) - multi_world.player_name = {} - multi_world.state = CollectionState(multi_world) +def generate_multiworld(players: int = 1) -> MultiWorld: + multiworld = MultiWorld(players) + multiworld.player_name = {} + multiworld.state = CollectionState(multiworld) for i in range(players): player_id = i+1 - world = World(multi_world, player_id) - multi_world.game[player_id] = f"Game {player_id}" - multi_world.worlds[player_id] = world - multi_world.player_name[player_id] = "Test Player " + str(player_id) - region = Region("Menu", player_id, multi_world, "Menu Region Hint") - multi_world.regions.append(region) + world = World(multiworld, player_id) + multiworld.game[player_id] = f"Game {player_id}" + multiworld.worlds[player_id] = world + multiworld.player_name[player_id] = "Test Player " + str(player_id) + region = Region("Menu", player_id, multiworld, "Menu Region Hint") + multiworld.regions.append(region) for option_key, option in Options.PerGameCommonOptions.type_hints.items(): - if hasattr(multi_world, option_key): - getattr(multi_world, option_key).setdefault(player_id, option.from_any(getattr(option, "default"))) + if hasattr(multiworld, option_key): + getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default"))) else: - setattr(multi_world, option_key, {player_id: option.from_any(getattr(option, "default"))}) + setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))}) # TODO - remove this loop once all worlds use options dataclasses - world.options = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id] + world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id] for option_key in world.options_dataclass.type_hints}) - multi_world.set_seed(0) + multiworld.set_seed(0) - return multi_world + return multiworld class PlayerDefinition(object): @@ -46,8 +46,8 @@ class PlayerDefinition(object): basic_items: List[Item] regions: List[Region] - def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []): - self.multiworld = world + def __init__(self, multiworld: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []): + self.multiworld = multiworld self.id = id self.menu = menu self.locations = locations @@ -72,7 +72,7 @@ def generate_region(self, parent: Region, size: int, access_rule: CollectionRule return region -def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]: +def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> List[Item]: items = items.copy() while len(items) > 0: location = region.locations.pop(0) @@ -80,7 +80,7 @@ def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[It if location.item: return items item = items.pop(0) - world.push_item(location, item, False) + multiworld.push_item(location, item, False) location.event = item.advancement return items @@ -94,15 +94,15 @@ def region_contains(region: Region, item: Item) -> bool: return False -def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition: - menu = multi_world.get_region("Menu", player_id) +def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition: + menu = multiworld.get_region("Menu", player_id) locations = generate_locations(location_count, player_id, None, menu) prog_items = generate_items(prog_item_count, player_id, True) - multi_world.itempool += prog_items + multiworld.itempool += prog_items basic_items = generate_items(basic_item_count, player_id, False) - multi_world.itempool += basic_items + multiworld.itempool += basic_items - return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items) + return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items) def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]: @@ -134,15 +134,15 @@ def names(objs: list) -> Iterable[str]: class TestFillRestrictive(unittest.TestCase): def test_basic_fill(self): """Tests `fill_restrictive` fills and removes the locations and items from their respective lists""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] item1 = player1.prog_items[1] loc0 = player1.locations[0] loc1 = player1.locations[1] - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items) self.assertEqual(loc0.item, item1) @@ -152,16 +152,16 @@ def test_basic_fill(self): def test_ordered_fill(self): """Tests `fill_restrictive` fulfills set rules""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items locations = player1.locations - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( items[0].name, player1.id) and state.has(items[1].name, player1.id) set_rule(locations[1], lambda state: state.has( items[0].name, player1.id)) - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) self.assertEqual(locations[0].item, items[0]) @@ -169,8 +169,8 @@ def test_ordered_fill(self): def test_partial_fill(self): """Tests that `fill_restrictive` returns unfilled locations""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 3, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 3, 2) item0 = player1.prog_items[0] item1 = player1.prog_items[1] @@ -178,14 +178,14 @@ def test_partial_fill(self): loc1 = player1.locations[1] loc2 = player1.locations[2] - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has( item0.name, player1.id)) # forces a swap set_rule(loc2, lambda state: state.has( item0.name, player1.id)) - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items) self.assertEqual(loc0.item, item0) @@ -195,19 +195,19 @@ def test_partial_fill(self): def test_minimal_fill(self): """Test that fill for minimal player can have unreachable items""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items locations = player1.locations - multi_world.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal) - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal) + multiworld.completion_condition[player1.id] = lambda state: state.has( items[1].name, player1.id) set_rule(locations[1], lambda state: state.has( items[0].name, player1.id)) - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) self.assertEqual(locations[0].item, items[1]) @@ -220,15 +220,15 @@ def test_minimal_mixed_fill(self): the non-minimal player get all items. """ - multi_world = generate_multi_world(2) - player1 = generate_player_data(multi_world, 1, 3, 3) - player2 = generate_player_data(multi_world, 2, 3, 3) + multiworld = generate_multiworld(2) + player1 = generate_player_data(multiworld, 1, 3, 3) + player2 = generate_player_data(multiworld, 2, 3, 3) - multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal - multi_world.accessibility[player2.id].value = multi_world.accessibility[player2.id].option_locations + multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal + multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations - multi_world.completion_condition[player1.id] = lambda state: True - multi_world.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id) + multiworld.completion_condition[player1.id] = lambda state: True + multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id) set_rule(player1.locations[1], lambda state: state.has(player1.prog_items[0].name, player1.id)) set_rule(player1.locations[2], lambda state: state.has(player1.prog_items[1].name, player1.id)) @@ -241,28 +241,28 @@ def test_minimal_mixed_fill(self): # fill remaining locations with remaining items location_pool = player1.locations[1:] + player2.locations item_pool = player1.prog_items[:-1] + player2.prog_items - fill_restrictive(multi_world, multi_world.state, location_pool, item_pool) - multi_world.state.sweep_for_events() # collect everything + fill_restrictive(multiworld, multiworld.state, location_pool, item_pool) + multiworld.state.sweep_for_events() # collect everything # all of player2's locations and items should be accessible (not all of player1's) for item in player2.prog_items: - self.assertTrue(multi_world.state.has(item.name, player2.id), + self.assertTrue(multiworld.state.has(item.name, player2.id), f'{item} is unreachable in {item.location}') def test_reversed_fill(self): """Test a different set of rules can be satisfied""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] item1 = player1.prog_items[1] loc0 = player1.locations[0] loc1 = player1.locations[1] - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has(item1.name, player1.id)) - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items) self.assertEqual(loc0.item, item1) @@ -270,13 +270,13 @@ def test_reversed_fill(self): def test_multi_step_fill(self): """Test that fill is able to satisfy multiple spheres""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 4, 4) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 4, 4) items = player1.prog_items locations = player1.locations - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( items[2].name, player1.id) and state.has(items[3].name, player1.id) set_rule(locations[1], lambda state: state.has( items[0].name, player1.id)) @@ -285,7 +285,7 @@ def test_multi_step_fill(self): set_rule(locations[3], lambda state: state.has( items[1].name, player1.id)) - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) self.assertEqual(locations[0].item, items[1]) @@ -295,25 +295,25 @@ def test_multi_step_fill(self): def test_impossible_fill(self): """Test that fill raises an error when it can't place any items""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) items = player1.prog_items locations = player1.locations - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( items[0].name, player1.id) and state.has(items[1].name, player1.id) set_rule(locations[1], lambda state: state.has( items[1].name, player1.id)) set_rule(locations[0], lambda state: state.has( items[0].name, player1.id)) - self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) def test_circular_fill(self): """Test that fill raises an error when it can't place all items""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 3, 3) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 3, 3) item0 = player1.prog_items[0] item1 = player1.prog_items[1] @@ -322,46 +322,46 @@ def test_circular_fill(self): loc1 = player1.locations[1] loc2 = player1.locations[2] - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id) set_rule(loc1, lambda state: state.has(item0.name, player1.id)) set_rule(loc2, lambda state: state.has(item1.name, player1.id)) set_rule(loc0, lambda state: state.has(item2.name, player1.id)) - self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) def test_competing_fill(self): """Test that fill raises an error when it can't place items in a way to satisfy the conditions""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) item0 = player1.prog_items[0] item1 = player1.prog_items[1] loc1 = player1.locations[1] - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has(item0.name, player1.id) and state.has(item1.name, player1.id)) - self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state, player1.locations.copy(), player1.prog_items.copy()) def test_multiplayer_fill(self): """Test that items can be placed across worlds""" - multi_world = generate_multi_world(2) - player1 = generate_player_data(multi_world, 1, 2, 2) - player2 = generate_player_data(multi_world, 2, 2, 2) + multiworld = generate_multiworld(2) + player1 = generate_player_data(multiworld, 1, 2, 2) + player2 = generate_player_data(multiworld, 2, 2, 2) - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( player1.prog_items[0].name, player1.id) and state.has( player1.prog_items[1].name, player1.id) - multi_world.completion_condition[player2.id] = lambda state: state.has( + multiworld.completion_condition[player2.id] = lambda state: state.has( player2.prog_items[0].name, player2.id) and state.has( player2.prog_items[1].name, player2.id) - fill_restrictive(multi_world, multi_world.state, player1.locations + + fill_restrictive(multiworld, multiworld.state, player1.locations + player2.locations, player1.prog_items + player2.prog_items) self.assertEqual(player1.locations[0].item, player1.prog_items[1]) @@ -371,21 +371,21 @@ def test_multiplayer_fill(self): def test_multiplayer_rules_fill(self): """Test that fill across worlds satisfies the rules""" - multi_world = generate_multi_world(2) - player1 = generate_player_data(multi_world, 1, 2, 2) - player2 = generate_player_data(multi_world, 2, 2, 2) + multiworld = generate_multiworld(2) + player1 = generate_player_data(multiworld, 1, 2, 2) + player2 = generate_player_data(multiworld, 2, 2, 2) - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( player1.prog_items[0].name, player1.id) and state.has( player1.prog_items[1].name, player1.id) - multi_world.completion_condition[player2.id] = lambda state: state.has( + multiworld.completion_condition[player2.id] = lambda state: state.has( player2.prog_items[0].name, player2.id) and state.has( player2.prog_items[1].name, player2.id) set_rule(player2.locations[1], lambda state: state.has( player2.prog_items[0].name, player2.id)) - fill_restrictive(multi_world, multi_world.state, player1.locations + + fill_restrictive(multiworld, multiworld.state, player1.locations + player2.locations, player1.prog_items + player2.prog_items) self.assertEqual(player1.locations[0].item, player2.prog_items[0]) @@ -395,10 +395,10 @@ def test_multiplayer_rules_fill(self): def test_restrictive_progress(self): """Test that various spheres with different requirements can be filled""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, prog_item_count=25) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, prog_item_count=25) items = player1.prog_items.copy() - multi_world.completion_condition[player1.id] = lambda state: state.has_all( + multiworld.completion_condition[player1.id] = lambda state: state.has_all( names(player1.prog_items), player1.id) player1.generate_region(player1.menu, 5) @@ -411,16 +411,16 @@ def test_restrictive_progress(self): player1.generate_region(player1.menu, 5, lambda state: state.has_all( names(items[17:22]), player1.id)) - locations = multi_world.get_unfilled_locations() + locations = multiworld.get_unfilled_locations() - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, locations, player1.prog_items) def test_swap_to_earlier_location_with_item_rule(self): """Test that item swap happens and works as intended""" # test for PR#1109 - multi_world = generate_multi_world(1) - player1 = generate_player_data(multi_world, 1, 4, 4) + multiworld = generate_multiworld(1) + player1 = generate_player_data(multiworld, 1, 4, 4) locations = player1.locations[:] # copy required items = player1.prog_items[:] # copy required # for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last @@ -437,15 +437,15 @@ def test_swap_to_earlier_location_with_item_rule(self): self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed") self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed") # fill has to place items[1] in locations[0] which will result in a swap because of placement order - fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items) # assert swap happened self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1") self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") def test_swap_to_earlier_location_with_item_rule2(self): """Test that swap works before all items are placed""" - multi_world = generate_multi_world(1) - player1 = generate_player_data(multi_world, 1, 5, 5) + multiworld = generate_multiworld(1) + player1 = generate_player_data(multiworld, 1, 5, 5) locations = player1.locations[:] # copy required items = player1.prog_items[:] # copy required # Two items provide access to sphere 2. @@ -477,7 +477,7 @@ def test_swap_to_earlier_location_with_item_rule2(self): # Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap, # which it will attempt before two_to_three and three_to_four are placed, testing the behavior. - fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items) # assert swap happened self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1") self.assertTrue(sphere1_loc1.item.name == one_to_two1 or @@ -486,29 +486,29 @@ def test_swap_to_earlier_location_with_item_rule2(self): def test_double_sweep(self): """Test that sweep doesn't duplicate Event items when sweeping""" # test for PR1114 - multi_world = generate_multi_world(1) - player1 = generate_player_data(multi_world, 1, 1, 1) + multiworld = generate_multiworld(1) + player1 = generate_player_data(multiworld, 1, 1, 1) location = player1.locations[0] location.address = None location.event = True item = player1.prog_items[0] item.code = None location.place_locked_item(item) - multi_world.state.sweep_for_events() - multi_world.state.sweep_for_events() - self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") - self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times") + multiworld.state.sweep_for_events() + multiworld.state.sweep_for_events() + self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") + self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times") def test_correct_item_instance_removed_from_pool(self): """Test that a placed item gets removed from the submitted pool""" - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) + multiworld = generate_multiworld() + player1 = generate_player_data(multiworld, 1, 2, 2) player1.prog_items[0].name = "Different_item_instance_but_same_item_name" player1.prog_items[1].name = "Different_item_instance_but_same_item_name" loc0 = player1.locations[0] - fill_restrictive(multi_world, multi_world.state, + fill_restrictive(multiworld, multiworld.state, [loc0], player1.prog_items) self.assertEqual(1, len(player1.prog_items)) @@ -518,14 +518,14 @@ def test_correct_item_instance_removed_from_pool(self): class TestDistributeItemsRestrictive(unittest.TestCase): def test_basic_distribute(self): """Test that distribute_items_restrictive is deterministic""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations prog_items = player1.prog_items basic_items = player1.basic_items - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertEqual(locations[0].item, basic_items[1]) self.assertFalse(locations[0].event) @@ -538,52 +538,52 @@ def test_basic_distribute(self): def test_excluded_distribute(self): """Test that distribute_items_restrictive doesn't put advancement items on excluded locations""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations locations[1].progress_type = LocationProgressType.EXCLUDED locations[2].progress_type = LocationProgressType.EXCLUDED - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertFalse(locations[1].item.advancement) self.assertFalse(locations[2].item.advancement) def test_non_excluded_item_distribute(self): """Test that useful items aren't placed on excluded locations""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations basic_items = player1.basic_items locations[1].progress_type = LocationProgressType.EXCLUDED basic_items[1].classification = ItemClassification.useful - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertEqual(locations[1].item, basic_items[0]) def test_too_many_excluded_distribute(self): """Test that fill fails if it can't place all progression items due to too many excluded locations""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations locations[0].progress_type = LocationProgressType.EXCLUDED locations[1].progress_type = LocationProgressType.EXCLUDED locations[2].progress_type = LocationProgressType.EXCLUDED - self.assertRaises(FillError, distribute_items_restrictive, multi_world) + self.assertRaises(FillError, distribute_items_restrictive, multiworld) def test_non_excluded_item_must_distribute(self): """Test that fill fails if it can't place useful items due to too many excluded locations""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations basic_items = player1.basic_items @@ -592,47 +592,47 @@ def test_non_excluded_item_must_distribute(self): basic_items[0].classification = ItemClassification.useful basic_items[1].classification = ItemClassification.useful - self.assertRaises(FillError, distribute_items_restrictive, multi_world) + self.assertRaises(FillError, distribute_items_restrictive, multiworld) def test_priority_distribute(self): """Test that priority locations receive advancement items""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations locations[0].progress_type = LocationProgressType.PRIORITY locations[3].progress_type = LocationProgressType.PRIORITY - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertTrue(locations[0].item.advancement) self.assertTrue(locations[3].item.advancement) def test_excess_priority_distribute(self): """Test that if there's more priority locations than advancement items, they can still fill""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) locations = player1.locations locations[0].progress_type = LocationProgressType.PRIORITY locations[1].progress_type = LocationProgressType.PRIORITY locations[2].progress_type = LocationProgressType.PRIORITY - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertFalse(locations[3].item.advancement) def test_multiple_world_priority_distribute(self): """Test that priority fill can be satisfied for multiple worlds""" - multi_world = generate_multi_world(3) + multiworld = generate_multiworld(3) player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) player2 = generate_player_data( - multi_world, 2, 4, prog_item_count=1, basic_item_count=3) + multiworld, 2, 4, prog_item_count=1, basic_item_count=3) player3 = generate_player_data( - multi_world, 3, 6, prog_item_count=4, basic_item_count=2) + multiworld, 3, 6, prog_item_count=4, basic_item_count=2) player1.locations[2].progress_type = LocationProgressType.PRIORITY player1.locations[3].progress_type = LocationProgressType.PRIORITY @@ -644,7 +644,7 @@ def test_multiple_world_priority_distribute(self): player3.locations[2].progress_type = LocationProgressType.PRIORITY player3.locations[3].progress_type = LocationProgressType.PRIORITY - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertTrue(player1.locations[2].item.advancement) self.assertTrue(player1.locations[3].item.advancement) @@ -656,9 +656,9 @@ def test_multiple_world_priority_distribute(self): def test_can_remove_locations_in_fill_hook(self): """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, 4, prog_item_count=2, basic_item_count=2) + multiworld, 1, 4, prog_item_count=2, basic_item_count=2) removed_item: list[Item] = [] removed_location: list[Location] = [] @@ -667,21 +667,21 @@ def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations): removed_item.append(filleritempool.pop(0)) removed_location.append(fill_locations.pop(0)) - multi_world.worlds[player1.id].fill_hook = fill_hook + multiworld.worlds[player1.id].fill_hook = fill_hook - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertIsNone(removed_item[0].location) self.assertIsNone(removed_location[0].item) def test_seed_robust_to_item_order(self): """Test deterministic fill""" - mw1 = generate_multi_world() + mw1 = generate_multiworld() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) distribute_items_restrictive(mw1) - mw2 = generate_multi_world() + mw2 = generate_multiworld() gen2 = generate_player_data( mw2, 1, 4, prog_item_count=2, basic_item_count=2) mw2.itempool.append(mw2.itempool.pop(0)) @@ -694,12 +694,12 @@ def test_seed_robust_to_item_order(self): def test_seed_robust_to_location_order(self): """Test deterministic fill even if locations in a region are reordered""" - mw1 = generate_multi_world() + mw1 = generate_multiworld() gen1 = generate_player_data( mw1, 1, 4, prog_item_count=2, basic_item_count=2) distribute_items_restrictive(mw1) - mw2 = generate_multi_world() + mw2 = generate_multiworld() gen2 = generate_player_data( mw2, 1, 4, prog_item_count=2, basic_item_count=2) reg = mw2.get_region("Menu", gen2.id) @@ -713,45 +713,45 @@ def test_seed_robust_to_location_order(self): def test_can_reserve_advancement_items_for_general_fill(self): """Test that priority locations fill still satisfies item rules""" - multi_world = generate_multi_world() + multiworld = generate_multiworld() player1 = generate_player_data( - multi_world, 1, location_count=5, prog_item_count=5) + multiworld, 1, location_count=5, prog_item_count=5) items = player1.prog_items - multi_world.completion_condition[player1.id] = lambda state: state.has_all( + multiworld.completion_condition[player1.id] = lambda state: state.has_all( names(items), player1.id) location = player1.locations[0] location.progress_type = LocationProgressType.PRIORITY location.item_rule = lambda item: item not in items[:4] - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) self.assertEqual(location.item, items[4]) def test_non_excluded_local_items(self): """Test that local items get placed locally in a multiworld""" - multi_world = generate_multi_world(2) + multiworld = generate_multiworld(2) player1 = generate_player_data( - multi_world, 1, location_count=5, basic_item_count=5) + multiworld, 1, location_count=5, basic_item_count=5) player2 = generate_player_data( - multi_world, 2, location_count=5, basic_item_count=5) + multiworld, 2, location_count=5, basic_item_count=5) - for item in multi_world.get_items(): + for item in multiworld.get_items(): item.classification = ItemClassification.useful - multi_world.local_items[player1.id].value = set(names(player1.basic_items)) - multi_world.local_items[player2.id].value = set(names(player2.basic_items)) - locality_rules(multi_world) + multiworld.local_items[player1.id].value = set(names(player1.basic_items)) + multiworld.local_items[player2.id].value = set(names(player2.basic_items)) + locality_rules(multiworld) - distribute_items_restrictive(multi_world) + distribute_items_restrictive(multiworld) - for item in multi_world.get_items(): + for item in multiworld.get_items(): self.assertEqual(item.player, item.location.player) self.assertFalse(item.location.event, False) def test_early_items(self) -> None: """Test that the early items API successfully places items early""" - mw = generate_multi_world(2) + mw = generate_multiworld(2) player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5) player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5) mw.early_items[1][player1.basic_items[0].name] = 1 @@ -810,19 +810,19 @@ def assertRegionContains(self, region: Region, item: Item) -> bool: "\n Contains" + str(list(map(lambda location: location.item, region.locations)))) def setUp(self) -> None: - multi_world = generate_multi_world(2) - self.multi_world = multi_world + multiworld = generate_multiworld(2) + self.multiworld = multiworld player1 = generate_player_data( - multi_world, 1, prog_item_count=2, basic_item_count=40) + multiworld, 1, prog_item_count=2, basic_item_count=40) self.player1 = player1 player2 = generate_player_data( - multi_world, 2, prog_item_count=2, basic_item_count=40) + multiworld, 2, prog_item_count=2, basic_item_count=40) self.player2 = player2 - multi_world.completion_condition[player1.id] = lambda state: state.has( + multiworld.completion_condition[player1.id] = lambda state: state.has( player1.prog_items[0].name, player1.id) and state.has( player1.prog_items[1].name, player1.id) - multi_world.completion_condition[player2.id] = lambda state: state.has( + multiworld.completion_condition[player2.id] = lambda state: state.has( player2.prog_items[0].name, player2.id) and state.has( player2.prog_items[1].name, player2.id) @@ -830,42 +830,42 @@ def setUp(self) -> None: # Sphere 1 region = player1.generate_region(player1.menu, 20) - items = fill_region(multi_world, region, [ + items = fill_region(multiworld, region, [ player1.prog_items[0]] + items) # Sphere 2 region = player1.generate_region( player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id)) items = fill_region( - multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items) + multiworld, region, [player1.prog_items[1], player2.prog_items[0]] + items) # Sphere 3 region = player2.generate_region( player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id)) - fill_region(multi_world, region, [player2.prog_items[1]] + items) + fill_region(multiworld, region, [player2.prog_items[1]] + items) def test_balances_progression(self) -> None: """Tests that progression balancing moves progression items earlier""" - self.multi_world.progression_balancing[self.player1.id].value = 50 - self.multi_world.progression_balancing[self.player2.id].value = 50 + self.multiworld.progression_balancing[self.player1.id].value = 50 + self.multiworld.progression_balancing[self.player2.id].value = 50 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) - balance_multiworld_progression(self.multi_world) + balance_multiworld_progression(self.multiworld) self.assertRegionContains( self.player1.regions[1], self.player2.prog_items[0]) def test_balances_progression_light(self) -> None: """Test that progression balancing still moves items earlier on minimum value""" - self.multi_world.progression_balancing[self.player1.id].value = 1 - self.multi_world.progression_balancing[self.player2.id].value = 1 + self.multiworld.progression_balancing[self.player1.id].value = 1 + self.multiworld.progression_balancing[self.player2.id].value = 1 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) - balance_multiworld_progression(self.multi_world) + balance_multiworld_progression(self.multiworld) # TODO: arrange for a result that's different from the default self.assertRegionContains( @@ -873,13 +873,13 @@ def test_balances_progression_light(self) -> None: def test_balances_progression_heavy(self) -> None: """Test that progression balancing moves items earlier on maximum value""" - self.multi_world.progression_balancing[self.player1.id].value = 99 - self.multi_world.progression_balancing[self.player2.id].value = 99 + self.multiworld.progression_balancing[self.player1.id].value = 99 + self.multiworld.progression_balancing[self.player2.id].value = 99 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) - balance_multiworld_progression(self.multi_world) + balance_multiworld_progression(self.multiworld) # TODO: arrange for a result that's different from the default self.assertRegionContains( @@ -887,25 +887,25 @@ def test_balances_progression_heavy(self) -> None: def test_skips_balancing_progression(self) -> None: """Test that progression balancing is skipped when players have it disabled""" - self.multi_world.progression_balancing[self.player1.id].value = 0 - self.multi_world.progression_balancing[self.player2.id].value = 0 + self.multiworld.progression_balancing[self.player1.id].value = 0 + self.multiworld.progression_balancing[self.player2.id].value = 0 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) - balance_multiworld_progression(self.multi_world) + balance_multiworld_progression(self.multiworld) self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) def test_ignores_priority_locations(self) -> None: """Test that progression items on priority locations don't get moved by balancing""" - self.multi_world.progression_balancing[self.player1.id].value = 50 - self.multi_world.progression_balancing[self.player2.id].value = 50 + self.multiworld.progression_balancing[self.player1.id].value = 50 + self.multiworld.progression_balancing[self.player2.id].value = 50 self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY - balance_multiworld_progression(self.multi_world) + balance_multiworld_progression(self.multiworld) self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) diff --git a/test/general/test_groups.py b/test/general/test_groups.py new file mode 100644 index 000000000000..486d3311fa6b --- /dev/null +++ b/test/general/test_groups.py @@ -0,0 +1,27 @@ +from unittest import TestCase + +from worlds.AutoWorld import AutoWorldRegister + + +class TestNameGroups(TestCase): + def test_item_name_groups_not_empty(self) -> None: + """ + Test that there are no empty item name groups, which is likely a bug. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + if not world_type.item_id_to_name: + continue # ignore worlds without items + with self.subTest(game=game_name): + for name, group in world_type.item_name_groups.items(): + self.assertTrue(group, f"Item name group \"{name}\" of \"{game_name}\" is empty") + + def test_location_name_groups_not_empty(self) -> None: + """ + Test that there are no empty location name groups, which is likely a bug. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + if not world_type.location_id_to_name: + continue # ignore worlds without locations + with self.subTest(game=game_name): + for name, group in world_type.location_name_groups.items(): + self.assertTrue(group, f"Location name group \"{name}\" of \"{game_name}\" is empty") diff --git a/test/general/test_items.py b/test/general/test_items.py index 2d8775d535b6..1612937225f2 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -1,5 +1,6 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister + +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -42,18 +43,18 @@ def test_item_name_group_conflict(self): with self.subTest(group_name, group_name=group_name): self.assertNotIn(group_name, world_type.item_name_to_id) - def test_item_count_greater_equal_locations(self): - """Test that by the pre_fill step under default settings, each game submits items >= locations""" + def test_item_count_equal_locations(self): + """Test that by the pre_fill step under default settings, each game submits items == locations""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): multiworld = setup_solo_multiworld(world_type) - self.assertGreaterEqual( + self.assertEqual( len(multiworld.itempool), len(multiworld.get_unfilled_locations()), - f"{game_name} Item count MUST meet or exceed the number of locations", + f"{game_name} Item count MUST match the number of locations", ) - def testItemsInDatapackage(self): + def test_items_in_datapackage(self): """Test that any created items in the itempool are in the datapackage""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): @@ -69,3 +70,20 @@ def test_item_descriptions_have_valid_names(self): with self.subTest("Name should be valid", game=game_name, item=name): self.assertIn(name, valid_names, "All item descriptions must match defined item names") + + def test_itempool_not_modified(self): + """Test that worlds don't modify the itempool after `create_items`""" + gen_steps = ("generate_early", "create_regions", "create_items") + additional_steps = ("set_rules", "generate_basic", "pre_fill") + excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3") + worlds_to_test = {game: world + for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games} + for game_name, world_type in worlds_to_test.items(): + with self.subTest("Game", game=game_name): + multiworld = setup_solo_multiworld(world_type, gen_steps) + created_items = multiworld.itempool.copy() + for step in additional_steps: + with self.subTest("step", step=step): + call_all(multiworld, step) + self.assertEqual(created_items, multiworld.itempool, + f"{game_name} modified the itempool during {step}") diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 725b48e62f72..2ac059312c17 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -11,14 +11,14 @@ def test_create_duplicate_locations(self): multiworld = setup_solo_multiworld(world_type) locations = Counter(location.name for location in multiworld.get_locations()) if locations: - self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location name {locations.most_common(1)}") + self.assertEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location name {locations.most_common(1)}") locations = Counter(location.address for location in multiworld.get_locations() if type(location.address) is int) if locations: - self.assertLessEqual(locations.most_common(1)[0][1], 1, - f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") + self.assertEqual(locations.most_common(1)[0][1], 1, + f"{world_type.game} has duplicate of location ID {locations.most_common(1)}") def test_locations_in_datapackage(self): """Tests that created locations not filled before fill starts exist in the datapackage.""" diff --git a/test/general/test_options.py b/test/general/test_options.py index e1136f93c96f..211704dfe6ba 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -10,3 +10,10 @@ def test_options_have_doc_string(self): for option_key, option in world_type.options_dataclass.type_hints.items(): with self.subTest(game=gamename, option=option_key): self.assertTrue(option.__doc__) + + def test_options_are_not_set_by_world(self): + """Test that options attribute is not already set""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertFalse(hasattr(world_type, "options"), + f"Unexpected assignment to {world_type.__name__}.options!") diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index 828912ee35a3..e57c398b7beb 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -36,15 +36,15 @@ def test_default_all_state_can_reach_everything(self): for game_name, world_type in AutoWorldRegister.world_types.items(): unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set()) with self.subTest("Game", game=game_name): - world = setup_solo_multiworld(world_type) - excluded = world.exclude_locations[1].value - state = world.get_all_state(False) - for location in world.get_locations(): + multiworld = setup_solo_multiworld(world_type) + excluded = multiworld.worlds[1].options.exclude_locations.value + state = multiworld.get_all_state(False) + for location in multiworld.get_locations(): if location.name not in excluded: with self.subTest("Location should be reached", location=location): self.assertTrue(location.can_reach(state), f"{location.name} unreachable") - for region in world.get_regions(): + for region in multiworld.get_regions(): if region.name in unreachable_regions: with self.subTest("Region should be unreachable", region=region): self.assertFalse(region.can_reach(state)) @@ -53,15 +53,15 @@ def test_default_all_state_can_reach_everything(self): self.assertTrue(region.can_reach(state)) with self.subTest("Completion Condition"): - self.assertTrue(world.can_beat_game(state)) + self.assertTrue(multiworld.can_beat_game(state)) def test_default_empty_state_can_reach_something(self): """Ensure empty state can reach at least one location with the defined options""" for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game=game_name): - world = setup_solo_multiworld(world_type) - state = CollectionState(world) - all_locations = world.get_locations() + multiworld = setup_solo_multiworld(world_type) + state = CollectionState(multiworld) + all_locations = multiworld.get_locations() if all_locations: locations = set() for location in all_locations: diff --git a/test/webhost/test_api_generate.py b/test/webhost/test_api_generate.py index b8bdcb38c764..bd78edd9c700 100644 --- a/test/webhost/test_api_generate.py +++ b/test/webhost/test_api_generate.py @@ -1,5 +1,7 @@ +import io import unittest import json +import yaml class TestDocs(unittest.TestCase): @@ -23,7 +25,7 @@ def test_correct_error_empty_request(self): response = self.client.post("/api/generate") self.assertIn("No options found. Expected file attachment or json weights.", response.text) - def test_generation_queued(self): + def test_generation_queued_weights(self): options = { "Tester1": { @@ -40,3 +42,19 @@ def test_generation_queued(self): json_data = response.get_json() self.assertTrue(json_data["text"].startswith("Generation of seed ")) self.assertTrue(json_data["text"].endswith(" started successfully.")) + + def test_generation_queued_file(self): + options = { + "game": "Archipelago", + "name": "Tester", + "Archipelago": {} + } + response = self.client.post( + "/api/generate", + data={ + 'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml") + }, + ) + json_data = response.get_json() + self.assertTrue(json_data["text"].startswith("Generation of seed ")) + self.assertTrue(json_data["text"].endswith(" started successfully.")) diff --git a/typings/kivy/graphics.pyi b/typings/kivy/graphics/__init__.pyi similarity index 54% rename from typings/kivy/graphics.pyi rename to typings/kivy/graphics/__init__.pyi index 1950910661f4..a1a5bc02f68e 100644 --- a/typings/kivy/graphics.pyi +++ b/typings/kivy/graphics/__init__.pyi @@ -1,24 +1,12 @@ -""" FillType_* is not a real kivy type - just something to fill unknown typing. """ - -from typing import Sequence - -FillType_Vec = Sequence[int] - - -class FillType_Drawable: - def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... - - -class FillType_Texture(FillType_Drawable): - pass +from .texture import FillType_Drawable, FillType_Vec, Texture class FillType_Shape(FillType_Drawable): - texture: FillType_Texture + texture: Texture def __init__(self, *, - texture: FillType_Texture = ..., + texture: Texture = ..., pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... @@ -35,6 +23,6 @@ class Rectangle(FillType_Shape): def __init__(self, *, source: str = ..., - texture: FillType_Texture = ..., + texture: Texture = ..., pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... diff --git a/typings/kivy/graphics/texture.pyi b/typings/kivy/graphics/texture.pyi new file mode 100644 index 000000000000..19e03aad69dd --- /dev/null +++ b/typings/kivy/graphics/texture.pyi @@ -0,0 +1,13 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Sequence + +FillType_Vec = Sequence[int] + + +class FillType_Drawable: + def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... + + +class Texture: + pass diff --git a/typings/kivy/uix/image.pyi b/typings/kivy/uix/image.pyi new file mode 100644 index 000000000000..fa014baec7c2 --- /dev/null +++ b/typings/kivy/uix/image.pyi @@ -0,0 +1,9 @@ +import io + +from kivy.graphics.texture import Texture + + +class CoreImage: + texture: Texture + + def __init__(self, data: io.BytesIO, ext: str) -> None: ... diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f56c39f69086..e8d48df58c53 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -79,8 +79,8 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut if "options_dataclass" not in dct and "option_definitions" in dct: # TODO - switch to deprecate after a version if __debug__: - from warnings import warn - warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") + logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. " + "Please use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) @@ -328,7 +328,7 @@ def create_regions(self) -> None: def create_items(self) -> None: """ - Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted + Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`. """ pass @@ -438,7 +438,7 @@ def collect_item(self, state: "CollectionState", item: "Item", remove: bool = Fa def get_pre_fill_items(self) -> List["Item"]: return [] - # following methods should not need to be overridden. + # these two methods can be extended for pseudo-items on state def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: diff --git a/worlds/Files.py b/worlds/Files.py index 52d3c7da1d35..336a3090937b 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -1,5 +1,6 @@ from __future__ import annotations +import abc import json import zipfile import os @@ -15,7 +16,7 @@ del os -class AutoPatchRegister(type): +class AutoPatchRegister(abc.ABCMeta): patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {} file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {} @@ -112,14 +113,25 @@ def get_manifest(self) -> Dict[str, Any]: } -class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): - """An APContainer that additionally has delta.bsdiff4 +class APPatch(APContainer, abc.ABC, metaclass=AutoPatchRegister): + """ + An abstract `APContainer` that defines the requirements for an object + to be used by the `Patch.create_rom_file` function. + """ + result_file_ending: str = ".sfc" + + @abc.abstractmethod + def patch(self, target: str) -> None: + """ create the output file with the file name `target` """ + + +class APDeltaPatch(APPatch): + """An APPatch that additionally has delta.bsdiff4 containing a delta patch to get the desired file, often a rom.""" hash: Optional[str] # base checksum of source file patch_file_ending: str = "" delta: Optional[bytes] = None - result_file_ending: str = ".sfc" source_data: bytes def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: diff --git a/worlds/__init__.py b/worlds/__init__.py index 66c91639b9f3..168bba7abf41 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -3,7 +3,9 @@ import sys import warnings import zipimport -from typing import Dict, List, NamedTuple, TypedDict +import time +import dataclasses +from typing import Dict, List, TypedDict, Optional from Utils import local_path, user_path @@ -34,10 +36,12 @@ class DataPackage(TypedDict): games: Dict[str, GamesPackage] -class WorldSource(NamedTuple): +@dataclasses.dataclass(order=True) +class WorldSource: path: str # typically relative path from this module is_zip: bool = False relative: bool = True # relative to regular world import folder + time_taken: Optional[float] = None def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})" @@ -50,6 +54,7 @@ def resolved_path(self) -> str: def load(self) -> bool: try: + start = time.perf_counter() if self.is_zip: importer = zipimport.zipimporter(self.resolved_path) if hasattr(importer, "find_spec"): # new in Python 3.10 @@ -69,6 +74,7 @@ def load(self) -> bool: importer.exec_module(mod) else: importlib.import_module(f".{self.path}", "worlds") + self.time_taken = time.perf_counter()-start return True except Exception: diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 105725bd053c..9b9b0d77d800 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -271,7 +271,7 @@ def pre_fill(self): overworld_locations_copy = overworld.locations.copy() all_locations = self.multiworld.get_locations(self.player) - locations_copy = all_locations.copy() + locations_copy = list(all_locations) for loc in all_locations: if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT: locations_copy.remove(loc) diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 90ffe9dcf4b1..965a86db008a 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -6,7 +6,7 @@ from Fill import FillError from .Options import LTTPBosses as Bosses from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \ - has_melee_weapon, has_fire_source + has_melee_weapon, has_fire_source, can_use_bombs if TYPE_CHECKING: from . import ALTTPWorld @@ -62,7 +62,8 @@ def MoldormDefeatRule(state, player: int) -> bool: def HelmasaurKingDefeatRule(state, player: int) -> bool: # TODO: technically possible with the hammer - return has_sword(state, player) or can_shoot_arrows(state, player) + return (can_use_bombs(state, player, 5) or state.has("Hammer", player)) and (has_sword(state, player) + or can_shoot_arrows(state, player)) def ArrghusDefeatRule(state, player: int) -> bool: @@ -143,7 +144,7 @@ def GanonDefeatRule(state, player: int) -> bool: can_hurt = has_beam_sword(state, player) common = can_hurt and has_fire_source(state, player) # silverless ganon may be needed in anything higher than no glitches - if state.multiworld.logic[player] != 'noglitches': + if state.multiworld.glitches_required[player] != 'no_glitches': # need to light torch a sufficient amount of times return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or ( state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index b456174f39b7..c886fce92079 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -9,7 +9,7 @@ from .Bosses import BossFactory, Boss from .Items import ItemFactory from .Regions import lookup_boss_drops, key_drop_data -from .Options import smallkey_shuffle +from .Options import small_key_shuffle if typing.TYPE_CHECKING: from .SubClasses import ALttPLocation, ALttPItem @@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): dungeon = Dungeon(name, dungeon_regions, big_key, - [] if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, + [] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys, dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 47c36b6cde33..37486a9cde07 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -23,7 +23,7 @@ def defval(value): multiargs, _ = parser.parse_known_args(argv) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'owglitches', 'hybridglitches', 'nologic'], + parser.add_argument('--logic', default=defval('no_glitches'), const='no_glitches', nargs='?', choices=['no_glitches', 'minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'], help='''\ Select Enforcement of Item Requirements. (default: %(default)s) No Glitches: @@ -49,7 +49,7 @@ def defval(value): instead of a bunny. ''') parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', - choices=['ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'], + choices=['ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals', 'ganon_pedestal'], help='''\ Select completion goal. (default: %(default)s) Ganon: Collect all crystals, beat Agahnim 2 then @@ -92,7 +92,7 @@ def defval(value): Hard: Reduced functionality. Expert: Greatly reduced functionality. ''') - parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'], + parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'], help='''\ Select game timer setting. Affects available itempool. (default: %(default)s) None: No timer. @@ -151,7 +151,7 @@ def defval(value): slightly biased to placing progression items with less restrictions. ''') - parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed'], + parser.add_argument('--shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed'], help='''\ Select Entrance Shuffling Algorithm. (default: %(default)s) Full: Mix cave and dungeon entrances freely while limiting @@ -178,9 +178,9 @@ def defval(value): parser.add_argument('--open_pyramid', default=defval('auto'), help='''\ Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon. - fast ganon goals are crystals, ganontriforcehunt, localganontriforcehunt, pedestalganon + fast ganon goals are crystals, ganon_triforce_hunt, local_ganon_triforce_hunt, pedestalganon auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle - is vanilla, dungeonssimple or dungeonsfull. + is vanilla, dungeons_simple or dungeons_full. goal - Opens pyramid hole if the goal specifies a fast ganon. yes - Always opens the pyramid hole. no - Never opens the pyramid hole. diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index 07bb587eebe3..fceba86a739e 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -21,17 +21,17 @@ def link_entrances(world, player): connect_simple(world, exitname, regionname, player) # if we do not shuffle, set default connections - if world.shuffle[player] == 'vanilla': + if world.entrance_shuffle[player] == 'vanilla': for exitname, regionname in default_connections: connect_simple(world, exitname, regionname, player) for exitname, regionname in default_dungeon_connections: connect_simple(world, exitname, regionname, player) - elif world.shuffle[player] == 'dungeonssimple': + elif world.entrance_shuffle[player] == 'dungeons_simple': for exitname, regionname in default_connections: connect_simple(world, exitname, regionname, player) simple_shuffle_dungeons(world, player) - elif world.shuffle[player] == 'dungeonsfull': + elif world.entrance_shuffle[player] == 'dungeons_full': for exitname, regionname in default_connections: connect_simple(world, exitname, regionname, player) @@ -63,9 +63,9 @@ def link_entrances(world, player): connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.shuffle[player] == 'dungeonscrossed': + elif world.entrance_shuffle[player] == 'dungeons_crossed': crossed_shuffle_dungeons(world, player) - elif world.shuffle[player] == 'simple': + elif world.entrance_shuffle[player] == 'simple': simple_shuffle_dungeons(world, player) old_man_entrances = list(Old_Man_Entrances) @@ -136,7 +136,7 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'restricted': + elif world.entrance_shuffle[player] == 'restricted': simple_shuffle_dungeons(world, player) lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) @@ -207,62 +207,8 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'restricted_legacy': - simple_shuffle_dungeons(world, player) - - lw_entrances = list(LW_Entrances) - dw_entrances = list(DW_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits) - three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # only use two exit caves to do mandatory dw connections - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - # add three exit doors to pool for remainder - caves.extend(three_exit_caves) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - world.random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.extend(old_man_entrances) - world.random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], Old_Man_House, player) - # connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - world.random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'full': + elif world.entrance_shuffle[player] == 'full': skull_woods_shuffle(world, player) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) @@ -368,7 +314,7 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'crossed': + elif world.entrance_shuffle[player] == 'crossed': skull_woods_shuffle(world, player) entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) @@ -445,337 +391,8 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, entrances, door_targets, player) - elif world.shuffle[player] == 'full_legacy': - skull_woods_shuffle(world, player) - - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - else: - caves.append(tuple(world.random.sample( - ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if world.random.randint(0, 1) == 0: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - world.random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - world.random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds - - # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place bomb shop, has limited options - world.random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'madness_legacy': - # here lie dragons, connections are no longer two way - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - - lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave', - 'Bat Cave Cave', - 'North Fairy Cave', - 'Sanctuary', - 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave'] + list( - Old_Man_Entrances) - dw_doors = list( - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + [ - 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', - 'Skull Woods Second Section Door (West)'] - - world.random.shuffle(lw_doors) - world.random.shuffle(dw_doors) - - dw_entrances_must_exits.append('Skull Woods Second Section Door (West)') - dw_entrances.append('Skull Woods Second Section Door (East)') - dw_entrances.append('Skull Woods First Section Door') - - lw_entrances.extend( - ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave']) - - lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'] - mandatory_dark_world = [] - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) - - # shuffle up holes - - lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] - dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), - (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - lw_doors.append('Hyrule Castle Secret Entrance Stairs') - lw_entrances.append('Hyrule Castle Secret Entrance Stairs') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - dw_hole_entrances.append('Pyramid Hole') - hole_targets.append(('Pyramid Exit', 'Pyramid')) - dw_entrances_must_exits.append('Pyramid Entrance') - dw_doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - world.random.shuffle(lw_hole_entrances) - world.random.shuffle(dw_hole_entrances) - world.random.shuffle(hole_targets) - - # decide if skull woods first section should be in light or dark world - sw_light = world.random.randint(0, 1) == 0 - if sw_light: - sw_hole_pool = lw_hole_entrances - mandatory_light_world.append('Skull Woods First Section Exit') - else: - sw_hole_pool = dw_hole_entrances - mandatory_dark_world.append('Skull Woods First Section Exit') - for target in ['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', - 'Skull Woods First Section (Top)']: - connect_entrance(world, sw_hole_pool.pop(), target, player) - - # sanctuary has to be in light world - connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player) - mandatory_light_world.append('Sanctuary Exit') - - # fill up remaining holes - for hole in dw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_dark_world.append(exits) - connect_entrance(world, hole, target, player) - - for hole in lw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_light_world.append(exits) - connect_entrance(world, hole, target, player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_doors.append('Hyrule Castle Entrance (South)') - lw_entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise KeyError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, general, worldspecific, worldoors): - # select which one is the primary option - if world.random.randint(0, 1) == 0: - primary = general - secondary = worldspecific - else: - primary = worldspecific - secondary = general - - try: - cave = extract_reachable_exit(primary) - except KeyError: - cave = extract_reachable_exit(secondary) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, worldoors.pop(), exit, player) - # rest of cave now is forced to be in this world - worldspecific.append(cave) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if world.random.randint(0, 1) == 0: - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - else: - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances] - world.random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player) - mandatory_light_world.append('Old Man Cave Exit (West)') - - # we connect up the mandatory associations we have found - for mandatory in mandatory_light_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, lw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, lw_doors.pop(), exit, player) - - for mandatory in mandatory_dark_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, dw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, dw_doors.pop(), exit, player) - # handle remaining caves - while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill - cave_candidate = (None, 0) - for i, cave in enumerate(caves): - if isinstance(cave, str): - cave = (cave,) - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = caves.pop(cave_candidate[0]) - - place_lightworld = world.random.randint(0, 1) == 0 - if place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - if isinstance(cave, str): - cave = (cave,) - - # check if we can still fit the cave into our target group - if len(target_doors) < len(cave): - if not place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - for exit in cave: - connect_exit(world, exit, target_entrances.pop(), player) - connect_entrance(world, target_doors.pop(), exit, player) - - # handle simple doors - - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - world.random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'insanity': + elif world.entrance_shuffle[player] == 'insanity': # beware ye who enter here entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] @@ -922,157 +539,15 @@ def connect_reachable_exit(entrance, caves, doors): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'insanity_legacy': - world.fix_fake_world[player] = False - # beware ye who enter here - - entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] - - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - world.random.shuffle(doors) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - # shuffle up holes - - hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', - 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)', - 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append('Hyrule Castle Secret Entrance') - doors.append('Hyrule Castle Secret Entrance Stairs') - entrances.append('Hyrule Castle Secret Entrance Stairs') - caves.append('Hyrule Castle Secret Entrance Exit') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - entrances.append('Ganons Tower') - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) - hole_entrances.append('Pyramid Hole') - hole_targets.append('Pyramid') - entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - world.random.shuffle(hole_entrances) - world.random.shuffle(hole_targets) - world.random.shuffle(entrances) - - # fill up holes - for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise KeyError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - world.random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) - caves.append('Old Man Cave Exit (West)') - - # handle remaining caves - for cave in caves: - if isinstance(cave, str): - cave = (cave,) - - for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) - - # handle simple doors - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - world.random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) else: raise NotImplementedError( - f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}') + f'{world.entrance_shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}') - if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: overworld_glitch_connections(world, player) # mandatory hybrid major glitches connections - if world.logic[player] in ['hybridglitches', 'nologic']: + if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: underworld_glitch_connections(world, player) # check for swamp palace fix @@ -1106,17 +581,17 @@ def link_inverted_entrances(world, player): connect_simple(world, exitname, regionname, player) # if we do not shuffle, set default connections - if world.shuffle[player] == 'vanilla': + if world.entrance_shuffle[player] == 'vanilla': for exitname, regionname in inverted_default_connections: connect_simple(world, exitname, regionname, player) for exitname, regionname in inverted_default_dungeon_connections: connect_simple(world, exitname, regionname, player) - elif world.shuffle[player] == 'dungeonssimple': + elif world.entrance_shuffle[player] == 'dungeons_simple': for exitname, regionname in inverted_default_connections: connect_simple(world, exitname, regionname, player) simple_shuffle_dungeons(world, player) - elif world.shuffle[player] == 'dungeonsfull': + elif world.entrance_shuffle[player] == 'dungeons_full': for exitname, regionname in inverted_default_connections: connect_simple(world, exitname, regionname, player) @@ -1171,9 +646,9 @@ def link_inverted_entrances(world, player): connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.shuffle[player] == 'dungeonscrossed': + elif world.entrance_shuffle[player] == 'dungeons_crossed': inverted_crossed_shuffle_dungeons(world, player) - elif world.shuffle[player] == 'simple': + elif world.entrance_shuffle[player] == 'simple': simple_shuffle_dungeons(world, player) old_man_entrances = list(Inverted_Old_Man_Entrances) @@ -1270,7 +745,7 @@ def link_inverted_entrances(world, player): # place remaining doors connect_doors(world, single_doors, door_targets, player) - elif world.shuffle[player] == 'restricted': + elif world.entrance_shuffle[player] == 'restricted': simple_shuffle_dungeons(world, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors) @@ -1355,7 +830,7 @@ def link_inverted_entrances(world, player): doors = lw_entrances + dw_entrances # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'full': + elif world.entrance_shuffle[player] == 'full': skull_woods_shuffle(world, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors) @@ -1506,7 +981,7 @@ def link_inverted_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'crossed': + elif world.entrance_shuffle[player] == 'crossed': skull_woods_shuffle(world, player) entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) @@ -1617,7 +1092,7 @@ def link_inverted_entrances(world, player): # place remaining doors connect_doors(world, entrances, door_targets, player) - elif world.shuffle[player] == 'insanity': + elif world.entrance_shuffle[player] == 'insanity': # beware ye who enter here entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)'] @@ -1776,10 +1251,10 @@ def connect_reachable_exit(entrance, caves, doors): else: raise NotImplementedError('Shuffling not supported yet') - if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: overworld_glitch_connections(world, player) # mandatory hybrid major glitches connections - if world.logic[player] in ['hybridglitches', 'nologic']: + if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: underworld_glitch_connections(world, player) # patch swamp drain @@ -1880,14 +1355,14 @@ def scramble_holes(world, player): hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': + if world.entrance_shuffle[player] == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) if world.shuffle_ganon: world.random.shuffle(hole_targets) exit, target = hole_targets.pop() connect_two_way(world, 'Pyramid Entrance', exit, player) connect_entrance(world, 'Pyramid Hole', target, player) - if world.shuffle[player] != 'crossed': + if world.entrance_shuffle[player] != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) world.random.shuffle(hole_targets) @@ -1922,14 +1397,14 @@ def scramble_inverted_holes(world, player): hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': + if world.entrance_shuffle[player] == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) if world.shuffle_ganon: world.random.shuffle(hole_targets) exit, target = hole_targets.pop() connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) connect_entrance(world, 'Inverted Pyramid Hole', target, player) - if world.shuffle[player] != 'crossed': + if world.entrance_shuffle[player] != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) world.random.shuffle(hole_targets) @@ -1958,7 +1433,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: from worlds.alttp import OverworldGlitchRules for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'): invalid_connections[entrance] = set() @@ -3038,6 +2513,7 @@ def plando_connect(world, player: int): ('Sanctuary Push Door', 'Sanctuary'), ('Sewer Drop', 'Sewers'), ('Sewers Back Door', 'Sewers (Dark)'), + ('Sewers Secret Room', 'Sewers Secret Room'), ('Agahnim 1', 'Agahnim 1'), ('Flute Spot 1', 'Death Mountain'), ('Death Mountain Entrance Rock', 'Death Mountain Entrance'), @@ -3053,6 +2529,8 @@ def plando_connect(world, player: int): ('Spiral Cave Ledge Access', 'Spiral Cave Ledge'), ('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'), ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), + ('Hookshot Cave Bomb Wall (South)', 'Hookshot Cave (Upper)'), + ('Hookshot Cave Bomb Wall (North)', 'Hookshot Cave'), ('East Death Mountain (Top)', 'East Death Mountain (Top)'), ('Death Mountain (Top)', 'Death Mountain (Top)'), ('Death Mountain Drop', 'Death Mountain'), @@ -3227,6 +2705,7 @@ def plando_connect(world, player: int): ('Sanctuary Push Door', 'Sanctuary'), ('Sewer Drop', 'Sewers'), ('Sewers Back Door', 'Sewers (Dark)'), + ('Sewers Secret Room', 'Sewers Secret Room'), ('Agahnim 1', 'Agahnim 1'), ('Death Mountain Entrance Rock', 'Death Mountain Entrance'), ('Death Mountain Entrance Drop', 'Light World'), @@ -3241,6 +2720,8 @@ def plando_connect(world, player: int): ('Spiral Cave Ledge Access', 'Spiral Cave Ledge'), ('Spiral Cave Ledge Drop', 'East Death Mountain (Bottom)'), ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), + ('Hookshot Cave Bomb Wall (South)', 'Hookshot Cave (Upper)'), + ('Hookshot Cave Bomb Wall (North)', 'Hookshot Cave'), ('East Death Mountain (Top)', 'East Death Mountain (Top)'), ('Death Mountain (Top)', 'Death Mountain (Top)'), ('Death Mountain Drop', 'Death Mountain'), @@ -3572,7 +3053,7 @@ def plando_connect(world, player: int): ('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'), ('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'), ('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Upper)'), ('Mimic Cave', 'Mimic Cave'), ('Pyramid Hole', 'Pyramid'), @@ -3703,7 +3184,7 @@ def plando_connect(world, player: int): ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), ('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'), ('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Upper)'), ('Mimic Cave', 'Mimic Cave'), ('Inverted Pyramid Hole', 'Pyramid'), ('Inverted Links House', 'Inverted Links House'), diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index f89eebec3339..2e30fde8cc85 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -133,7 +133,7 @@ def create_inverted_regions(world, player): create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'), create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']), - create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'), + create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], @@ -176,8 +176,9 @@ def create_inverted_regions(world, player): 'Throne Room']), create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', - 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), + create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), + create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + 'Sewers - Secret Room - Right']), create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']), create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), @@ -346,7 +347,9 @@ def create_inverted_regions(world, player): create_cave_region(world, player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], - ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']), + ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']), + create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + 'Hookshot Cave Bomb Wall (North)']), create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']), create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), @@ -380,8 +383,8 @@ def create_inverted_regions(world, player): create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']), + create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 1c3f3e44f72c..bb5bbaa61a02 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -5,12 +5,12 @@ from Fill import FillError from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType -from .Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations +from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType from .Bosses import place_bosses from .Dungeons import get_dungeon_item_pool_player from .EntranceShuffle import connect_entrance -from .Items import ItemFactory, GetBeemizerItem -from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses +from .Items import ItemFactory, GetBeemizerItem, trap_replaceable, item_name_groups +from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_shuffle, TriforcePiecesMode from .StateHelpers import has_triforce_pieces, has_melee_weapon from .Regions import key_drop_data @@ -189,104 +189,62 @@ ), } -ice_rod_hunt_difficulties = dict() -for diff in {'easy', 'normal', 'hard', 'expert'}: - ice_rod_hunt_difficulties[diff] = Difficulty( - baseitems=['Nothing'] * 41, - bottles=['Nothing'] * 4, - bottle_count=difficulties[diff].bottle_count, - same_bottle=difficulties[diff].same_bottle, - progressiveshield=['Nothing'] * 3, - basicshield=['Nothing'] * 3, - progressivearmor=['Nothing'] * 2, - basicarmor=['Nothing'] * 2, - swordless=['Nothing'] * 4, - progressivemagic=['Nothing'] * 2, - basicmagic=['Nothing'] * 2, - progressivesword=['Nothing'] * 4, - basicsword=['Nothing'] * 4, - progressivebow=['Nothing'] * 2, - basicbow=['Nothing'] * 2, - timedohko=difficulties[diff].timedohko, - timedother=difficulties[diff].timedother, - progressiveglove=['Nothing'] * 2, - basicglove=['Nothing'] * 2, - alwaysitems=['Ice Rod'] + ['Nothing'] * 19, - legacyinsanity=['Nothing'] * 2, - universal_keys=['Nothing'] * 29, - extras=[['Nothing'] * 15, ['Nothing'] * 15, ['Nothing'] * 10, ['Nothing'] * 5, ['Nothing'] * 25], - progressive_sword_limit=difficulties[diff].progressive_sword_limit, - progressive_shield_limit=difficulties[diff].progressive_shield_limit, - progressive_armor_limit=difficulties[diff].progressive_armor_limit, - progressive_bow_limit=difficulties[diff].progressive_bow_limit, - progressive_bottle_limit=difficulties[diff].progressive_bottle_limit, - boss_heart_container_limit=difficulties[diff].boss_heart_container_limit, - heart_piece_limit=difficulties[diff].heart_piece_limit, - ) + +items_reduction_table = ( + ("Piece of Heart", "Boss Heart Container", 4, 1), + # the order of the upgrades is important + ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 8, 4), + ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 7, 4), + ("Arrow Upgrade (+5)", "Arrow Upgrade (+10)", 6, 3), + ("Arrow Upgrade (+10)", "Arrow Upgrade (70)", 4, 1), + ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 8, 4), + ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 7, 4), + ("Bomb Upgrade (+5)", "Bomb Upgrade (+10)", 6, 3), + ("Bomb Upgrade (+10)", "Bomb Upgrade (50)", 5, 1), + ("Bomb Upgrade (+10)", "Bomb Upgrade (50)", 4, 1), + ("Progressive Sword", 4), + ("Fighter Sword", 1), + ("Master Sword", 1), + ("Tempered Sword", 1), + ("Golden Sword", 1), + ("Progressive Shield", 3), + ("Blue Shield", 1), + ("Red Shield", 1), + ("Mirror Shield", 1), + ("Progressive Mail", 2), + ("Blue Mail", 1), + ("Red Mail", 1), + ("Progressive Bow", 2), + ("Bow", 1), + ("Silver Bow", 1), + ("Lamp", 1), + ("Bottles",) +) def generate_itempool(world): player = world.player multiworld = world.multiworld - if multiworld.difficulty[player] not in difficulties: - raise NotImplementedError(f"Diffulty {multiworld.difficulty[player]}") - if multiworld.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', - 'ganontriforcehunt', 'localganontriforcehunt', 'crystals', 'ganonpedestal'}: + if multiworld.item_pool[player].current_key not in difficulties: + raise NotImplementedError(f"Diffulty {multiworld.item_pool[player]}") + if multiworld.goal[player] not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt', + 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals', + 'ganon_pedestal'): raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}") - if multiworld.mode[player] not in {'open', 'standard', 'inverted'}: + if multiworld.mode[player] not in ('open', 'standard', 'inverted'): raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}") - if multiworld.timer[player] not in {False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'}: + if multiworld.timer[player] not in {False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'}: raise NotImplementedError(f"Timer {multiworld.mode[player]} for player {player}") - if multiworld.timer[player] in ['ohko', 'timed-ohko']: + if multiworld.timer[player] in ['ohko', 'timed_ohko']: multiworld.can_take_damage[player] = False - if multiworld.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']: + if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']: multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Nothing', player), False) else: multiworld.push_item(multiworld.get_location('Ganon', player), ItemFactory('Triforce', player), False) - if multiworld.goal[player] == 'icerodhunt': - multiworld.progression_balancing[player].value = 0 - loc = multiworld.get_location('Turtle Rock - Boss', player) - multiworld.push_item(loc, ItemFactory('Triforce Piece', player), False) - multiworld.treasure_hunt_count[player] = 1 - if multiworld.boss_shuffle[player] != 'none': - if isinstance(multiworld.boss_shuffle[player].value, str) and 'turtle rock-' not in multiworld.boss_shuffle[player].value: - multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}') - elif isinstance(multiworld.boss_shuffle[player].value, int): - multiworld.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{multiworld.boss_shuffle[player].current_key}') - else: - logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}') - loc.event = True - loc.locked = True - itemdiff = difficulties[multiworld.difficulty[player]] - itempool = [] - itempool.extend(itemdiff.alwaysitems) - itempool.remove('Ice Rod') - - itempool.extend(['Single Arrow', 'Sanctuary Heart Container']) - itempool.extend(['Boss Heart Container'] * itemdiff.boss_heart_container_limit) - itempool.extend(['Piece of Heart'] * itemdiff.heart_piece_limit) - itempool.extend(itemdiff.bottles) - itempool.extend(itemdiff.basicbow) - itempool.extend(itemdiff.basicarmor) - if not multiworld.swordless[player]: - itempool.extend(itemdiff.basicsword) - itempool.extend(itemdiff.basicmagic) - itempool.extend(itemdiff.basicglove) - itempool.extend(itemdiff.basicshield) - itempool.extend(itemdiff.legacyinsanity) - itempool.extend(['Rupees (300)'] * 34) - itempool.extend(['Bombs (10)'] * 5) - itempool.extend(['Arrows (10)'] * 7) - if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: - itempool.extend(itemdiff.universal_keys) - - for item in itempool: - multiworld.push_precollected(ItemFactory(item, player)) - - if multiworld.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: + if multiworld.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: region = multiworld.get_region('Light World', player) loc = ALttPLocation(player, "Murahdahla", parent=region) @@ -308,7 +266,8 @@ def generate_itempool(world): ('Missing Smith', 'Return Smith'), ('Floodgate', 'Open Floodgate'), ('Agahnim 1', 'Beat Agahnim 1'), - ('Flute Activation Spot', 'Activated Flute') + ('Flute Activation Spot', 'Activated Flute'), + ('Capacity Upgrade Shop', 'Capacity Upgrade Shop') ] for location_name, event_name in event_pairs: location = multiworld.get_location(location_name, player) @@ -340,17 +299,31 @@ def generate_itempool(world): if not found_sword: found_sword = True possible_weapons.append(item) - if item in ['Progressive Bow', 'Bow'] and not found_bow: + elif item in ['Progressive Bow', 'Bow'] and not found_bow: found_bow = True possible_weapons.append(item) - if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: + elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) + elif (item == 'Bombs (10)' and (not multiworld.bombless_start[player]) and item not in + possible_weapons): + possible_weapons.append(item) + elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and multiworld.bombless_start[player] and item + not in possible_weapons): + possible_weapons.append(item) + starting_weapon = multiworld.random.choice(possible_weapons) placed_items["Link's Uncle"] = starting_weapon pool.remove(starting_weapon) - if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']: - multiworld.escape_assist[player].append('bombs') + if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)', + 'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']): + if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]: + if 'Bow' in placed_items["Link's Uncle"]: + multiworld.escape_assist[player].append('arrows') + elif 'Cane' in placed_items["Link's Uncle"]: + multiworld.escape_assist[player].append('magic') + else: + multiworld.escape_assist[player].append('bombs') for (location, item) in placed_items.items(): multiworld.get_location(location, player).place_locked_item(ItemFactory(item, player)) @@ -377,7 +350,7 @@ def generate_itempool(world): for key_loc in key_drop_data: key_data = key_drop_data[key_loc] drop_item = ItemFactory(key_data[3], player) - if multiworld.goal[player] == 'icerodhunt' or not multiworld.key_drop_shuffle[player]: + if not multiworld.key_drop_shuffle[player]: if drop_item in dungeon_items: dungeon_items.remove(drop_item) else: @@ -391,88 +364,151 @@ def generate_itempool(world): world.dungeons[dungeon].small_keys.remove(drop_item) elif world.dungeons[dungeon].big_key is not None and world.dungeons[dungeon].big_key == drop_item: world.dungeons[dungeon].big_key = None - if not multiworld.key_drop_shuffle[player]: - # key drop item was removed from the pool because key drop shuffle is off - # and it will now place the removed key into its original location + loc = multiworld.get_location(key_loc, player) loc.place_locked_item(drop_item) loc.address = None - elif multiworld.goal[player] == 'icerodhunt': - # key drop item removed because of icerodhunt - multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) - multiworld.push_precollected(drop_item) - elif "Small" in key_data[3] and multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: # key drop shuffle and universal keys are on. Add universal keys in place of key drop keys. - multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Small Key (Universal)'), player)) + multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), player)) dungeon_item_replacements = sum(difficulties[multiworld.difficulty[player]].extras, []) * 2 multiworld.random.shuffle(dungeon_item_replacements) - if multiworld.goal[player] == 'icerodhunt': - for item in dungeon_items: - multiworld.itempool.append(ItemFactory(GetBeemizerItem(multiworld, player, 'Nothing'), player)) + + for x in range(len(dungeon_items)-1, -1, -1): + item = dungeon_items[x] + if ((multiworld.small_key_shuffle[player] == small_key_shuffle.option_start_with and item.type == 'SmallKey') + or (multiworld.big_key_shuffle[player] == big_key_shuffle.option_start_with and item.type == 'BigKey') + or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') + or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): + dungeon_items.pop(x) multiworld.push_precollected(item) + multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) + multiworld.itempool.extend([item for item in dungeon_items]) + + set_up_shops(multiworld, player) + + if multiworld.retro_bow[player]: + shop_items = 0 + shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if + shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if + location.shop_slot is not None] + for location in shop_locations: + if location.shop.inventory[location.shop_slot]["item"] == "Single Arrow": + location.place_locked_item(ItemFactory("Single Arrow", player)) + else: + shop_items += 1 else: - for x in range(len(dungeon_items)-1, -1, -1): - item = dungeon_items[x] - if ((multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') - or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') - or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') - or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): - dungeon_items.pop(x) - multiworld.push_precollected(item) - multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) - multiworld.itempool.extend([item for item in dungeon_items]) - # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) - # rather than making all hearts/heart pieces progression items (which slows down generation considerably) - # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) - if multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0): - next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression - elif multiworld.goal[player] != 'icerodhunt' and multiworld.difficulty[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4): - adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') - for i in range(4): - next(adv_heart_pieces).classification = ItemClassification.progression - - - progressionitems = [] - nonprogressionitems = [] - for item in items: - if item.advancement or item.type: - progressionitems.append(item) + shop_items = min(multiworld.shop_item_slots[player], 30 if multiworld.include_witch_hut[player] else 27) + + if multiworld.shuffle_capacity_upgrades[player]: + shop_items += 2 + chance_100 = int(multiworld.retro_bow[player]) * 0.25 + int( + multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal) * 0.5 + for _ in range(shop_items): + if multiworld.random.random() < chance_100: + items.append(ItemFactory(GetBeemizerItem(multiworld, player, "Rupees (100)"), player)) else: - nonprogressionitems.append(GetBeemizerItem(multiworld, item.player, item)) - multiworld.random.shuffle(nonprogressionitems) - - if additional_triforce_pieces: - if additional_triforce_pieces > len(nonprogressionitems): - raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player " - f"{multiworld.get_player_name(player)}.") - progressionitems += [ItemFactory("Triforce Piece", player) for _ in range(additional_triforce_pieces)] - nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool - nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] - multiworld.random.shuffle(nonprogressionitems) - - # shuffle medallions - if multiworld.required_medallions[player][0] == "random": - mm_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos']) - else: - mm_medallion = multiworld.required_medallions[player][0] - if multiworld.required_medallions[player][1] == "random": - tr_medallion = multiworld.random.choice(['Ether', 'Quake', 'Bombos']) + items.append(ItemFactory(GetBeemizerItem(multiworld, player, "Rupees (50)"), player)) + + multiworld.random.shuffle(items) + pool_count = len(items) + new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)] + if multiworld.shuffle_capacity_upgrades[player] or multiworld.bombless_start[player]: + progressive = multiworld.progressive[player] + progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on' + if multiworld.shuffle_capacity_upgrades[player] == "on_combined": + new_items.append("Bomb Upgrade (50)") + elif multiworld.shuffle_capacity_upgrades[player] == "on": + new_items += ["Bomb Upgrade (+5)"] * 6 + new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") + if multiworld.shuffle_capacity_upgrades[player] != "on_combined" and multiworld.bombless_start[player]: + new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") + + if multiworld.shuffle_capacity_upgrades[player] and not multiworld.retro_bow[player]: + if multiworld.shuffle_capacity_upgrades[player] == "on_combined": + new_items += ["Arrow Upgrade (70)"] + else: + new_items += ["Arrow Upgrade (+5)"] * 6 + new_items.append("Arrow Upgrade (+5)" if progressive else "Arrow Upgrade (+10)") + + items += [ItemFactory(item, player) for item in new_items] + removed_filler = [] + + multiworld.random.shuffle(items) # Decide what gets tossed randomly. + + while len(items) > pool_count: + for i, item in enumerate(items): + if item.classification in (ItemClassification.filler, ItemClassification.trap): + removed_filler.append(items.pop(i)) + break + else: + # no more junk to remove, condense progressive items + def condense_items(items, small_item, big_item, rem, add): + small_item = ItemFactory(small_item, player) + # while (len(items) >= pool_count + rem - 1 # minus 1 to account for the replacement item + # and items.count(small_item) >= rem): + if items.count(small_item) >= rem: + for _ in range(rem): + items.remove(small_item) + removed_filler.append(ItemFactory(small_item.name, player)) + items += [ItemFactory(big_item, player) for _ in range(add)] + return True + return False + + def cut_item(items, item_to_cut, minimum_items): + item_to_cut = ItemFactory(item_to_cut, player) + if items.count(item_to_cut) > minimum_items: + items.remove(item_to_cut) + removed_filler.append(ItemFactory(item_to_cut.name, player)) + return True + return False + + while len(items) > pool_count: + items_were_cut = False + for reduce_item in items_reduction_table: + if len(items) <= pool_count: + break + if len(reduce_item) == 2: + items_were_cut = items_were_cut or cut_item(items, *reduce_item) + elif len(reduce_item) == 4: + items_were_cut = items_were_cut or condense_items(items, *reduce_item) + elif len(reduce_item) == 1: # Bottles + bottles = [item for item in items if item.name in item_name_groups["Bottles"]] + if len(bottles) > 4: + bottle = multiworld.random.choice(bottles) + items.remove(bottle) + removed_filler.append(bottle) + items_were_cut = True + assert items_were_cut, f"Failed to limit item pool size for player {player}" + if len(items) < pool_count: + items += removed_filler[len(items) - pool_count:] + + if multiworld.randomize_cost_types[player]: + # Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic + for item in items: + if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart") + or "Arrow Upgrade" in item.name): + item.classification = ItemClassification.progression else: - tr_medallion = multiworld.required_medallions[player][1] - multiworld.required_medallions[player] = (mm_medallion, tr_medallion) + # Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives) + # rather than making all hearts/heart pieces progression items (which slows down generation considerably) + # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) + if multiworld.item_pool[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0): + next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression + elif multiworld.item_pool[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4): + adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') + for i in range(4): + next(adv_heart_pieces).classification = ItemClassification.progression + + multiworld.required_medallions[player] = (multiworld.misery_mire_medallion[player].current_key.title(), + multiworld.turtle_rock_medallion[player].current_key.title()) place_bosses(world) - set_up_shops(multiworld, player) - - if multiworld.shop_shuffle[player]: - shuffle_shops(multiworld, nonprogressionitems, player) - multiworld.itempool += progressionitems + nonprogressionitems + multiworld.itempool += items if multiworld.retro_caves[player]: set_up_take_anys(multiworld, player) # depends on world.itempool to be set - # set_up_take_anys needs to run first - create_dynamic_shop_locations(multiworld, player) take_any_locations = { @@ -516,9 +552,14 @@ def set_up_take_anys(world, player): sword = world.random.choice(swords) world.itempool.remove(sword) world.itempool.append(ItemFactory('Rupees (20)', player)) - old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True) + old_man_take_any.shop.add_inventory(0, sword.name, 0, 0) + loc_name = "Old Man Sword Cave" + location = ALttPLocation(player, loc_name, shop_table_by_location[loc_name], parent=old_man_take_any) + location.shop_slot = 0 + old_man_take_any.locations.append(location) + location.place_locked_item(sword) else: - old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=True) + old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0) for num in range(4): take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, world) @@ -532,18 +573,22 @@ def set_up_take_anys(world, player): take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1) world.shops.append(take_any.shop) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) - take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True) + take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) + location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any) + location.shop_slot = 1 + take_any.locations.append(location) + location.place_locked_item(ItemFactory("Boss Heart Container", player)) def get_pool_core(world, player: int): - shuffle = world.shuffle[player] - difficulty = world.difficulty[player] - timer = world.timer[player] - goal = world.goal[player] - mode = world.mode[player] + shuffle = world.entrance_shuffle[player].current_key + difficulty = world.item_pool[player].current_key + timer = world.timer[player].current_key + goal = world.goal[player].current_key + mode = world.mode[player].current_key swordless = world.swordless[player] retro_bow = world.retro_bow[player] - logic = world.logic[player] + logic = world.glitches_required[player] pool = [] placed_items = {} @@ -552,7 +597,7 @@ def get_pool_core(world, player: int): treasure_hunt_count = None treasure_hunt_icon = None - diff = ice_rod_hunt_difficulties[difficulty] if goal == 'icerodhunt' else difficulties[difficulty] + diff = difficulties[difficulty] pool.extend(diff.alwaysitems) def place_item(loc, item): @@ -560,7 +605,7 @@ def place_item(loc, item): placed_items[loc] = item # provide boots to major glitch dependent seeds - if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': + if logic in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.glitch_boots[player]: precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') @@ -611,7 +656,7 @@ def place_item(loc, item): if want_progressives(world.random): pool.extend(diff.progressivebow) world.worlds[player].has_progressive_bows = True - elif (swordless or logic == 'noglitches') and goal != 'icerodhunt': + elif (swordless or logic == 'no_glitches'): swordless_bows = ['Bow', 'Silver Bow'] if difficulty == "easy": swordless_bows *= 2 @@ -627,21 +672,32 @@ def place_item(loc, item): extraitems = total_items_to_place - len(pool) - len(placed_items) - if timer in ['timed', 'timed-countdown']: + if timer in ['timed', 'timed_countdown']: pool.extend(diff.timedother) extraitems -= len(diff.timedother) clock_mode = 'stopwatch' if timer == 'timed' else 'countdown' - elif timer == 'timed-ohko': + elif timer == 'timed_ohko': pool.extend(diff.timedohko) extraitems -= len(diff.timedohko) clock_mode = 'countdown-ohko' additional_pieces_to_place = 0 - if 'triforcehunt' in goal: - pieces_in_core = min(extraitems, world.triforce_pieces_available[player]) - additional_pieces_to_place = world.triforce_pieces_available[player] - pieces_in_core + if 'triforce_hunt' in goal: + + if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra: + triforce_pieces = world.triforce_pieces_available[player].value + world.triforce_pieces_extra[player].value + elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage: + percentage = float(max(100, world.triforce_pieces_percentage[player].value)) / 100 + triforce_pieces = int(round(world.triforce_pieces_required[player].value * percentage, 0)) + else: # available + triforce_pieces = world.triforce_pieces_available[player].value + + triforce_pieces = max(triforce_pieces, world.triforce_pieces_required[player].value) + + pieces_in_core = min(extraitems, triforce_pieces) + additional_pieces_to_place = triforce_pieces - pieces_in_core pool.extend(["Triforce Piece"] * pieces_in_core) extraitems -= pieces_in_core - treasure_hunt_count = world.triforce_pieces_required[player] + treasure_hunt_count = world.triforce_pieces_required[player].value treasure_hunt_icon = 'Triforce Piece' for extra in diff.extras: @@ -659,12 +715,12 @@ def place_item(loc, item): pool.remove("Rupees (20)") if retro_bow: - replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'} + replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'} pool = ['Rupees (5)' if item in replace else item for item in pool] - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if world.small_key_shuffle[player] == small_key_shuffle.option_universal: pool.extend(diff.universal_keys) if mode == 'standard': - if world.key_drop_shuffle[player] and world.goal[player] != 'icerodhunt': + if world.key_drop_shuffle[player]: key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop'] key_location = world.random.choice(key_locations) key_locations.remove(key_location) @@ -688,8 +744,8 @@ def place_item(loc, item): def make_custom_item_pool(world, player): - shuffle = world.shuffle[player] - difficulty = world.difficulty[player] + shuffle = world.entrance_shuffle[player] + difficulty = world.item_pool[player] timer = world.timer[player] goal = world.goal[player] mode = world.mode[player] @@ -798,9 +854,9 @@ def place_item(loc, item): treasure_hunt_count = world.triforce_pieces_required[player] treasure_hunt_icon = 'Triforce Piece' - if timer in ['display', 'timed', 'timed-countdown']: - clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch' - elif timer == 'timed-ohko': + if timer in ['display', 'timed', 'timed_countdown']: + clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch' + elif timer == 'timed_ohko': clock_mode = 'countdown-ohko' elif timer == 'ohko': clock_mode = 'ohko' @@ -810,7 +866,7 @@ def place_item(loc, item): itemtotal = itemtotal + 1 if mode == 'standard': - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if world.small_key_shuffle[player] == small_key_shuffle.option_universal: key_location = world.random.choice( ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) @@ -833,7 +889,7 @@ def place_item(loc, item): pool.extend(['Magic Mirror'] * customitemarray[22]) pool.extend(['Moon Pearl'] * customitemarray[28]) - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if world.small_key_shuffle[player] == small_key_shuffle.option_universal: itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode if world.key_drop_shuffle[player]: itemtotal = itemtotal - (len(key_drop_data) - 1) diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index 18f96b2ddb81..8e513552ad10 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -112,13 +112,15 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]: 'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'), - 'Arrow Upgrade (+10)': ItemData(IC.filler, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Arrow Upgrade (+5)': ItemData(IC.filler, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), 'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), - 'Bomb Upgrade (+10)': ItemData(IC.filler, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Bomb Upgrade (+5)': ItemData(IC.filler, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+10)': ItemData(IC.progression, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+5)': ItemData(IC.progression, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (50)': ItemData(IC.progression, None, 0x4C, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), 'Blue Mail': ItemData(IC.useful, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'), 'Red Mail': ItemData(IC.useful, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'), 'Progressive Mail': ItemData(IC.useful, None, 0x60, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), @@ -222,6 +224,7 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]: 'Return Smith': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), 'Pick Up Purple Chest': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), 'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), + 'Capacity Upgrade Shop': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), } item_init_table = {name: data.as_init_dict() for name, data in item_table.items()} @@ -287,5 +290,5 @@ def as_init_dict(self) -> typing.Dict[str, typing.Any]: item_table[name].classification in {IC.progression, IC.progression_skip_balancing}} item_name_groups['Progression Items'] = progression_items item_name_groups['Non Progression Items'] = everything - progression_items - +item_name_groups['Upgrades'] = {name for name in everything if 'Upgrade' in name} trap_replaceable = item_name_groups['Rupees'] | {'Arrows (10)', 'Single Bomb', 'Bombs (3)', 'Bombs (10)', 'Nothing'} diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index a89a9adb83a7..ed6af6dd674f 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,10 +1,18 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses - - -class Logic(Choice): +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\ + FreeText + + +class GlitchesRequired(Choice): + """Determine the logic required to complete the seed + None: No glitches required + Minor Glitches: Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic + Overworld Glitches: Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches + Hybrid Major Glitches: In addition to overworld glitches, also requires underworld clips between dungeons. + No Logic: Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.""" + display_name = "Glitches Required" option_no_glitches = 0 option_minor_glitches = 1 option_overworld_glitches = 2 @@ -12,20 +20,122 @@ class Logic(Choice): option_no_logic = 4 alias_owg = 2 alias_hmg = 3 + alias_none = 0 -class Objective(Choice): - option_crystals = 0 - # option_pendants = 1 - option_triforce_pieces = 2 - option_pedestal = 3 - option_bingo = 4 +class DarkRoomLogic(Choice): + """Logic for unlit dark rooms. Lamp: require the Lamp for these rooms to be considered accessible. + Torches: in addition to lamp, allow the fire rod and presence of easily accessible torches for access. + None: all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness.""" + display_name = "Dark Room Logic" + option_lamp = 0 + option_torches = 1 + option_none = 2 + default = 0 class Goal(Choice): - option_kill_ganon = 0 - option_kill_ganon_and_gt_agahnim = 1 - option_hand_in = 2 + """Ganon: Climb GT, defeat Agahnim 2, and then kill Ganon + Crystals: Only killing Ganon is required. However, items may still be placed in GT + Bosses: Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2) + Pedestal: Pull the Triforce from the Master Sword pedestal + Ganon Pedestal: Pull the Master Sword pedestal, then kill Ganon + Triforce Hunt: Collect Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle + Local Triforce Hunt: Collect Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle + Ganon Triforce Hunt: Collect Triforce pieces spread throughout the worlds, then kill Ganon + Local Ganon Triforce Hunt: Collect Triforce pieces spread throughout your world, then kill Ganon + Ice Rod Hunt: You start with everything except Ice Rod. Find the Ice rod, then kill Trinexx at Turtle rock.""" + display_name = "Goal" + default = 0 + option_ganon = 0 + option_crystals = 1 + option_bosses = 2 + option_pedestal = 3 + option_ganon_pedestal = 4 + option_triforce_hunt = 5 + option_local_triforce_hunt = 6 + option_ganon_triforce_hunt = 7 + option_local_ganon_triforce_hunt = 8 + + +class EntranceShuffle(Choice): + """Dungeons Simple: Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon. + Dungeons Full: Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world. + Dungeons Crossed: like dungeons_full, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal. + Simple: Entrances are grouped together before being randomized. Interiors with two entrances are grouped shuffled together with each other, + and Death Mountain entrances are shuffled only on Death Mountain. Dungeons are swapped entirely. + Restricted: Like Simple, but single entrance interiors, multi entrance interiors, and Death Mountain interior entrances are all shuffled with each other. + Full: Like Restricted, but all Dungeon entrances are shuffled with all non-Dungeon entrances. + Crossed: Like Full, but interiors with multiple entrances are no longer confined to the same world, which may allow crossing worlds. + Insanity: Like Crossed, but entrances and exits may be decoupled from each other, so that leaving through an exit may not return you to the entrance you entered from.""" + display_name = "Entrance Shuffle" + default = 0 + alias_none = 0 + option_vanilla = 0 + option_dungeons_simple = 1 + option_dungeons_full = 2 + option_dungeons_crossed = 3 + option_simple = 4 + option_restricted = 5 + option_full = 6 + option_crossed = 7 + option_insanity = 8 + alias_dungeonssimple = 1 + alias_dungeonsfull = 2 + alias_dungeonscrossed = 3 + + +class EntranceShuffleSeed(FreeText): + """You can specify a number to use as an entrance shuffle seed, or a group name. Everyone with the same group name + will get the same entrance shuffle result as long as their Entrance Shuffle, Mode, Retro Caves, and Glitches + Required options are the same.""" + default = "random" + display_name = "Entrance Shuffle Seed" + + +class TriforcePiecesMode(Choice): + """Determine how to calculate the extra available triforce pieces. + Extra: available = triforce_pieces_extra + triforce_pieces_required + Percentage: available = (triforce_pieces_percentage /100) * triforce_pieces_required + Available: available = triforce_pieces_available""" + display_name = "Triforce Pieces Mode" + default = 2 + option_extra = 0 + option_percentage = 1 + option_available = 2 + + +class TriforcePiecesPercentage(Range): + """Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.""" + display_name = "Triforce Pieces Percentage" + range_start = 100 + range_end = 1000 + default = 150 + + +class TriforcePiecesAvailable(Range): + """Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1""" + display_name = "Triforce Pieces Available" + range_start = 1 + range_end = 90 + default = 30 + + +class TriforcePiecesRequired(Range): + """Set to how many out of X triforce pieces you need to win the game in a triforce hunt. + Default is 20. Max is 90, Min is 1.""" + display_name = "Triforce Pieces Required" + range_start = 1 + range_end = 90 + default = 20 + + +class TriforcePiecesExtra(Range): + """Set to how many extra triforces pieces are available to collect in the world.""" + display_name = "Triforce Pieces Extra" + range_start = 0 + range_end = 89 + default = 10 class OpenPyramid(Choice): @@ -44,10 +154,10 @@ class OpenPyramid(Choice): def to_bool(self, world: MultiWorld, player: int) -> bool: if self.value == self.option_goal: - return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + return world.goal[player] in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} elif self.value == self.option_auto: - return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \ - and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not + return world.goal[player] in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \ + and (world.entrance_shuffle[player] in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not world.shuffle_ganon) elif self.value == self.option_open: return True @@ -76,13 +186,13 @@ def hints_useful(self): return self.value in {1, 2, 3, 4} -class bigkey_shuffle(DungeonItem): +class big_key_shuffle(DungeonItem): """Big Key Placement""" item_name_group = "Big Keys" display_name = "Big Key Shuffle" -class smallkey_shuffle(DungeonItem): +class small_key_shuffle(DungeonItem): """Small Key Placement""" option_universal = 5 item_name_group = "Small Keys" @@ -107,6 +217,149 @@ class key_drop_shuffle(Toggle): display_name = "Key Drop Shuffle" + +class DungeonCounters(Choice): + """On: Always display amount of items checked in a dungeon. Pickup: Show when compass is picked up. + Default: Show when compass is picked up if the compass itself is shuffled. Off: Never show item count in dungeons.""" + display_name = "Dungeon Counters" + default = 1 + option_on = 0 + option_pickup = 1 + option_default = 2 + option_off = 4 + + +class Mode(Choice): + """Standard: Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary + Open: Begin the game from your choice of Link's House or the Sanctuary + Inverted: Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered""" + option_standard = 0 + option_open = 1 + option_inverted = 2 + default = 1 + display_name = "Mode" + + +class ItemPool(Choice): + """Easy: Doubled upgrades, progressives, and etc. Normal: Item availability remains unchanged from vanilla game. + Hard: Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless). + Expert: Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless).""" + display_name = "Item Pool" + default = 1 + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_expert = 3 + + +class ItemFunctionality(Choice): + """Easy: Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere. + Normal: Vanilla item functionality + Hard: Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon) + Expert: Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)""" + display_name = "Item Functionality" + default = 1 + option_easy = 0 + option_normal = 1 + option_hard = 2 + option_expert = 3 + + +class EnemyHealth(Choice): + """Default: Vanilla enemy HP. Easy: Enemies have reduced health. Hard: Enemies have increased health. + Expert: Enemies have greatly increased health.""" + display_name = "Enemy Health" + default = 1 + option_easy = 0 + option_default = 1 + option_hard = 2 + option_expert = 3 + + +class EnemyDamage(Choice): + """Default: Vanilla enemy damage. Shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps. + Chaos: Enemies deal 0 to 8 hearts and armor just reshuffles the damage.""" + display_name = "Enemy Damage" + default = 0 + option_default = 0 + option_shuffled = 2 + option_chaos = 3 + + +class ShufflePrizes(Choice): + """Shuffle "general" prize packs, as in enemy, tree pull, dig etc.; "bonk" prizes; or both.""" + display_name = "Shuffle Prizes" + default = 1 + option_off = 0 + option_general = 1 + option_bonk = 2 + option_both = 3 + + +class Medallion(Choice): + default = "random" + option_ether = 0 + option_bombos = 1 + option_quake = 2 + + +class MiseryMireMedallion(Medallion): + """Required medallion to open Misery Mire front entrance.""" + display_name = "Misery Mire Medallion" + + +class TurtleRockMedallion(Medallion): + """Required medallion to open Turtle Rock front entrance.""" + display_name = "Turtle Rock Medallion" + + +class Timer(Choice): + """None: No timer will be displayed. OHKO: Timer always at zero. Permanent OHKO. + Timed: Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end. + Timed OHKO: Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit. + Timed Countdown: Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though. + Display: Displays a timer, but otherwise does not affect gameplay or the item pool.""" + display_name = "Timer" + option_none = 0 + option_timed = 1 + option_timed_ohko = 2 + option_ohko = 3 + option_timed_countdown = 4 + option_display = 5 + default = 0 + + +class CountdownStartTime(Range): + """For Timed OHKO and Timed Countdown timer modes, the amount of time in minutes to start with.""" + display_name = "Countdown Start Time" + range_start = 0 + range_end = 480 + default = 10 + + +class ClockTime(Range): + range_start = -60 + range_end = 60 + + +class RedClockTime(ClockTime): + """For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock.""" + display_name = "Red Clock Time" + default = -2 + + +class BlueClockTime(ClockTime): + """For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock.""" + display_name = "Blue Clock Time" + default = 2 + + +class GreenClockTime(ClockTime): + """For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock.""" + display_name = "Green Clock Time" + default = 4 + + class Crystals(Range): range_start = 0 range_end = 7 @@ -137,18 +390,52 @@ class ShopItemSlots(Range): range_end = 30 +class RandomizeShopInventories(Choice): + """Generate new default inventories for overworld/underworld shops, and unique shops; or each shop independently""" + display_name = "Randomize Shop Inventories" + default = 0 + option_default = 0 + option_randomize_by_shop_type = 1 + option_randomize_each = 2 + + +class ShuffleShopInventories(Toggle): + """Shuffle default inventories of the shops around""" + display_name = "Shuffle Shop Inventories" + + +class RandomizeShopPrices(Toggle): + """Randomize the prices of the items in shop inventories""" + display_name = "Randomize Shop Prices" + + +class RandomizeCostTypes(Toggle): + """Prices of the items in shop inventories may cost hearts, arrow, or bombs instead of rupees""" + display_name = "Randomize Cost Types" + + class ShopPriceModifier(Range): """Percentage modifier for shuffled item prices in shops""" - display_name = "Shop Price Cost Percent" + display_name = "Shop Price Modifier" range_start = 0 default = 100 range_end = 400 -class WorldState(Choice): - option_standard = 1 - option_open = 0 - option_inverted = 2 +class IncludeWitchHut(Toggle): + """Consider witch's hut like any other shop and shuffle/randomize it too""" + display_name = "Include Witch's Hut" + + +class ShuffleCapacityUpgrades(Choice): + """Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld). + On Combined will shuffle only a single bomb upgrade and arrow upgrade each which bring you to the maximum capacity.""" + display_name = "Shuffle Capacity Upgrades" + option_off = 0 + option_on = 1 + option_on_combined = 2 + alias_false = 0 + alias_true = 1 class LTTPBosses(PlandoBosses): @@ -236,6 +523,11 @@ class Swordless(Toggle): display_name = "Swordless" +class BomblessStart(Toggle): + """Start with a max of 0 bombs available, requiring Bomb Upgrade items in order to use bombs""" + display_name = "Bombless Start" + + # Might be a decent idea to split "Bow" into its own option with choices of # Defer to Progressive Option (default), Progressive, Non-Progressive, Bow + Silvers, Retro class RetroBow(Toggle): @@ -433,29 +725,66 @@ class AllowCollect(Toggle): alttp_options: typing.Dict[str, type(Option)] = { + "start_inventory_from_pool": StartInventoryPool, + "goal": Goal, + "mode": Mode, + "glitches_required": GlitchesRequired, + "dark_room_logic": DarkRoomLogic, + "open_pyramid": OpenPyramid, "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, - "open_pyramid": OpenPyramid, - "bigkey_shuffle": bigkey_shuffle, - "smallkey_shuffle": smallkey_shuffle, + "triforce_pieces_mode": TriforcePiecesMode, + "triforce_pieces_percentage": TriforcePiecesPercentage, + "triforce_pieces_required": TriforcePiecesRequired, + "triforce_pieces_available": TriforcePiecesAvailable, + "triforce_pieces_extra": TriforcePiecesExtra, + "entrance_shuffle": EntranceShuffle, + "entrance_shuffle_seed": EntranceShuffleSeed, + "big_key_shuffle": big_key_shuffle, + "small_key_shuffle": small_key_shuffle, "key_drop_shuffle": key_drop_shuffle, "compass_shuffle": compass_shuffle, "map_shuffle": map_shuffle, + "restrict_dungeon_item_on_boss": RestrictBossItem, + "item_pool": ItemPool, + "item_functionality": ItemFunctionality, + "enemy_health": EnemyHealth, + "enemy_damage": EnemyDamage, "progressive": Progressive, "swordless": Swordless, + "dungeon_counters": DungeonCounters, "retro_bow": RetroBow, "retro_caves": RetroCaves, "hints": Hints, "scams": Scams, - "restrict_dungeon_item_on_boss": RestrictBossItem, "boss_shuffle": LTTPBosses, "pot_shuffle": PotShuffle, "enemy_shuffle": EnemyShuffle, "killable_thieves": KillableThieves, "bush_shuffle": BushShuffle, "shop_item_slots": ShopItemSlots, + "randomize_shop_inventories": RandomizeShopInventories, + "shuffle_shop_inventories": ShuffleShopInventories, + "include_witch_hut": IncludeWitchHut, + "randomize_shop_prices": RandomizeShopPrices, + "randomize_cost_types": RandomizeCostTypes, "shop_price_modifier": ShopPriceModifier, + "shuffle_capacity_upgrades": ShuffleCapacityUpgrades, + "bombless_start": BomblessStart, + "shuffle_prizes": ShufflePrizes, "tile_shuffle": TileShuffle, + "misery_mire_medallion": MiseryMireMedallion, + "turtle_rock_medallion": TurtleRockMedallion, + "glitch_boots": GlitchBoots, + "beemizer_total_chance": BeemizerTotalChance, + "beemizer_trap_chance": BeemizerTrapChance, + "timer": Timer, + "countdown_start_time": CountdownStartTime, + "red_clock_time": RedClockTime, + "blue_clock_time": BlueClockTime, + "green_clock_time": GreenClockTime, + "death_link": DeathLink, + "allow_collect": AllowCollect, "ow_palettes": OWPalette, "uw_palettes": UWPalette, "hud_palettes": HUDPalette, @@ -469,10 +798,4 @@ class AllowCollect(Toggle): "music": Music, "reduceflashing": ReduceFlashing, "triforcehud": TriforceHud, - "glitch_boots": GlitchBoots, - "beemizer_total_chance": BeemizerTotalChance, - "beemizer_trap_chance": BeemizerTrapChance, - "death_link": DeathLink, - "allow_collect": AllowCollect, - "start_inventory_from_pool": StartInventoryPool, } diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 0cc8a3d6a71f..dc3adb108af1 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -94,7 +94,7 @@ def create_regions(world, player): create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'), create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']), - create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'), + create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']), create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']), create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'), @@ -121,8 +121,9 @@ def create_regions(world, player): ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', - 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), + create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']), + create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + 'Sewers - Secret Room - Right']), create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']), create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), @@ -275,7 +276,9 @@ def create_regions(world, player): create_cave_region(world, player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], - ['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']), + ['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']), + create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + 'Hookshot Cave Bomb Wall (North)']), create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']), create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']), @@ -311,8 +314,8 @@ def create_regions(world, player): create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']), + create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), @@ -735,6 +738,7 @@ def mark_light_world_regions(world, player: int): 'Missing Smith': (None, None, False, None), 'Dark Blacksmith Ruins': (None, None, False, None), 'Flute Activation Spot': (None, None, False, None), + 'Capacity Upgrade Shop': (None, None, False, None), 'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'), 'Tower of Hera - Prize': ( diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index b80cec578a97..ff4947bb0198 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -4,7 +4,7 @@ import worlds.Files LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173" -RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f" +RANDOMIZERBASEHASH: str = "35d010bc148e0ea0ee68e81e330223f1" ROM_PLAYER_LIMIT: int = 255 import io @@ -36,7 +36,7 @@ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from .Items import ItemFactory, item_table, item_name_groups, progression_items from .EntranceShuffle import door_addresses -from .Options import smallkey_shuffle +from .Options import small_key_shuffle try: from maseya import z3pr @@ -294,7 +294,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): 'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value, 'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default', 'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[ - multiworld.enemy_health[player]], + multiworld.enemy_health[player].current_key], 'OHKO': False, 'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default', 'AllowEnemyZeroDamage': True, @@ -858,13 +858,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # Thanks to Zarby89 for originally finding these values # todo fix screen scrolling - if world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness_legacy'} and \ + if world.entrance_shuffle[player] != 'insanity' and \ exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \ - (world.logic[player] not in ['hybridglitches', 'nologic'] or + (world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic'] or exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}): # For exits that connot be reached from another, no need to apply offset fixes. rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else @@ -907,7 +907,9 @@ def credits_digit(num): if world.retro_caves[player]: # Old man cave and Take any caves will count towards collection rate. credits_total += 5 if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle. - credits_total += 30 if 'w' in world.shop_shuffle[player] else 27 + credits_total += 30 if world.include_witch_hut[player] else 27 + if world.shuffle_capacity_upgrades[player]: + credits_total += 2 rom.write_byte(0x187010, credits_total) # dynamic credits @@ -1059,7 +1061,7 @@ def credits_digit(num): # Set stun items rom.write_byte(0x180180, 0x03) # All standard items # Set overflow items for progressive equipment - if world.timer[player] in ['timed', 'timed-countdown', 'timed-ohko']: + if world.timer[player] in ['timed', 'timed_countdown', 'timed_ohko']: overflow_replacement = GREEN_CLOCK else: overflow_replacement = GREEN_TWENTY_RUPEES @@ -1079,7 +1081,7 @@ def credits_digit(num): difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code]) if difficulty.progressive_bow_limit < 2 and ( - world.swordless[player] or world.logic[player] == 'noglitches'): + world.swordless[player] or world.glitches_required[player] == 'no_glitches'): rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code]) rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup @@ -1095,7 +1097,7 @@ def credits_digit(num): prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee - if "g" in world.shuffle_prizes[player]: + if world.shuffle_prizes[player] in ("general", "both"): # shuffle prize packs prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, @@ -1157,7 +1159,7 @@ def chunk(l, n): byte = int(rom.read_byte(address)) rom.write_byte(address, prize_replacements.get(byte, byte)) - if "b" in world.shuffle_prizes[player]: + if world.shuffle_prizes[player] in ("bonk", "both"): # set bonk prizes bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, @@ -1274,7 +1276,7 @@ def chunk(l, n): rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed gametype = 0x04 # item - if world.shuffle[player] != 'vanilla': + if world.entrance_shuffle[player] != 'vanilla': gametype |= 0x02 # entrance if enemized: gametype |= 0x01 # enemizer @@ -1312,7 +1314,7 @@ def chunk(l, n): equip[0x36C] = 0x18 equip[0x36D] = 0x18 equip[0x379] = 0x68 - starting_max_bombs = 10 + starting_max_bombs = 0 if world.bombless_start[player] else 10 starting_max_arrows = 30 startingstate = CollectionState(world) @@ -1430,8 +1432,8 @@ def chunk(l, n): 'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8} rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300} - bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10} - arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} + bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10, 'Bomb Upgrade (50)': 50} + arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10, 'Arrow Upgrade (70)': 70} bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} arrows = {'Single Arrow': 1, 'Arrows (10)': 10} @@ -1498,7 +1500,7 @@ def chunk(l, n): rom.write_byte(0x3A96D, 0xF0 if world.mode[ player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader)) rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader)) - if 'u' in world.shop_shuffle[player]: + if world.shuffle_capacity_upgrades[player]: rom.write_bytes(0x180080, [5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10) else: @@ -1509,11 +1511,11 @@ def chunk(l, n): (0x02 if 'bombs' in world.escape_assist[player] else 0x00) | (0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist - if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt', 'icerodhunt']: + if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']: rom.write_byte(0x18003E, 0x01) # make ganon invincible - elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: + elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected - elif world.goal[player] in ['ganonpedestal']: + elif world.goal[player] in ['ganon_pedestal']: rom.write_byte(0x18003E, 0x06) elif world.goal[player] in ['bosses']: rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat @@ -1534,12 +1536,12 @@ def chunk(l, n): # c - enabled for inside compasses # s - enabled for inside small keys # block HC upstairs doors in rain state in standard mode - rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00) + rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.entrance_shuffle[player] != 'vanilla' else 0x00) - rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00) + rom.write_byte(0x18016A, 0x10 | ((0x01 if world.small_key_shuffle[player] else 0x00) | (0x02 if world.compass_shuffle[player] else 0x00) | (0x04 if world.map_shuffle[player] else 0x00) - | (0x08 if world.bigkey_shuffle[ + | (0x08 if world.big_key_shuffle[ player] else 0x00))) # free roaming item text boxes rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld @@ -1561,9 +1563,9 @@ def chunk(l, n): # b - Big Key # a - Small Key # - rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or - world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01) - | (0x02 if world.bigkey_shuffle[player] else 0x00) + rom.write_byte(0x180045, ((0x00 if (world.small_key_shuffle[player] == small_key_shuffle.option_original_dungeon or + world.small_key_shuffle[player] == small_key_shuffle.option_universal) else 0x01) + | (0x02 if world.big_key_shuffle[player] else 0x00) | (0x04 if world.map_shuffle[player] else 0x00) | (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu @@ -1595,8 +1597,8 @@ def get_reveal_bytes(itemName): rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[ player] else 0x0000) # Bomb Shop Reveal - rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[ - player] == smallkey_shuffle.option_universal else 0x00) # universal keys + rom.write_byte(0x180172, 0x01 if world.small_key_shuffle[ + player] == small_key_shuffle.option_universal else 0x00) # universal keys rom.write_byte(0x18637E, 0x01 if world.retro_bow[player] else 0x00) # Skip quiver in item shops once bought rom.write_byte(0x180175, 0x01 if world.retro_bow[player] else 0x00) # rupee bow rom.write_byte(0x180176, 0x0A if world.retro_bow[player] else 0x00) # wood arrow cost @@ -1613,9 +1615,9 @@ def get_reveal_bytes(itemName): rom.write_byte(0x180020, digging_game_rng) rom.write_byte(0xEFD95, digging_game_rng) rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills - rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix - rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.logic[ - player] == 'nologic' else 0x00) # disable glitching to Triforce from Ganons Room + rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix + rom.write_byte(0x186383, 0x01 if world.glitch_triforce or world.glitches_required[ + player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill # remove shield from uncle @@ -1660,8 +1662,8 @@ def get_reveal_bytes(itemName): 0x4F]) # allow smith into multi-entrance caves in appropriate shuffles - if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity', 'madness'] or ( - world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): + if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or ( + world.entrance_shuffle[player] == 'simple' and world.mode[player] == 'inverted'): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item @@ -1758,8 +1760,8 @@ def write_custom_shops(rom, world, player): if item is None: break if world.shop_item_slots[player] or shop.type == ShopType.TakeAny: - count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \ - shop.region.name != 'Capacity Upgrade' + count_shop = (shop.region.name != 'Potion Shop' or world.include_witch_hut[player]) and \ + (shop.region.name != 'Capacity Upgrade' or world.shuffle_capacity_upgrades[player]) rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0) if item['item'] == 'Single Arrow' and item['player'] == 0: arrow_mask |= 1 << index @@ -2201,7 +2203,7 @@ def write_strings(rom, world, player): tt.removeUnwantedText() # Let's keep this guy's text accurate to the shuffle setting. - if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' @@ -2255,7 +2257,7 @@ def hint_text(dest, ped_hint=False): entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) else: entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) - if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + if world.entrance_shuffle[player] in ['simple', 'restricted']: for entrance in all_entrances: if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( @@ -2265,9 +2267,9 @@ def hint_text(dest, ped_hint=False): break # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. entrances_to_hint.update(InconvenientOtherEntrances) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: hint_count = 0 - elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + elif world.entrance_shuffle[player] in ['simple', 'restricted']: hint_count = 2 else: hint_count = 4 @@ -2284,14 +2286,14 @@ def hint_text(dest, ped_hint=False): # Next we handle hints for randomly selected other entrances, # curating the selection intelligently based on shuffle. - if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: + if world.entrance_shuffle[player] not in ['simple', 'restricted']: entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(DungeonEntrances) if world.mode[player] == 'inverted': entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'}) else: entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) - elif world.shuffle[player] == 'restricted': + elif world.entrance_shuffle[player] == 'restricted': entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(OtherEntrances) if world.mode[player] == 'inverted': @@ -2301,15 +2303,15 @@ def hint_text(dest, ped_hint=False): else: entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) - if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: + if world.entrance_shuffle[player] != 'insanity': entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: if world.mode[player] == 'inverted': entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) - hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 0 + hint_count = 4 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', + 'dungeons_crossed'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: if hint_count: @@ -2323,11 +2325,11 @@ def hint_text(dest, ped_hint=False): # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. locations_to_hint = InconvenientLocations.copy() - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: locations_to_hint.extend(InconvenientVanillaLocations) local_random.shuffle(locations_to_hint) - hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 5 + hint_count = 3 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', + 'dungeons_crossed'] else 5 for location in locations_to_hint[:hint_count]: if location == 'Swamp Left': if local_random.randint(0, 1): @@ -2381,16 +2383,16 @@ def hint_text(dest, ped_hint=False): # Lastly we write hints to show where certain interesting items are. items_to_hint = RelevantItems.copy() - if world.smallkey_shuffle[player].hints_useful: + if world.small_key_shuffle[player].hints_useful: items_to_hint |= item_name_groups["Small Keys"] - if world.bigkey_shuffle[player].hints_useful: + if world.big_key_shuffle[player].hints_useful: items_to_hint |= item_name_groups["Big Keys"] if world.hints[player] == "full": hint_count = len(hint_locations) # fill all remaining hint locations with Item hints. else: - hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 8 + hint_count = 5 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full', + 'dungeons_crossed'] else 8 hint_count = min(hint_count, len(items_to_hint), len(hint_locations)) if hint_count: locations = world.find_items_in_locations(items_to_hint, player, True) @@ -2417,7 +2419,7 @@ def hint_text(dest, ped_hint=False): tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or ( - world.swordless[player] or world.logic[player] == 'noglitches')): + world.swordless[player] or world.glitches_required[player] == 'no_glitches')): prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) world.per_slot_randoms[player].shuffle(prog_bow_locs) found_bow = False @@ -2448,7 +2450,7 @@ def hint_text(dest, ped_hint=False): if world.goal[player] == 'bosses': tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.' - elif world.goal[player] == 'ganonpedestal': + elif world.goal[player] == 'ganon_pedestal': tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.' elif world.goal[player] == "ganon": if world.crystals_needed_for_ganon[player] == 1: @@ -2456,14 +2458,6 @@ def hint_text(dest, ped_hint=False): else: tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \ f'have beaten Agahnim atop Ganons Tower' - elif world.goal[player] == "icerodhunt": - tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx, then talk to Murahdahla... Ganon is invincible!' - tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.' - tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' - tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ - "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ - "hidden in a hollow tree. " \ - "If you bring me the Triforce piece from Turtle Rock, I can reassemble it." else: if world.crystals_needed_for_ganon[player] == 1: tt['sign_ganon'] = 'You need a crystal to beat Ganon.' @@ -2478,10 +2472,10 @@ def hint_text(dest, ped_hint=False): tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] - if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']: + if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']: tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' - if world.goal[player] == 'triforcehunt' and world.players > 1: + if world.goal[player] == 'triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!' else: tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!' @@ -2504,17 +2498,17 @@ def hint_text(dest, ped_hint=False): tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' if world.treasure_hunt_count[player] > 1: - if world.goal[player] == 'ganontriforcehunt' and world.players > 1: + if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) - elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: + elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) else: - if world.goal[player] == 'ganontriforcehunt' and world.players > 1: + if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) - elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: + elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \ (world.treasure_hunt_count[player], world.triforce_pieces_available[player]) @@ -2614,12 +2608,12 @@ def set_inverted_mode(world, player, rom): rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof # the following bytes should only be written in vanilla # or they'll overwrite the randomizer's shuffles - if world.shuffle[player] == 'vanilla': + if world.entrance_shuffle[player] == 'vanilla': rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT rom.write_byte(0xDBB73 + 0x36, 0x24) rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0) rom.write_int16(0x15AEE + 2 * 0x25, 0x000C) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(0x15B8C, 0x6C) rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x52, 0x01) @@ -2677,7 +2671,7 @@ def set_inverted_mode(world, player, rom): rom.write_int16(snes_to_pc(0x02D9A6), 0x005A) rom.write_byte(snes_to_pc(0x02D9B3), 0x12) # keep the old man spawn point at old man house unless shuffle is vanilla - if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1) rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03]) @@ -2740,7 +2734,7 @@ def set_inverted_mode(world, player, rom): rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B]) rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance rom.write_int16(snes_to_pc(0x308320), 0x001B) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(snes_to_pc(0x308340), 0x7B) rom.write_int16(snes_to_pc(0x1af504), 0x148B) rom.write_int16(snes_to_pc(0x1af50c), 0x149B) @@ -2777,10 +2771,10 @@ def set_inverted_mode(world, player, rom): rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_byte(0xDBB73 + 0x35, 0x36) rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area rom.write_byte(0x15B8C + 0x37, 0x1B) rom.write_int16(0x15BDB + 2 * 0x37, 0x0418) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 8a04f87afa02..17061842dde9 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -9,25 +9,25 @@ from . import OverworldGlitchRules from .Bosses import GanonDefeatRule from .Items import ItemFactory, item_name_groups, item_table, progression_items -from .Options import smallkey_shuffle +from .Options import small_key_shuffle from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules from .Regions import LTTPRegionType, location_table from .StateHelpers import (can_extend_magic, can_kill_most_things, can_lift_heavy_rocks, can_lift_rocks, can_melt_things, can_retrieve_tablet, can_shoot_arrows, has_beam_sword, has_crystals, - has_fire_source, has_hearts, + has_fire_source, has_hearts, has_melee_weapon, has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, - has_triforce_pieces) + has_triforce_pieces, can_use_bombs, can_bomb_or_bonk) from .UnderworldGlitchRules import underworld_glitches_rules def set_rules(world): player = world.player world = world.multiworld - if world.logic[player] == 'nologic': + if world.glitches_required[player] == 'no_logic': if player == next(player_id for player_id in world.get_game_players("A Link to the Past") - if world.logic[player_id] == 'nologic'): # only warn one time + if world.glitches_required[player_id] == 'no_logic'): # only warn one time logging.info( 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') @@ -45,8 +45,8 @@ def set_rules(world): else: world.completion_condition[player] = lambda state: state.has('Triforce', player) - global_rules(world, player) dungeon_boss_rules(world, player) + global_rules(world, player) if world.mode[player] != 'inverted': default_rules(world, player) @@ -61,24 +61,24 @@ def set_rules(world): else: raise NotImplementedError(f'World state {world.mode[player]} is not implemented yet') - if world.logic[player] == 'noglitches': + if world.glitches_required[player] == 'no_glitches': no_glitches_rules(world, player) - elif world.logic[player] == 'owglitches': + elif world.glitches_required[player] == 'overworld_glitches': # Initially setting no_glitches_rules to set the baseline rules for some # entrances. The overworld_glitches_rules set is primarily additive. no_glitches_rules(world, player) fake_flipper_rules(world, player) overworld_glitches_rules(world, player) - elif world.logic[player] in ['hybridglitches', 'nologic']: + elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']: no_glitches_rules(world, player) fake_flipper_rules(world, player) overworld_glitches_rules(world, player) underworld_glitches_rules(world, player) - elif world.logic[player] == 'minorglitches': + elif world.glitches_required[player] == 'minor_glitches': no_glitches_rules(world, player) fake_flipper_rules(world, player) else: - raise NotImplementedError(f'Not implemented yet: Logic - {world.logic[player]}') + raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}') if world.goal[player] == 'bosses': # require all bosses to beat ganon @@ -89,7 +89,7 @@ def set_rules(world): if world.mode[player] != 'inverted': set_big_bomb_rules(world, player) - if world.logic[player] in {'owglitches', 'hybridglitches', 'nologic'} and world.shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}: + if world.glitches_required[player] in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.entrance_shuffle[player] not in {'insanity', 'insanity_legacy', 'madness'}: path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') else: @@ -97,18 +97,18 @@ def set_rules(world): # if swamp and dam have not been moved we require mirror for swamp palace # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. - if not world.swamp_patch_required[player] and world.logic[player] not in ['hybridglitches', 'nologic']: + if not world.swamp_patch_required[player] and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # GT Entrance may be required for Turtle Rock for OWG and < 7 required ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player) - if world.crystals_needed_for_gt[player] == 7 and not (world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and world.mode[player] != 'inverted'): + if world.crystals_needed_for_gt[player] == 7 and not (world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and world.mode[player] != 'inverted'): set_rule(ganons_tower, lambda state: False) set_trock_key_rules(world, player) set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player)) - if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']: + if world.mode[player] != 'inverted' and world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') set_bunny_rules(world, player, world.mode[player] == 'inverted') @@ -139,6 +139,7 @@ def set_defeat_dungeon_boss_rule(location): add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + def set_always_allow(spot, rule): spot.always_allow = rule @@ -184,6 +185,7 @@ def dungeon_boss_rules(world, player): for location in boss_locations: set_defeat_dungeon_boss_rule(world.get_location(location, player)) + def global_rules(world, player): # ganon can only carry triforce add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player) @@ -213,14 +215,61 @@ def global_rules(world, player): set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player)) - set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith + set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player)) set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) set_rule(world.get_location('Library', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)) + + if world.enemy_shuffle[player]: + set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and + can_kill_most_things(state, player, 4)) + else: + set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) + and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4)) + or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player) + or has_beam_sword(state, player))) + set_rule(world.get_location('Sahasrahla', player), lambda state: state.has('Green Pendant', player)) + set_rule(world.get_location('Aginah\'s Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Blind\'s Hideout - Top', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Chicken House', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Kakariko Well - Top', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Graveyard Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Sahasrahla\'s Hut - Left', player), lambda state: can_bomb_or_bonk(state, player)) + set_rule(world.get_location('Sahasrahla\'s Hut - Middle', player), lambda state: can_bomb_or_bonk(state, player)) + set_rule(world.get_location('Sahasrahla\'s Hut - Right', player), lambda state: can_bomb_or_bonk(state, player)) + set_rule(world.get_location('Paradox Cave Lower - Left', player), lambda state: can_use_bombs(state, player) + or has_beam_sword(state, player) or can_shoot_arrows(state, player) + or state.has_any(["Fire Rod", "Cane of Somaria"], player)) + set_rule(world.get_location('Paradox Cave Lower - Right', player), lambda state: can_use_bombs(state, player) + or has_beam_sword(state, player) or can_shoot_arrows(state, player) + or state.has_any(["Fire Rod", "Cane of Somaria"], player)) + set_rule(world.get_location('Paradox Cave Lower - Far Right', player), lambda state: can_use_bombs(state, player) + or has_beam_sword(state, player) or can_shoot_arrows(state, player) + or state.has_any(["Fire Rod", "Cane of Somaria"], player)) + set_rule(world.get_location('Paradox Cave Lower - Middle', player), lambda state: can_use_bombs(state, player) + or has_beam_sword(state, player) or can_shoot_arrows(state, player) + or state.has_any(["Fire Rod", "Cane of Somaria"], player)) + set_rule(world.get_location('Paradox Cave Lower - Far Left', player), lambda state: can_use_bombs(state, player) + or has_beam_sword(state, player) or can_shoot_arrows(state, player) + or state.has_any(["Fire Rod", "Cane of Somaria"], player)) + set_rule(world.get_location('Paradox Cave Upper - Left', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Paradox Cave Upper - Right', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Mini Moldorm Cave - Far Left', player), lambda state: can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Mini Moldorm Cave - Left', player), lambda state: can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Mini Moldorm Cave - Far Right', player), lambda state: can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Mini Moldorm Cave - Right', player), lambda state: can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Mini Moldorm Cave - Generous Guy', player), lambda state: can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Hype Cave - Bottom', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Hype Cave - Middle Left', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Hype Cave - Middle Right', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_location('Hype Cave - Top', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Light World Death Mountain Shop', player), lambda state: can_use_bombs(state, player)) + + set_rule(world.get_entrance('Two Brothers House Exit (West)', player), lambda state: can_bomb_or_bonk(state, player)) + set_rule(world.get_entrance('Two Brothers House Exit (East)', player), lambda state: can_bomb_or_bonk(state, player)) set_rule(world.get_location('Spike Cave', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and @@ -238,61 +287,81 @@ def global_rules(world, player): set_rule(world.get_entrance('Sewers Door', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or ( - world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[ + world.small_key_shuffle[player] == small_key_shuffle.option_universal and world.mode[ player] == 'standard')) # standard universal small keys cannot access the shop set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)) + set_rule(world.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player)) + set_rule(world.get_entrance('Agahnim 1', player), lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 4)) - set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8)) + set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 4)) set_rule(world.get_location('Castle Tower - Dark Maze', player), - lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)', player)) set_rule(world.get_location('Castle Tower - Dark Archer Key Drop', player), - lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_location('Castle Tower - Circle of Pots Key Drop', player), - lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + lambda state: can_kill_most_things(state, player, 4) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 3)) set_always_allow(world.get_location('Eastern Palace - Big Key Chest', player), lambda state, item: item.name == 'Big Key (Eastern Palace)' and item.player == player) set_rule(world.get_location('Eastern Palace - Big Key Chest', player), - lambda state: state._lttp_has_key('Small Key (Eastern Palace)', player, 2) or - ((location_item_name(state, 'Eastern Palace - Big Key Chest', player) == ('Big Key (Eastern Palace)', player) - and state.has('Small Key (Eastern Palace)', player)))) + lambda state: can_kill_most_things(state, player, 5) and (state._lttp_has_key('Small Key (Eastern Palace)', + player, 2) or ((location_item_name(state, 'Eastern Palace - Big Key Chest', player) + == ('Big Key (Eastern Palace)', player) and state.has('Small Key (Eastern Palace)', + player))))) set_rule(world.get_location('Eastern Palace - Dark Eyegore Key Drop', player), - lambda state: state.has('Big Key (Eastern Palace)', player)) + lambda state: state.has('Big Key (Eastern Palace)', player) and can_kill_most_things(state, player, 1)) set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player)) + # not bothering to check for can_kill_most_things in the rooms leading to boss, as if you can kill a boss you should + # be able to get through these rooms ep_boss = world.get_location('Eastern Palace - Boss', player) - set_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and + add_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and ep_boss.parent_region.dungeon.boss.can_defeat(state)) ep_prize = world.get_location('Eastern Palace - Prize', player) - set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and + add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and ep_prize.parent_region.dungeon.boss.can_defeat(state)) if not world.enemy_shuffle[player]: add_rule(ep_boss, lambda state: can_shoot_arrows(state, player)) add_rule(ep_prize, lambda state: can_shoot_arrows(state, player)) + # You can always kill the Stalfos' with the pots on easy/normal + if world.enemy_health[player] in ("hard", "expert") or world.enemy_shuffle[player]: + stalfos_rule = lambda state: can_kill_most_things(state, player, 4) + for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Eastern Palace - Big Key Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize']: + add_rule(world.get_location(location, player), stalfos_rule) + set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4)) - set_rule(world.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player)) - set_rule(world.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player)) - set_rule(world.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player)) - set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) - set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player, 3)) + set_rule(world.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player, 4)) + set_rule(world.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player, 4)) + add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + add_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) # logic patch to prevent placing a crystal in Desert that's required to reach the required keys - if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): + if not (world.small_key_shuffle[player] and world.big_key_shuffle[player]): add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state)) set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) + if world.enemy_shuffle[player]: + add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3)) + else: + add_rule(world.get_entrance('Tower of Hera Big Key Door', player), + lambda state: (has_melee_weapon(state, player) or (state.has('Silver Bow', player) + and can_shoot_arrows(state, player)) or state.has("Cane of Byrna", player) + or state.has("Cane of Somaria", player))) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) if world.accessibility[player] != 'locations': @@ -300,9 +369,13 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player)) + set_rule(world.get_location('Swamp Palace - Map Chest', player), lambda state: can_use_bombs(state, player)) set_rule(world.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2)) set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3)) set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player)) + if world.pot_shuffle[player]: + # it could move the key to the top right platform which can only be reached with bombs + add_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player)) set_rule(world.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6) if state.has('Hookshot', player) else state._lttp_has_key('Small Key (Swamp Palace)', player, 4)) @@ -310,15 +383,18 @@ def global_rules(world, player): if world.accessibility[player] != 'locations': allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) - if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']: + if not world.small_key_shuffle[player] and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) set_rule(world.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) set_rule(world.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) + if world.pot_shuffle[player]: + # key can (and probably will) be moved behind bombable wall + set_rule(world.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player)) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) if world.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind": - set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) + set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player)) set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player, 3)) and state.has('Hammer', player)) @@ -334,7 +410,7 @@ def global_rules(world, player): set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) - set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) + set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player)) if world.accessibility[player] != 'locations': allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain @@ -342,7 +418,13 @@ def global_rules(world, player): add_rule(world.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player)) - set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player) and can_use_bombs(state, player)) + if not world.enemy_shuffle[player]: + # Stalfos Knights can be killed by damaging them repeatedly with boomerang, swords, etc. if bombs are + # unavailable. If bombs are available, the pots can be thrown at them, so no other weapons are needed + add_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (can_use_bombs(state, player) + or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player) or has_sword(state, player) or state.has("Hammer", player))) set_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5)))) @@ -387,16 +469,21 @@ def global_rules(world, player): else state._lttp_has_key('Small Key (Misery Mire)', player, 6)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player)) - set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player) and can_use_bombs(state, player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_location('Turtle Rock - Pokey 1 Key Drop', player), lambda state: can_kill_most_things(state, player, 5)) + set_rule(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), lambda state: can_kill_most_things(state, player, 5)) set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) - set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player)) + set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10)) + set_rule(world.get_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, player) and can_kill_most_things(state, player, 10)) + set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player) + or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player)) set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock (Dark Room) (South)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) @@ -405,16 +492,22 @@ def global_rules(world, player): set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) - if not world.enemy_shuffle[player]: - set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player)) + if world.enemy_shuffle[player]: + set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3)) + else: + set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)) - set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) + set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player)) + set_rule(world.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player)) + if world.pot_shuffle[player]: + # chest switch may be up on ledge where bombs are required + set_rule(world.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( - location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))) + set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( + location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))) if world.accessibility[player] != 'locations': set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) @@ -430,13 +523,9 @@ def global_rules(world, player): compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key'] back_chests = ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest'] - set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) - if world.pot_shuffle[player]: - # Pot Shuffle can move this check into the hookshot room - set_rule(world.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or ( location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))) @@ -465,17 +554,17 @@ def global_rules(world, player): item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(back_chests, [player] * len(back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))) # Actual requirements for location in compass_room_chests: - set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( + set_rule(world.get_location(location, player), lambda state: (can_use_bombs(state, player) or state.has("Cane of Somaria", player)) and state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) set_rule(world.get_location('Ganons Tower - Big Key Room - Left', player), - lambda state: state.multiworld.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Left', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_location('Ganons Tower - Big Key Chest', player), - lambda state: state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_location('Ganons Tower - Big Key Room - Right', player), - lambda state: state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) + lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state)) if world.enemy_shuffle[player]: set_rule(world.get_entrance('Ganons Tower Big Key Door', player), lambda state: state.has('Big Key (Ganons Tower)', player)) @@ -483,7 +572,8 @@ def global_rules(world, player): set_rule(world.get_entrance('Ganons Tower Big Key Door', player), lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player)) set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), - lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) + lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) + set_rule(world.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1)) set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7)) set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), @@ -493,9 +583,9 @@ def global_rules(world, player): set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) ganon = world.get_location('Ganon', player) set_rule(ganon, lambda state: GanonDefeatRule(state, player)) - if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']: + if world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: add_rule(ganon, lambda state: has_triforce_pieces(state, player)) - elif world.goal[player] == 'ganonpedestal': + elif world.goal[player] == 'ganon_pedestal': add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player)) else: add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player)) @@ -507,6 +597,12 @@ def global_rules(world, player): def default_rules(world, player): """Default world rules when world state is not inverted.""" # overworld requirements + + set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) @@ -562,12 +658,12 @@ def default_rules(world, player): set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? - set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required + set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required - set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required + set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull set_rule(world.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush set_rule(world.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush @@ -621,9 +717,9 @@ def inverted_rules(world, player): # overworld requirements set_rule(world.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player)) + set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) @@ -669,7 +765,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player)) # need bomb + set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy @@ -715,6 +811,11 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + set_rule(world.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player)) + set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player)) + + set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) @@ -900,20 +1001,25 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal def open_rules(world, player): + + set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), + lambda state: can_kill_most_things(state, player, 1)) + def basement_key_rule(state): if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player): return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2) else: return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3) - set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), basement_key_rule) + set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2)) set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), basement_key_rule) set_rule(world.get_location('Sewers - Key Rat Key Drop', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)) + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1)) set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)) + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1)) set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and state.has('Big Key (Hyrule Castle)', player)) @@ -924,6 +1030,7 @@ def swordless_rules(world, player): set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop @@ -954,7 +1061,7 @@ def standard_rules(world, player): set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal: + if world.small_key_shuffle[player] != small_key_shuffle.option_universal: set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)) set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), @@ -968,6 +1075,9 @@ def standard_rules(world, player): set_rule(world.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)) + else: + set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + lambda state: state.has('Big Key (Hyrule Castle)', player)) def toss_junk_item(world, player): items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', @@ -1059,15 +1169,15 @@ def tr_big_key_chest_keys_needed(state): return 6 # If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential - if not can_reach_front and not world.smallkey_shuffle[player]: + if not can_reach_front and not world.small_key_shuffle[player]: # Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player) if not can_reach_big_chest: # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player) - if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt': - if world.bigkey_shuffle[player] and can_reach_big_chest: + if world.accessibility[player] == 'locations': + if world.big_key_shuffle[player] and can_reach_big_chest: # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop', @@ -1512,8 +1622,10 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # regions for the exits of multi-entrance caves/drops that bunny cannot pass # Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements. - bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)', - 'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', 'Spiral Cave (Top)', 'Desert Palace Main (Inner)', 'Fairy Ascension Cave (Drop)'] + bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Skull Woods First Section (Right)', + 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)', 'Turtle Rock (Entrance)', 'Turtle Rock (Second Section)', + 'Turtle Rock (Big Chest)', 'Skull Woods Second Section (Drop)', 'Turtle Rock (Eye Bridge)', 'Sewers', 'Pyramid', + 'Spiral Cave (Top)', 'Desert Palace Main (Inner)', 'Fairy Ascension Cave (Drop)'] bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', @@ -1546,7 +1658,7 @@ def is_link(region): def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or # bunny revival accessible. - if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic']: + if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch @@ -1586,7 +1698,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None): seen.add(new_region) if not is_link(new_region): # For glitch rulesets, establish superbunny and revival rules. - if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): + if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() @@ -1623,7 +1735,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None): # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival for entrance in world.get_entrances(player): if is_bunny(entrance.connected_region): - if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] : + if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance)) @@ -1631,7 +1743,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None): if entrance.connected_region.name == 'Turtle Rock (Entrance)': add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) for location in entrance.connected_region.locations: - if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): + if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): continue if location.name in bunny_accessible_locations: continue diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index c0f2e2236e69..64a385a18587 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -5,11 +5,14 @@ from Utils import int16_as_bytes +from worlds.generic.Rules import add_rule + +from BaseClasses import CollectionState from .SubClasses import ALttPLocation from .EntranceShuffle import door_addresses -from .Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem -from .Options import smallkey_shuffle - +from .Items import item_name_groups +from .Options import small_key_shuffle, RandomizeShopInventories +from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows logger = logging.getLogger("Shops") @@ -36,9 +39,9 @@ class ShopPriceType(IntEnum): Item = 10 -class Shop(): +class Shop: slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots - blacklist: Set[str] = set() # items that don't work, todo: actually check against this + blacklist: Set[str] = set() # items that don't work type = ShopType.Shop slot_names: Dict[int, str] = { 0: " Left", @@ -103,7 +106,7 @@ def clear_inventory(self): self.inventory = [None] * self.slots def add_inventory(self, slot: int, item: str, price: int, max: int = 0, - replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False, + replacement: Optional[str] = None, replacement_price: int = 0, player: int = 0, price_type: int = ShopPriceType.Rupees, replacement_price_type: int = ShopPriceType.Rupees): self.inventory[slot] = { @@ -114,33 +117,23 @@ def add_inventory(self, slot: int, item: str, price: int, max: int = 0, 'replacement': replacement, 'replacement_price': replacement_price, 'replacement_price_type': replacement_price_type, - 'create_location': create_location, 'player': player } def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0, price_type: int = ShopPriceType.Rupees): - if not self.inventory[slot]: - raise ValueError("Inventory can't be pushed back if it doesn't exist") - - if not self.can_push_inventory(slot): - logging.warning(f'Warning, there is already an item pushed into this slot.') self.inventory[slot] = { 'item': item, 'price': price, 'price_type': price_type, 'max': max, - 'replacement': self.inventory[slot]["item"], - 'replacement_price': self.inventory[slot]["price"], - 'replacement_price_type': self.inventory[slot]["price_type"], - 'create_location': self.inventory[slot]["create_location"], + 'replacement': self.inventory[slot]["item"] if self.inventory[slot] else None, + 'replacement_price': self.inventory[slot]["price"] if self.inventory[slot] else 0, + 'replacement_price_type': self.inventory[slot]["price_type"] if self.inventory[slot] else ShopPriceType.Rupees, 'player': player } - def can_push_inventory(self, slot: int): - return self.inventory[slot] and not self.inventory[slot]["replacement"] - class TakeAny(Shop): type = ShopType.TakeAny @@ -156,6 +149,10 @@ class UpgradeShop(Shop): # Potions break due to VRAM flags set in UpgradeShop. # Didn't check for more things breaking as not much else can be shuffled here currently blacklist = item_name_groups["Potions"] + slot_names: Dict[int, str] = { + 0: " Left", + 1: " Right" + } shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop, @@ -163,191 +160,84 @@ class UpgradeShop(Shop): ShopType.TakeAny: TakeAny} -def FillDisabledShopSlots(world): - shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops) - for location in shop_locations - if location.shop_slot is not None and location.shop_slot_disabled} - for location in shop_slots: - location.shop_slot_disabled = True - shop: Shop = location.parent_region.shop - location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player) - location.item_rule = lambda item: item.name == location.item.name and item.player == location.player - location.locked = True +def push_shop_inventories(multiworld): + shop_slots = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if shop.type + != ShopType.TakeAny) for location in shop_locations if location.shop_slot is not None] + for location in shop_slots: + item_name = location.item.name + # Retro Bow arrows will already have been pushed + if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player) + != ("Single Arrow", location.player)): + location.shop.push_inventory(location.shop_slot, item_name, location.shop_price, + 1, location.item.player if location.item.player != location.player else 0, + location.shop_price_type) + location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price, + get_price(multiworld, location.shop.inventory[location.shop_slot], location.player, + location.shop_price_type)[1]) -def ShopSlotFill(multiworld): - shop_slots: Set[ALttPLocation] = {location for shop_locations in - (shop.region.locations for shop in multiworld.shops if shop.type != ShopType.TakeAny) - for location in shop_locations if location.shop_slot is not None} - removed = set() - for location in shop_slots: - shop: Shop = location.parent_region.shop - if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled: - location.shop_slot_disabled = True - removed.add(location) - - if removed: - shop_slots -= removed - - if shop_slots: - logger.info("Filling LttP Shop Slots") - del shop_slots - - from Fill import swap_location_item - # TODO: allow each game to register a blacklist to be used here? - blacklist_words = {"Rupee"} - blacklist_words = {item_name for item_name in item_table if any( - blacklist_word in item_name for blacklist_word in blacklist_words)} - blacklist_words.add("Bee") - - locations_per_sphere = [sorted(sphere, key=lambda location: (location.name, location.player)) - for sphere in multiworld.get_spheres()] - - # currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory - # Potentially create Locations as needed and make inventory the only source, to prevent divergence - cumu_weights = [] - shops_per_sphere = [] - candidates_per_sphere = [] - - # sort spheres into piles of valid candidates and shops - for sphere in locations_per_sphere: - current_shops_slots = [] - current_candidates = [] - shops_per_sphere.append(current_shops_slots) - candidates_per_sphere.append(current_candidates) - for location in sphere: - if isinstance(location, ALttPLocation) and location.shop_slot is not None: - if not location.shop_slot_disabled: - current_shops_slots.append(location) - elif not location.locked and location.item.name not in blacklist_words: - current_candidates.append(location) - if cumu_weights: - x = cumu_weights[-1] - else: - x = 0 - cumu_weights.append(len(current_candidates) + x) - - multiworld.random.shuffle(current_candidates) - - del locations_per_sphere - - for i, current_shop_slots in enumerate(shops_per_sphere): - if current_shop_slots: - # getting all candidates and shuffling them feels cpu expensive, there may be a better method - candidates = [(location, i) for i, candidates in enumerate(candidates_per_sphere[i:], start=i) - for location in candidates] - multiworld.random.shuffle(candidates) - for location in current_shop_slots: - shop: Shop = location.parent_region.shop - for index, (c, swapping_sphere_id) in enumerate(candidates): # chosen item locations - if c.item_rule(location.item) and location.item_rule(c.item): - swap_location_item(c, location, check_locked=False) - logger.debug(f"Swapping {c} into {location}:: {location.item}") - # remove candidate - candidates_per_sphere[swapping_sphere_id].remove(c) - candidates.pop(index) - break - - else: - # This *should* never happen. But let's fail safely just in case. - logger.warning("Ran out of ShopShuffle Item candidate locations.") - location.shop_slot_disabled = True - continue - - item_name = location.item.name - if location.item.game != "A Link to the Past": - if location.item.advancement: - price = multiworld.random.randrange(8, 56) - elif location.item.useful: - price = multiworld.random.randrange(4, 28) - else: - price = multiworld.random.randrange(2, 14) - elif any(x in item_name for x in - ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): - price = multiworld.random.randrange(1, 7) - elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']): - price = multiworld.random.randrange(2, 14) - elif any(x in item_name for x in ['Small Key', 'Heart']): - price = multiworld.random.randrange(4, 28) - else: - price = multiworld.random.randrange(8, 56) - - shop.push_inventory(location.shop_slot, item_name, - min(int(price * multiworld.shop_price_modifier[location.player] / 100) * 5, 9999), 1, - location.item.player if location.item.player != location.player else 0) - if 'P' in multiworld.shop_shuffle[location.player]: - price_to_funny_price(multiworld, shop.inventory[location.shop_slot], location.player) - - FillDisabledShopSlots(multiworld) - - -def create_shops(world, player: int): - option = world.shop_shuffle[player] +def create_shops(multiworld, player: int): player_shop_table = shop_table.copy() - if "w" in option: + if multiworld.include_witch_hut[player]: player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) dynamic_shop_slots = total_dynamic_shop_slots + 3 else: dynamic_shop_slots = total_dynamic_shop_slots + if multiworld.shuffle_capacity_upgrades[player]: + player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False) - num_slots = min(dynamic_shop_slots, world.shop_item_slots[player]) + num_slots = min(dynamic_shop_slots, multiworld.shop_item_slots[player]) single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots) - world.random.shuffle(single_purchase_slots) + multiworld.random.shuffle(single_purchase_slots) - if 'g' in option or 'f' in option: + if multiworld.randomize_shop_inventories[player]: default_shop_table = [i for l in [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if - not world.retro_bow[player] or x != 'arrows'] for i in l] - new_basic_shop = world.random.sample(default_shop_table, k=3) - new_dark_shop = world.random.sample(default_shop_table, k=3) + not multiworld.retro_bow[player] or x != 'arrows'] for i in l] + new_basic_shop = multiworld.random.sample(default_shop_table, k=3) + new_dark_shop = multiworld.random.sample(default_shop_table, k=3) for name, shop in player_shop_table.items(): typ, shop_id, keeper, custom, locked, items, sram_offset = shop if not locked: - new_items = world.random.sample(default_shop_table, k=3) - if 'f' not in option: + new_items = multiworld.random.sample(default_shop_table, k=len(items)) + if multiworld.randomize_shop_inventories[player] == RandomizeShopInventories.option_randomize_by_shop_type: if items == _basic_shop_defaults: new_items = new_basic_shop elif items == _dark_world_shop_defaults: new_items = new_dark_shop - keeper = world.random.choice([0xA0, 0xC1, 0xFF]) + keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF]) player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset) - if world.mode[player] == "inverted": + if multiworld.mode[player] == "inverted": # make sure that blue potion is available in inverted, special case locked = None; lock when done. player_shop_table["Dark Lake Hylia Shop"] = \ player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None) - chance_100 = int(world.retro_bow[player]) * 0.25 + int( - world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5 for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items(): - region = world.get_region(region_name, player) + region = multiworld.get_region(region_name, player) shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset) # special case: allow shop slots, but do not allow overwriting of base inventory behind them if locked is None: shop.locked = True region.shop = shop - world.shops.append(shop) + multiworld.shops.append(shop) for index, item in enumerate(inventory): shop.add_inventory(index, *item) - if not locked and num_slots: + if not locked and (num_slots or type == ShopType.UpgradeShop): slot_name = f"{region.name}{shop.slot_names[index]}" loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name], parent=region, hint_text="for sale") + loc.shop_price_type, loc.shop_price = get_price(multiworld, None, player) + loc.item_rule = lambda item, spot=loc: not any(i for i in price_blacklist[spot.shop_price_type] if i in item.name) + add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot)) + loc.shop = shop loc.shop_slot = index - loc.locked = True - if single_purchase_slots.pop(): - if world.goal[player] != 'icerodhunt': - if world.random.random() < chance_100: - additional_item = 'Rupees (100)' - else: - additional_item = 'Rupees (50)' - else: - additional_item = GetBeemizerItem(world, player, 'Nothing') - loc.item = ItemFactory(additional_item, player) - else: - loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) + if ((not (multiworld.shuffle_capacity_upgrades[player] and type == ShopType.UpgradeShop)) + and not single_purchase_slots.pop()): loc.shop_slot_disabled = True - shop.region.locations.append(loc) + loc.locked = True + else: + shop.region.locations.append(loc) class ShopData(NamedTuple): @@ -387,9 +277,10 @@ class ShopData(NamedTuple): SHOP_ID_START = 0x400000 shop_table_by_location_id = dict(enumerate( - (f"{name}{Shop.slot_names[num]}" for name, shop_data in - sorted(shop_table.items(), key=lambda item: item[1].sram_offset) - for num in range(3)), start=SHOP_ID_START)) + (f"{name}{UpgradeShop.slot_names[num]}" if shop_data.type == ShopType.UpgradeShop else + f"{name}{Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), + key=lambda item: item[1].sram_offset) + for num in range(2 if shop_data.type == ShopType.UpgradeShop else 3)), start=SHOP_ID_START)) shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave" shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1" @@ -409,114 +300,54 @@ class ShopData(NamedTuple): } -def set_up_shops(world, player: int): +def set_up_shops(multiworld, player: int): # TODO: move hard+ mode changes for shields here, utilizing the new shops - if world.retro_bow[player]: - rss = world.get_region('Red Shield Shop', player).shop + if multiworld.retro_bow[player]: + rss = multiworld.get_region('Red Shield Shop', player).shop replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50], ['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them. - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: replacement_items.append(['Small Key (Universal)', 100]) - replacement_item = world.random.choice(replacement_items) + replacement_item = multiworld.random.choice(replacement_items) rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1]) rss.locked = True - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro_bow[player]: - for shop in world.random.sample([s for s in world.shops if - s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player], - 5): + if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal or multiworld.retro_bow[player]: + for shop in multiworld.random.sample([s for s in multiworld.shops if + s.custom and not s.locked and s.type == ShopType.Shop + and s.region.player == player], 5): shop.locked = True slots = [0, 1, 2] - world.random.shuffle(slots) + multiworld.random.shuffle(slots) slots = iter(slots) - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: shop.add_inventory(next(slots), 'Small Key (Universal)', 100) - if world.retro_bow[player]: + if multiworld.retro_bow[player]: shop.push_inventory(next(slots), 'Single Arrow', 80) - -def shuffle_shops(world, items, player: int): - option = world.shop_shuffle[player] - if 'u' in option: - progressive = world.progressive[player] - progressive = world.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on' - progressive &= world.goal == 'icerodhunt' - new_items = ["Bomb Upgrade (+5)"] * 6 - new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)") - - if not world.retro_bow[player]: - new_items += ["Arrow Upgrade (+5)"] * 6 - new_items.append("Arrow Upgrade (+5)" if progressive else "Arrow Upgrade (+10)") - - world.random.shuffle(new_items) # Decide what gets tossed randomly if it can't insert everything. - - capacityshop: Optional[Shop] = None - for shop in world.shops: + if multiworld.shuffle_capacity_upgrades[player]: + for shop in multiworld.shops: if shop.type == ShopType.UpgradeShop and shop.region.player == player and \ shop.region.name == "Capacity Upgrade": shop.clear_inventory() - capacityshop = shop - - if world.goal[player] != 'icerodhunt': - for i, item in enumerate(items): - if item.name in trap_replaceable: - items[i] = ItemFactory(new_items.pop(), player) - if not new_items: - break - else: - logging.warning( - f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.") - bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item) - arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item) - slots = iter(range(2)) - if bombupgrades: - capacityshop.add_inventory(next(slots), 'Bomb Upgrade (+5)', 100, bombupgrades) - if arrowupgrades: - capacityshop.add_inventory(next(slots), 'Arrow Upgrade (+5)', 100, arrowupgrades) - else: - for item in new_items: - world.push_precollected(ItemFactory(item, player)) - if any(setting in option for setting in 'ipP'): + if (multiworld.shuffle_shop_inventories[player] or multiworld.randomize_shop_prices[player] + or multiworld.randomize_cost_types[player]): shops = [] - upgrade_shops = [] total_inventory = [] - for shop in world.shops: + for shop in multiworld.shops: if shop.region.player == player: - if shop.type == ShopType.UpgradeShop: - upgrade_shops.append(shop) - elif shop.type == ShopType.Shop and not shop.locked: + if shop.type == ShopType.Shop and not shop.locked: shops.append(shop) total_inventory.extend(shop.inventory) - if 'p' in option: - def price_adjust(price: int) -> int: - # it is important that a base price of 0 always returns 0 as new price! - adjust = 2 if price < 100 else 5 - return int((price / adjust) * (0.5 + world.random.random() * 1.5)) * adjust - - def adjust_item(item): - if item: - item["price"] = price_adjust(item["price"]) - item['replacement_price'] = price_adjust(item["price"]) - - for item in total_inventory: - adjust_item(item) - for shop in upgrade_shops: - for item in shop.inventory: - adjust_item(item) - - if 'P' in option: - for item in total_inventory: - price_to_funny_price(world, item, player) - # Don't apply to upgrade shops - # Upgrade shop is only one place, and will generally be too easy to - # replenish hearts and bombs - - if 'i' in option: - world.random.shuffle(total_inventory) + for item in total_inventory: + item["price_type"], item["price"] = get_price(multiworld, item, player) + + if multiworld.shuffle_shop_inventories[player]: + multiworld.random.shuffle(total_inventory) i = 0 for shop in shops: @@ -539,16 +370,18 @@ def adjust_item(item): } price_chart = { - ShopPriceType.Rupees: lambda p: p, - ShopPriceType.Hearts: lambda p: min(5, p // 5) * 8, # Each heart is 0x8 in memory, Max of 5 hearts (20 total??) - ShopPriceType.Magic: lambda p: min(15, p // 5) * 8, # Each pip is 0x8 in memory, Max of 15 pips (16 total...) - ShopPriceType.Bombs: lambda p: max(1, min(10, p // 5)), # 10 Bombs max - ShopPriceType.Arrows: lambda p: max(1, min(30, p // 5)), # 30 Arrows Max - ShopPriceType.HeartContainer: lambda p: 0x8, - ShopPriceType.BombUpgrade: lambda p: 0x1, - ShopPriceType.ArrowUpgrade: lambda p: 0x1, - ShopPriceType.Keys: lambda p: min(3, (p // 100) + 1), # Max of 3 keys for a price - ShopPriceType.Potion: lambda p: (p // 5) % 5, + ShopPriceType.Rupees: lambda p, d: p, + # Each heart is 0x8 in memory, Max of 19 hearts on easy/normal, 9 on hard, 7 on expert + ShopPriceType.Hearts: lambda p, d: max(8, min([19, 19, 9, 7][d], p // 14) * 8), + # Each pip is 0x8 in memory, Max of 15 pips (16 total) + ShopPriceType.Magic: lambda p, d: max(8, min(15, p // 18) * 8), + ShopPriceType.Bombs: lambda p, d: max(1, min(50, p // 5)), # 50 Bombs max + ShopPriceType.Arrows: lambda p, d: max(1, min(70, p // 4)), # 70 Arrows Max + ShopPriceType.HeartContainer: lambda p, d: 0x8, + ShopPriceType.BombUpgrade: lambda p, d: 0x1, + ShopPriceType.ArrowUpgrade: lambda p, d: 0x1, + ShopPriceType.Keys: lambda p, d: max(1, min(3, (p // 90) + 1)), # Max of 3 keys for a price + ShopPriceType.Potion: lambda p, d: (p // 5) % 5, } price_type_display_name = { @@ -557,6 +390,8 @@ def adjust_item(item): ShopPriceType.Bombs: "Bombs", ShopPriceType.Arrows: "Arrows", ShopPriceType.Keys: "Keys", + ShopPriceType.Item: "Item", + ShopPriceType.Magic: "Magic" } # price division @@ -565,57 +400,74 @@ def adjust_item(item): ShopPriceType.Magic: 8, } -# prices with no? logic requirements -simple_price_types = [ - ShopPriceType.Rupees, - ShopPriceType.Hearts, - ShopPriceType.Bombs, - ShopPriceType.Arrows, - ShopPriceType.Keys -] + +def get_price_modifier(item): + if item.game == "A Link to the Past": + if any(x in item.name for x in + ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): + return 0.125 + elif any(x in item.name for x in + ['Arrow', 'Bomb', 'Clock']) and item.name != "Bombos" and "(50)" not in item.name: + return 0.25 + elif any(x in item.name for x in ['Small Key', 'Heart']): + return 0.5 + else: + return 1 + if item.advancement: + return 1 + elif item.useful: + return 0.5 + else: + return 0.25 -def price_to_funny_price(world, item: dict, player: int): - """ - Converts a raw Rupee price into a special price type - """ +def get_price(multiworld, item, player: int, price_type=None): + """Converts a raw Rupee price into a special price type""" + + if price_type: + price_types = [price_type] + else: + price_types = [ShopPriceType.Rupees] # included as a chance to not change price + if multiworld.randomize_cost_types[player]: + price_types += [ + ShopPriceType.Hearts, + ShopPriceType.Bombs, + ShopPriceType.Magic, + ] + if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: + if item and item["item"] == "Small Key (Universal)": + price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys + else: + price_types.append(ShopPriceType.Keys) + if multiworld.retro_bow[player]: + if item and item["item"] == "Single Arrow": + price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows + else: + price_types.append(ShopPriceType.Arrows) + diff = multiworld.item_pool[player].value if item: - price_types = [ - ShopPriceType.Rupees, # included as a chance to not change price type - ShopPriceType.Hearts, - ShopPriceType.Bombs, - ] - # don't pay in universal keys to get access to universal keys - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal \ - and not "Small Key (Universal)" == item['replacement']: - price_types.append(ShopPriceType.Keys) - if not world.retro_bow[player]: - price_types.append(ShopPriceType.Arrows) - world.random.shuffle(price_types) + # This is for a shop's regular inventory, the item is already determined, and we will decide the price here + price = item["price"] + if multiworld.randomize_shop_prices[player]: + adjust = 2 if price < 100 else 5 + price = int((price / adjust) * (0.5 + multiworld.random.random() * 1.5)) * adjust + multiworld.random.shuffle(price_types) for p_type in price_types: - # Ignore rupee prices - if p_type == ShopPriceType.Rupees: - return if any(x in item['item'] for x in price_blacklist[p_type]): continue - else: - item['price'] = min(price_chart[p_type](item['price']), 255) - item['price_type'] = p_type - break - - -def create_dynamic_shop_locations(world, player): - for shop in world.shops: - if shop.region.player == player: - for i, item in enumerate(shop.inventory): - if item is None: - continue - if item['create_location']: - slot_name = f"{shop.region.name}{shop.slot_names[i]}" - loc = ALttPLocation(player, slot_name, - address=shop_table_by_location[slot_name], parent=shop.region) - loc.place_locked_item(ItemFactory(item['item'], player)) - if shop.type == ShopType.TakeAny: - loc.shop_slot_disabled = True - shop.region.locations.append(loc) - loc.shop_slot = i + return p_type, price_chart[p_type](price, diff) + else: + # This is an AP location and the price will be adjusted after an item is shuffled into it + p_type = multiworld.random.choice(price_types) + return p_type, price_chart[p_type](min(int(multiworld.random.randint(8, 56) + * multiworld.shop_price_modifier[player] / 100) * 5, 9999), diff) + + +def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation): + if location.shop_price_type == ShopPriceType.Hearts: + return has_hearts(state, player, (location.shop_price / 8) + 1) + elif location.shop_price_type == ShopPriceType.Bombs: + return can_use_bombs(state, player, location.shop_price) + elif location.shop_price_type == ShopPriceType.Arrows: + return can_hold_arrows(state, player, location.shop_price) + return True diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 38ce00ef4537..4ed1b1caf205 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -10,7 +10,7 @@ def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> boo def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: - return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) + return can_use_bombs(state, player) and is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: @@ -83,13 +83,47 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, return basemagic >= smallmagic +def can_hold_arrows(state: CollectionState, player: int, quantity: int): + arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10) + + (state.count("Bomb Upgrade (50)", player) * 50)) + # Arrow Upgrade (+5) beyond the 6th gives +10 + arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10)) + return min(70, arrows) >= quantity + + +def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool: + bombs = 0 if state.multiworld.bombless_start[player] else 10 + bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10) + + (state.count("Bomb Upgrade (50)", player) * 50)) + # Bomb Upgrade (+5) beyond the 6th gives +10 + bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10)) + if (not state.multiworld.shuffle_capacity_upgrades[player]) and state.has("Capacity Upgrade Shop", player): + bombs += 40 + return bombs >= min(quantity, 50) + + +def can_bomb_or_bonk(state: CollectionState, player: int) -> bool: + return state.has("Pegasus Boots", player) or can_use_bombs(state, player) + + def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: - return (has_melee_weapon(state, player) - or state.has('Cane of Somaria', player) - or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) - or can_shoot_arrows(state, player) - or state.has('Fire Rod', player) - or (state.has('Bombs (10)', player) and enemies < 6)) + if state.multiworld.enemy_shuffle[player]: + # I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any. + # Just go with maximal requirements for now. + return (has_melee_weapon(state, player) + and state.has('Cane of Somaria', player) + and state.has('Cane of Byrna', player) and can_extend_magic(state, player) + and can_shoot_arrows(state, player) + and state.has('Fire Rod', player) + and can_use_bombs(state, player, enemies * 4)) + else: + return (has_melee_weapon(state, player) + or state.has('Cane of Somaria', player) + or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player))) + or can_shoot_arrows(state, player) + or state.has('Fire Rod', player) + or (state.multiworld.enemy_health[player] in ("easy", "default") + and can_use_bombs(state, player, enemies * 4))) def can_get_good_bee(state: CollectionState, player: int) -> bool: @@ -159,4 +193,4 @@ def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])] if state.multiworld.mode[player] != 'inverted': rules.append(state.has('Moon Pearl', player)) - return all(rules) \ No newline at end of file + return all(rules) diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 64e4adaec9a2..769dcc199852 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -14,9 +14,12 @@ class ALttPLocation(Location): crystal: bool player_address: Optional[int] _hint_text: Optional[str] + shop: None shop_slot: Optional[int] = None """If given as integer, shop_slot is the shop's inventory index.""" shop_slot_disabled: bool = False + shop_price = 0 + shop_price_type = None parent_region: "LTTPRegion" def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, @@ -26,6 +29,13 @@ def __init__(self, player: int, name: str, address: Optional[int] = None, crysta self.player_address = player_address self._hint_text = hint_text + @property + def hint_text(self) -> str: + hint_text = getattr(self, "_hint_text", None) + if hint_text: + return hint_text + return "at " + self.name.replace("_", " ").replace("-", " ") + class ALttPItem(Item): game: str = "A Link to the Past" diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index a6aefc74129a..497d5de496c3 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -42,7 +42,7 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du fix_fake_worlds = world.fix_fake_world[player] dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0] - if not fix_dungeon_exits: # vanilla, simple, restricted, dungeonssimple; should never have fake worlds fix + if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix # Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially. if dungeon_entrance.name == 'Skull Woods Final Section': set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side @@ -52,12 +52,12 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier # Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally. add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) - elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix + elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix # Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region. add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player))) # exiting restriction add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) - # Otherwise, the shuffle type is crossed, dungeonscrossed, or insanity; all of these do not need additional rules on where we can go, + # Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go, # since the clip links directly to the exterior region. @@ -93,7 +93,7 @@ def underworld_glitches_rules(world, player): # We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT. # First we require a certain type of entrance shuffle, then build the rule from its pieces. if not world.swamp_patch_required[player]: - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: rule_map = { 'Misery Mire (Entrance)': (lambda state: True), 'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player)) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3f380d0037a2..e1216010e2b3 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -13,14 +13,14 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem -from .Options import alttp_options, smallkey_shuffle +from .Options import alttp_options, small_key_shuffle from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ is_main_entrance, key_drop_data from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules -from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name +from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin from .StateHelpers import can_buy_unlimited @@ -213,7 +213,7 @@ class ALTTPWorld(World): item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} location_name_to_id = lookup_name_to_id - data_version = 8 + data_version = 9 required_client_version = (0, 4, 1) web = ALTTPWeb() @@ -290,33 +290,34 @@ def generate_early(self): self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) if multiworld.mode[player] == 'standard': - if multiworld.smallkey_shuffle[player]: - if (multiworld.smallkey_shuffle[player] not in - (smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons, - smallkey_shuffle.option_start_with)): + if multiworld.small_key_shuffle[player]: + if (multiworld.small_key_shuffle[player] not in + (small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons, + small_key_shuffle.option_start_with)): self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)") self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)") - if multiworld.bigkey_shuffle[player]: + if multiworld.big_key_shuffle[player]: self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)") self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)") # system for sharing ER layouts self.er_seed = str(multiworld.random.randint(0, 2 ** 64)) - if "-" in multiworld.shuffle[player]: - shuffle, seed = multiworld.shuffle[player].split("-", 1) - multiworld.shuffle[player] = shuffle + if multiworld.entrance_shuffle[player] != "vanilla" and multiworld.entrance_shuffle_seed[player] != "random": + shuffle = multiworld.entrance_shuffle[player].current_key if shuffle == "vanilla": self.er_seed = "vanilla" - elif seed.startswith("group-") or multiworld.is_race: + elif (not multiworld.entrance_shuffle_seed[player].value.isdigit()) or multiworld.is_race: self.er_seed = get_same_seed(multiworld, ( - shuffle, seed, multiworld.retro_caves[player], multiworld.mode[player], multiworld.logic[player])) + shuffle, multiworld.entrance_shuffle_seed[player].value, multiworld.retro_caves[player], multiworld.mode[player], + multiworld.glitches_required[player])) else: # not a race or group seed, use set seed as is. - self.er_seed = seed - elif multiworld.shuffle[player] == "vanilla": + self.er_seed = int(multiworld.entrance_shuffle_seed[player].value) + elif multiworld.entrance_shuffle[player] == "vanilla": self.er_seed = "vanilla" - for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]: + + for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]: option = getattr(multiworld, dungeon_item)[player] if option == "own_world": multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group] @@ -329,10 +330,10 @@ def generate_early(self): if option == "original_dungeon": self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group] - multiworld.difficulty_requirements[player] = difficulties[multiworld.difficulty[player]] + multiworld.difficulty_requirements[player] = difficulties[multiworld.item_pool[player].current_key] # enforce pre-defined local items. - if multiworld.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: + if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]: multiworld.local_items[player].value.add('Triforce Piece') # Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too). @@ -345,9 +346,6 @@ def create_regions(self): player = self.player world = self.multiworld - world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], - world.triforce_pieces_required[player]) - if world.mode[player] != 'inverted': create_regions(world, player) else: @@ -355,8 +353,8 @@ def create_regions(self): create_shops(world, player) self.create_dungeons() - if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \ - {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: + if world.glitches_required[player] not in ["no_glitches", "minor_glitches"] and world.entrance_shuffle[player] in \ + {"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"}: world.fix_fake_world[player] = False # seeded entrance shuffle @@ -455,7 +453,7 @@ def collect_item(self, state: CollectionState, item: Item, remove=False): if state.has('Silver Bow', item.player): return elif state.has('Bow', item.player) and (self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 2 - or self.multiworld.logic[item.player] == 'noglitches' + or self.multiworld.glitches_required[item.player] == 'no_glitches' or self.multiworld.swordless[item.player]): # modes where silver bow is always required for ganon return 'Silver Bow' elif self.multiworld.difficulty_requirements[item.player].progressive_bow_limit >= 1: @@ -499,9 +497,9 @@ def pre_fill(self): break else: raise FillError('Unable to place dungeon prizes') - if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \ - and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal and \ - world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons: + if world.mode[player] == 'standard' and world.small_key_shuffle[player] \ + and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \ + world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons: world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1 @classmethod @@ -509,10 +507,9 @@ def stage_pre_fill(cls, world): from .Dungeons import fill_dungeons_restrictive fill_dungeons_restrictive(world) - @classmethod def stage_post_fill(cls, world): - ShopSlotFill(world) + push_shop_inventories(world) @property def use_enemizer(self) -> bool: @@ -579,7 +576,7 @@ def generate_output(self, output_directory: str): @classmethod def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]): er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if - world.shuffle[player] != "vanilla" or world.retro_caves[player]} + world.entrance_shuffle[player] != "vanilla" or world.retro_caves[player]} for region in world.regions: if region.player in er_hint_data and region.locations: @@ -645,9 +642,9 @@ def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fi trash_counts = {} for player in world.get_game_players("A Link to the Past"): if not world.ganonstower_vanilla[player] or \ - world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: + world.glitches_required[player] in {'overworld_glitches', 'hybrid_major_glitches', "no_logic"}: pass - elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1): + elif 'triforce_hunt' in world.goal[player].current_key and ('local' in world.goal[player].current_key or world.players == 1): trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2, world.crystals_needed_for_gt[player] * 4) else: @@ -681,35 +678,6 @@ def bool_to_text(variable: typing.Union[bool, str]) -> str: return variable return "Yes" if variable else "No" - spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player]) - spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player]) - spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player]) - spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player]) - if "triforce" in self.multiworld.goal[self.player]: # triforce hunt - spoiler_handle.write("Pieces available for Triforce: %s\n" % - self.multiworld.triforce_pieces_available[self.player]) - spoiler_handle.write("Pieces required for Triforce: %s\n" % - self.multiworld.triforce_pieces_required[self.player]) - spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player]) - spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player]) - spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player]) - if self.multiworld.shuffle[self.player] != "vanilla": - spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed) - spoiler_handle.write('Shop inventory shuffle: %s\n' % - bool_to_text("i" in self.multiworld.shop_shuffle[self.player])) - spoiler_handle.write('Shop price shuffle: %s\n' % - bool_to_text("p" in self.multiworld.shop_shuffle[self.player])) - spoiler_handle.write('Shop upgrade shuffle: %s\n' % - bool_to_text("u" in self.multiworld.shop_shuffle[self.player])) - spoiler_handle.write('New Shop inventory: %s\n' % - bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or - "f" in self.multiworld.shop_shuffle[self.player])) - spoiler_handle.write('Custom Potion Shop: %s\n' % - bool_to_text("w" in self.multiworld.shop_shuffle[self.player])) - spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player]) - spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player]) - spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player]) - def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: player_name = self.multiworld.get_player_name(self.player) spoiler_handle.write("\n\nMedallions:\n") @@ -783,7 +751,7 @@ def build_shop_info(shop: Shop) -> typing.Dict[str, str]: if item["replacement"] is None: continue shop_data["item_{}".format(index)] +=\ - f", {item['replacement']} - {item['replacement_price']}" \ + f", {item['replacement']} - {item['replacement_price'] // price_rate_display.get(item['replacement_price_type'], 1)}" \ f" {price_type_display_name[item['replacement_price_type']]}" return shop_data @@ -796,10 +764,7 @@ def build_shop_info(shop: Shop) -> typing.Dict[str, str]: item))) def get_filler_item_name(self) -> str: - if self.multiworld.goal[self.player] == "icerodhunt": - item = "Nothing" - else: - item = self.multiworld.random.choice(extras_list) + item = self.multiworld.random.choice(extras_list) return GetBeemizerItem(self.multiworld, self.player, item) def get_pre_fill_items(self): @@ -819,20 +784,20 @@ def fill_slot_data(self): # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid", - "bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle", + "big_key_shuffle", "small_key_shuffle", "compass_shuffle", "map_shuffle", "progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots", - "boss_shuffle", "pot_shuffle", "enemy_shuffle", "key_drop_shuffle"] + "boss_shuffle", "pot_shuffle", "enemy_shuffle", "key_drop_shuffle", "bombless_start", + "randomize_shop_inventories", "shuffle_shop_inventories", "shuffle_capacity_upgrades", + "entrance_shuffle", "dark_room_logic", "goal", "mode", + "triforce_pieces_mode", "triforce_pieces_percentage", "triforce_pieces_required", + "triforce_pieces_available", "triforce_pieces_extra", + ] slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options} slot_data.update({ - 'mode': self.multiworld.mode[self.player], - 'goal': self.multiworld.goal[self.player], - 'dark_room_logic': self.multiworld.dark_room_logic[self.player], 'mm_medalion': self.multiworld.required_medallions[self.player][0], 'tr_medalion': self.multiworld.required_medallions[self.player][1], - 'shop_shuffle': self.multiworld.shop_shuffle[self.player], - 'entrance_shuffle': self.multiworld.shuffle[self.player], } ) return slot_data @@ -849,8 +814,8 @@ def get_same_seed(world, seed_def: tuple) -> str: class ALttPLogic(LogicMixin): def _lttp_has_key(self, item, player, count: int = 1): - if self.multiworld.logic[player] == 'nologic': + if self.multiworld.glitches_required[player] == 'no_logic': return True - if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + if self.multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal: return can_buy_unlimited(self, 'Small Key (Universal)', player) return self.prog_items[player][item] >= count diff --git a/worlds/alttp/test/dungeons/TestAgahnimsTower.py b/worlds/alttp/test/dungeons/TestAgahnimsTower.py index 94e785485882..c44a92be1ece 100644 --- a/worlds/alttp/test/dungeons/TestAgahnimsTower.py +++ b/worlds/alttp/test/dungeons/TestAgahnimsTower.py @@ -7,25 +7,25 @@ def testTower(self): self.starting_regions = ['Agahnims Tower'] self.run_tests([ ["Castle Tower - Room 03", False, []], - ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Room 03", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Room 03", True, ['Progressive Sword']], ["Castle Tower - Dark Maze", False, []], ["Castle Tower - Dark Maze", False, [], ['Small Key (Agahnims Tower)']], ["Castle Tower - Dark Maze", False, [], ['Lamp']], - ["Castle Tower - Dark Maze", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Dark Maze", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Dark Maze", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Lamp']], ["Castle Tower - Dark Archer Key Drop", False, []], ["Castle Tower - Dark Archer Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']], ["Castle Tower - Dark Archer Key Drop", False, [], ['Lamp']], - ["Castle Tower - Dark Archer Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Dark Archer Key Drop", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Dark Archer Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']], ["Castle Tower - Circle of Pots Key Drop", False, []], ["Castle Tower - Circle of Pots Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']], ["Castle Tower - Circle of Pots Key Drop", False, [], ['Lamp']], - ["Castle Tower - Circle of Pots Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Circle of Pots Key Drop", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Circle of Pots Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']], ["Agahnim 1", False, []], diff --git a/worlds/alttp/test/dungeons/TestDarkPalace.py b/worlds/alttp/test/dungeons/TestDarkPalace.py index e3974e777da3..3912fbd282d9 100644 --- a/worlds/alttp/test/dungeons/TestDarkPalace.py +++ b/worlds/alttp/test/dungeons/TestDarkPalace.py @@ -11,29 +11,37 @@ def testDarkPalace(self): ["Palace of Darkness - The Arena - Ledge", False, []], ["Palace of Darkness - The Arena - Ledge", False, [], ['Progressive Bow']], - ["Palace of Darkness - The Arena - Ledge", True, ['Progressive Bow']], + ["Palace of Darkness - The Arena - Ledge", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Palace of Darkness - The Arena - Ledge", True, ['Progressive Bow', 'Bomb Upgrade (+5)']], ["Palace of Darkness - Map Chest", False, []], ["Palace of Darkness - Map Chest", False, [], ['Progressive Bow']], - ["Palace of Darkness - Map Chest", True, ['Progressive Bow']], + ["Palace of Darkness - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Palace of Darkness - Map Chest", True, ['Progressive Bow', 'Bomb Upgrade (+5)']], + ["Palace of Darkness - Map Chest", True, ['Progressive Bow', 'Pegasus Boots']], #Lower requirement for self-locking key #No lower requirement when bow/hammer is out of logic ["Palace of Darkness - Big Key Chest", False, []], ["Palace of Darkness - Big Key Chest", False, [key]*5, [key]], - ["Palace of Darkness - Big Key Chest", True, [key]*6], + ["Palace of Darkness - Big Key Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Palace of Darkness - Big Key Chest", True, [key]*6 + ['Bomb Upgrade (+5)']], ["Palace of Darkness - The Arena - Bridge", False, []], ["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Progressive Bow']], ["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Hammer']], + ["Palace of Darkness - The Arena - Bridge", False, [], [key, 'Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], ["Palace of Darkness - The Arena - Bridge", True, [key]], - ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer']], + ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer', 'Bomb Upgrade (+5)']], + ["Palace of Darkness - The Arena - Bridge", True, ['Progressive Bow', 'Hammer', 'Pegasus Boots']], ["Palace of Darkness - Stalfos Basement", False, []], ["Palace of Darkness - Stalfos Basement", False, [], [key, 'Progressive Bow']], ["Palace of Darkness - Stalfos Basement", False, [], [key, 'Hammer']], + ["Palace of Darkness - Stalfos Basement", False, [], [key, 'Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], ["Palace of Darkness - Stalfos Basement", True, [key]], - ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer']], + ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer', 'Bomb Upgrade (+5)']], + ["Palace of Darkness - Stalfos Basement", True, ['Progressive Bow', 'Hammer', 'Pegasus Boots']], ["Palace of Darkness - Compass Chest", False, []], ["Palace of Darkness - Compass Chest", False, [key]*3, [key]], @@ -67,8 +75,9 @@ def testDarkPalace(self): ["Palace of Darkness - Big Chest", False, []], ["Palace of Darkness - Big Chest", False, [], ['Lamp']], ["Palace of Darkness - Big Chest", False, [], ['Big Key (Palace of Darkness)']], + ["Palace of Darkness - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], ["Palace of Darkness - Big Chest", False, [key]*5, [key]], - ["Palace of Darkness - Big Chest", True, ['Lamp', 'Big Key (Palace of Darkness)'] + [key]*6], + ["Palace of Darkness - Big Chest", True, ['Bomb Upgrade (+5)', 'Lamp', 'Big Key (Palace of Darkness)'] + [key]*6], ["Palace of Darkness - Boss", False, []], ["Palace of Darkness - Boss", False, [], ['Lamp']], diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 8ca2791dcfe4..1f8288ace07f 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -14,6 +14,8 @@ def setUp(self): self.starting_regions = [] # Where to start exploring self.remove_exits = [] # Block dungeon exits self.multiworld.difficulty_requirements[1] = difficulties['normal'] + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True create_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) diff --git a/worlds/alttp/test/dungeons/TestEasternPalace.py b/worlds/alttp/test/dungeons/TestEasternPalace.py index 35c1b9928394..c1a978343b84 100644 --- a/worlds/alttp/test/dungeons/TestEasternPalace.py +++ b/worlds/alttp/test/dungeons/TestEasternPalace.py @@ -18,8 +18,8 @@ def testEastern(self): ["Eastern Palace - Big Key Chest", False, []], ["Eastern Palace - Big Key Chest", False, [], ['Lamp']], - ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)']], - ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Big Key (Eastern Palace)']], + ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)', 'Progressive Sword']], + ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Big Key (Eastern Palace)', 'Progressive Sword']], #@todo: Advanced? ["Eastern Palace - Boss", False, []], diff --git a/worlds/alttp/test/dungeons/TestGanonsTower.py b/worlds/alttp/test/dungeons/TestGanonsTower.py index d22dc92b366f..98bc6fa552e2 100644 --- a/worlds/alttp/test/dungeons/TestGanonsTower.py +++ b/worlds/alttp/test/dungeons/TestGanonsTower.py @@ -103,16 +103,16 @@ def testGanonsTower(self): ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], ["Ganons Tower - Big Key Chest", False, []], - ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Big Key Room - Left", False, []], - ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Room - Left", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Room - Left", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Big Key Room - Right", False, []], - ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Room - Right", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Room - Right", True, ['Bomb Upgrade (+5)', 'Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Mini Helmasaur Room - Left", False, []], ["Ganons Tower - Mini Helmasaur Room - Left", False, [], ['Progressive Bow']], diff --git a/worlds/alttp/test/dungeons/TestIcePalace.py b/worlds/alttp/test/dungeons/TestIcePalace.py index edc9f1fbae9e..7a15c5c09718 100644 --- a/worlds/alttp/test/dungeons/TestIcePalace.py +++ b/worlds/alttp/test/dungeons/TestIcePalace.py @@ -11,8 +11,9 @@ def testIcePalace(self): ["Ice Palace - Big Key Chest", False, [], ['Progressive Glove']], ["Ice Palace - Big Key Chest", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Big Key Chest", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], - ["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Big Key Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Big Key Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], #@todo: Change from item randomizer - Right side key door is only in logic if big key is in there #["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], #["Ice Palace - Big Key Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], @@ -30,8 +31,9 @@ def testIcePalace(self): ["Ice Palace - Map Chest", False, [], ['Progressive Glove']], ["Ice Palace - Map Chest", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Map Chest", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Map Chest", True, ['Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], - ["Ice Palace - Map Chest", True, ['Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Map Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Fire Rod', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Map Chest", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Bombos', 'Progressive Sword', 'Hammer', 'Hookshot', 'Small Key (Ice Palace)']], #["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], #["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cane of Byrna', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], #["Ice Palace - Map Chest", True, ['Progressive Glove', 'Cape', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], @@ -40,8 +42,9 @@ def testIcePalace(self): ["Ice Palace - Spike Room", False, []], ["Ice Palace - Spike Room", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Spike Room", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Spike Room", True, ['Fire Rod', 'Hookshot', 'Small Key (Ice Palace)']], - ["Ice Palace - Spike Room", True, ['Bombos', 'Progressive Sword', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Spike Room", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Spike Room", True, ['Bomb Upgrade (+5)', 'Fire Rod', 'Hookshot', 'Small Key (Ice Palace)']], + ["Ice Palace - Spike Room", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword', 'Hookshot', 'Small Key (Ice Palace)']], #["Ice Palace - Spike Room", True, ['Cape', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], #["Ice Palace - Spike Room", True, ['Cape', 'Bombos', 'Progressive Sword', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], #["Ice Palace - Spike Room", True, ['Cane of Byrna', 'Fire Rod', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], @@ -50,21 +53,24 @@ def testIcePalace(self): ["Ice Palace - Freezor Chest", False, []], ["Ice Palace - Freezor Chest", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Freezor Chest", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Freezor Chest", True, ['Fire Rod']], - ["Ice Palace - Freezor Chest", True, ['Bombos', 'Progressive Sword']], + ["Ice Palace - Freezor Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Freezor Chest", True, ['Bomb Upgrade (+5)', 'Fire Rod']], + ["Ice Palace - Freezor Chest", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword']], ["Ice Palace - Iced T Room", False, []], ["Ice Palace - Iced T Room", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Iced T Room", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Iced T Room", True, ['Fire Rod']], - ["Ice Palace - Iced T Room", True, ['Bombos', 'Progressive Sword']], + ["Ice Palace - Iced T Room", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Iced T Room", True, ['Bomb Upgrade (+5)', 'Fire Rod']], + ["Ice Palace - Iced T Room", True, ['Bomb Upgrade (+5)', 'Bombos', 'Progressive Sword']], ["Ice Palace - Big Chest", False, []], ["Ice Palace - Big Chest", False, [], ['Big Key (Ice Palace)']], ["Ice Palace - Big Chest", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Big Chest", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Big Chest", True, ['Big Key (Ice Palace)', 'Fire Rod']], - ["Ice Palace - Big Chest", True, ['Big Key (Ice Palace)', 'Bombos', 'Progressive Sword']], + ["Ice Palace - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Palace - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Ice Palace)', 'Fire Rod']], + ["Ice Palace - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword']], ["Ice Palace - Boss", False, []], ["Ice Palace - Boss", False, [], ['Hammer']], @@ -72,9 +78,10 @@ def testIcePalace(self): ["Ice Palace - Boss", False, [], ['Big Key (Ice Palace)']], ["Ice Palace - Boss", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Boss", False, [], ['Fire Rod', 'Progressive Sword']], + ["Ice Palace - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], # need hookshot now to reach the right side for the 6th key - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestMiseryMire.py b/worlds/alttp/test/dungeons/TestMiseryMire.py index ea5fb288450d..6cbf42922fa4 100644 --- a/worlds/alttp/test/dungeons/TestMiseryMire.py +++ b/worlds/alttp/test/dungeons/TestMiseryMire.py @@ -78,7 +78,8 @@ def testMiseryMire(self): ["Misery Mire - Boss", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow']], ["Misery Mire - Boss", False, [], ['Big Key (Misery Mire)']], ["Misery Mire - Boss", False, [], ['Pegasus Boots', 'Hookshot']], - ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']], - ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']], - ["Misery Mire - Boss", True, ['Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']], + ["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']], + ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']], + ["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestSkullWoods.py b/worlds/alttp/test/dungeons/TestSkullWoods.py index 7f97c4d2f823..55c8d2e29a21 100644 --- a/worlds/alttp/test/dungeons/TestSkullWoods.py +++ b/worlds/alttp/test/dungeons/TestSkullWoods.py @@ -8,7 +8,8 @@ def testSkullWoodsFrontAllEntrances(self): self.run_tests([ ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']], - ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']], + ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']], ["Skull Woods - Compass Chest", True, []], @@ -64,7 +65,8 @@ def testSkullWoodsBackOnly(self): self.run_tests([ ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']], - ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']], + ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']], ["Skull Woods - Compass Chest", False, []], ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], diff --git a/worlds/alttp/test/dungeons/TestSwampPalace.py b/worlds/alttp/test/dungeons/TestSwampPalace.py index 51440f6ccc4d..bddf40616f18 100644 --- a/worlds/alttp/test/dungeons/TestSwampPalace.py +++ b/worlds/alttp/test/dungeons/TestSwampPalace.py @@ -30,7 +30,8 @@ def testSwampPalace(self): ["Swamp Palace - Map Chest", False, [], ['Flippers']], ["Swamp Palace - Map Chest", False, [], ['Open Floodgate']], ["Swamp Palace - Map Chest", False, [], ['Small Key (Swamp Palace)']], - ["Swamp Palace - Map Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers']], + ["Swamp Palace - Map Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Swamp Palace - Map Chest", True, ['Bomb Upgrade (+5)', 'Open Floodgate', 'Small Key (Swamp Palace)', 'Flippers']], ["Swamp Palace - West Chest", False, []], ["Swamp Palace - West Chest", False, [], ['Flippers']], diff --git a/worlds/alttp/test/dungeons/TestThievesTown.py b/worlds/alttp/test/dungeons/TestThievesTown.py index 01f1570a2581..752b5305772a 100644 --- a/worlds/alttp/test/dungeons/TestThievesTown.py +++ b/worlds/alttp/test/dungeons/TestThievesTown.py @@ -41,8 +41,9 @@ def testThievesTown(self): ["Thieves' Town - Boss", False, []], ["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']], ["Thieves' Town - Boss", False, [], ['Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna']], - ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']], - ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']], - ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Somaria']], - ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Byrna']], + ["Thieves' Town - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']], + ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']], + ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Somaria']], + ["Thieves' Town - Boss", True, ['Bomb Upgrade (+5)', 'Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Cane of Byrna']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestTowerOfHera.py b/worlds/alttp/test/dungeons/TestTowerOfHera.py index 04685a66a876..3299e20291b0 100644 --- a/worlds/alttp/test/dungeons/TestTowerOfHera.py +++ b/worlds/alttp/test/dungeons/TestTowerOfHera.py @@ -18,11 +18,11 @@ def testTowerOfHera(self): ["Tower of Hera - Compass Chest", False, []], ["Tower of Hera - Compass Chest", False, [], ['Big Key (Tower of Hera)']], - ["Tower of Hera - Compass Chest", True, ['Big Key (Tower of Hera)']], + ["Tower of Hera - Compass Chest", True, ['Big Key (Tower of Hera)', 'Progressive Sword']], ["Tower of Hera - Big Chest", False, []], ["Tower of Hera - Big Chest", False, [], ['Big Key (Tower of Hera)']], - ["Tower of Hera - Big Chest", True, ['Big Key (Tower of Hera)']], + ["Tower of Hera - Big Chest", True, ['Big Key (Tower of Hera)', 'Progressive Sword']], ["Tower of Hera - Boss", False, []], ["Tower of Hera - Boss", False, [], ['Big Key (Tower of Hera)']], diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index f5608ba07b2d..f2c585e46500 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -14,7 +14,9 @@ class TestInverted(TestBase, LTTPTestBase): def setUp(self): self.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] - self.multiworld.mode[1] = "inverted" + self.multiworld.mode[1].value = 2 + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True create_inverted_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() create_shops(self.multiworld, 1) diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index d9eacb5ad98b..83a25812c9b6 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -11,8 +11,8 @@ class TestInvertedBombRules(LTTPTestBase): def setUp(self): self.world_setup() - self.multiworld.mode[1] = "inverted" self.multiworld.difficulty_requirements[1] = difficulties['normal'] + self.multiworld.mode[1].value = 2 create_inverted_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() diff --git a/worlds/alttp/test/inverted/TestInvertedDarkWorld.py b/worlds/alttp/test/inverted/TestInvertedDarkWorld.py index 710ee07f2b6d..16b837ee6556 100644 --- a/worlds/alttp/test/inverted/TestInvertedDarkWorld.py +++ b/worlds/alttp/test/inverted/TestInvertedDarkWorld.py @@ -5,7 +5,8 @@ class TestInvertedDarkWorld(TestInverted): def testNorthWest(self): self.run_location_tests([ - ["Brewery", True, []], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Brewery", True, ['Bomb Upgrade (+5)']], ["C-Shaped House", True, []], @@ -77,15 +78,16 @@ def testNorthEast(self): def testSouth(self): self.run_location_tests([ - ["Hype Cave - Top", True, []], - - ["Hype Cave - Middle Right", True, []], - - ["Hype Cave - Middle Left", True, []], - - ["Hype Cave - Bottom", True, []], - - ["Hype Cave - Generous Guy", True, []], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']], ["Stumpy", True, []], diff --git a/worlds/alttp/test/inverted/TestInvertedDeathMountain.py b/worlds/alttp/test/inverted/TestInvertedDeathMountain.py index aedec2a1daa6..605a9dc3f3a9 100644 --- a/worlds/alttp/test/inverted/TestInvertedDeathMountain.py +++ b/worlds/alttp/test/inverted/TestInvertedDeathMountain.py @@ -40,10 +40,12 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']], + ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']], + ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Moon Pearl']], @@ -52,10 +54,12 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']], + ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']], + ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']], + ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']], @@ -64,10 +68,12 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Middle", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']], + ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']], + ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']], + ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Moon Pearl']], @@ -76,10 +82,12 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']], + ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']], + ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']], + ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']], @@ -88,10 +96,12 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Cane of Somaria', 'Fire Rod']], + ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Fire Rod']], + ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl', 'Progressive Bow']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Moon Pearl']], @@ -100,10 +110,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Upper - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Moon Pearl']], @@ -112,20 +123,22 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Upper - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Mimic Cave", False, []], ["Mimic Cave", False, [], ['Moon Pearl']], ["Mimic Cave", False, [], ['Hammer']], ["Mimic Cave", False, [], ['Progressive Glove', 'Flute']], ["Mimic Cave", False, [], ['Lamp', 'Flute']], - ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Hammer', 'Hookshot']], - ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']], - ["Mimic Cave", True, ['Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']], - ["Mimic Cave", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']], + ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Bow', 'Cane of Somaria', 'Progressive Sword']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Flute', 'Moon Pearl', 'Hammer', 'Hookshot']], + ["Mimic Cave", True, ['Progressive Bow', 'Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']], + ["Mimic Cave", True, ['Cane of Somaria', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']], + ["Mimic Cave", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']], ["Ether Tablet", False, []], ["Ether Tablet", False, [], ['Moon Pearl']], diff --git a/worlds/alttp/test/inverted/TestInvertedLightWorld.py b/worlds/alttp/test/inverted/TestInvertedLightWorld.py index 9d4b9099daae..77af09317241 100644 --- a/worlds/alttp/test/inverted/TestInvertedLightWorld.py +++ b/worlds/alttp/test/inverted/TestInvertedLightWorld.py @@ -44,15 +44,17 @@ def testKakariko(self): ["Chicken House", False, []], ["Chicken House", False, [], ['Moon Pearl']], - ["Chicken House", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Kakariko Well - Top", False, []], ["Kakariko Well - Top", False, [], ['Moon Pearl']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Kakariko Well - Left", False, []], ["Kakariko Well - Left", False, [], ['Moon Pearl']], @@ -80,9 +82,10 @@ def testKakariko(self): ["Blind's Hideout - Top", False, []], ["Blind's Hideout - Top", False, [], ['Moon Pearl']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Blind's Hideout - Left", False, []], ["Blind's Hideout - Left", False, [], ['Moon Pearl']], @@ -161,9 +164,10 @@ def testKakariko(self): ["Maze Race", False, []], ["Maze Race", False, [], ['Moon Pearl']], - ["Maze Race", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']], + ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ]) def testSouthLightWorld(self): @@ -184,9 +188,10 @@ def testSouthLightWorld(self): ["Aginah's Cave", False, []], ["Aginah's Cave", False, [], ['Moon Pearl']], - ["Aginah's Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Bombos Tablet", False, []], ["Bombos Tablet", False, ['Progressive Sword'], ['Progressive Sword']], @@ -212,39 +217,45 @@ def testSouthLightWorld(self): ["Mini Moldorm Cave - Far Left", False, []], ["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']], ["Mini Moldorm Cave - Left", False, []], ["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']], ["Mini Moldorm Cave - Generous Guy", False, []], ["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']], ["Mini Moldorm Cave - Right", False, []], ["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']], ["Mini Moldorm Cave - Far Right", False, []], ["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Sword']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Progressive Sword']], ["Ice Rod Cave", False, []], ["Ice Rod Cave", False, [], ['Moon Pearl']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ]) def testZoraArea(self): @@ -302,21 +313,24 @@ def testLightWorld(self): ["Sahasrahla's Hut - Left", False, []], ["Sahasrahla's Hut - Left", False, [], ['Moon Pearl']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Sahasrahla's Hut - Middle", False, []], ["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Sahasrahla's Hut - Right", False, []], ["Sahasrahla's Hut - Right", False, [], ['Moon Pearl']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Sahasrahla", False, []], ["Sahasrahla", False, [], ['Green Pendant']], @@ -346,9 +360,10 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Moon Pearl']], - ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Graveyard Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], ["Potion Shop", False, []], ["Potion Shop", False, [], ['Mushroom']], diff --git a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py index fe8979c1ef02..f3698c90ff06 100644 --- a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py +++ b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py @@ -21,10 +21,10 @@ def testTurtleRock(self): ["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], + ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], - ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']], + ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Fire Rod']], ["Turtle Rock - Roller Room - Left", False, []], ["Turtle Rock - Roller Room - Left", False, [], ['Cane of Somaria']], @@ -54,8 +54,8 @@ def testTurtleRock(self): ["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], @@ -68,10 +68,10 @@ def testTurtleRock(self): ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], # Mirror in from ledge, use left side entrance, have enough keys to get to the chest - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Crystaroller Room", False, []], ["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Magic Mirror']], @@ -102,8 +102,11 @@ def testTurtleRock(self): ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']] + ]) + + def testEyeBridge(self): for location in ["Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Bottom Right", "Turtle Rock - Eye Bridge - Bottom Left"]: diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py index 69f564489700..dd4a74b6c4d2 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDarkWorld.py @@ -5,7 +5,8 @@ class TestInvertedDarkWorld(TestInvertedMinor): def testNorthWest(self): self.run_location_tests([ - ["Brewery", True, []], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Brewery", True, ['Bomb Upgrade (+5)']], ["C-Shaped House", True, []], @@ -67,15 +68,16 @@ def testNorthEast(self): def testSouth(self): self.run_location_tests([ - ["Hype Cave - Top", True, []], - - ["Hype Cave - Middle Right", True, []], - - ["Hype Cave - Middle Left", True, []], - - ["Hype Cave - Bottom", True, []], - - ["Hype Cave - Generous Guy", True, []], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']], ["Stumpy", True, []], diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py index c68a8e5f0c89..c189d107d976 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedDeathMountain.py @@ -40,10 +40,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']], + ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Moon Pearl']], @@ -52,10 +53,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']], + ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']], @@ -64,10 +66,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Middle", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']], + ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Moon Pearl']], @@ -76,10 +79,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']], + ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']], @@ -88,10 +92,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod']], + ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Moon Pearl']], @@ -100,10 +105,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Left", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Upper - Left", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Moon Pearl']], @@ -112,20 +118,21 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Right", False, ['Progressive Glove'], ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot', 'Moon Pearl']], ["Paradox Cave Upper - Right", False, ['Flute', 'Progressive Glove', 'Hammer', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], ["Mimic Cave", False, []], ["Mimic Cave", False, [], ['Moon Pearl']], ["Mimic Cave", False, [], ['Hammer']], ["Mimic Cave", False, [], ['Progressive Glove', 'Flute']], ["Mimic Cave", False, [], ['Lamp', 'Flute']], - ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Hammer', 'Hookshot']], - ["Mimic Cave", True, ['Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']], - ["Mimic Cave", True, ['Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']], - ["Mimic Cave", True, ['Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Flute', 'Moon Pearl', 'Hammer', 'Hookshot']], + ["Mimic Cave", True, ['Progressive Sword', 'Progressive Sword', 'Flute', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hammer']], + ["Mimic Cave", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer', 'Hookshot']], + ["Mimic Cave", True, ['Cane of Somaria', 'Progressive Glove', 'Progressive Glove', 'Lamp', 'Moon Pearl', 'Hammer']], ["Ether Tablet", False, []], ["Ether Tablet", False, [], ['Moon Pearl']], diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py index 376e7b4bec49..086c1c92b5dd 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedLightWorld.py @@ -43,16 +43,18 @@ def testKakariko(self): ["Chicken House", False, []], ["Chicken House", False, [], ['Moon Pearl']], - ["Chicken House", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Chicken House", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Chicken House", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], # top can't be bombed as super bunny and needs Moon Pearl ["Kakariko Well - Top", False, []], ["Kakariko Well - Top", False, [], ['Moon Pearl']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Kakariko Well - Top", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Kakariko Well - Left", False, []], ["Kakariko Well - Left", True, ['Beat Agahnim 1']], @@ -76,9 +78,10 @@ def testKakariko(self): ["Blind's Hideout - Top", False, []], ["Blind's Hideout - Top", False, [], ['Moon Pearl', 'Magic Mirror']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Blind's Hideout - Top", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Blind's Hideout - Left", False, []], ["Blind's Hideout - Left", False, [], ['Moon Pearl', 'Magic Mirror']], @@ -157,9 +160,10 @@ def testKakariko(self): ["Maze Race", False, []], ["Maze Race", False, [], ['Moon Pearl']], - ["Maze Race", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Maze Race", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Maze Race", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)','Bomb Upgrade (50)', 'Pegasus Boots']], + ["Maze Race", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Maze Race", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ]) def testSouthLightWorld(self): @@ -179,9 +183,10 @@ def testSouthLightWorld(self): ["Aginah's Cave", False, []], ["Aginah's Cave", False, [], ['Moon Pearl']], - ["Aginah's Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Aginah's Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Bombos Tablet", False, []], ["Bombos Tablet", False, ['Progressive Sword'], ['Progressive Sword']], @@ -209,39 +214,45 @@ def testSouthLightWorld(self): ["Mini Moldorm Cave - Far Left", False, []], ["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Mini Moldorm Cave - Left", False, []], ["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Mini Moldorm Cave - Generous Guy", False, []], ["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Mini Moldorm Cave - Right", False, []], ["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Mini Moldorm Cave - Far Right", False, []], ["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Beat Agahnim 1']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ["Ice Rod Cave", False, []], ["Ice Rod Cave", False, [], ['Moon Pearl']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], ]) def testZoraArea(self): @@ -297,25 +308,28 @@ def testLightWorld(self): ["Sahasrahla's Hut - Left", False, []], ["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Magic Mirror']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Left", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Left", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], # super bunny bonk ["Sahasrahla's Hut - Left", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']], ["Sahasrahla's Hut - Middle", False, []], ["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Magic Mirror']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], # super bunny bonk ["Sahasrahla's Hut - Middle", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']], ["Sahasrahla's Hut - Right", False, []], ["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Magic Mirror']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], # super bunny bonk ["Sahasrahla's Hut - Right", True, ['Magic Mirror', 'Beat Agahnim 1', 'Pegasus Boots']], @@ -347,9 +361,10 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Moon Pearl']], - ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Graveyard Cave", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Graveyard Cave", True, ['Moon Pearl', 'Beat Agahnim 1']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], ["Potion Shop", False, []], ["Potion Shop", False, [], ['Mushroom']], diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index 33e582298185..0219332e0748 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -13,8 +13,10 @@ class TestInvertedMinor(TestBase, LTTPTestBase): def setUp(self): self.world_setup() - self.multiworld.mode[1] = "inverted" - self.multiworld.logic[1] = "minorglitches" + self.multiworld.mode[1].value = 2 + self.multiworld.glitches_required[1] = "minor_glitches" + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py index d7b5c9f79788..3c75a2c3684b 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py @@ -22,10 +22,10 @@ def testTurtleRock(self): ["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], + ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], - ["Turtle Rock - Chain Chomps", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']], + ["Turtle Rock - Chain Chomps", True, ['Fire Rod', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']], ["Turtle Rock - Roller Room - Left", False, []], ["Turtle Rock - Roller Room - Left", False, [], ['Cane of Somaria']], @@ -55,8 +55,8 @@ def testTurtleRock(self): ["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], @@ -69,10 +69,10 @@ def testTurtleRock(self): ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], # Mirror in from ledge, use left side entrance, have enough keys to get to the chest - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Crystaroller Room", False, []], ["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Magic Mirror']], @@ -98,7 +98,7 @@ def testTurtleRock(self): ["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']], ["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], diff --git a/worlds/alttp/test/inverted_owg/TestDarkWorld.py b/worlds/alttp/test/inverted_owg/TestDarkWorld.py index e7e720d2b853..8fb234f0b50a 100644 --- a/worlds/alttp/test/inverted_owg/TestDarkWorld.py +++ b/worlds/alttp/test/inverted_owg/TestDarkWorld.py @@ -5,15 +5,16 @@ class TestDarkWorld(TestInvertedOWG): def testSouthDarkWorld(self): self.run_location_tests([ - ["Hype Cave - Top", True, []], - - ["Hype Cave - Middle Right", True, []], - - ["Hype Cave - Middle Left", True, []], - - ["Hype Cave - Bottom", True, []], - - ["Hype Cave - Generous Guy", True, []], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)']], ["Stumpy", True, []], @@ -22,7 +23,8 @@ def testSouthDarkWorld(self): def testWestDarkWorld(self): self.run_location_tests([ - ["Brewery", True, []], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Brewery", True, ['Bomb Upgrade (+5)']], ["C-Shaped House", True, []], diff --git a/worlds/alttp/test/inverted_owg/TestDeathMountain.py b/worlds/alttp/test/inverted_owg/TestDeathMountain.py index 79796a7aeb1e..b509643d0c5e 100644 --- a/worlds/alttp/test/inverted_owg/TestDeathMountain.py +++ b/worlds/alttp/test/inverted_owg/TestDeathMountain.py @@ -24,36 +24,38 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, []], ["Paradox Cave Lower - Far Left", False, [], ['Moon Pearl']], - ["Paradox Cave Lower - Far Left", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Moon Pearl']], - ["Paradox Cave Lower - Left", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Moon Pearl']], - ["Paradox Cave Lower - Middle", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Moon Pearl']], - ["Paradox Cave Lower - Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Moon Pearl']], - ["Paradox Cave Lower - Far Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Moon Pearl']], - ["Paradox Cave Upper - Left", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Moon Pearl']], - ["Paradox Cave Upper - Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Mimic Cave", False, []], ["Mimic Cave", False, [], ['Moon Pearl']], ["Mimic Cave", False, [], ['Hammer']], - ["Mimic Cave", True, ['Moon Pearl', 'Hammer', 'Pegasus Boots']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Hammer', 'Pegasus Boots']], ["Ether Tablet", False, []], ["Ether Tablet", False, ['Progressive Sword'], ['Progressive Sword']], diff --git a/worlds/alttp/test/inverted_owg/TestDungeons.py b/worlds/alttp/test/inverted_owg/TestDungeons.py index f5d07544aaea..0d8445895eca 100644 --- a/worlds/alttp/test/inverted_owg/TestDungeons.py +++ b/worlds/alttp/test/inverted_owg/TestDungeons.py @@ -13,16 +13,16 @@ def testFirstDungeonChests(self): ["Sanctuary", False, []], ["Sanctuary", False, ['Beat Agahnim 1']], ["Sanctuary", True, ['Magic Mirror', 'Beat Agahnim 1']], - ["Sanctuary", True, ['Lamp', 'Beat Agahnim 1', 'Small Key (Hyrule Castle)']], + ["Sanctuary", True, ['Progressive Sword', 'Lamp', 'Beat Agahnim 1', 'Small Key (Hyrule Castle)']], ["Sanctuary", True, ['Moon Pearl', 'Pegasus Boots']], ["Sanctuary", True, ['Magic Mirror', 'Pegasus Boots']], ["Sewers - Secret Room - Left", False, []], ["Sewers - Secret Room - Left", True, ['Moon Pearl', 'Progressive Glove', 'Pegasus Boots']], - ["Sewers - Secret Room - Left", True, ['Moon Pearl', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']], - ["Sewers - Secret Room - Left", True, - ['Magic Mirror', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']], - ["Sewers - Secret Room - Left", True, ['Beat Agahnim 1', 'Lamp', 'Small Key (Hyrule Castle)']], + ["Sewers - Secret Room - Left", True, ['Progressive Sword', 'Moon Pearl', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']], + ["Sewers - Secret Room - Left", True, ['Progressive Sword', 'Magic Mirror', 'Pegasus Boots', 'Lamp', 'Small Key (Hyrule Castle)']], + ["Sewers - Secret Room - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Beat Agahnim 1', 'Lamp', 'Small Key (Hyrule Castle)']], + ["Sewers - Secret Room - Left", True, ['Bomb Upgrade (+10)', 'Beat Agahnim 1', 'Lamp', 'Small Key (Hyrule Castle)']], ["Eastern Palace - Compass Chest", False, []], ["Eastern Palace - Compass Chest", True, ['Moon Pearl', 'Pegasus Boots']], @@ -45,7 +45,7 @@ def testFirstDungeonChests(self): ["Tower of Hera - Basement Cage", True, ['Pegasus Boots', 'Moon Pearl']], ["Castle Tower - Room 03", False, []], - ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Room 03", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Room 03", True, ['Pegasus Boots', 'Progressive Sword']], ["Castle Tower - Room 03", True, ['Pegasus Boots', 'Progressive Bow']], @@ -62,7 +62,8 @@ def testFirstDungeonChests(self): ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']], - ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']], + ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Big Key (Skull Woods)']], ["Skull Woods - Big Key Chest", True, []], @@ -89,7 +90,16 @@ def testFirstDungeonChests(self): ["Turtle Rock - Compass Chest", True, ['Pegasus Boots', 'Quake', 'Progressive Sword', 'Cane of Somaria']], ["Turtle Rock - Chain Chomps", False, []], - ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Hookshot', 'Progressive Sword', 'Progressive Bow', 'Blue Boomerang', 'Red Boomerang', 'Cane of Somaria', 'Fire Rod', 'Ice Rod']], + ["Turtle Rock - Chain Chomps", True, ['Bomb Upgrade (+5)', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Hookshot', 'Pegasus Boots']], + ["Turtle Rock - Chain Chomps", True, ['Progressive Bow', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Blue Boomerang', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Red Boomerang', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Cane of Somaria', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Fire Rod', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Ice Rod', 'Pegasus Boots', 'Magic Mirror', 'Moon Pearl']], + ["Turtle Rock - Chain Chomps", True, ['Progressive Sword', 'Progressive Sword', 'Pegasus Boots']], ["Turtle Rock - Crystaroller Room", False, []], ["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)']], diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index a4e84fce9b62..849f06098a44 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -13,8 +13,10 @@ class TestInvertedOWG(TestBase, LTTPTestBase): def setUp(self): self.world_setup() - self.multiworld.logic[1] = "owglitches" - self.multiworld.mode[1] = "inverted" + self.multiworld.glitches_required[1] = "overworld_glitches" + self.multiworld.mode[1].value = 2 + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True self.multiworld.difficulty_requirements[1] = difficulties['normal'] create_inverted_regions(self.multiworld, 1) self.multiworld.worlds[1].create_dungeons() diff --git a/worlds/alttp/test/inverted_owg/TestLightWorld.py b/worlds/alttp/test/inverted_owg/TestLightWorld.py index de92b4ef854d..bd18259bec3f 100644 --- a/worlds/alttp/test/inverted_owg/TestLightWorld.py +++ b/worlds/alttp/test/inverted_owg/TestLightWorld.py @@ -40,40 +40,46 @@ def testLightWorld(self): ["Chicken House", False, []], ["Chicken House", False, [], ['Moon Pearl']], - ["Chicken House", True, ['Moon Pearl', 'Pegasus Boots']], + ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Aginah's Cave", False, []], ["Aginah's Cave", False, [], ['Moon Pearl']], - ["Aginah's Cave", True, ['Moon Pearl', 'Pegasus Boots']], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Sahasrahla's Hut - Left", False, []], ["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Magic Mirror']], ["Sahasrahla's Hut - Left", False, [], ['Moon Pearl', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Pegasus Boots']], ["Sahasrahla's Hut - Left", True, ['Magic Mirror', 'Pegasus Boots']], ##todo: Damage boost superbunny not in logic #["Sahasrahla's Hut - Left", True, ['Beat Agahnim 1', 'Pegasus Boots']], - ["Sahasrahla's Hut - Left", True, ['Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], ["Sahasrahla's Hut - Middle", False, []], ["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Magic Mirror']], ["Sahasrahla's Hut - Middle", False, [], ['Moon Pearl', 'Pegasus Boots']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Pegasus Boots']], ["Sahasrahla's Hut - Middle", True, ['Magic Mirror', 'Pegasus Boots']], #["Sahasrahla's Hut - Middle", True, ['Beat Agahnim 1', 'Pegasus Boots']], - ["Sahasrahla's Hut - Middle", True, ['Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Middle", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], ["Sahasrahla's Hut - Right", False, []], ["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Magic Mirror']], ["Sahasrahla's Hut - Right", False, [], ['Moon Pearl', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Pegasus Boots']], ["Sahasrahla's Hut - Right", True, ['Magic Mirror', 'Pegasus Boots']], #["Sahasrahla's Hut - Right", True, ['Beat Agahnim 1', 'Pegasus Boots']], - ["Sahasrahla's Hut - Right", True, ['Moon Pearl', 'Beat Agahnim 1']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1']], ["Kakariko Well - Top", False, []], ["Kakariko Well - Top", False, [], ['Moon Pearl']], - ["Kakariko Well - Top", True, ['Moon Pearl', 'Pegasus Boots']], + ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Kakariko Well - Left", False, []], ["Kakariko Well - Left", True, ['Moon Pearl', 'Pegasus Boots']], @@ -101,7 +107,8 @@ def testLightWorld(self): ["Blind's Hideout - Top", False, []], ["Blind's Hideout - Top", False, [], ['Moon Pearl']], - ["Blind's Hideout - Top", True, ['Moon Pearl', 'Pegasus Boots']], + ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Blind's Hideout - Left", False, []], ["Blind's Hideout - Left", False, [], ['Moon Pearl', 'Magic Mirror']], @@ -134,27 +141,33 @@ def testLightWorld(self): ["Mini Moldorm Cave - Far Left", False, []], ["Mini Moldorm Cave - Far Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Left", True, ['Moon Pearl', 'Pegasus Boots']], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']], ["Mini Moldorm Cave - Left", False, []], ["Mini Moldorm Cave - Left", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Left", True, ['Moon Pearl', 'Pegasus Boots']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']], ["Mini Moldorm Cave - Right", False, []], ["Mini Moldorm Cave - Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']], ["Mini Moldorm Cave - Far Right", False, []], ["Mini Moldorm Cave - Far Right", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Far Right", True, ['Moon Pearl', 'Pegasus Boots']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']], ["Mini Moldorm Cave - Generous Guy", False, []], ["Mini Moldorm Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Mini Moldorm Cave - Generous Guy", True, ['Moon Pearl', 'Pegasus Boots']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword', 'Moon Pearl', 'Pegasus Boots']], ["Ice Rod Cave", False, []], ["Ice Rod Cave", False, [], ['Moon Pearl']], - ["Ice Rod Cave", True, ['Moon Pearl', 'Pegasus Boots']], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], #I don't think so #["Ice Rod Cave", True, ['Magic Mirror', 'Pegasus Boots', 'BigRedBomb']], #["Ice Rod Cave", True, ['Magic Mirror', 'Beat Agahnim 1', 'BigRedBomb']], @@ -236,7 +249,8 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Moon Pearl']], - ["Graveyard Cave", True, ['Moon Pearl', 'Pegasus Boots']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], ["Checkerboard Cave", False, []], ["Checkerboard Cave", False, [], ['Progressive Glove']], diff --git a/worlds/alttp/test/minor_glitches/TestDarkWorld.py b/worlds/alttp/test/minor_glitches/TestDarkWorld.py index 3a6f97254c95..9b0e43ea9494 100644 --- a/worlds/alttp/test/minor_glitches/TestDarkWorld.py +++ b/worlds/alttp/test/minor_glitches/TestDarkWorld.py @@ -7,43 +7,48 @@ def testSouthDarkWorld(self): self.run_location_tests([ ["Hype Cave - Top", False, []], ["Hype Cave - Top", False, [], ['Moon Pearl']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Right", False, []], ["Hype Cave - Middle Right", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Left", False, []], ["Hype Cave - Middle Left", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Bottom", False, []], ["Hype Cave - Bottom", False, [], ['Moon Pearl']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Generous Guy", False, []], ["Hype Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Stumpy", False, []], ["Stumpy", False, [], ['Moon Pearl']], @@ -66,10 +71,11 @@ def testWestDarkWorld(self): self.run_location_tests([ ["Brewery", False, []], ["Brewery", False, [], ['Moon Pearl']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["C-Shaped House", False, []], ["C-Shaped House", False, [], ['Moon Pearl']], diff --git a/worlds/alttp/test/minor_glitches/TestDeathMountain.py b/worlds/alttp/test/minor_glitches/TestDeathMountain.py index 2603aaeb7b9e..4446ee7e8f88 100644 --- a/worlds/alttp/test/minor_glitches/TestDeathMountain.py +++ b/worlds/alttp/test/minor_glitches/TestDeathMountain.py @@ -48,7 +48,8 @@ def testEastDeathMountain(self): ["Mimic Cave", False, [], ['Moon Pearl']], ["Mimic Cave", False, [], ['Cane of Somaria']], ["Mimic Cave", False, ['Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Mimic Cave", True, ['Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Spiral Cave", False, []], ["Spiral Cave", False, [], ['Progressive Glove', 'Flute']], @@ -73,10 +74,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Progressive Glove', 'Flute']], @@ -87,10 +89,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Progressive Glove', 'Flute']], @@ -101,10 +104,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Progressive Glove', 'Flute']], @@ -115,10 +119,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Progressive Glove', 'Flute']], @@ -129,10 +134,11 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Progressive Glove', 'Flute']], @@ -143,10 +149,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Upper - Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Progressive Glove', 'Flute']], @@ -157,10 +164,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Upper - Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ]) def testWestDarkWorldDeathMountain(self): diff --git a/worlds/alttp/test/minor_glitches/TestLightWorld.py b/worlds/alttp/test/minor_glitches/TestLightWorld.py index bdfdc2349691..017f2d64a8f8 100644 --- a/worlds/alttp/test/minor_glitches/TestLightWorld.py +++ b/worlds/alttp/test/minor_glitches/TestLightWorld.py @@ -29,17 +29,21 @@ def testLightWorld(self): ["Kakariko Tavern", True, []], - ["Chicken House", True, []], + ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)']], - ["Aginah's Cave", True, []], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)']], - ["Sahasrahla's Hut - Left", True, []], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']], - ["Sahasrahla's Hut - Middle", True, []], - - ["Sahasrahla's Hut - Right", True, []], - - ["Kakariko Well - Top", True, []], + ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']], ["Kakariko Well - Left", True, []], @@ -49,7 +53,8 @@ def testLightWorld(self): ["Kakariko Well - Bottom", True, []], - ["Blind's Hideout - Top", True, []], + ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']], ["Blind's Hideout - Left", True, []], @@ -63,15 +68,19 @@ def testLightWorld(self): ["Bonk Rock Cave", False, [], ['Pegasus Boots']], ["Bonk Rock Cave", True, ['Pegasus Boots']], - ["Mini Moldorm Cave - Far Left", True, []], - - ["Mini Moldorm Cave - Left", True, []], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], - ["Mini Moldorm Cave - Right", True, []], - - ["Mini Moldorm Cave - Far Right", True, []], - - ["Ice Rod Cave", True, []], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']], ["Bottle Merchant", True, []], @@ -131,11 +140,12 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Magic Mirror']], ["Graveyard Cave", False, [], ['Moon Pearl']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Checkerboard Cave", False, []], ["Checkerboard Cave", False, [], ['Progressive Glove']], @@ -143,8 +153,6 @@ def testLightWorld(self): ["Checkerboard Cave", False, [], ['Magic Mirror']], ["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Mini Moldorm Cave - Generous Guy", True, []], - ["Library", False, []], ["Library", False, [], ['Pegasus Boots']], ["Library", True, ['Pegasus Boots']], @@ -155,7 +163,10 @@ def testLightWorld(self): ["Potion Shop", False, [], ['Mushroom']], ["Potion Shop", True, ['Mushroom']], - ["Maze Race", True, []], + ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']], + ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Maze Race", True, ['Bomb Upgrade (+5)']], + ["Maze Race", True, ['Pegasus Boots']], ["Desert Ledge", False, []], ["Desert Ledge", False, [], ['Book of Mudora', 'Flute']], diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index d5cfd3095b9c..c7de74d3a6c3 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -10,7 +10,9 @@ class TestMinor(TestBase, LTTPTestBase): def setUp(self): self.world_setup() - self.multiworld.logic[1] = "minorglitches" + self.multiworld.glitches_required[1] = "minor_glitches" + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].create_regions() diff --git a/worlds/alttp/test/owg/TestDarkWorld.py b/worlds/alttp/test/owg/TestDarkWorld.py index 93324656bd93..c671f6485c92 100644 --- a/worlds/alttp/test/owg/TestDarkWorld.py +++ b/worlds/alttp/test/owg/TestDarkWorld.py @@ -7,48 +7,53 @@ def testSouthDarkWorld(self): self.run_location_tests([ ["Hype Cave - Top", False, []], ["Hype Cave - Top", False, [], ['Moon Pearl']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Pegasus Boots']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Right", False, []], ["Hype Cave - Middle Right", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Pegasus Boots']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Left", False, []], ["Hype Cave - Middle Left", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Pegasus Boots']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Bottom", False, []], ["Hype Cave - Bottom", False, [], ['Moon Pearl']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Pegasus Boots']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Generous Guy", False, []], ["Hype Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Pegasus Boots']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Stumpy", False, []], ["Stumpy", False, [], ['Moon Pearl']], @@ -129,13 +134,14 @@ def testWestDarkWorld(self): self.run_location_tests([ ["Brewery", False, []], ["Brewery", False, [], ['Moon Pearl']], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], ["Brewery", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot', 'Progressive Glove']], - ["Brewery", True, ['Moon Pearl', 'Pegasus Boots']], - ["Brewery", True, ['Moon Pearl', 'Flute', 'Magic Mirror']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Flute', 'Magic Mirror']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["C-Shaped House", False, []], ["C-Shaped House", False, [], ['Moon Pearl', 'Magic Mirror']], diff --git a/worlds/alttp/test/owg/TestDeathMountain.py b/worlds/alttp/test/owg/TestDeathMountain.py index 41031c65c593..0933b2881e2d 100644 --- a/worlds/alttp/test/owg/TestDeathMountain.py +++ b/worlds/alttp/test/owg/TestDeathMountain.py @@ -48,9 +48,10 @@ def testEastDeathMountain(self): ["Mimic Cave", False, [], ['Hammer']], ["Mimic Cave", False, [], ['Pegasus Boots', 'Flute', 'Lamp']], ["Mimic Cave", False, [], ['Pegasus Boots', 'Flute', 'Progressive Glove']], - ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Pegasus Boots']], - ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Progressive Glove', 'Lamp']], - ["Mimic Cave", True, ['Magic Mirror', 'Hammer', 'Flute']], + ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Hookshot', 'Hammer']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Pegasus Boots']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Progressive Glove', 'Lamp']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Magic Mirror', 'Hammer', 'Flute']], ["Spiral Cave", False, []], ["Spiral Cave", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], @@ -64,65 +65,72 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, []], ["Paradox Cave Lower - Far Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Lower - Far Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Pegasus Boots']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Lower - Far Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Left", True, ['Fire Rod', 'Pegasus Boots']], + ["Paradox Cave Lower - Far Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Lower - Far Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Lower - Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Pegasus Boots']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Lower - Left", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Left", True, ['Fire Rod', 'Pegasus Boots']], + ["Paradox Cave Lower - Left", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Left", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Left", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Lower - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Lower - Middle", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Pegasus Boots']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Lower - Middle", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Middle", True, ['Fire Rod', 'Pegasus Boots']], + ["Paradox Cave Lower - Middle", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Middle", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Middle", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Lower - Middle", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Lower - Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Pegasus Boots']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Lower - Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Right", True, ['Fire Rod', 'Pegasus Boots']], + ["Paradox Cave Lower - Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Lower - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Lower - Far Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Pegasus Boots']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Lower - Far Right", False, ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Progressive Sword', 'Progressive Bow', 'Fire Rod', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Right", True, ['Fire Rod', 'Pegasus Boots']], + ["Paradox Cave Lower - Far Right", True, ['Cane of Somaria', 'Flute', 'Hookshot']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Sword', 'Progressive Sword', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Bow', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Lower - Far Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Upper - Left", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Pegasus Boots']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Pegasus Boots', 'Progressive Glove', 'Flute']], ["Paradox Cave Upper - Right", False, [], ['Pegasus Boots', 'Magic Mirror', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Pegasus Boots']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror']], + ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror']], ]) def testWestDarkWorldDeathMountain(self): diff --git a/worlds/alttp/test/owg/TestDungeons.py b/worlds/alttp/test/owg/TestDungeons.py index 4f878969679a..f4688b7a35f9 100644 --- a/worlds/alttp/test/owg/TestDungeons.py +++ b/worlds/alttp/test/owg/TestDungeons.py @@ -6,13 +6,14 @@ class TestDungeons(TestVanillaOWG): def testFirstDungeonChests(self): self.run_location_tests([ ["Hyrule Castle - Map Chest", True, []], - ["Hyrule Castle - Map Guard Key Drop", True, []], + ["Hyrule Castle - Map Guard Key Drop", False, []], + ["Hyrule Castle - Map Guard Key Drop", True, ['Progressive Sword']], ["Sanctuary", True, []], ["Sewers - Secret Room - Left", False, []], - ["Sewers - Secret Room - Left", True, ['Progressive Glove']], - ["Sewers - Secret Room - Left", True, ['Lamp', 'Small Key (Hyrule Castle)']], + ["Sewers - Secret Room - Left", True, ['Pegasus Boots', 'Progressive Glove']], + ["Sewers - Secret Room - Left", True, ['Progressive Sword', 'Bomb Upgrade (+5)', 'Lamp', 'Small Key (Hyrule Castle)']], ["Eastern Palace - Compass Chest", True, []], @@ -41,10 +42,9 @@ def testFirstDungeonChests(self): ["Castle Tower - Room 03", False, []], ["Castle Tower - Room 03", False, ['Progressive Sword'], ['Progressive Sword', 'Cape', 'Beat Agahnim 1']], - ["Castle Tower - Room 03", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Room 03", True, ['Progressive Sword', 'Progressive Sword']], - ["Castle Tower - Room 03", True, ['Cape', 'Progressive Bow']], - ["Castle Tower - Room 03", True, ['Beat Agahnim 1', 'Fire Rod']], + ["Castle Tower - Room 03", True, ['Progressive Sword', 'Cape']], + ["Castle Tower - Room 03", True, ['Progressive Sword', 'Beat Agahnim 1']], ["Palace of Darkness - Shooter Room", False, []], ["Palace of Darkness - Shooter Room", False, [], ['Moon Pearl']], @@ -69,9 +69,10 @@ def testFirstDungeonChests(self): ["Skull Woods - Big Chest", False, []], ["Skull Woods - Big Chest", False, [], ['Big Key (Skull Woods)']], + ["Skull Woods - Big Chest", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], #todo: Bomb Jump in logic? #["Skull Woods - Big Chest", True, ['Magic Mirror', 'Pegasus Boots', 'Big Key (Skull Woods)']], - ["Skull Woods - Big Chest", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Skull Woods)']], + ["Skull Woods - Big Chest", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Pegasus Boots', 'Big Key (Skull Woods)']], ["Skull Woods - Big Key Chest", False, []], ["Skull Woods - Big Key Chest", True, ['Magic Mirror', 'Pegasus Boots']], @@ -111,8 +112,8 @@ def testFirstDungeonChests(self): ["Turtle Rock - Chain Chomps", False, []], #todo: does clip require sword? #["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots']], - ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots', 'Progressive Sword']], - ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror']], + ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Pegasus Boots', 'Progressive Sword', 'Progressive Sword']], + ["Turtle Rock - Chain Chomps", True, ['Pegasus Boots', 'Magic Mirror', 'Progressive Bow']], ["Turtle Rock - Crystaroller Room", False, []], ["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)']], diff --git a/worlds/alttp/test/owg/TestLightWorld.py b/worlds/alttp/test/owg/TestLightWorld.py index f3f1ba0c2703..84342a33c856 100644 --- a/worlds/alttp/test/owg/TestLightWorld.py +++ b/worlds/alttp/test/owg/TestLightWorld.py @@ -25,17 +25,21 @@ def testLightWorld(self): ["Kakariko Tavern", True, []], - ["Chicken House", True, []], + ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)']], - ["Aginah's Cave", True, []], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)']], - ["Sahasrahla's Hut - Left", True, []], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']], - ["Sahasrahla's Hut - Middle", True, []], - - ["Sahasrahla's Hut - Right", True, []], - - ["Kakariko Well - Top", True, []], + ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']], ["Kakariko Well - Left", True, []], @@ -45,7 +49,8 @@ def testLightWorld(self): ["Kakariko Well - Bottom", True, []], - ["Blind's Hideout - Top", True, []], + ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']], ["Blind's Hideout - Left", True, []], @@ -59,15 +64,19 @@ def testLightWorld(self): ["Bonk Rock Cave", False, [], ['Pegasus Boots']], ["Bonk Rock Cave", True, ['Pegasus Boots']], - ["Mini Moldorm Cave - Far Left", True, []], - - ["Mini Moldorm Cave - Left", True, []], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], - ["Mini Moldorm Cave - Right", True, []], - - ["Mini Moldorm Cave - Far Right", True, []], - - ["Ice Rod Cave", True, []], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']], ["Bottle Merchant", True, []], @@ -126,12 +135,13 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Pegasus Boots', 'Magic Mirror']], ["Graveyard Cave", False, [], ['Pegasus Boots', 'Moon Pearl']], - ["Graveyard Cave", True, ['Pegasus Boots']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Pegasus Boots']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Checkerboard Cave", False, []], ["Checkerboard Cave", False, [], ['Progressive Glove']], @@ -140,8 +150,6 @@ def testLightWorld(self): ["Checkerboard Cave", True, ['Pegasus Boots', 'Progressive Glove']], ["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Mini Moldorm Cave - Generous Guy", True, []], - ["Library", False, []], ["Library", False, [], ['Pegasus Boots']], ["Library", True, ['Pegasus Boots']], @@ -152,7 +160,10 @@ def testLightWorld(self): ["Potion Shop", False, [], ['Mushroom']], ["Potion Shop", True, ['Mushroom']], - ["Maze Race", True, []], + ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']], + ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Maze Race", True, ['Bomb Upgrade (+5)']], + ["Maze Race", True, ['Pegasus Boots']], ["Desert Ledge", False, []], ["Desert Ledge", False, [], ['Pegasus Boots', 'Book of Mudora', 'Flute']], diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index 37b0b6ccb868..1f8f2707edaa 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -11,7 +11,9 @@ class TestVanillaOWG(TestBase, LTTPTestBase): def setUp(self): self.world_setup() self.multiworld.difficulty_requirements[1] = difficulties['normal'] - self.multiworld.logic[1] = "owglitches" + self.multiworld.glitches_required[1] = "overworld_glitches" + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].create_regions() self.multiworld.worlds[1].create_items() diff --git a/worlds/alttp/test/vanilla/TestDarkWorld.py b/worlds/alttp/test/vanilla/TestDarkWorld.py index ecb3e5583098..8ff09c527de8 100644 --- a/worlds/alttp/test/vanilla/TestDarkWorld.py +++ b/worlds/alttp/test/vanilla/TestDarkWorld.py @@ -7,43 +7,48 @@ def testSouthDarkWorld(self): self.run_location_tests([ ["Hype Cave - Top", False, []], ["Hype Cave - Top", False, [], ['Moon Pearl']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Top", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Top", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Right", False, []], ["Hype Cave - Middle Right", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Right", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Right", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Middle Left", False, []], ["Hype Cave - Middle Left", False, [], ['Moon Pearl']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Middle Left", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Middle Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Middle Left", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Bottom", False, []], ["Hype Cave - Bottom", False, [], ['Moon Pearl']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Bottom", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Bottom", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Bottom", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Hype Cave - Generous Guy", False, []], ["Hype Cave - Generous Guy", False, [], ['Moon Pearl']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Hype Cave - Generous Guy", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Hype Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Hype Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Stumpy", False, []], ["Stumpy", False, [], ['Moon Pearl']], @@ -66,10 +71,11 @@ def testWestDarkWorld(self): self.run_location_tests([ ["Brewery", False, []], ["Brewery", False, [], ['Moon Pearl']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove']], - ["Brewery", True, ['Moon Pearl', 'Progressive Glove', 'Hammer']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Brewery", True, ['Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Brewery", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Progressive Glove']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Progressive Glove', 'Hammer']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Brewery", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["C-Shaped House", False, []], ["C-Shaped House", False, [], ['Moon Pearl']], diff --git a/worlds/alttp/test/vanilla/TestDeathMountain.py b/worlds/alttp/test/vanilla/TestDeathMountain.py index ecb3831f6ad1..d77f1a8dd274 100644 --- a/worlds/alttp/test/vanilla/TestDeathMountain.py +++ b/worlds/alttp/test/vanilla/TestDeathMountain.py @@ -48,7 +48,8 @@ def testEastDeathMountain(self): ["Mimic Cave", False, [], ['Moon Pearl']], ["Mimic Cave", False, [], ['Cane of Somaria']], ["Mimic Cave", False, ['Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Mimic Cave", True, ['Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Mimic Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mimic Cave", True, ['Bomb Upgrade (+5)', 'Quake', 'Progressive Sword', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Moon Pearl', 'Cane of Somaria', 'Magic Mirror', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Spiral Cave", False, []], ["Spiral Cave", False, [], ['Progressive Glove', 'Flute']], @@ -73,10 +74,10 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Far Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Left", True, ['Flute', 'Hookshot', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Far Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Far Left", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']], ["Paradox Cave Lower - Left", False, []], ["Paradox Cave Lower - Left", False, [], ['Progressive Glove', 'Flute']], @@ -87,10 +88,10 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Left", True, ['Flute', 'Hookshot', 'Cane of Somaria']], + ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Left", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']], ["Paradox Cave Lower - Middle", False, []], ["Paradox Cave Lower - Middle", False, [], ['Progressive Glove', 'Flute']], @@ -101,10 +102,10 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Middle", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Middle", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Middle", True, ['Flute', 'Hookshot', 'Cane of Somaria']], + ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Middle", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Middle", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']], ["Paradox Cave Lower - Right", False, []], ["Paradox Cave Lower - Right", False, [], ['Progressive Glove', 'Flute']], @@ -115,10 +116,10 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Right", True, ['Flute', 'Hookshot', 'Cane of Somaria']], + ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Right", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']], ["Paradox Cave Lower - Far Right", False, []], ["Paradox Cave Lower - Far Right", False, [], ['Progressive Glove', 'Flute']], @@ -129,10 +130,10 @@ def testEastDeathMountain(self): ["Paradox Cave Lower - Far Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Lower - Far Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Lower - Far Right", True, ['Flute', 'Hookshot', 'Cane of Somaria']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Hookshot', 'Bomb Upgrade (+5)']], + ["Paradox Cave Lower - Far Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer', 'Progressive Sword', 'Progressive Sword']], + ["Paradox Cave Lower - Far Right", True, ['Flute', 'Magic Mirror', 'Hammer', 'Fire Rod']], ["Paradox Cave Upper - Left", False, []], ["Paradox Cave Upper - Left", False, [], ['Progressive Glove', 'Flute']], @@ -143,10 +144,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Left", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Left", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Upper - Left", False, ['Flute', 'Hammer']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Left", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Upper - Left", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Left", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ["Paradox Cave Upper - Right", False, []], ["Paradox Cave Upper - Right", False, [], ['Progressive Glove', 'Flute']], @@ -157,10 +159,11 @@ def testEastDeathMountain(self): ["Paradox Cave Upper - Right", False, ['Progressive Glove', 'Hookshot']], ["Paradox Cave Upper - Right", False, ['Flute', 'Magic Mirror']], ["Paradox Cave Upper - Right", False, ['Flute', 'Hammer']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Hookshot']], - ["Paradox Cave Upper - Right", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], - ["Paradox Cave Upper - Right", True, ['Flute', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Hookshot']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Progressive Glove', 'Lamp', 'Magic Mirror', 'Hammer']], + ["Paradox Cave Upper - Right", True, ['Bomb Upgrade (+5)', 'Flute', 'Magic Mirror', 'Hammer']], ]) def testWestDarkWorldDeathMountain(self): diff --git a/worlds/alttp/test/vanilla/TestLightWorld.py b/worlds/alttp/test/vanilla/TestLightWorld.py index 977e807290d1..6d9284aba0d3 100644 --- a/worlds/alttp/test/vanilla/TestLightWorld.py +++ b/worlds/alttp/test/vanilla/TestLightWorld.py @@ -29,17 +29,21 @@ def testLightWorld(self): ["Kakariko Tavern", True, []], - ["Chicken House", True, []], + ["Chicken House", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Chicken House", True, ['Bomb Upgrade (+5)']], - ["Aginah's Cave", True, []], + ["Aginah's Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Aginah's Cave", True, ['Bomb Upgrade (+5)']], - ["Sahasrahla's Hut - Left", True, []], + ["Sahasrahla's Hut - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Left", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Middle", True, ['Pegasus Boots']], + ["Sahasrahla's Hut - Middle", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Pegasus Boots']], + ["Sahasrahla's Hut - Right", True, ['Bomb Upgrade (+5)']], + ["Sahasrahla's Hut - Right", True, ['Pegasus Boots']], - ["Sahasrahla's Hut - Middle", True, []], - - ["Sahasrahla's Hut - Right", True, []], - - ["Kakariko Well - Top", True, []], + ["Kakariko Well - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Kakariko Well - Top", True, ['Bomb Upgrade (+5)']], ["Kakariko Well - Left", True, []], @@ -49,7 +53,8 @@ def testLightWorld(self): ["Kakariko Well - Bottom", True, []], - ["Blind's Hideout - Top", True, []], + ["Blind's Hideout - Top", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Blind's Hideout - Top", True, ['Bomb Upgrade (+5)']], ["Blind's Hideout - Left", True, []], @@ -63,15 +68,19 @@ def testLightWorld(self): ["Bonk Rock Cave", False, [], ['Pegasus Boots']], ["Bonk Rock Cave", True, ['Pegasus Boots']], - ["Mini Moldorm Cave - Far Left", True, []], - - ["Mini Moldorm Cave - Left", True, []], - - ["Mini Moldorm Cave - Right", True, []], - - ["Mini Moldorm Cave - Far Right", True, []], + ["Mini Moldorm Cave - Far Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Left", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Left", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Far Right", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Far Right", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], + ["Mini Moldorm Cave - Generous Guy", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Mini Moldorm Cave - Generous Guy", True, ['Bomb Upgrade (+5)', 'Progressive Sword']], - ["Ice Rod Cave", True, []], + ["Ice Rod Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Ice Rod Cave", True, ['Bomb Upgrade (+5)']], ["Bottle Merchant", True, []], @@ -136,11 +145,12 @@ def testLightWorld(self): ["Graveyard Cave", False, []], ["Graveyard Cave", False, [], ['Magic Mirror']], ["Graveyard Cave", False, [], ['Moon Pearl']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], - ["Graveyard Cave", True, ['Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], + ["Graveyard Cave", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Progressive Glove', 'Hammer']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Hammer', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Progressive Glove', 'Hookshot']], + ["Graveyard Cave", True, ['Bomb Upgrade (+5)', 'Moon Pearl', 'Magic Mirror', 'Beat Agahnim 1', 'Flippers', 'Hookshot']], ["Checkerboard Cave", False, []], ["Checkerboard Cave", False, [], ['Progressive Glove']], @@ -148,7 +158,6 @@ def testLightWorld(self): ["Checkerboard Cave", False, [], ['Magic Mirror']], ["Checkerboard Cave", True, ['Flute', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], - ["Mini Moldorm Cave - Generous Guy", True, []], ["Library", False, []], ["Library", False, [], ['Pegasus Boots']], @@ -160,7 +169,10 @@ def testLightWorld(self): ["Potion Shop", False, [], ['Mushroom']], ["Potion Shop", True, ['Mushroom']], - ["Maze Race", True, []], + ["Maze Race", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)', 'Magic Mirror', 'Pegasus Boots']], + ["Maze Race", True, ['Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Moon Pearl']], + ["Maze Race", True, ['Bomb Upgrade (+5)']], + ["Maze Race", True, ['Pegasus Boots']], ["Desert Ledge", False, []], ["Desert Ledge", False, [], ['Book of Mudora', 'Flute']], diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index 3c983e98504c..3f4fbad8c2b6 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -9,8 +9,10 @@ class TestVanilla(TestBase, LTTPTestBase): def setUp(self): self.world_setup() - self.multiworld.logic[1] = "noglitches" + self.multiworld.glitches_required[1] = "no_glitches" self.multiworld.difficulty_requirements[1] = difficulties['normal'] + self.multiworld.bombless_start[1].value = True + self.multiworld.shuffle_capacity_upgrades[1].value = True self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].create_regions() self.multiworld.worlds[1].create_items() diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index 36d863bb4475..195339c38070 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -7,16 +7,25 @@ class Bk_SudokuWebWorld(WebWorld): options_page = "games/Sudoku/info/en" theme = 'partyTime' - tutorials = [ - Tutorial( - tutorial_name='Setup Guide', - description='A guide to playing BK Sudoku', - language='English', - file_name='setup_en.md', - link='setup/en', - authors=['Jarno'] - ) - ] + + setup_en = Tutorial( + tutorial_name='Setup Guide', + description='A guide to playing BK Sudoku', + language='English', + file_name='setup_en.md', + link='setup/en', + authors=['Jarno'] + ) + setup_de = Tutorial( + tutorial_name='Setup Anleitung', + description='Eine Anleitung um BK-Sudoku zu spielen', + language='Deutsch', + file_name='setup_de.md', + link='setup/de', + authors=['Held_der_Zeit'] + ) + + tutorials = [setup_en, setup_de] class Bk_SudokuWorld(World): diff --git a/worlds/bk_sudoku/docs/de_Sudoku.md b/worlds/bk_sudoku/docs/de_Sudoku.md new file mode 100644 index 000000000000..abb50c5498d1 --- /dev/null +++ b/worlds/bk_sudoku/docs/de_Sudoku.md @@ -0,0 +1,21 @@ +# BK-Sudoku + +## Was ist das für ein Spiel? + +BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder +beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis +für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein +weitere „Checks” zu erreichen. +(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu +spielen/generieren.) + +## Wie werden Hinweise freigeschalten? + +Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen +Gegenstand der noch nicht gefunden wurde. + +## Wo ist die Seite für die Einstellungen? + +Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen +kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der +Schwierigkeitsgrad des Sudoku ausgewählt werden. diff --git a/worlds/bk_sudoku/docs/setup_de.md b/worlds/bk_sudoku/docs/setup_de.md new file mode 100644 index 000000000000..71a8e5f6245d --- /dev/null +++ b/worlds/bk_sudoku/docs/setup_de.md @@ -0,0 +1,27 @@ +# BK-Sudoku Setup Anleitung + +## Benötigte Software +- [Bk-Sudoku](https://github.com/Jarno458/sudoku) +- Windows 8 oder höher + +## Generelles Konzept + +Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku +spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten. + +Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig +eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt. + +## Installationsprozess + +Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases). +Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei. + +## Verbinden mit einer Multiworld + +1. Starte `Bk_Sudoku.exe` +2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest +3. Trage die Server-URL und den Port ein +4. Drücke auf Verbinden (connect) +5. Wähle deinen Schwierigkeitsgrad +6. Versuche das Sudoku zu Lösen diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index 583838904726..30c0e47f818e 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -11,16 +11,26 @@ class CliqueWebWorld(WebWorld): theme = "partyTime" - tutorials = [ - Tutorial( - tutorial_name="Start Guide", - description="A guide to playing Clique.", - language="English", - file_name="guide_en.md", - link="guide/en", - authors=["Phar"] - ) - ] + + setup_en = Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Clique.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["Phar"] + ) + + setup_de = Tutorial( + tutorial_name="Anleitung zum Anfangen", + description="Eine Anleitung um Clique zu spielen.", + language="Deutsch", + file_name="guide_de.md", + link="guide/de", + authors=["Held_der_Zeit"] + ) + + tutorials = [setup_en, setup_de] class CliqueWorld(World): diff --git a/worlds/clique/docs/de_Clique.md b/worlds/clique/docs/de_Clique.md new file mode 100644 index 000000000000..cde0a23cf6fe --- /dev/null +++ b/worlds/clique/docs/de_Clique.md @@ -0,0 +1,18 @@ +# Clique + +## Was ist das für ein Spiel? + +~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~ +~~(rote) Knöpfe zu drücken.~~ + +Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach +es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten +Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand +anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann. + +Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden. + +## Wo ist die Seite für die Einstellungen? + +Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um +eine YAML-Datei zu konfigurieren und zu exportieren. diff --git a/worlds/clique/docs/guide_de.md b/worlds/clique/docs/guide_de.md new file mode 100644 index 000000000000..26e08dbbdd7e --- /dev/null +++ b/worlds/clique/docs/guide_de.md @@ -0,0 +1,25 @@ +# Clique Anleitung + +Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib +Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden). + +Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten. + +Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst. +Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf +deinem Handy starten und produktiv sein während du wartest! + +Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche +(mindestens) eins der Folgenden: + +- Dein Zimmer aufräumen. +- Die Wäsche machen. +- Etwas Essen von einem X-Belieben Fast Food Restaruant holen. +- Das tägliche Wordle machen. +- ~~Deine Seele an **Phar** verkaufen.~~ +- Deine Hausaufgaben erledigen. +- Deine Post abholen. + + +~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~ +~~Discord kontaktieren. *zwinker* *zwinker*~~ diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index c22b7cd9847b..ca7a0157cb5c 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -13,14 +13,23 @@ class DLCqwebworld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Tutorial", "A guide to setting up the Archipelago DLCQuest game on your computer.", "English", "setup_en.md", "setup/en", ["axe_y"] - )] + ) + setup_fr = Tutorial( + "Guide de configuration MultiWorld", + "Un guide pour configurer DLCQuest sur votre PC.", + "Français", + "setup_fr.md", + "setup/fr", + ["Deoxis"] + ) + tutorials = [setup_en, setup_fr] class DLCqworld(World): diff --git a/worlds/dlcquest/docs/fr_DLCQuest.md b/worlds/dlcquest/docs/fr_DLCQuest.md new file mode 100644 index 000000000000..95a8048dfe5e --- /dev/null +++ b/worlds/dlcquest/docs/fr_DLCQuest.md @@ -0,0 +1,49 @@ +# DLC Quest + +## Où se trouve la page des paramètres ? + +La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier. + + +## Quel est l'effet de la randomisation sur ce jeu ? + +Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest. + +## Quel est le but de DLC Quest ? + +DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie. +Il peut également choisir de faire les deux campagnes. + + +## Quels sont les emplacements dans DLC quest ? + +Les emplacements dans DLC Quest comprennent toujours +- les achats de DLC auprès du commerçant +- Les objectifs liés aux récompenses + - Tuer des moutons dans DLC Quest + - Objectifs spécifiques de l'attribution dans Live Freemium or Die + +Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection : +- Objets que votre personnage peut obtenir de différentes manières + - Swords + - Gun + - Box of Various Supplies + - Humble Indie Bindle + - Pickaxe +- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés + +## Quels objets peuvent se trouver dans le monde d'un autre joueur ? + +Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock + +Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille. +- Zombie Sheep +- Loading Screens +- Temporary Spikes + +## Que se passe-t-il lorsque le joueur reçoit un objet ? + +Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur. +Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception. + +Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion. \ No newline at end of file diff --git a/worlds/dlcquest/docs/setup_fr.md b/worlds/dlcquest/docs/setup_fr.md new file mode 100644 index 000000000000..78c69eb5a729 --- /dev/null +++ b/worlds/dlcquest/docs/setup_fr.md @@ -0,0 +1,55 @@ +# # Guide de configuration MultiWorld de DLCQuest + +## Logiciels requis + +- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/)) +- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases) +- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète) + +## Logiciels optionnels +- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases) + - (Uniquement pour le TextClient) + +## Créer un fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Où puis-je obtenir un fichier YAML ? + +Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings). + +## Rejoindre une partie multi-monde + +### Installer le mod + +- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous + + +- Extraire l'archive .zip à l'emplacement de votre choix + + +- Exécutez "DLCQuestipelagoInstaller.exe". + +![image](https://i.imgur.com/2sPhMgs.png) +- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire. + - Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut + - Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès. + - Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé. + +### Se connecter au MultiServer + +- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés. + +- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable. +![image](https://i.imgur.com/ZUiFrhf.png) + +- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes. +- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez. + +### Interagir avec le MultiWorld depuis le jeu + +Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte. +Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés. +Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes. \ No newline at end of file diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index c53f275017af..7de486314c6c 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ async def game_watcher(self, ctx): 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 == b'\x00' or check_2 == b'\x00': + if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): return def get_range(data_range): diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index 3eab5dd532a6..d0898d7e81c8 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -223,11 +223,6 @@ def yaml_item(text): def create_items(self) -> None: items = [] starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") - if self.multiworld.progressive_gear[self.player]: - for item_group in prog_map: - if starting_weapon in self.item_name_groups[item_group]: - starting_weapon = prog_map[item_group] - break 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": diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index 98ecd28986df..9daee9f643a9 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -3,7 +3,7 @@ import zipfile from copy import deepcopy from .Regions import object_id_table -from Main import __version__ +from Utils import __version__ from worlds.Files import APContainer import pkgutil diff --git a/worlds/hk/ExtractedData.py b/worlds/hk/ExtractedData.py index cf796050e47f..0cbbc8bf8558 100644 --- a/worlds/hk/ExtractedData.py +++ b/worlds/hk/ExtractedData.py @@ -3,7 +3,7 @@ connectors = {'Room_temple[left1]': 'Crossroads_02[door1]', 'Tutorial_01[right1]': 'Town[left1]', 'Tutorial_01[top1]': None, 'Tutorial_01[top2]': 'Cliffs_02[bot1]', 'Town[left1]': 'Tutorial_01[right1]', 'Town[bot1]': 'Crossroads_01[top1]', 'Town[right1]': 'Mines_10[left1]', 'Town[top1]': None, 'Town[door_station]': 'Room_Town_Stag_Station[left1]', 'Town[door_sly]': 'Room_shop[left1]', 'Town[door_mapper]': 'Room_mapper[left1]', 'Town[door_jiji]': 'Room_Ouiji[left1]', 'Town[door_bretta]': 'Room_Bretta[right1]', 'Town[room_divine]': 'Grimm_Divine[left1]', 'Town[room_grimm]': 'Grimm_Main_Tent[left1]', 'Room_shop[left1]': 'Town[door_sly]', 'Room_Town_Stag_Station[left1]': 'Town[door_station]', 'Room_mapper[left1]': 'Town[door_mapper]', 'Room_Bretta[right1]': 'Town[door_bretta]', 'Room_Ouiji[left1]': 'Town[door_jiji]', 'Grimm_Divine[left1]': 'Town[room_divine]', 'Grimm_Main_Tent[left1]': 'Town[room_grimm]', 'Crossroads_01[top1]': 'Town[bot1]', 'Crossroads_01[left1]': 'Crossroads_07[right1]', 'Crossroads_01[right1]': 'Crossroads_02[left1]', 'Crossroads_02[left1]': 'Crossroads_01[right1]', 'Crossroads_02[door1]': 'Room_temple[left1]', 'Crossroads_02[right1]': 'Crossroads_39[left1]', 'Crossroads_03[right1]': 'Crossroads_15[left1]', 'Crossroads_03[right2]': 'Mines_33[left1]', 'Crossroads_03[left1]': 'Crossroads_21[right1]', 'Crossroads_03[left2]': 'Crossroads_47[right1]', 'Crossroads_03[bot1]': 'Crossroads_19[top1]', 'Crossroads_03[top1]': 'Crossroads_16[bot1]', 'Crossroads_04[left1]': 'Crossroads_19[right1]', 'Crossroads_04[top1]': 'Crossroads_27[bot1]', 'Crossroads_04[door_Mender_House]': 'Room_Mender_House[left1]', 'Crossroads_04[door1]': 'Room_ruinhouse[left1]', 'Crossroads_04[door_charmshop]': 'Room_Charm_Shop[left1]', 'Crossroads_04[right1]': 'Crossroads_50[left1]', 'Crossroads_05[left1]': 'Crossroads_07[right2]', 'Crossroads_05[right1]': 'Crossroads_40[left1]', 'Crossroads_06[left1]': 'Crossroads_33[right1]', 'Crossroads_06[door1]': 'Crossroads_ShamanTemple[left1]', 'Crossroads_06[right1]': 'Crossroads_10[left1]', 'Crossroads_07[left1]': 'Crossroads_38[right1]', 'Crossroads_07[left2]': 'Crossroads_11_alt[right1]', 'Crossroads_07[left3]': 'Crossroads_25[right1]', 'Crossroads_07[right1]': 'Crossroads_01[left1]', 'Crossroads_07[right2]': 'Crossroads_05[left1]', 'Crossroads_07[bot1]': 'Crossroads_33[top1]', 'Crossroads_08[left1]': 'Crossroads_33[right2]', 'Crossroads_08[left2]': 'Crossroads_18[right1]', 'Crossroads_08[right1]': 'Crossroads_30[left1]', 'Crossroads_08[right2]': 'Crossroads_13[left1]', 'Crossroads_09[left1]': 'Crossroads_36[right2]', 'Crossroads_09[right1]': 'Crossroads_33[left1]', 'Crossroads_10[left1]': 'Crossroads_06[right1]', 'Crossroads_10[right1]': 'Crossroads_21[left1]', 'Crossroads_11_alt[left1]': 'Fungus1_01[right1]', 'Crossroads_11_alt[right1]': 'Crossroads_07[left2]', 'Crossroads_12[left1]': 'Crossroads_35[right1]', 'Crossroads_12[right1]': 'Crossroads_33[left2]', 'Crossroads_13[left1]': 'Crossroads_08[right2]', 'Crossroads_13[right1]': 'Crossroads_42[left1]', 'Crossroads_14[left1]': 'Crossroads_39[right1]', 'Crossroads_14[left2]': 'Crossroads_16[right1]', 'Crossroads_14[right1]': 'Crossroads_48[left1]', 'Crossroads_14[right2]': 'Crossroads_45[left1]', 'Crossroads_15[left1]': 'Crossroads_03[right1]', 'Crossroads_15[right1]': 'Crossroads_27[left1]', 'Crossroads_16[left1]': 'Crossroads_40[right1]', 'Crossroads_16[right1]': 'Crossroads_14[left2]', 'Crossroads_16[bot1]': 'Crossroads_03[top1]', 'Crossroads_18[right1]': 'Crossroads_08[left2]', 'Crossroads_18[right2]': 'Crossroads_52[left1]', 'Crossroads_18[bot1]': 'Fungus2_06[top1]', 'Crossroads_19[right1]': 'Crossroads_04[left1]', 'Crossroads_19[top1]': 'Crossroads_03[bot1]', 'Crossroads_19[left1]': 'Crossroads_42[right1]', 'Crossroads_19[left2]': 'Crossroads_43[right1]', 'Crossroads_21[left1]': 'Crossroads_10[right1]', 'Crossroads_21[right1]': 'Crossroads_03[left1]', 'Crossroads_21[top1]': 'Crossroads_22[bot1]', 'Crossroads_22[bot1]': 'Crossroads_21[top1]', 'Crossroads_25[right1]': 'Crossroads_07[left3]', 'Crossroads_25[left1]': 'Crossroads_36[right1]', 'Crossroads_27[right1]': 'Crossroads_46[left1]', 'Crossroads_27[bot1]': 'Crossroads_04[top1]', 'Crossroads_27[left1]': 'Crossroads_15[right1]', 'Crossroads_27[left2]': 'Crossroads_31[right1]', 'Crossroads_30[left1]': 'Crossroads_08[right1]', 'Crossroads_31[right1]': 'Crossroads_27[left2]', 'Crossroads_33[top1]': 'Crossroads_07[bot1]', 'Crossroads_33[left1]': 'Crossroads_09[right1]', 'Crossroads_33[left2]': 'Crossroads_12[right1]', 'Crossroads_33[right1]': 'Crossroads_06[left1]', 'Crossroads_33[right2]': 'Crossroads_08[left1]', 'Crossroads_35[bot1]': 'Fungus3_26[top1]', 'Crossroads_35[right1]': 'Crossroads_12[left1]', 'Crossroads_36[right1]': 'Crossroads_25[left1]', 'Crossroads_36[right2]': 'Crossroads_09[left1]', 'Crossroads_37[right1]': 'Crossroads_49[left1]', 'Crossroads_38[right1]': 'Crossroads_07[left1]', 'Crossroads_39[right1]': 'Crossroads_14[left1]', 'Crossroads_39[left1]': 'Crossroads_02[right1]', 'Crossroads_40[right1]': 'Crossroads_16[left1]', 'Crossroads_40[left1]': 'Crossroads_05[right1]', 'Crossroads_42[left1]': 'Crossroads_13[right1]', 'Crossroads_42[right1]': 'Crossroads_19[left1]', 'Crossroads_43[left1]': 'Crossroads_49[right1]', 'Crossroads_43[right1]': 'Crossroads_19[left2]', 'Crossroads_45[right1]': 'Mines_01[left1]', 'Crossroads_45[left1]': 'Crossroads_14[right2]', 'Crossroads_46[left1]': 'Crossroads_27[right1]', 'Crossroads_46b[right1]': 'RestingGrounds_02[left1]', 'Crossroads_ShamanTemple[left1]': 'Crossroads_06[door1]', 'Crossroads_47[right1]': 'Crossroads_03[left2]', 'Crossroads_48[left1]': 'Crossroads_14[right1]', 'Crossroads_49[right1]': 'Crossroads_43[left1]', 'Crossroads_49[left1]': 'Crossroads_37[right1]', 'Crossroads_49b[right1]': 'Ruins1_28[left1]', 'Crossroads_50[right1]': 'RestingGrounds_06[left1]', 'Crossroads_50[left1]': 'Crossroads_04[right1]', 'Crossroads_52[left1]': 'Crossroads_18[right2]', 'Room_ruinhouse[left1]': 'Crossroads_04[door1]', 'Room_Charm_Shop[left1]': 'Crossroads_04[door_charmshop]', 'Room_Mender_House[left1]': 'Crossroads_04[door_Mender_House]', 'Fungus1_01[left1]': 'Fungus1_01b[right1]', 'Fungus1_01[right1]': 'Crossroads_11_alt[left1]', 'Fungus1_01b[left1]': 'Fungus1_02[right1]', 'Fungus1_01b[right1]': 'Fungus1_01[left1]', 'Fungus1_02[left1]': 'Fungus1_17[right1]', 'Fungus1_02[right1]': 'Fungus1_01b[left1]', 'Fungus1_02[right2]': 'Fungus1_06[left1]', 'Fungus1_03[left1]': 'Fungus1_31[right1]', 'Fungus1_03[right1]': 'Fungus1_17[left1]', 'Fungus1_03[bot1]': 'Fungus1_05[top1]', 'Fungus1_04[left1]': 'Fungus1_25[right1]', 'Fungus1_04[right1]': 'Fungus1_21[left1]', 'Fungus1_05[right1]': 'Fungus1_14[left1]', 'Fungus1_05[bot1]': 'Fungus1_10[top1]', 'Fungus1_05[top1]': 'Fungus1_03[bot1]', 'Fungus1_06[left1]': 'Fungus1_02[right2]', 'Fungus1_06[bot1]': 'Fungus1_07[top1]', 'Fungus1_07[top1]': 'Fungus1_06[bot1]', 'Fungus1_07[left1]': 'Fungus1_19[right1]', 'Fungus1_07[right1]': 'Fungus1_08[left1]', 'Fungus1_08[left1]': 'Fungus1_07[right1]', 'Fungus1_09[left1]': 'Fungus1_15[right1]', 'Fungus1_09[right1]': 'Fungus1_30[left1]', 'Fungus1_10[left1]': 'Fungus1_30[right1]', 'Fungus1_10[right1]': 'Fungus1_19[left1]', 'Fungus1_10[top1]': 'Fungus1_05[bot1]', 'Fungus1_11[top1]': 'Fungus1_19[bot1]', 'Fungus1_11[right1]': 'Fungus1_34[left1]', 'Fungus1_11[right2]': 'Fungus1_37[left1]', 'Fungus1_11[left1]': 'Fungus1_29[right1]', 'Fungus1_11[bot1]': 'Fungus3_01[top1]', 'Fungus1_12[left1]': 'Fungus1_13[right1]', 'Fungus1_12[right1]': 'Fungus1_29[left1]', 'Fungus1_13[right1]': 'Fungus1_12[left1]', 'Fungus1_13[left1]': 'Fungus3_22[right1]', 'Fungus1_14[left1]': 'Fungus1_05[right1]', 'Fungus1_15[door1]': 'Room_nailmaster_02[left1]', 'Fungus1_15[right1]': 'Fungus1_09[left1]', 'Fungus1_16_alt[right1]': 'Fungus1_22[left1]', 'Fungus1_17[left1]': 'Fungus1_03[right1]', 'Fungus1_17[right1]': 'Fungus1_02[left1]', 'Fungus1_19[left1]': 'Fungus1_10[right1]', 'Fungus1_19[right1]': 'Fungus1_07[left1]', 'Fungus1_19[bot1]': 'Fungus1_11[top1]', 'Fungus1_20_v02[bot1]': 'Fungus1_21[top1]', 'Fungus1_20_v02[bot2]': 'Fungus1_32[top1]', 'Fungus1_20_v02[right1]': 'Fungus1_28[left2]', 'Fungus1_21[bot1]': 'Fungus1_22[top1]', 'Fungus1_21[top1]': 'Fungus1_20_v02[bot1]', 'Fungus1_21[left1]': 'Fungus1_04[right1]', 'Fungus1_21[right1]': 'Fungus1_32[left1]', 'Fungus1_22[bot1]': 'Fungus1_30[top1]', 'Fungus1_22[top1]': 'Fungus1_21[bot1]', 'Fungus1_22[left1]': 'Fungus1_16_alt[right1]', 'Fungus1_23[left1]': 'Fungus3_48[right2]', 'Fungus1_23[right1]': 'Fungus3_13[left1]', 'Fungus1_24[left1]': 'Fungus3_05[right1]', 'Fungus1_25[right1]': 'Fungus1_04[left1]', 'Fungus1_25[left1]': 'Fungus1_26[right1]', 'Fungus1_26[right1]': 'Fungus1_25[left1]', 'Fungus1_26[left1]': 'Fungus1_Slug[right1]', 'Fungus1_26[door_SlugShrine]': 'Room_Slug_Shrine[left1]', 'Fungus1_28[left1]': 'Cliffs_01[right3]', 'Fungus1_28[left2]': 'Fungus1_20_v02[right1]', 'Fungus1_29[left1]': 'Fungus1_12[right1]', 'Fungus1_29[right1]': 'Fungus1_11[left1]', 'Fungus1_30[top1]': 'Fungus1_22[bot1]', 'Fungus1_30[top3]': 'Fungus1_31[bot1]', 'Fungus1_30[left1]': 'Fungus1_09[right1]', 'Fungus1_30[right1]': 'Fungus1_10[left1]', 'Fungus1_31[top1]': 'Fungus1_32[bot1]', 'Fungus1_31[bot1]': 'Fungus1_30[top3]', 'Fungus1_31[right1]': 'Fungus1_03[left1]', 'Fungus1_32[bot1]': 'Fungus1_31[top1]', 'Fungus1_32[top1]': 'Fungus1_20_v02[bot2]', 'Fungus1_32[left1]': 'Fungus1_21[right1]', 'Fungus1_34[door1]': 'Fungus1_35[left1]', 'Fungus1_34[left1]': 'Fungus1_11[right1]', 'Fungus1_35[left1]': 'Fungus1_34[door1]', 'Fungus1_35[right1]': 'Fungus1_36[left1]', 'Fungus1_36[left1]': 'Fungus1_35[right1]', 'Fungus1_37[left1]': 'Fungus1_11[right2]', 'Fungus1_Slug[right1]': 'Fungus1_26[left1]', 'Room_Slug_Shrine[left1]': 'Fungus1_26[door_SlugShrine]', 'Room_nailmaster_02[left1]': 'Fungus1_15[door1]', 'Fungus3_01[top1]': 'Fungus1_11[bot1]', 'Fungus3_01[right1]': 'Fungus3_25[left1]', 'Fungus3_01[left1]': 'Fungus3_24[right1]', 'Fungus3_01[right2]': 'Fungus3_02[left1]', 'Fungus3_02[left1]': 'Fungus3_01[right2]', 'Fungus3_02[left2]': 'Fungus3_03[right1]', 'Fungus3_02[left3]': 'Fungus3_35[right1]', 'Fungus3_02[right1]': 'Fungus3_47[left1]', 'Fungus3_02[right2]': 'Fungus2_01[left1]', 'Fungus3_03[right1]': 'Fungus3_02[left2]', 'Fungus3_03[left1]': 'Fungus3_34[right1]', 'Fungus3_24[right1]': 'Fungus3_01[left1]', 'Fungus3_24[left1]': 'Fungus3_44[right1]', 'Fungus3_24[top1]': 'Fungus3_30[bot1]', 'Fungus3_25[right1]': 'Fungus3_25b[left1]', 'Fungus3_25[left1]': 'Fungus3_01[right1]', 'Fungus3_25b[right1]': 'Fungus3_26[left2]', 'Fungus3_25b[left1]': 'Fungus3_25[right1]', 'Fungus3_26[top1]': 'Crossroads_35[bot1]', 'Fungus3_26[left1]': 'Fungus3_28[right1]', 'Fungus3_26[left2]': 'Fungus3_25b[right1]', 'Fungus3_26[left3]': 'Fungus3_27[right1]', 'Fungus3_26[right1]': 'Fungus2_33[left1]', 'Fungus3_27[left1]': 'Fungus3_47[right1]', 'Fungus3_27[right1]': 'Fungus3_26[left3]', 'Fungus3_28[right1]': 'Fungus3_26[left1]', 'Fungus3_30[bot1]': 'Fungus3_24[top1]', 'Fungus3_35[right1]': 'Fungus3_02[left3]', 'Fungus3_44[bot1]': 'Fungus3_34[top1]', 'Fungus3_44[door1]': 'Room_Fungus_Shaman[left1]', 'Fungus3_44[right1]': 'Fungus3_24[left1]', 'Fungus3_47[left1]': 'Fungus3_02[right1]', 'Fungus3_47[right1]': 'Fungus3_27[left1]', 'Fungus3_47[door1]': 'Fungus3_archive[left1]', 'Room_Fungus_Shaman[left1]': 'Fungus3_44[door1]', 'Fungus3_archive[left1]': 'Fungus3_47[door1]', 'Fungus3_archive[bot1]': 'Fungus3_archive_02[top1]', 'Fungus3_archive_02[top1]': 'Fungus3_archive[bot1]', 'Fungus2_01[left1]': 'Fungus3_02[right2]', 'Fungus2_01[left2]': 'Fungus2_02[right1]', 'Fungus2_01[left3]': 'Fungus2_34[right1]', 'Fungus2_01[right1]': 'Fungus2_03[left1]', 'Fungus2_02[right1]': 'Fungus2_01[left2]', 'Fungus2_34[right1]': 'Fungus2_01[left3]', 'Fungus2_03[left1]': 'Fungus2_01[right1]', 'Fungus2_03[bot1]': 'Fungus2_18[top1]', 'Fungus2_03[right1]': 'Fungus2_04[left1]', 'Fungus2_04[top1]': 'Fungus2_05[bot1]', 'Fungus2_04[right1]': 'Fungus2_28[left1]', 'Fungus2_04[left1]': 'Fungus2_03[right1]', 'Fungus2_04[right2]': 'Fungus2_28[left2]', 'Fungus2_05[bot1]': 'Fungus2_04[top1]', 'Fungus2_05[right1]': 'Fungus2_06[left1]', 'Fungus2_06[top1]': 'Crossroads_18[bot1]', 'Fungus2_06[left1]': 'Fungus2_05[right1]', 'Fungus2_06[left2]': 'Fungus2_33[right1]', 'Fungus2_06[right1]': 'Fungus2_26[left1]', 'Fungus2_06[right2]': 'Fungus2_07[left1]', 'Fungus2_07[left1]': 'Fungus2_06[right2]', 'Fungus2_07[right1]': 'Fungus2_08[left1]', 'Fungus2_08[left1]': 'Fungus2_07[right1]', 'Fungus2_08[left2]': 'Fungus2_09[right1]', 'Fungus2_08[right1]': 'Fungus2_32[left1]', 'Fungus2_09[left1]': 'Fungus2_10[right1]', 'Fungus2_09[right1]': 'Fungus2_08[left2]', 'Fungus2_10[right1]': 'Fungus2_09[left1]', 'Fungus2_10[right2]': 'Fungus2_21[left1]', 'Fungus2_10[bot1]': 'Fungus2_11[top1]', 'Fungus2_11[top1]': 'Fungus2_10[bot1]', 'Fungus2_11[left1]': 'Fungus2_18[right1]', 'Fungus2_11[left2]': 'Fungus2_17[right1]', 'Fungus2_11[right1]': 'Fungus2_12[left1]', 'Fungus2_12[left1]': 'Fungus2_11[right1]', 'Fungus2_12[bot1]': 'Fungus2_13[top1]', 'Fungus2_13[top1]': 'Fungus2_12[bot1]', 'Fungus2_13[left2]': 'Fungus2_14[right1]', 'Fungus2_13[left3]': 'Fungus2_23[right1]', 'Fungus2_14[top1]': 'Fungus2_17[bot1]', 'Fungus2_14[right1]': 'Fungus2_13[left2]', 'Fungus2_14[bot3]': 'Fungus2_15[top3]', 'Fungus2_15[top3]': 'Fungus2_14[bot3]', 'Fungus2_15[right1]': 'Fungus2_31[left1]', 'Fungus2_15[left1]': 'Fungus2_25[right1]', 'Fungus2_17[left1]': 'Fungus2_29[right1]', 'Fungus2_17[right1]': 'Fungus2_11[left2]', 'Fungus2_17[bot1]': 'Fungus2_14[top1]', 'Fungus2_18[right1]': 'Fungus2_11[left1]', 'Fungus2_18[bot1]': 'Fungus2_19[top1]', 'Fungus2_18[top1]': 'Fungus2_03[bot1]', 'Fungus2_19[top1]': 'Fungus2_18[bot1]', 'Fungus2_19[left1]': 'Fungus2_20[right1]', 'Fungus2_20[right1]': 'Fungus2_19[left1]', 'Fungus2_20[left1]': 'Deepnest_01[right1]', 'Fungus2_21[right1]': 'Ruins1_01[left1]', 'Fungus2_21[left1]': 'Fungus2_10[right2]', 'Fungus2_23[right1]': 'Fungus2_13[left3]', 'Fungus2_23[right2]': 'Waterways_09[left1]', 'Fungus2_26[left1]': 'Fungus2_06[right1]', 'Fungus2_28[left1]': 'Fungus2_04[right1]', 'Fungus2_28[left2]': 'Fungus2_04[right2]', 'Fungus2_29[right1]': 'Fungus2_17[left1]', 'Fungus2_29[bot1]': 'Fungus2_30[top1]', 'Fungus2_30[bot1]': 'Fungus2_25[top2]', 'Fungus2_30[top1]': 'Fungus2_29[bot1]', 'Fungus2_31[left1]': 'Fungus2_15[right1]', 'Fungus2_32[left1]': 'Fungus2_08[right1]', 'Fungus2_33[right1]': 'Fungus2_06[left2]', 'Fungus2_33[left1]': 'Fungus3_26[right1]', 'Deepnest_01[right1]': 'Fungus2_20[left1]', 'Deepnest_01[bot1]': 'Deepnest_01b[top1]', 'Deepnest_01[bot2]': 'Deepnest_01b[top2]', 'Deepnest_01[left1]': 'Fungus3_39[right1]', 'Deepnest_01b[top1]': 'Deepnest_01[bot1]', 'Deepnest_01b[top2]': None, 'Deepnest_01b[right1]': 'Deepnest_02[left1]', 'Deepnest_01b[right2]': 'Deepnest_02[left2]', 'Deepnest_01b[bot1]': 'Deepnest_17[top1]', 'Deepnest_02[left1]': 'Deepnest_01b[right1]', 'Deepnest_02[left2]': 'Deepnest_01b[right2]', 'Deepnest_02[right1]': 'Deepnest_36[left1]', 'Deepnest_03[right1]': 'Deepnest_30[left1]', 'Deepnest_03[left1]': 'Deepnest_34[right1]', 'Deepnest_03[top1]': 'Deepnest_33[bot1]', 'Deepnest_03[left2]': 'Deepnest_31[right1]', 'Deepnest_09[left1]': 'Deepnest_10[right1]', 'Deepnest_10[right1]': 'Deepnest_09[left1]', 'Deepnest_10[right2]': 'Deepnest_41[left1]', 'Deepnest_10[right3]': 'Deepnest_41[left2]', 'Deepnest_10[door1]': 'Deepnest_Spider_Town[left1]', 'Deepnest_10[door2]': 'Room_spider_small[left1]', 'Room_spider_small[left1]': 'Deepnest_10[door2]', 'Deepnest_Spider_Town[left1]': 'Deepnest_10[door1]', 'Deepnest_14[right1]': 'Deepnest_17[left1]', 'Deepnest_14[left1]': 'Deepnest_26[right1]', 'Deepnest_14[bot1]': 'Deepnest_33[top1]', 'Deepnest_14[bot2]': 'Deepnest_33[top2]', 'Deepnest_16[left1]': 'Deepnest_17[right1]', 'Deepnest_16[bot1]': 'Fungus2_25[top1]', 'Deepnest_17[left1]': 'Deepnest_14[right1]', 'Deepnest_17[right1]': 'Deepnest_16[left1]', 'Deepnest_17[top1]': 'Deepnest_01b[bot1]', 'Deepnest_17[bot1]': 'Deepnest_30[top1]', 'Fungus2_25[top1]': 'Deepnest_16[bot1]', 'Fungus2_25[top2]': None, 'Fungus2_25[right1]': 'Fungus2_15[left1]', 'Deepnest_26[left1]': 'Deepnest_26b[right1]', 'Deepnest_26[left2]': 'Deepnest_26b[right2]', 'Deepnest_26[right1]': 'Deepnest_14[left1]', 'Deepnest_26[bot1]': 'Deepnest_35[top1]', 'Deepnest_26b[right2]': 'Deepnest_26[left2]', 'Deepnest_26b[right1]': 'Deepnest_26[left1]', 'Deepnest_30[left1]': 'Deepnest_03[right1]', 'Deepnest_30[top1]': 'Deepnest_17[bot1]', 'Deepnest_30[right1]': 'Deepnest_37[left1]', 'Deepnest_31[right1]': 'Deepnest_03[left2]', 'Deepnest_31[right2]': 'Deepnest_32[left1]', 'Deepnest_32[left1]': 'Deepnest_31[right2]', 'Deepnest_33[top1]': 'Deepnest_14[bot1]', 'Deepnest_33[top2]': 'Deepnest_14[bot2]', 'Deepnest_33[bot1]': 'Deepnest_03[top1]', 'Deepnest_34[left1]': 'Deepnest_39[right1]', 'Deepnest_34[right1]': 'Deepnest_03[left1]', 'Deepnest_34[top1]': 'Deepnest_35[bot1]', 'Deepnest_35[left1]': 'Deepnest_40[right1]', 'Deepnest_35[top1]': 'Deepnest_26[bot1]', 'Deepnest_35[bot1]': 'Deepnest_34[top1]', 'Deepnest_36[left1]': 'Deepnest_02[right1]', 'Deepnest_37[left1]': 'Deepnest_30[right1]', 'Deepnest_37[right1]': 'Abyss_03_b[left1]', 'Deepnest_37[top1]': 'Deepnest_38[bot1]', 'Deepnest_37[bot1]': 'Deepnest_44[top1]', 'Deepnest_38[bot1]': 'Deepnest_37[top1]', 'Deepnest_39[left1]': 'Deepnest_41[right1]', 'Deepnest_39[top1]': 'Deepnest_42[bot1]', 'Deepnest_39[door1]': 'Deepnest_45_v02[left1]', 'Deepnest_39[right1]': 'Deepnest_34[left1]', 'Deepnest_40[right1]': 'Deepnest_35[left1]', 'Deepnest_41[right1]': 'Deepnest_39[left1]', 'Deepnest_41[left1]': 'Deepnest_10[right2]', 'Deepnest_41[left2]': 'Deepnest_10[right3]', 'Deepnest_42[bot1]': 'Deepnest_39[top1]', 'Deepnest_42[left1]': 'Room_Mask_Maker[right1]', 'Deepnest_42[top1]': 'Deepnest_43[bot1]', 'Deepnest_43[bot1]': 'Deepnest_42[top1]', 'Deepnest_43[left1]': 'Fungus3_50[right1]', 'Deepnest_43[right1]': 'Fungus3_08[left1]', 'Deepnest_44[top1]': 'Deepnest_37[bot1]', 'Deepnest_45_v02[left1]': 'Deepnest_39[door1]', 'Room_Mask_Maker[right1]': 'Deepnest_42[left1]', 'Deepnest_East_01[bot1]': 'Abyss_03_c[top1]', 'Deepnest_East_01[right1]': 'Hive_03_c[left1]', 'Deepnest_East_01[top1]': 'Deepnest_East_02[bot1]', 'Deepnest_East_02[bot1]': 'Deepnest_East_01[top1]', 'Deepnest_East_02[bot2]': 'Hive_03[top1]', 'Deepnest_East_02[top1]': 'Waterways_14[bot2]', 'Deepnest_East_02[right1]': 'Deepnest_East_03[left2]', 'Deepnest_East_03[left1]': 'Ruins2_07[right1]', 'Deepnest_East_03[left2]': 'Deepnest_East_02[right1]', 'Deepnest_East_03[top1]': 'Deepnest_East_07[bot1]', 'Deepnest_East_03[top2]': None, 'Deepnest_East_03[right1]': 'Deepnest_East_04[left1]', 'Deepnest_East_03[right2]': 'Deepnest_East_06[left1]', 'Deepnest_East_04[left1]': 'Deepnest_East_03[right1]', 'Deepnest_East_04[left2]': 'Deepnest_East_07[right1]', 'Deepnest_East_04[right2]': 'Deepnest_East_15[left1]', 'Deepnest_East_04[right1]': 'Deepnest_East_11[left1]', 'Deepnest_East_06[top1]': 'Deepnest_East_18[bot1]', 'Deepnest_East_06[left1]': 'Deepnest_East_03[right2]', 'Deepnest_East_06[bot1]': 'Deepnest_East_14b[top1]', 'Deepnest_East_06[door1]': 'Room_nailmaster_03[left1]', 'Deepnest_East_06[right1]': 'Deepnest_East_16[left1]', 'Deepnest_East_07[bot1]': 'Deepnest_East_03[top1]', 'Deepnest_East_07[bot2]': 'Deepnest_East_03[top2]', 'Deepnest_East_07[left1]': 'Deepnest_East_08[right1]', 'Deepnest_East_07[left2]': 'Ruins2_11_b[right1]', 'Deepnest_East_07[right1]': 'Deepnest_East_04[left2]', 'Deepnest_East_08[right1]': 'Deepnest_East_07[left1]', 'Deepnest_East_08[top1]': 'Deepnest_East_09[bot1]', 'Deepnest_East_09[right1]': 'Room_Colosseum_01[left1]', 'Deepnest_East_09[left1]': 'Ruins2_10b[right1]', 'Deepnest_East_09[bot1]': 'Deepnest_East_08[top1]', 'Deepnest_East_10[left1]': 'Deepnest_East_18[right2]', 'Deepnest_East_11[right1]': 'Deepnest_East_12[left1]', 'Deepnest_East_11[left1]': 'Deepnest_East_04[right1]', 'Deepnest_East_11[top1]': 'Deepnest_East_13[bot1]', 'Deepnest_East_11[bot1]': 'Deepnest_East_18[top1]', 'Deepnest_East_12[right1]': 'Deepnest_East_Hornet[left1]', 'Deepnest_East_12[left1]': 'Deepnest_East_11[right1]', 'Deepnest_East_13[bot1]': 'Deepnest_East_11[top1]', 'Deepnest_East_14[top2]': 'Deepnest_East_16[bot1]', 'Deepnest_East_14[left1]': 'Deepnest_East_14b[right1]', 'Deepnest_East_14[door1]': 'Deepnest_East_17[left1]', 'Deepnest_East_14b[right1]': 'Deepnest_East_14[left1]', 'Deepnest_East_14b[top1]': 'Deepnest_East_06[bot1]', 'Deepnest_East_15[left1]': 'Deepnest_East_04[right2]', 'Deepnest_East_16[left1]': 'Deepnest_East_06[right1]', 'Deepnest_East_16[bot1]': 'Deepnest_East_14[top2]', 'Deepnest_East_17[left1]': 'Deepnest_East_14[door1]', 'Deepnest_East_18[top1]': 'Deepnest_East_11[bot1]', 'Deepnest_East_18[bot1]': 'Deepnest_East_06[top1]', 'Deepnest_East_18[right2]': 'Deepnest_East_10[left1]', 'Room_nailmaster_03[left1]': 'Deepnest_East_06[door1]', 'Deepnest_East_Hornet[left1]': 'Deepnest_East_12[right1]', 'Deepnest_East_Hornet[left2]': 'Room_Wyrm[right1]', 'Room_Wyrm[right1]': 'Deepnest_East_Hornet[left2]', 'GG_Lurker[left1]': 'Room_Colosseum_Spectate[right1]', 'Hive_01[left1]': 'Abyss_03_c[right1]', 'Hive_01[right1]': 'Hive_02[left2]', 'Hive_01[right2]': 'Hive_02[left3]', 'Hive_02[left1]': 'Hive_03_c[right3]', 'Hive_02[left2]': 'Hive_01[right1]', 'Hive_02[left3]': 'Hive_01[right2]', 'Hive_03_c[left1]': 'Deepnest_East_01[right1]', 'Hive_03_c[right2]': 'Hive_04[left2]', 'Hive_03_c[right3]': 'Hive_02[left1]', 'Hive_03_c[top1]': 'Hive_03[bot1]', 'Hive_03[bot1]': 'Hive_03_c[top1]', 'Hive_03[right1]': 'Hive_04[left1]', 'Hive_03[top1]': 'Deepnest_East_02[bot2]', 'Hive_04[left1]': 'Hive_03[right1]', 'Hive_04[left2]': 'Hive_03_c[right2]', 'Hive_04[right1]': 'Hive_05[left1]', 'Hive_05[left1]': 'Hive_04[right1]', 'Room_Colosseum_01[left1]': 'Deepnest_East_09[right1]', 'Room_Colosseum_01[bot1]': 'Room_Colosseum_02[top1]', 'Room_Colosseum_02[top1]': 'Room_Colosseum_01[bot1]', 'Room_Colosseum_02[top2]': 'Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[bot1]': 'Room_Colosseum_02[top2]', 'Room_Colosseum_Spectate[right1]': 'GG_Lurker[left1]', 'Abyss_01[left1]': 'Waterways_05[right1]', 'Abyss_01[left2]': 'Waterways_06[right1]', 'Abyss_01[left3]': 'Abyss_02[right1]', 'Abyss_01[right1]': 'Ruins2_04[left2]', 'Abyss_01[right2]': 'Waterways_07[left1]', 'Abyss_02[right1]': 'Abyss_01[left3]', 'Abyss_02[bot1]': 'Abyss_03[top1]', 'Abyss_03[bot1]': 'Abyss_17[top1]', 'Abyss_03[bot2]': 'Abyss_04[top1]', 'Abyss_03[top1]': 'Abyss_02[bot1]', 'Abyss_03_b[left1]': 'Deepnest_37[right1]', 'Abyss_03_c[right1]': 'Hive_01[left1]', 'Abyss_03_c[top1]': 'Deepnest_East_01[bot1]', 'Abyss_04[top1]': 'Abyss_03[bot2]', 'Abyss_04[left1]': 'Abyss_18[right1]', 'Abyss_04[bot1]': 'Abyss_06_Core[top1]', 'Abyss_04[right1]': 'Abyss_05[left1]', 'Abyss_05[left1]': 'Abyss_04[right1]', 'Abyss_05[right1]': 'Abyss_22[left1]', 'Abyss_06_Core[top1]': 'Abyss_04[bot1]', 'Abyss_06_Core[left1]': 'Abyss_08[right1]', 'Abyss_06_Core[left3]': 'Abyss_12[right1]', 'Abyss_06_Core[right2]': 'Abyss_16[left1]', 'Abyss_06_Core[bot1]': 'Abyss_15[top1]', 'Abyss_08[right1]': 'Abyss_06_Core[left1]', 'Abyss_09[right1]': 'Abyss_10[left1]', 'Abyss_09[right2]': 'Abyss_Lighthouse_room[left1]', 'Abyss_09[right3]': 'Abyss_10[left2]', 'Abyss_09[left1]': 'Abyss_16[right1]', 'Abyss_10[left1]': 'Abyss_09[right1]', 'Abyss_10[left2]': 'Abyss_09[right3]', 'Abyss_12[right1]': 'Abyss_06_Core[left3]', 'Abyss_15[top1]': 'Abyss_06_Core[bot1]', 'Abyss_16[left1]': 'Abyss_06_Core[right2]', 'Abyss_16[right1]': 'Abyss_09[left1]', 'Abyss_17[top1]': 'Abyss_03[bot1]', 'Abyss_18[left1]': 'Abyss_19[right1]', 'Abyss_18[right1]': 'Abyss_04[left1]', 'Abyss_19[left1]': 'Abyss_21[right1]', 'Abyss_19[right1]': 'Abyss_18[left1]', 'Abyss_19[bot1]': 'Abyss_20[top1]', 'Abyss_19[bot2]': 'Abyss_20[top2]', 'Abyss_20[top1]': 'Abyss_19[bot1]', 'Abyss_20[top2]': 'Abyss_19[bot2]', 'Abyss_21[right1]': 'Abyss_19[left1]', 'Abyss_22[left1]': 'Abyss_05[right1]', 'Abyss_Lighthouse_room[left1]': 'Abyss_09[right2]', 'Waterways_01[top1]': 'Ruins1_05b[bot1]', 'Waterways_01[left1]': 'Waterways_04[right1]', 'Waterways_01[right1]': 'Waterways_03[left1]', 'Waterways_01[bot1]': 'Waterways_02[top1]', 'Waterways_02[top1]': 'Waterways_01[bot1]', 'Waterways_02[top2]': 'Waterways_05[bot1]', 'Waterways_02[top3]': 'Waterways_04[bot1]', 'Waterways_02[bot1]': 'Waterways_08[top1]', 'Waterways_02[bot2]': 'Waterways_06[top1]', 'Waterways_03[left1]': 'Waterways_01[right1]', 'Waterways_04[bot1]': 'Waterways_02[top3]', 'Waterways_04[right1]': 'Waterways_01[left1]', 'Waterways_04[left1]': 'Waterways_04b[right1]', 'Waterways_04[left2]': 'Waterways_04b[right2]', 'Waterways_04b[right1]': 'Waterways_04[left1]', 'Waterways_04b[right2]': 'Waterways_04[left2]', 'Waterways_04b[left1]': 'Waterways_09[right1]', 'Waterways_05[right1]': 'Abyss_01[left1]', 'Waterways_05[bot1]': 'Waterways_02[top2]', 'Waterways_05[bot2]': 'Waterways_15[top1]', 'Waterways_06[right1]': 'Abyss_01[left2]', 'Waterways_06[top1]': 'Waterways_02[bot2]', 'Waterways_07[right1]': 'Waterways_13[left1]', 'Waterways_07[right2]': 'Waterways_13[left2]', 'Waterways_07[left1]': 'Abyss_01[right2]', 'Waterways_07[door1]': 'Ruins_House_03[left2]', 'Waterways_07[top1]': 'Waterways_14[bot1]', 'Waterways_08[top1]': 'Waterways_02[bot1]', 'Waterways_08[left1]': 'Waterways_12[right1]', 'Waterways_08[left2]': 'GG_Pipeway[right1]', 'Waterways_09[right1]': 'Waterways_04b[left1]', 'Waterways_09[left1]': 'Fungus2_23[right2]', 'Waterways_12[right1]': 'Waterways_08[left1]', 'Waterways_13[left1]': 'Waterways_07[right1]', 'Waterways_13[left2]': 'Waterways_07[right2]', 'Waterways_14[bot1]': 'Waterways_07[top1]', 'Waterways_14[bot2]': 'Deepnest_East_02[top1]', 'Waterways_15[top1]': 'Waterways_05[bot2]', 'GG_Pipeway[right1]': 'Waterways_08[left2]', 'GG_Pipeway[left1]': 'GG_Waterways[right1]', 'GG_Waterways[right1]': 'GG_Pipeway[left1]', 'GG_Waterways[door1]': 'Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[left1]': 'GG_Waterways[door1]', 'Room_GG_Shortcut[top1]': 'Ruins1_04[bot1]', 'Ruins1_01[left1]': 'Fungus2_21[right1]', 'Ruins1_01[top1]': 'Ruins1_17[bot1]', 'Ruins1_01[bot1]': 'Ruins1_02[top1]', 'Ruins1_02[top1]': 'Ruins1_01[bot1]', 'Ruins1_02[bot1]': 'Ruins1_03[top1]', 'Ruins1_03[top1]': 'Ruins1_02[bot1]', 'Ruins1_03[left1]': 'Ruins1_04[right1]', 'Ruins1_03[right1]': 'Ruins1_05c[left2]', 'Ruins1_03[right2]': 'Ruins1_05b[left1]', 'Ruins1_04[right1]': 'Ruins1_03[left1]', 'Ruins1_04[door1]': 'Room_nailsmith[left1]', 'Ruins1_04[bot1]': 'Room_GG_Shortcut[top1]', 'Ruins1_05b[left1]': 'Ruins1_03[right2]', 'Ruins1_05b[top1]': 'Ruins1_05c[bot1]', 'Ruins1_05b[bot1]': 'Waterways_01[top1]', 'Ruins1_05b[right1]': 'Ruins1_27[left1]', 'Ruins1_05c[left2]': 'Ruins1_03[right1]', 'Ruins1_05c[bot1]': 'Ruins1_05b[top1]', 'Ruins1_05c[top1]': 'Ruins1_05[bot1]', 'Ruins1_05c[top2]': 'Ruins1_05[bot2]', 'Ruins1_05c[top3]': 'Ruins1_05[bot3]', 'Ruins1_05[bot1]': 'Ruins1_05c[top1]', 'Ruins1_05[bot2]': 'Ruins1_05c[top2]', 'Ruins1_05[bot3]': 'Ruins1_05c[top3]', 'Ruins1_05[right1]': 'Ruins1_09[left1]', 'Ruins1_05[right2]': 'Ruins1_18[left1]', 'Ruins1_05[top1]': 'Ruins1_31[bot1]', 'Ruins1_06[left1]': 'Ruins1_17[right1]', 'Ruins1_06[right1]': 'Ruins1_31[left1]', 'Ruins1_09[top1]': 'Ruins1_23[bot1]', 'Ruins1_09[left1]': 'Ruins1_05[right1]', 'Ruins1_17[top1]': 'Ruins1_28[bot1]', 'Ruins1_17[right1]': 'Ruins1_06[left1]', 'Ruins1_17[bot1]': 'Ruins1_01[top1]', 'Ruins1_18[left1]': 'Ruins1_05[right2]', 'Ruins1_18[right1]': 'Ruins2_03b[left1]', 'Ruins1_18[right2]': 'Ruins2_01[left2]', 'Ruins1_23[top1]': 'Ruins1_30[bot1]', 'Ruins1_23[right1]': 'Ruins1_25[left2]', 'Ruins1_23[right2]': 'Ruins1_25[left3]', 'Ruins1_23[bot1]': 'Ruins1_09[top1]', 'Ruins1_23[left1]': 'Ruins1_31[right1]', 'Ruins1_24[left1]': 'Ruins1_32[right1]', 'Ruins1_24[right1]': 'Ruins1_30[left1]', 'Ruins1_24[left2]': 'Ruins1_32[right2]', 'Ruins1_24[right2]': 'Ruins1_30[left2]', 'Ruins1_25[left1]': 'Ruins1_30[right1]', 'Ruins1_25[left2]': 'Ruins1_23[right1]', 'Ruins1_25[left3]': 'Ruins1_23[right2]', 'Ruins1_27[left1]': 'Ruins1_05b[right1]', 'Ruins1_27[right1]': 'Ruins2_01_b[left1]', 'Ruins1_28[left1]': 'Crossroads_49b[right1]', 'Ruins1_28[right1]': 'Ruins1_29[left1]', 'Ruins1_28[bot1]': 'Ruins1_17[top1]', 'Ruins1_29[left1]': 'Ruins1_28[right1]', 'Ruins1_30[left1]': 'Ruins1_24[right1]', 'Ruins1_30[left2]': 'Ruins1_24[right2]', 'Ruins1_30[bot1]': 'Ruins1_23[top1]', 'Ruins1_30[right1]': 'Ruins1_25[left1]', 'Ruins1_31[bot1]': 'Ruins1_05[top1]', 'Ruins1_31[left1]': 'Ruins1_06[right1]', 'Ruins1_31[left2]': 'Ruins1_31b[right1]', 'Ruins1_31[left3]': 'Ruins1_31b[right2]', 'Ruins1_31[right1]': 'Ruins1_23[left1]', 'Ruins1_31b[right1]': 'Ruins1_31[left2]', 'Ruins1_31b[right2]': 'Ruins1_31[left3]', 'Ruins1_32[right1]': 'Ruins1_24[left1]', 'Ruins1_32[right2]': 'Ruins1_24[left2]', 'Room_nailsmith[left1]': 'Ruins1_04[door1]', 'Ruins2_01[top1]': 'Ruins2_03b[bot1]', 'Ruins2_01[bot1]': 'Ruins2_01_b[top1]', 'Ruins2_01[left2]': 'Ruins1_18[right2]', 'Ruins2_01_b[top1]': 'Ruins2_01[bot1]', 'Ruins2_01_b[left1]': 'Ruins1_27[right1]', 'Ruins2_01_b[right1]': 'Ruins2_04[left1]', 'Ruins2_03b[top1]': 'Ruins2_03[bot1]', 'Ruins2_03b[top2]': 'Ruins2_03[bot2]', 'Ruins2_03b[left1]': 'Ruins1_18[right1]', 'Ruins2_03b[bot1]': 'Ruins2_01[top1]', 'Ruins2_03[top1]': 'Ruins2_Watcher_Room[bot1]', 'Ruins2_03[bot1]': 'Ruins2_03b[top1]', 'Ruins2_03[bot2]': 'Ruins2_03b[top2]', 'Ruins2_04[left1]': 'Ruins2_01_b[right1]', 'Ruins2_04[left2]': 'Abyss_01[right1]', 'Ruins2_04[right1]': 'Ruins2_06[left1]', 'Ruins2_04[right2]': 'Ruins2_06[left2]', 'Ruins2_04[door_Ruin_House_01]': 'Ruins_House_01[left1]', 'Ruins2_04[door_Ruin_House_02]': 'Ruins_House_02[left1]', 'Ruins2_04[door_Ruin_House_03]': 'Ruins_House_03[left1]', 'Ruins2_04[door_Ruin_Elevator]': 'Ruins_Elevator[left1]', 'Ruins2_05[left1]': 'Ruins2_10b[right2]', 'Ruins2_05[top1]': 'Ruins2_09[bot1]', 'Ruins2_05[bot1]': 'Ruins2_06[top1]', 'Ruins2_06[left1]': 'Ruins2_04[right1]', 'Ruins2_06[left2]': 'Ruins2_04[right2]', 'Ruins2_06[right1]': 'Ruins2_08[left1]', 'Ruins2_06[right2]': 'Ruins2_07[left1]', 'Ruins2_06[top1]': 'Ruins2_05[bot1]', 'Ruins2_07[right1]': 'Deepnest_East_03[left1]', 'Ruins2_07[left1]': 'Ruins2_06[right2]', 'Ruins2_07[top1]': 'Ruins2_11_b[bot1]', 'Ruins2_08[left1]': 'Ruins2_06[right1]', 'Ruins2_09[bot1]': 'Ruins2_05[top1]', 'Ruins2_10[right1]': 'RestingGrounds_10[left1]', 'Ruins2_10[left1]': 'RestingGrounds_06[right1]', 'Ruins2_10b[right1]': 'Deepnest_East_09[left1]', 'Ruins2_10b[right2]': 'Ruins2_05[left1]', 'Ruins2_10b[left1]': 'Ruins_Bathhouse[right1]', 'Ruins2_11_b[right1]': 'Deepnest_East_07[left2]', 'Ruins2_11_b[left1]': 'Ruins2_11[right1]', 'Ruins2_11_b[bot1]': 'Ruins2_07[top1]', 'Ruins2_11[right1]': 'Ruins2_11_b[left1]', 'Ruins2_Watcher_Room[bot1]': 'Ruins2_03[top1]', 'Ruins_House_01[left1]': 'Ruins2_04[door_Ruin_House_01]', 'Ruins_House_02[left1]': 'Ruins2_04[door_Ruin_House_02]', 'Ruins_House_03[left1]': 'Ruins2_04[door_Ruin_House_03]', 'Ruins_House_03[left2]': 'Waterways_07[door1]', 'Ruins_Elevator[left1]': 'Ruins2_04[door_Ruin_Elevator]', 'Ruins_Elevator[left2]': 'Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[door1]': 'Ruins_Elevator[left2]', 'Ruins_Bathhouse[right1]': 'Ruins2_10b[left1]', 'RestingGrounds_02[right1]': 'RestingGrounds_04[left1]', 'RestingGrounds_02[left1]': 'Crossroads_46b[right1]', 'RestingGrounds_02[bot1]': 'RestingGrounds_06[top1]', 'RestingGrounds_02[top1]': None, 'RestingGrounds_04[left1]': 'RestingGrounds_02[right1]', 'RestingGrounds_04[right1]': 'RestingGrounds_05[left1]', 'RestingGrounds_05[left1]': 'RestingGrounds_04[right1]', 'RestingGrounds_05[left2]': 'RestingGrounds_07[right1]', 'RestingGrounds_05[left3]': 'RestingGrounds_17[right1]', 'RestingGrounds_05[right1]': 'RestingGrounds_08[left1]', 'RestingGrounds_05[right2]': 'RestingGrounds_09[left1]', 'RestingGrounds_05[bot1]': 'RestingGrounds_10[top1]', 'RestingGrounds_06[left1]': 'Crossroads_50[right1]', 'RestingGrounds_06[right1]': 'Ruins2_10[left1]', 'RestingGrounds_06[top1]': 'RestingGrounds_02[bot1]', 'RestingGrounds_07[right1]': 'RestingGrounds_05[left2]', 'RestingGrounds_08[left1]': 'RestingGrounds_05[right1]', 'RestingGrounds_09[left1]': 'RestingGrounds_05[right2]', 'RestingGrounds_10[left1]': 'Ruins2_10[right1]', 'RestingGrounds_10[top1]': 'RestingGrounds_05[bot1]', 'RestingGrounds_10[top2]': 'RestingGrounds_12[bot1]', 'RestingGrounds_12[bot1]': 'RestingGrounds_10[top2]', 'RestingGrounds_12[door_Mansion]': 'Room_Mansion[left1]', 'RestingGrounds_17[right1]': 'RestingGrounds_05[left3]', 'Room_Mansion[left1]': 'RestingGrounds_12[door_Mansion]', 'Mines_01[bot1]': 'Mines_02[top1]', 'Mines_01[left1]': 'Crossroads_45[right1]', 'Mines_02[top1]': 'Mines_01[bot1]', 'Mines_02[top2]': 'Mines_03[bot1]', 'Mines_02[left1]': 'Mines_33[right1]', 'Mines_02[right1]': 'Mines_29[left1]', 'Mines_03[right1]': 'Mines_17[left1]', 'Mines_03[bot1]': 'Mines_02[top2]', 'Mines_03[top1]': 'Mines_05[bot1]', 'Mines_04[right1]': 'Mines_07[left1]', 'Mines_04[top1]': 'Mines_37[bot1]', 'Mines_04[left1]': 'Mines_17[right1]', 'Mines_04[left2]': 'Mines_29[right1]', 'Mines_04[left3]': 'Mines_29[right2]', 'Mines_05[right1]': 'Mines_19[left1]', 'Mines_05[top1]': 'Mines_11[bot1]', 'Mines_05[bot1]': 'Mines_03[top1]', 'Mines_05[left1]': 'Mines_30[right1]', 'Mines_05[left2]': 'Mines_06[right1]', 'Mines_06[right1]': 'Mines_05[left2]', 'Mines_06[left1]': 'Mines_36[right1]', 'Mines_07[right1]': 'Mines_28[left1]', 'Mines_07[left1]': 'Mines_04[right1]', 'Mines_10[right1]': 'Mines_30[left1]', 'Mines_10[left1]': 'Town[right1]', 'Mines_10[bot1]': 'Mines_16[top1]', 'Mines_11[right1]': 'Mines_18[left1]', 'Mines_11[top1]': 'Mines_13[bot1]', 'Mines_11[bot1]': 'Mines_05[top1]', 'Mines_13[right1]': 'Mines_20[left1]', 'Mines_13[top1]': None, 'Mines_13[bot1]': 'Mines_11[top1]', 'Mines_16[top1]': 'Mines_10[bot1]', 'Mines_17[right1]': 'Mines_04[left1]', 'Mines_17[left1]': 'Mines_03[right1]', 'Mines_18[top1]': 'Mines_32[bot1]', 'Mines_18[left1]': 'Mines_11[right1]', 'Mines_18[right1]': 'Mines_20[left2]', 'Mines_19[left1]': 'Mines_05[right1]', 'Mines_19[right1]': 'Mines_20[left3]', 'Mines_20[left1]': 'Mines_13[right1]', 'Mines_20[left2]': 'Mines_18[right1]', 'Mines_20[left3]': 'Mines_19[right1]', 'Mines_20[bot1]': 'Mines_37[top1]', 'Mines_20[right1]': 'Mines_23[left1]', 'Mines_20[right2]': 'Mines_31[left1]', 'Mines_23[left1]': 'Mines_20[right1]', 'Mines_23[right1]': 'Mines_25[left1]', 'Mines_23[right2]': 'Mines_24[left1]', 'Mines_23[top1]': None, 'Mines_24[left1]': 'Mines_23[right2]', 'Mines_25[left1]': 'Mines_23[right1]', 'Mines_25[top1]': 'Mines_34[bot1]', 'Mines_28[left1]': 'Mines_07[right1]', 'Mines_28[bot1]': 'RestingGrounds_02[top1]', 'Mines_28[door1]': 'Mines_35[left1]', 'Mines_29[left1]': 'Mines_02[right1]', 'Mines_29[right1]': 'Mines_04[left2]', 'Mines_29[right2]': 'Mines_04[left3]', 'Mines_30[left1]': 'Mines_10[right1]', 'Mines_30[right1]': 'Mines_05[left1]', 'Mines_31[left1]': 'Mines_20[right2]', 'Mines_32[bot1]': 'Mines_18[top1]', 'Mines_33[right1]': 'Mines_02[left1]', 'Mines_33[left1]': 'Crossroads_03[right2]', 'Mines_34[bot1]': 'Mines_25[top1]', 'Mines_34[bot2]': 'Mines_23[top1]', 'Mines_34[left1]': 'Mines_13[top1]', 'Mines_35[left1]': 'Mines_28[door1]', 'Mines_36[right1]': 'Mines_06[left1]', 'Mines_37[bot1]': 'Mines_04[top1]', 'Mines_37[top1]': 'Mines_20[bot1]', 'Fungus3_04[left1]': 'Fungus3_21[right1]', 'Fungus3_04[left2]': 'Fungus3_13[right1]', 'Fungus3_04[right1]': 'Fungus3_34[left1]', 'Fungus3_04[right2]': 'Fungus3_05[left1]', 'Fungus3_05[left1]': 'Fungus3_04[right2]', 'Fungus3_05[right1]': 'Fungus1_24[left1]', 'Fungus3_05[right2]': 'Fungus3_11[left1]', 'Fungus3_08[left1]': 'Deepnest_43[right1]', 'Fungus3_08[right1]': 'Fungus3_11[left2]', 'Fungus3_08[top1]': 'Fungus3_10[bot1]', 'Fungus3_10[top1]': 'Fungus3_13[bot1]', 'Fungus3_10[bot1]': 'Fungus3_08[top1]', 'Fungus3_11[left1]': 'Fungus3_05[right2]', 'Fungus3_11[left2]': 'Fungus3_08[right1]', 'Fungus3_11[right1]': 'Fungus3_39[left1]', 'Fungus3_13[left1]': 'Fungus1_23[right1]', 'Fungus3_13[left2]': 'Fungus3_40[right1]', 'Fungus3_13[left3]': 'Fungus3_49[right1]', 'Fungus3_13[bot1]': 'Fungus3_10[top1]', 'Fungus3_13[right1]': 'Fungus3_04[left2]', 'Fungus3_21[right1]': 'Fungus3_04[left1]', 'Fungus3_21[top1]': 'Fungus3_22[bot1]', 'Fungus3_22[right1]': 'Fungus1_13[left1]', 'Fungus3_22[left1]': 'Fungus3_23[right1]', 'Fungus3_22[bot1]': 'Fungus3_21[top1]', 'Fungus3_23[right1]': 'Fungus3_22[left1]', 'Fungus3_23[left1]': 'Fungus3_48[right1]', 'Fungus3_34[right1]': 'Fungus3_03[left1]', 'Fungus3_34[left1]': 'Fungus3_04[right1]', 'Fungus3_34[top1]': 'Fungus3_44[bot1]', 'Fungus3_39[right1]': 'Deepnest_01[left1]', 'Fungus3_39[left1]': 'Fungus3_11[right1]', 'Fungus3_40[right1]': 'Fungus3_13[left2]', 'Fungus3_40[top1]': 'Fungus3_48[bot1]', 'Fungus3_48[right1]': 'Fungus3_23[left1]', 'Fungus3_48[right2]': 'Fungus1_23[left1]', 'Fungus3_48[door1]': 'Room_Queen[left1]', 'Fungus3_48[bot1]': 'Fungus3_40[top1]', 'Fungus3_49[right1]': 'Fungus3_13[left3]', 'Fungus3_50[right1]': 'Deepnest_43[left1]', 'Room_Queen[left1]': 'Fungus3_48[door1]', 'Cliffs_01[right1]': 'Cliffs_02[left1]', 'Cliffs_01[right2]': 'Cliffs_04[left1]', 'Cliffs_01[right3]': 'Fungus1_28[left1]', 'Cliffs_01[right4]': 'Cliffs_06[left1]', 'Cliffs_02[right1]': 'Town[top1]', 'Cliffs_02[bot1]': 'Tutorial_01[top2]', 'Cliffs_02[bot2]': 'Tutorial_01[top1]', 'Cliffs_02[door1]': 'Room_nailmaster[left1]', 'Cliffs_02[left1]': 'Cliffs_01[right1]', 'Cliffs_02[left2]': 'Cliffs_03[right1]', 'Cliffs_03[right1]': 'Cliffs_02[left2]', 'Cliffs_04[right1]': 'Cliffs_05[left1]', 'Cliffs_04[left1]': 'Cliffs_01[right2]', 'Cliffs_05[left1]': 'Cliffs_04[right1]', 'Cliffs_06[left1]': 'Cliffs_01[right4]', 'Room_nailmaster[left1]': 'Cliffs_02[door1]', 'White_Palace_01[left1]': 'White_Palace_11[door2]', 'White_Palace_01[right1]': 'White_Palace_02[left1]', 'White_Palace_01[top1]': 'White_Palace_03_hub[bot1]', 'White_Palace_02[left1]': 'White_Palace_01[right1]', 'White_Palace_03_hub[left1]': 'White_Palace_14[right1]', 'White_Palace_03_hub[left2]': 'White_Palace_04[right2]', 'White_Palace_03_hub[right1]': 'White_Palace_15[left1]', 'White_Palace_03_hub[top1]': 'White_Palace_06[bot1]', 'White_Palace_03_hub[bot1]': 'White_Palace_01[top1]', 'White_Palace_04[top1]': 'White_Palace_14[bot1]', 'White_Palace_04[right2]': 'White_Palace_03_hub[left2]', 'White_Palace_05[left1]': 'White_Palace_15[right1]', 'White_Palace_05[left2]': 'White_Palace_15[right2]', 'White_Palace_05[right1]': 'White_Palace_16[left1]', 'White_Palace_05[right2]': 'White_Palace_16[left2]', 'White_Palace_06[left1]': 'White_Palace_18[right1]', 'White_Palace_06[top1]': 'White_Palace_07[bot1]', 'White_Palace_06[bot1]': 'White_Palace_03_hub[top1]', 'White_Palace_07[top1]': 'White_Palace_12[bot1]', 'White_Palace_07[bot1]': 'White_Palace_06[top1]', 'White_Palace_08[left1]': 'White_Palace_13[right1]', 'White_Palace_08[right1]': 'White_Palace_13[left3]', 'White_Palace_09[right1]': 'White_Palace_13[left1]', 'White_Palace_11[door2]': 'White_Palace_01[left1]', 'White_Palace_12[right1]': 'White_Palace_13[left2]', 'White_Palace_12[bot1]': 'White_Palace_07[top1]', 'White_Palace_13[right1]': 'White_Palace_08[left1]', 'White_Palace_13[left1]': 'White_Palace_09[right1]', 'White_Palace_13[left2]': 'White_Palace_12[right1]', 'White_Palace_13[left3]': 'White_Palace_08[right1]', 'White_Palace_14[bot1]': 'White_Palace_04[top1]', 'White_Palace_14[right1]': 'White_Palace_03_hub[left1]', 'White_Palace_15[left1]': 'White_Palace_03_hub[right1]', 'White_Palace_15[right1]': 'White_Palace_05[left1]', 'White_Palace_15[right2]': 'White_Palace_05[left2]', 'White_Palace_16[left1]': 'White_Palace_05[right1]', 'White_Palace_16[left2]': 'White_Palace_05[right2]', 'White_Palace_17[right1]': 'White_Palace_19[left1]', 'White_Palace_17[bot1]': 'White_Palace_18[top1]', 'White_Palace_18[top1]': 'White_Palace_17[bot1]', 'White_Palace_18[right1]': 'White_Palace_06[left1]', 'White_Palace_19[top1]': 'White_Palace_20[bot1]', 'White_Palace_19[left1]': 'White_Palace_17[right1]', 'White_Palace_20[bot1]': 'White_Palace_19[top1]'} event_names = {'Abyss_01', 'Abyss_03', 'Abyss_03_b', 'Abyss_03_c', 'Abyss_04', 'Abyss_05', 'Abyss_06_Core', 'Abyss_09', 'Abyss_19', 'Broke_Sanctum_Glass_Floor', 'Can_Bench', 'Can_Repair_Fragile_Charms', 'Can_Replenish_Geo', 'Can_Replenish_Geo-Crossroads', 'Can_Stag', 'Cliffs_01', 'Cliffs_02', 'Completed_Path_of_Pain', 'Crossroads_03', 'Crossroads_07', 'Crossroads_08', 'Crossroads_14', 'Crossroads_18', 'Crossroads_19', 'Crossroads_21', 'Crossroads_27', 'Crossroads_33', 'Deepnest_01', 'Deepnest_01b', 'Deepnest_02', 'Deepnest_03', 'Deepnest_10', 'Deepnest_14', 'Deepnest_17', 'Deepnest_26', 'Deepnest_34', 'Deepnest_35', 'Deepnest_37', 'Deepnest_39', 'Deepnest_41', 'Deepnest_42', 'Deepnest_East_02', 'Deepnest_East_03', 'Deepnest_East_04', 'Deepnest_East_07', 'Deepnest_East_11', 'Deepnest_East_18', 'Defeated_Broken_Vessel', 'Defeated_Brooding_Mawlek', 'Defeated_Collector', 'Defeated_Colosseum_1', 'Defeated_Colosseum_2', 'Defeated_Colosseum_Zote', 'Defeated_Crystal_Guardian', 'Defeated_Dung_Defender', 'Defeated_Elder_Hu', 'Defeated_Elegant_Warrior', 'Defeated_Enraged_Guardian', 'Defeated_Failed_Champion', 'Defeated_False_Knight', 'Defeated_Flukemarm', 'Defeated_Galien', 'Defeated_Gorb', 'Defeated_Grey_Prince_Zote', 'Defeated_Grimm', 'Defeated_Gruz_Mother', 'Defeated_Hive_Knight', 'Defeated_Hornet_1', 'Defeated_Hornet_2', "Defeated_King's_Station_Arena", 'Defeated_Lost_Kin', 'Defeated_Mantis_Lords', 'Defeated_Markoth', 'Defeated_Marmu', 'Defeated_No_Eyes', 'Defeated_Nosk', 'Defeated_Pale_Lurker', 'Defeated_Path_of_Pain_Arena', 'Defeated_Sanctum_Warrior', 'Defeated_Shrumal_Ogre_Arena', 'Defeated_Soul_Master', 'Defeated_Soul_Tyrant', 'Defeated_Traitor_Lord', 'Defeated_Uumuu', 'Defeated_Watcher_Knights', "Defeated_West_Queen's_Gardens_Arena", 'Defeated_White_Defender', 'Defeated_Xero', 'First_Grimmchild_Upgrade', 'Fungus1_11', 'Fungus1_21', 'Fungus1_30', 'Fungus2_01', 'Fungus2_03', 'Fungus2_04', 'Fungus2_06', 'Fungus2_11', 'Fungus2_13', 'Fungus2_14', 'Fungus2_17', 'Fungus2_20', 'Fungus2_23', 'Fungus3_01', 'Fungus3_02', 'Fungus3_04', 'Fungus3_11', 'Fungus3_13', 'Fungus3_22', 'Fungus3_26', 'Fungus3_34', 'Fungus3_40', 'Fungus3_44', 'Fungus3_47', 'Hive_03_c', 'Left_Elevator', 'Lever-Dung_Defender', 'Lever-Shade_Soul', 'Lit_Abyss_Lighthouse', 'Lower_Tram', 'Mines_02', 'Mines_03', 'Mines_04', 'Mines_05', 'Mines_10', 'Mines_11', 'Mines_18', 'Mines_20', 'Mines_23', 'Nightmare_Lantern_Lit', 'Opened_Archives_Exit_Wall', 'Opened_Black_Egg_Temple', 'Opened_Dung_Defender_Wall', 'Opened_Emilitia_Door', 'Opened_Gardens_Stag_Exit', 'Opened_Glade_Door', "Opened_Lower_Kingdom's_Edge_Wall", 'Opened_Mawlek_Wall', 'Opened_Pleasure_House_Wall', 'Opened_Resting_Grounds_Catacombs_Wall', 'Opened_Resting_Grounds_Floor', 'Opened_Shaman_Pillar', 'Opened_Tramway_Exit_Gate', 'Opened_Waterways_Exit', 'Opened_Waterways_Manhole', 'Palace_Atrium_Gates_Opened', 'Palace_Entrance_Lantern_Lit', 'Palace_Left_Lantern_Lit', 'Palace_Right_Lantern_Lit', 'Rescued_Bretta', 'Rescued_Deepnest_Zote', 'Rescued_Sly', 'RestingGrounds_02', 'RestingGrounds_05', 'RestingGrounds_10', 'Right_Elevator', 'Ruins1_03', 'Ruins1_05', 'Ruins1_05b', 'Ruins1_05c', 'Ruins1_23', 'Ruins1_28', 'Ruins1_30', 'Ruins1_31', 'Ruins2_01', 'Ruins2_01_b', 'Ruins2_03b', 'Ruins2_04', 'Ruins2_10', 'Second_Grimmchild_Upgrade', 'Town', 'Tutorial_01', 'Upper_Tram', 'Warp-Lifeblood_Core_to_Abyss', 'Warp-Palace_Grounds_to_White_Palace', 'Warp-Path_of_Pain_Complete', 'Warp-White_Palace_Atrium_to_Palace_Grounds', 'Warp-White_Palace_Entrance_to_Palace_Grounds', 'Waterways_01', 'Waterways_02', 'Waterways_04', 'Waterways_04b', 'Waterways_07', 'White_Palace_01', 'White_Palace_03_hub', 'White_Palace_13'} exits = {'Room_temple': ['Room_temple[left1]'], 'Tutorial_01': ['Tutorial_01[right1]', 'Tutorial_01[top1]', 'Tutorial_01[top2]'], 'Town': ['Town[left1]', 'Town[bot1]', 'Town[right1]', 'Town[top1]', 'Town[door_station]', 'Town[door_sly]', 'Town[door_mapper]', 'Town[door_jiji]', 'Town[door_bretta]', 'Town[room_divine]', 'Town[room_grimm]'], 'Room_shop': ['Room_shop[left1]'], 'Room_Town_Stag_Station': ['Room_Town_Stag_Station[left1]'], 'Room_mapper': ['Room_mapper[left1]'], 'Room_Bretta': ['Room_Bretta[right1]'], 'Room_Ouiji': ['Room_Ouiji[left1]'], 'Grimm_Divine': ['Grimm_Divine[left1]'], 'Grimm_Main_Tent': ['Grimm_Main_Tent[left1]'], 'Crossroads_01': ['Crossroads_01[top1]', 'Crossroads_01[left1]', 'Crossroads_01[right1]'], 'Crossroads_02': ['Crossroads_02[left1]', 'Crossroads_02[door1]', 'Crossroads_02[right1]'], 'Crossroads_03': ['Crossroads_03[right1]', 'Crossroads_03[right2]', 'Crossroads_03[left1]', 'Crossroads_03[left2]', 'Crossroads_03[bot1]', 'Crossroads_03[top1]'], 'Crossroads_04': ['Crossroads_04[left1]', 'Crossroads_04[top1]', 'Crossroads_04[door_Mender_House]', 'Crossroads_04[door1]', 'Crossroads_04[door_charmshop]', 'Crossroads_04[right1]'], 'Crossroads_05': ['Crossroads_05[left1]', 'Crossroads_05[right1]'], 'Crossroads_06': ['Crossroads_06[left1]', 'Crossroads_06[door1]', 'Crossroads_06[right1]'], 'Crossroads_07': ['Crossroads_07[left1]', 'Crossroads_07[left2]', 'Crossroads_07[left3]', 'Crossroads_07[right1]', 'Crossroads_07[right2]', 'Crossroads_07[bot1]'], 'Crossroads_08': ['Crossroads_08[left1]', 'Crossroads_08[left2]', 'Crossroads_08[right1]', 'Crossroads_08[right2]'], 'Crossroads_09': ['Crossroads_09[left1]', 'Crossroads_09[right1]'], 'Crossroads_10': ['Crossroads_10[left1]', 'Crossroads_10[right1]'], 'Crossroads_11_alt': ['Crossroads_11_alt[left1]', 'Crossroads_11_alt[right1]'], 'Crossroads_12': ['Crossroads_12[left1]', 'Crossroads_12[right1]'], 'Crossroads_13': ['Crossroads_13[left1]', 'Crossroads_13[right1]'], 'Crossroads_14': ['Crossroads_14[left1]', 'Crossroads_14[left2]', 'Crossroads_14[right1]', 'Crossroads_14[right2]'], 'Crossroads_15': ['Crossroads_15[left1]', 'Crossroads_15[right1]'], 'Crossroads_16': ['Crossroads_16[left1]', 'Crossroads_16[right1]', 'Crossroads_16[bot1]'], 'Crossroads_18': ['Crossroads_18[right1]', 'Crossroads_18[right2]', 'Crossroads_18[bot1]'], 'Crossroads_19': ['Crossroads_19[right1]', 'Crossroads_19[top1]', 'Crossroads_19[left1]', 'Crossroads_19[left2]'], 'Crossroads_21': ['Crossroads_21[left1]', 'Crossroads_21[right1]', 'Crossroads_21[top1]'], 'Crossroads_22': ['Crossroads_22[bot1]'], 'Crossroads_25': ['Crossroads_25[right1]', 'Crossroads_25[left1]'], 'Crossroads_27': ['Crossroads_27[right1]', 'Crossroads_27[bot1]', 'Crossroads_27[left1]', 'Crossroads_27[left2]'], 'Crossroads_30': ['Crossroads_30[left1]'], 'Crossroads_31': ['Crossroads_31[right1]'], 'Crossroads_33': ['Crossroads_33[top1]', 'Crossroads_33[left1]', 'Crossroads_33[left2]', 'Crossroads_33[right1]', 'Crossroads_33[right2]'], 'Crossroads_35': ['Crossroads_35[bot1]', 'Crossroads_35[right1]'], 'Crossroads_36': ['Crossroads_36[right1]', 'Crossroads_36[right2]'], 'Crossroads_37': ['Crossroads_37[right1]'], 'Crossroads_38': ['Crossroads_38[right1]'], 'Crossroads_39': ['Crossroads_39[right1]', 'Crossroads_39[left1]'], 'Crossroads_40': ['Crossroads_40[right1]', 'Crossroads_40[left1]'], 'Crossroads_42': ['Crossroads_42[left1]', 'Crossroads_42[right1]'], 'Crossroads_43': ['Crossroads_43[left1]', 'Crossroads_43[right1]'], 'Crossroads_45': ['Crossroads_45[right1]', 'Crossroads_45[left1]'], 'Crossroads_46': ['Crossroads_46[left1]'], 'Crossroads_46b': ['Crossroads_46b[right1]'], 'Crossroads_ShamanTemple': ['Crossroads_ShamanTemple[left1]'], 'Crossroads_47': ['Crossroads_47[right1]'], 'Crossroads_48': ['Crossroads_48[left1]'], 'Crossroads_49': ['Crossroads_49[right1]', 'Crossroads_49[left1]'], 'Crossroads_49b': ['Crossroads_49b[right1]'], 'Crossroads_50': ['Crossroads_50[right1]', 'Crossroads_50[left1]'], 'Crossroads_52': ['Crossroads_52[left1]'], 'Room_ruinhouse': ['Room_ruinhouse[left1]'], 'Room_Charm_Shop': ['Room_Charm_Shop[left1]'], 'Room_Mender_House': ['Room_Mender_House[left1]'], 'Fungus1_01': ['Fungus1_01[left1]', 'Fungus1_01[right1]'], 'Fungus1_01b': ['Fungus1_01b[left1]', 'Fungus1_01b[right1]'], 'Fungus1_02': ['Fungus1_02[left1]', 'Fungus1_02[right1]', 'Fungus1_02[right2]'], 'Fungus1_03': ['Fungus1_03[left1]', 'Fungus1_03[right1]', 'Fungus1_03[bot1]'], 'Fungus1_04': ['Fungus1_04[left1]', 'Fungus1_04[right1]'], 'Fungus1_05': ['Fungus1_05[right1]', 'Fungus1_05[bot1]', 'Fungus1_05[top1]'], 'Fungus1_06': ['Fungus1_06[left1]', 'Fungus1_06[bot1]'], 'Fungus1_07': ['Fungus1_07[top1]', 'Fungus1_07[left1]', 'Fungus1_07[right1]'], 'Fungus1_08': ['Fungus1_08[left1]'], 'Fungus1_09': ['Fungus1_09[left1]', 'Fungus1_09[right1]'], 'Fungus1_10': ['Fungus1_10[left1]', 'Fungus1_10[right1]', 'Fungus1_10[top1]'], 'Fungus1_11': ['Fungus1_11[top1]', 'Fungus1_11[right1]', 'Fungus1_11[right2]', 'Fungus1_11[left1]', 'Fungus1_11[bot1]'], 'Fungus1_12': ['Fungus1_12[left1]', 'Fungus1_12[right1]'], 'Fungus1_13': ['Fungus1_13[right1]', 'Fungus1_13[left1]'], 'Fungus1_14': ['Fungus1_14[left1]'], 'Fungus1_15': ['Fungus1_15[door1]', 'Fungus1_15[right1]'], 'Fungus1_16_alt': ['Fungus1_16_alt[right1]'], 'Fungus1_17': ['Fungus1_17[left1]', 'Fungus1_17[right1]'], 'Fungus1_19': ['Fungus1_19[left1]', 'Fungus1_19[right1]', 'Fungus1_19[bot1]'], 'Fungus1_20_v02': ['Fungus1_20_v02[bot1]', 'Fungus1_20_v02[bot2]', 'Fungus1_20_v02[right1]'], 'Fungus1_21': ['Fungus1_21[bot1]', 'Fungus1_21[top1]', 'Fungus1_21[left1]', 'Fungus1_21[right1]'], 'Fungus1_22': ['Fungus1_22[bot1]', 'Fungus1_22[top1]', 'Fungus1_22[left1]'], 'Fungus1_23': ['Fungus1_23[left1]', 'Fungus1_23[right1]'], 'Fungus1_24': ['Fungus1_24[left1]'], 'Fungus1_25': ['Fungus1_25[right1]', 'Fungus1_25[left1]'], 'Fungus1_26': ['Fungus1_26[right1]', 'Fungus1_26[left1]', 'Fungus1_26[door_SlugShrine]'], 'Fungus1_28': ['Fungus1_28[left1]', 'Fungus1_28[left2]'], 'Fungus1_29': ['Fungus1_29[left1]', 'Fungus1_29[right1]'], 'Fungus1_30': ['Fungus1_30[top1]', 'Fungus1_30[top3]', 'Fungus1_30[left1]', 'Fungus1_30[right1]'], 'Fungus1_31': ['Fungus1_31[top1]', 'Fungus1_31[bot1]', 'Fungus1_31[right1]'], 'Fungus1_32': ['Fungus1_32[bot1]', 'Fungus1_32[top1]', 'Fungus1_32[left1]'], 'Fungus1_34': ['Fungus1_34[door1]', 'Fungus1_34[left1]'], 'Fungus1_35': ['Fungus1_35[left1]', 'Fungus1_35[right1]'], 'Fungus1_36': ['Fungus1_36[left1]'], 'Fungus1_37': ['Fungus1_37[left1]'], 'Fungus1_Slug': ['Fungus1_Slug[right1]'], 'Room_Slug_Shrine': ['Room_Slug_Shrine[left1]'], 'Room_nailmaster_02': ['Room_nailmaster_02[left1]'], 'Fungus3_01': ['Fungus3_01[top1]', 'Fungus3_01[right1]', 'Fungus3_01[left1]', 'Fungus3_01[right2]'], 'Fungus3_02': ['Fungus3_02[left1]', 'Fungus3_02[left2]', 'Fungus3_02[left3]', 'Fungus3_02[right1]', 'Fungus3_02[right2]'], 'Fungus3_03': ['Fungus3_03[right1]', 'Fungus3_03[left1]'], 'Fungus3_24': ['Fungus3_24[right1]', 'Fungus3_24[left1]', 'Fungus3_24[top1]'], 'Fungus3_25': ['Fungus3_25[right1]', 'Fungus3_25[left1]'], 'Fungus3_25b': ['Fungus3_25b[right1]', 'Fungus3_25b[left1]'], 'Fungus3_26': ['Fungus3_26[top1]', 'Fungus3_26[left1]', 'Fungus3_26[left2]', 'Fungus3_26[left3]', 'Fungus3_26[right1]'], 'Fungus3_27': ['Fungus3_27[left1]', 'Fungus3_27[right1]'], 'Fungus3_28': ['Fungus3_28[right1]'], 'Fungus3_30': ['Fungus3_30[bot1]'], 'Fungus3_35': ['Fungus3_35[right1]'], 'Fungus3_44': ['Fungus3_44[bot1]', 'Fungus3_44[door1]', 'Fungus3_44[right1]'], 'Fungus3_47': ['Fungus3_47[left1]', 'Fungus3_47[right1]', 'Fungus3_47[door1]'], 'Room_Fungus_Shaman': ['Room_Fungus_Shaman[left1]'], 'Fungus3_archive': ['Fungus3_archive[left1]', 'Fungus3_archive[bot1]'], 'Fungus3_archive_02': ['Fungus3_archive_02[top1]'], 'Fungus2_01': ['Fungus2_01[left1]', 'Fungus2_01[left2]', 'Fungus2_01[left3]', 'Fungus2_01[right1]'], 'Fungus2_02': ['Fungus2_02[right1]'], 'Fungus2_34': ['Fungus2_34[right1]'], 'Fungus2_03': ['Fungus2_03[left1]', 'Fungus2_03[bot1]', 'Fungus2_03[right1]'], 'Fungus2_04': ['Fungus2_04[top1]', 'Fungus2_04[right1]', 'Fungus2_04[left1]', 'Fungus2_04[right2]'], 'Fungus2_05': ['Fungus2_05[bot1]', 'Fungus2_05[right1]'], 'Fungus2_06': ['Fungus2_06[top1]', 'Fungus2_06[left1]', 'Fungus2_06[left2]', 'Fungus2_06[right1]', 'Fungus2_06[right2]'], 'Fungus2_07': ['Fungus2_07[left1]', 'Fungus2_07[right1]'], 'Fungus2_08': ['Fungus2_08[left1]', 'Fungus2_08[left2]', 'Fungus2_08[right1]'], 'Fungus2_09': ['Fungus2_09[left1]', 'Fungus2_09[right1]'], 'Fungus2_10': ['Fungus2_10[right1]', 'Fungus2_10[right2]', 'Fungus2_10[bot1]'], 'Fungus2_11': ['Fungus2_11[top1]', 'Fungus2_11[left1]', 'Fungus2_11[left2]', 'Fungus2_11[right1]'], 'Fungus2_12': ['Fungus2_12[left1]', 'Fungus2_12[bot1]'], 'Fungus2_13': ['Fungus2_13[top1]', 'Fungus2_13[left2]', 'Fungus2_13[left3]'], 'Fungus2_14': ['Fungus2_14[top1]', 'Fungus2_14[right1]', 'Fungus2_14[bot3]'], 'Fungus2_15': ['Fungus2_15[top3]', 'Fungus2_15[right1]', 'Fungus2_15[left1]'], 'Fungus2_17': ['Fungus2_17[left1]', 'Fungus2_17[right1]', 'Fungus2_17[bot1]'], 'Fungus2_18': ['Fungus2_18[right1]', 'Fungus2_18[bot1]', 'Fungus2_18[top1]'], 'Fungus2_19': ['Fungus2_19[top1]', 'Fungus2_19[left1]'], 'Fungus2_20': ['Fungus2_20[right1]', 'Fungus2_20[left1]'], 'Fungus2_21': ['Fungus2_21[right1]', 'Fungus2_21[left1]'], 'Fungus2_23': ['Fungus2_23[right1]', 'Fungus2_23[right2]'], 'Fungus2_26': ['Fungus2_26[left1]'], 'Fungus2_28': ['Fungus2_28[left1]', 'Fungus2_28[left2]'], 'Fungus2_29': ['Fungus2_29[right1]', 'Fungus2_29[bot1]'], 'Fungus2_30': ['Fungus2_30[bot1]', 'Fungus2_30[top1]'], 'Fungus2_31': ['Fungus2_31[left1]'], 'Fungus2_32': ['Fungus2_32[left1]'], 'Fungus2_33': ['Fungus2_33[right1]', 'Fungus2_33[left1]'], 'Deepnest_01': ['Deepnest_01[right1]', 'Deepnest_01[bot1]', 'Deepnest_01[bot2]', 'Deepnest_01[left1]'], 'Deepnest_01b': ['Deepnest_01b[top1]', 'Deepnest_01b[top2]', 'Deepnest_01b[right1]', 'Deepnest_01b[right2]', 'Deepnest_01b[bot1]'], 'Deepnest_02': ['Deepnest_02[left1]', 'Deepnest_02[left2]', 'Deepnest_02[right1]'], 'Deepnest_03': ['Deepnest_03[right1]', 'Deepnest_03[left1]', 'Deepnest_03[top1]', 'Deepnest_03[left2]'], 'Deepnest_09': ['Deepnest_09[left1]'], 'Deepnest_10': ['Deepnest_10[right1]', 'Deepnest_10[right2]', 'Deepnest_10[right3]', 'Deepnest_10[door1]', 'Deepnest_10[door2]'], 'Room_spider_small': ['Room_spider_small[left1]'], 'Deepnest_Spider_Town': ['Deepnest_Spider_Town[left1]'], 'Deepnest_14': ['Deepnest_14[right1]', 'Deepnest_14[left1]', 'Deepnest_14[bot1]', 'Deepnest_14[bot2]'], 'Deepnest_16': ['Deepnest_16[left1]', 'Deepnest_16[bot1]'], 'Deepnest_17': ['Deepnest_17[left1]', 'Deepnest_17[right1]', 'Deepnest_17[top1]', 'Deepnest_17[bot1]'], 'Fungus2_25': ['Fungus2_25[top1]', 'Fungus2_25[top2]', 'Fungus2_25[right1]'], 'Deepnest_26': ['Deepnest_26[left1]', 'Deepnest_26[left2]', 'Deepnest_26[right1]', 'Deepnest_26[bot1]'], 'Deepnest_26b': ['Deepnest_26b[right2]', 'Deepnest_26b[right1]'], 'Deepnest_30': ['Deepnest_30[left1]', 'Deepnest_30[top1]', 'Deepnest_30[right1]'], 'Deepnest_31': ['Deepnest_31[right1]', 'Deepnest_31[right2]'], 'Deepnest_32': ['Deepnest_32[left1]'], 'Deepnest_33': ['Deepnest_33[top1]', 'Deepnest_33[top2]', 'Deepnest_33[bot1]'], 'Deepnest_34': ['Deepnest_34[left1]', 'Deepnest_34[right1]', 'Deepnest_34[top1]'], 'Deepnest_35': ['Deepnest_35[left1]', 'Deepnest_35[top1]', 'Deepnest_35[bot1]'], 'Deepnest_36': ['Deepnest_36[left1]'], 'Deepnest_37': ['Deepnest_37[left1]', 'Deepnest_37[right1]', 'Deepnest_37[top1]', 'Deepnest_37[bot1]'], 'Deepnest_38': ['Deepnest_38[bot1]'], 'Deepnest_39': ['Deepnest_39[left1]', 'Deepnest_39[top1]', 'Deepnest_39[door1]', 'Deepnest_39[right1]'], 'Deepnest_40': ['Deepnest_40[right1]'], 'Deepnest_41': ['Deepnest_41[right1]', 'Deepnest_41[left1]', 'Deepnest_41[left2]'], 'Deepnest_42': ['Deepnest_42[bot1]', 'Deepnest_42[left1]', 'Deepnest_42[top1]'], 'Deepnest_43': ['Deepnest_43[bot1]', 'Deepnest_43[left1]', 'Deepnest_43[right1]'], 'Deepnest_44': ['Deepnest_44[top1]'], 'Deepnest_45_v02': ['Deepnest_45_v02[left1]'], 'Room_Mask_Maker': ['Room_Mask_Maker[right1]'], 'Deepnest_East_01': ['Deepnest_East_01[bot1]', 'Deepnest_East_01[right1]', 'Deepnest_East_01[top1]'], 'Deepnest_East_02': ['Deepnest_East_02[bot1]', 'Deepnest_East_02[bot2]', 'Deepnest_East_02[top1]', 'Deepnest_East_02[right1]'], 'Deepnest_East_03': ['Deepnest_East_03[left1]', 'Deepnest_East_03[left2]', 'Deepnest_East_03[top1]', 'Deepnest_East_03[top2]', 'Deepnest_East_03[right1]', 'Deepnest_East_03[right2]'], 'Deepnest_East_04': ['Deepnest_East_04[left1]', 'Deepnest_East_04[left2]', 'Deepnest_East_04[right2]', 'Deepnest_East_04[right1]'], 'Deepnest_East_06': ['Deepnest_East_06[top1]', 'Deepnest_East_06[left1]', 'Deepnest_East_06[bot1]', 'Deepnest_East_06[door1]', 'Deepnest_East_06[right1]'], 'Deepnest_East_07': ['Deepnest_East_07[bot1]', 'Deepnest_East_07[bot2]', 'Deepnest_East_07[left1]', 'Deepnest_East_07[left2]', 'Deepnest_East_07[right1]'], 'Deepnest_East_08': ['Deepnest_East_08[right1]', 'Deepnest_East_08[top1]'], 'Deepnest_East_09': ['Deepnest_East_09[right1]', 'Deepnest_East_09[left1]', 'Deepnest_East_09[bot1]'], 'Deepnest_East_10': ['Deepnest_East_10[left1]'], 'Deepnest_East_11': ['Deepnest_East_11[right1]', 'Deepnest_East_11[left1]', 'Deepnest_East_11[top1]', 'Deepnest_East_11[bot1]'], 'Deepnest_East_12': ['Deepnest_East_12[right1]', 'Deepnest_East_12[left1]'], 'Deepnest_East_13': ['Deepnest_East_13[bot1]'], 'Deepnest_East_14': ['Deepnest_East_14[top2]', 'Deepnest_East_14[left1]', 'Deepnest_East_14[door1]'], 'Deepnest_East_14b': ['Deepnest_East_14b[right1]', 'Deepnest_East_14b[top1]'], 'Deepnest_East_15': ['Deepnest_East_15[left1]'], 'Deepnest_East_16': ['Deepnest_East_16[left1]', 'Deepnest_East_16[bot1]'], 'Deepnest_East_17': ['Deepnest_East_17[left1]'], 'Deepnest_East_18': ['Deepnest_East_18[top1]', 'Deepnest_East_18[bot1]', 'Deepnest_East_18[right2]'], 'Room_nailmaster_03': ['Room_nailmaster_03[left1]'], 'Deepnest_East_Hornet': ['Deepnest_East_Hornet[left1]', 'Deepnest_East_Hornet[left2]'], 'Room_Wyrm': ['Room_Wyrm[right1]'], 'GG_Lurker': ['GG_Lurker[left1]'], 'Hive_01': ['Hive_01[left1]', 'Hive_01[right1]', 'Hive_01[right2]'], 'Hive_02': ['Hive_02[left1]', 'Hive_02[left2]', 'Hive_02[left3]'], 'Hive_03_c': ['Hive_03_c[left1]', 'Hive_03_c[right2]', 'Hive_03_c[right3]', 'Hive_03_c[top1]'], 'Hive_03': ['Hive_03[bot1]', 'Hive_03[right1]', 'Hive_03[top1]'], 'Hive_04': ['Hive_04[left1]', 'Hive_04[left2]', 'Hive_04[right1]'], 'Hive_05': ['Hive_05[left1]'], 'Room_Colosseum_01': ['Room_Colosseum_01[left1]', 'Room_Colosseum_01[bot1]'], 'Room_Colosseum_02': ['Room_Colosseum_02[top1]', 'Room_Colosseum_02[top2]'], 'Room_Colosseum_Spectate': ['Room_Colosseum_Spectate[bot1]', 'Room_Colosseum_Spectate[right1]'], 'Abyss_01': ['Abyss_01[left1]', 'Abyss_01[left2]', 'Abyss_01[left3]', 'Abyss_01[right1]', 'Abyss_01[right2]'], 'Abyss_02': ['Abyss_02[right1]', 'Abyss_02[bot1]'], 'Abyss_03': ['Abyss_03[bot1]', 'Abyss_03[bot2]', 'Abyss_03[top1]'], 'Abyss_03_b': ['Abyss_03_b[left1]'], 'Abyss_03_c': ['Abyss_03_c[right1]', 'Abyss_03_c[top1]'], 'Abyss_04': ['Abyss_04[top1]', 'Abyss_04[left1]', 'Abyss_04[bot1]', 'Abyss_04[right1]'], 'Abyss_05': ['Abyss_05[left1]', 'Abyss_05[right1]'], 'Abyss_06_Core': ['Abyss_06_Core[top1]', 'Abyss_06_Core[left1]', 'Abyss_06_Core[left3]', 'Abyss_06_Core[right2]', 'Abyss_06_Core[bot1]'], 'Abyss_08': ['Abyss_08[right1]'], 'Abyss_09': ['Abyss_09[right1]', 'Abyss_09[right2]', 'Abyss_09[right3]', 'Abyss_09[left1]'], 'Abyss_10': ['Abyss_10[left1]', 'Abyss_10[left2]'], 'Abyss_12': ['Abyss_12[right1]'], 'Abyss_15': ['Abyss_15[top1]'], 'Abyss_16': ['Abyss_16[left1]', 'Abyss_16[right1]'], 'Abyss_17': ['Abyss_17[top1]'], 'Abyss_18': ['Abyss_18[left1]', 'Abyss_18[right1]'], 'Abyss_19': ['Abyss_19[left1]', 'Abyss_19[right1]', 'Abyss_19[bot1]', 'Abyss_19[bot2]'], 'Abyss_20': ['Abyss_20[top1]', 'Abyss_20[top2]'], 'Abyss_21': ['Abyss_21[right1]'], 'Abyss_22': ['Abyss_22[left1]'], 'Abyss_Lighthouse_room': ['Abyss_Lighthouse_room[left1]'], 'Waterways_01': ['Waterways_01[top1]', 'Waterways_01[left1]', 'Waterways_01[right1]', 'Waterways_01[bot1]'], 'Waterways_02': ['Waterways_02[top1]', 'Waterways_02[top2]', 'Waterways_02[top3]', 'Waterways_02[bot1]', 'Waterways_02[bot2]'], 'Waterways_03': ['Waterways_03[left1]'], 'Waterways_04': ['Waterways_04[bot1]', 'Waterways_04[right1]', 'Waterways_04[left1]', 'Waterways_04[left2]'], 'Waterways_04b': ['Waterways_04b[right1]', 'Waterways_04b[right2]', 'Waterways_04b[left1]'], 'Waterways_05': ['Waterways_05[right1]', 'Waterways_05[bot1]', 'Waterways_05[bot2]'], 'Waterways_06': ['Waterways_06[right1]', 'Waterways_06[top1]'], 'Waterways_07': ['Waterways_07[right1]', 'Waterways_07[right2]', 'Waterways_07[left1]', 'Waterways_07[door1]', 'Waterways_07[top1]'], 'Waterways_08': ['Waterways_08[top1]', 'Waterways_08[left1]', 'Waterways_08[left2]'], 'Waterways_09': ['Waterways_09[right1]', 'Waterways_09[left1]'], 'Waterways_12': ['Waterways_12[right1]'], 'Waterways_13': ['Waterways_13[left1]', 'Waterways_13[left2]'], 'Waterways_14': ['Waterways_14[bot1]', 'Waterways_14[bot2]'], 'Waterways_15': ['Waterways_15[top1]'], 'GG_Pipeway': ['GG_Pipeway[right1]', 'GG_Pipeway[left1]'], 'GG_Waterways': ['GG_Waterways[right1]', 'GG_Waterways[door1]'], 'Room_GG_Shortcut': ['Room_GG_Shortcut[left1]', 'Room_GG_Shortcut[top1]'], 'Ruins1_01': ['Ruins1_01[left1]', 'Ruins1_01[top1]', 'Ruins1_01[bot1]'], 'Ruins1_02': ['Ruins1_02[top1]', 'Ruins1_02[bot1]'], 'Ruins1_03': ['Ruins1_03[top1]', 'Ruins1_03[left1]', 'Ruins1_03[right1]', 'Ruins1_03[right2]'], 'Ruins1_04': ['Ruins1_04[right1]', 'Ruins1_04[door1]', 'Ruins1_04[bot1]'], 'Ruins1_05b': ['Ruins1_05b[left1]', 'Ruins1_05b[top1]', 'Ruins1_05b[bot1]', 'Ruins1_05b[right1]'], 'Ruins1_05c': ['Ruins1_05c[left2]', 'Ruins1_05c[bot1]', 'Ruins1_05c[top1]', 'Ruins1_05c[top2]', 'Ruins1_05c[top3]'], 'Ruins1_05': ['Ruins1_05[bot1]', 'Ruins1_05[bot2]', 'Ruins1_05[bot3]', 'Ruins1_05[right1]', 'Ruins1_05[right2]', 'Ruins1_05[top1]'], 'Ruins1_06': ['Ruins1_06[left1]', 'Ruins1_06[right1]'], 'Ruins1_09': ['Ruins1_09[top1]', 'Ruins1_09[left1]'], 'Ruins1_17': ['Ruins1_17[top1]', 'Ruins1_17[right1]', 'Ruins1_17[bot1]'], 'Ruins1_18': ['Ruins1_18[left1]', 'Ruins1_18[right1]', 'Ruins1_18[right2]'], 'Ruins1_23': ['Ruins1_23[top1]', 'Ruins1_23[right1]', 'Ruins1_23[right2]', 'Ruins1_23[bot1]', 'Ruins1_23[left1]'], 'Ruins1_24': ['Ruins1_24[left1]', 'Ruins1_24[right1]', 'Ruins1_24[left2]', 'Ruins1_24[right2]'], 'Ruins1_25': ['Ruins1_25[left1]', 'Ruins1_25[left2]', 'Ruins1_25[left3]'], 'Ruins1_27': ['Ruins1_27[left1]', 'Ruins1_27[right1]'], 'Ruins1_28': ['Ruins1_28[left1]', 'Ruins1_28[right1]', 'Ruins1_28[bot1]'], 'Ruins1_29': ['Ruins1_29[left1]'], 'Ruins1_30': ['Ruins1_30[left1]', 'Ruins1_30[left2]', 'Ruins1_30[bot1]', 'Ruins1_30[right1]'], 'Ruins1_31': ['Ruins1_31[bot1]', 'Ruins1_31[left1]', 'Ruins1_31[left2]', 'Ruins1_31[left3]', 'Ruins1_31[right1]'], 'Ruins1_31b': ['Ruins1_31b[right1]', 'Ruins1_31b[right2]'], 'Ruins1_32': ['Ruins1_32[right1]', 'Ruins1_32[right2]'], 'Room_nailsmith': ['Room_nailsmith[left1]'], 'Ruins2_01': ['Ruins2_01[top1]', 'Ruins2_01[bot1]', 'Ruins2_01[left2]'], 'Ruins2_01_b': ['Ruins2_01_b[top1]', 'Ruins2_01_b[left1]', 'Ruins2_01_b[right1]'], 'Ruins2_03b': ['Ruins2_03b[top1]', 'Ruins2_03b[top2]', 'Ruins2_03b[left1]', 'Ruins2_03b[bot1]'], 'Ruins2_03': ['Ruins2_03[top1]', 'Ruins2_03[bot1]', 'Ruins2_03[bot2]'], 'Ruins2_04': ['Ruins2_04[left1]', 'Ruins2_04[left2]', 'Ruins2_04[right1]', 'Ruins2_04[right2]', 'Ruins2_04[door_Ruin_House_01]', 'Ruins2_04[door_Ruin_House_02]', 'Ruins2_04[door_Ruin_House_03]', 'Ruins2_04[door_Ruin_Elevator]'], 'Ruins2_05': ['Ruins2_05[left1]', 'Ruins2_05[top1]', 'Ruins2_05[bot1]'], 'Ruins2_06': ['Ruins2_06[left1]', 'Ruins2_06[left2]', 'Ruins2_06[right1]', 'Ruins2_06[right2]', 'Ruins2_06[top1]'], 'Ruins2_07': ['Ruins2_07[right1]', 'Ruins2_07[left1]', 'Ruins2_07[top1]'], 'Ruins2_08': ['Ruins2_08[left1]'], 'Ruins2_09': ['Ruins2_09[bot1]'], 'Ruins2_10': ['Ruins2_10[right1]', 'Ruins2_10[left1]'], 'Ruins2_10b': ['Ruins2_10b[right1]', 'Ruins2_10b[right2]', 'Ruins2_10b[left1]'], 'Ruins2_11_b': ['Ruins2_11_b[right1]', 'Ruins2_11_b[left1]', 'Ruins2_11_b[bot1]'], 'Ruins2_11': ['Ruins2_11[right1]'], 'Ruins2_Watcher_Room': ['Ruins2_Watcher_Room[bot1]'], 'Ruins_House_01': ['Ruins_House_01[left1]'], 'Ruins_House_02': ['Ruins_House_02[left1]'], 'Ruins_House_03': ['Ruins_House_03[left1]', 'Ruins_House_03[left2]'], 'Ruins_Elevator': ['Ruins_Elevator[left1]', 'Ruins_Elevator[left2]'], 'Ruins_Bathhouse': ['Ruins_Bathhouse[door1]', 'Ruins_Bathhouse[right1]'], 'RestingGrounds_02': ['RestingGrounds_02[right1]', 'RestingGrounds_02[left1]', 'RestingGrounds_02[bot1]', 'RestingGrounds_02[top1]'], 'RestingGrounds_04': ['RestingGrounds_04[left1]', 'RestingGrounds_04[right1]'], 'RestingGrounds_05': ['RestingGrounds_05[left1]', 'RestingGrounds_05[left2]', 'RestingGrounds_05[left3]', 'RestingGrounds_05[right1]', 'RestingGrounds_05[right2]', 'RestingGrounds_05[bot1]'], 'RestingGrounds_06': ['RestingGrounds_06[left1]', 'RestingGrounds_06[right1]', 'RestingGrounds_06[top1]'], 'RestingGrounds_07': ['RestingGrounds_07[right1]'], 'RestingGrounds_08': ['RestingGrounds_08[left1]'], 'RestingGrounds_09': ['RestingGrounds_09[left1]'], 'RestingGrounds_10': ['RestingGrounds_10[left1]', 'RestingGrounds_10[top1]', 'RestingGrounds_10[top2]'], 'RestingGrounds_12': ['RestingGrounds_12[bot1]', 'RestingGrounds_12[door_Mansion]'], 'RestingGrounds_17': ['RestingGrounds_17[right1]'], 'Room_Mansion': ['Room_Mansion[left1]'], 'Mines_01': ['Mines_01[bot1]', 'Mines_01[left1]'], 'Mines_02': ['Mines_02[top1]', 'Mines_02[top2]', 'Mines_02[left1]', 'Mines_02[right1]'], 'Mines_03': ['Mines_03[right1]', 'Mines_03[bot1]', 'Mines_03[top1]'], 'Mines_04': ['Mines_04[right1]', 'Mines_04[top1]', 'Mines_04[left1]', 'Mines_04[left2]', 'Mines_04[left3]'], 'Mines_05': ['Mines_05[right1]', 'Mines_05[top1]', 'Mines_05[bot1]', 'Mines_05[left1]', 'Mines_05[left2]'], 'Mines_06': ['Mines_06[right1]', 'Mines_06[left1]'], 'Mines_07': ['Mines_07[right1]', 'Mines_07[left1]'], 'Mines_10': ['Mines_10[right1]', 'Mines_10[left1]', 'Mines_10[bot1]'], 'Mines_11': ['Mines_11[right1]', 'Mines_11[top1]', 'Mines_11[bot1]'], 'Mines_13': ['Mines_13[right1]', 'Mines_13[top1]', 'Mines_13[bot1]'], 'Mines_16': ['Mines_16[top1]'], 'Mines_17': ['Mines_17[right1]', 'Mines_17[left1]'], 'Mines_18': ['Mines_18[top1]', 'Mines_18[left1]', 'Mines_18[right1]'], 'Mines_19': ['Mines_19[left1]', 'Mines_19[right1]'], 'Mines_20': ['Mines_20[left1]', 'Mines_20[left2]', 'Mines_20[left3]', 'Mines_20[bot1]', 'Mines_20[right1]', 'Mines_20[right2]'], 'Mines_23': ['Mines_23[left1]', 'Mines_23[right1]', 'Mines_23[right2]', 'Mines_23[top1]'], 'Mines_24': ['Mines_24[left1]'], 'Mines_25': ['Mines_25[left1]', 'Mines_25[top1]'], 'Mines_28': ['Mines_28[left1]', 'Mines_28[bot1]', 'Mines_28[door1]'], 'Mines_29': ['Mines_29[left1]', 'Mines_29[right1]', 'Mines_29[right2]'], 'Mines_30': ['Mines_30[left1]', 'Mines_30[right1]'], 'Mines_31': ['Mines_31[left1]'], 'Mines_32': ['Mines_32[bot1]'], 'Mines_33': ['Mines_33[right1]', 'Mines_33[left1]'], 'Mines_34': ['Mines_34[bot1]', 'Mines_34[bot2]', 'Mines_34[left1]'], 'Mines_35': ['Mines_35[left1]'], 'Mines_36': ['Mines_36[right1]'], 'Mines_37': ['Mines_37[bot1]', 'Mines_37[top1]'], 'Fungus3_04': ['Fungus3_04[left1]', 'Fungus3_04[left2]', 'Fungus3_04[right1]', 'Fungus3_04[right2]'], 'Fungus3_05': ['Fungus3_05[left1]', 'Fungus3_05[right1]', 'Fungus3_05[right2]'], 'Fungus3_08': ['Fungus3_08[left1]', 'Fungus3_08[right1]', 'Fungus3_08[top1]'], 'Fungus3_10': ['Fungus3_10[top1]', 'Fungus3_10[bot1]'], 'Fungus3_11': ['Fungus3_11[left1]', 'Fungus3_11[left2]', 'Fungus3_11[right1]'], 'Fungus3_13': ['Fungus3_13[left1]', 'Fungus3_13[left2]', 'Fungus3_13[left3]', 'Fungus3_13[bot1]', 'Fungus3_13[right1]'], 'Fungus3_21': ['Fungus3_21[right1]', 'Fungus3_21[top1]'], 'Fungus3_22': ['Fungus3_22[right1]', 'Fungus3_22[left1]', 'Fungus3_22[bot1]'], 'Fungus3_23': ['Fungus3_23[right1]', 'Fungus3_23[left1]'], 'Fungus3_34': ['Fungus3_34[right1]', 'Fungus3_34[left1]', 'Fungus3_34[top1]'], 'Fungus3_39': ['Fungus3_39[right1]', 'Fungus3_39[left1]'], 'Fungus3_40': ['Fungus3_40[right1]', 'Fungus3_40[top1]'], 'Fungus3_48': ['Fungus3_48[right1]', 'Fungus3_48[right2]', 'Fungus3_48[door1]', 'Fungus3_48[bot1]'], 'Fungus3_49': ['Fungus3_49[right1]'], 'Fungus3_50': ['Fungus3_50[right1]'], 'Room_Queen': ['Room_Queen[left1]'], 'Cliffs_01': ['Cliffs_01[right1]', 'Cliffs_01[right2]', 'Cliffs_01[right3]', 'Cliffs_01[right4]'], 'Cliffs_02': ['Cliffs_02[right1]', 'Cliffs_02[bot1]', 'Cliffs_02[bot2]', 'Cliffs_02[door1]', 'Cliffs_02[left1]', 'Cliffs_02[left2]'], 'Cliffs_03': ['Cliffs_03[right1]'], 'Cliffs_04': ['Cliffs_04[right1]', 'Cliffs_04[left1]'], 'Cliffs_05': ['Cliffs_05[left1]'], 'Cliffs_06': ['Cliffs_06[left1]'], 'Room_nailmaster': ['Room_nailmaster[left1]'], 'White_Palace_01': ['White_Palace_01[left1]', 'White_Palace_01[right1]', 'White_Palace_01[top1]'], 'White_Palace_02': ['White_Palace_02[left1]'], 'White_Palace_03_hub': ['White_Palace_03_hub[left1]', 'White_Palace_03_hub[left2]', 'White_Palace_03_hub[right1]', 'White_Palace_03_hub[top1]', 'White_Palace_03_hub[bot1]'], 'White_Palace_04': ['White_Palace_04[top1]', 'White_Palace_04[right2]'], 'White_Palace_05': ['White_Palace_05[left1]', 'White_Palace_05[left2]', 'White_Palace_05[right1]', 'White_Palace_05[right2]'], 'White_Palace_06': ['White_Palace_06[left1]', 'White_Palace_06[top1]', 'White_Palace_06[bot1]'], 'White_Palace_07': ['White_Palace_07[top1]', 'White_Palace_07[bot1]'], 'White_Palace_08': ['White_Palace_08[left1]', 'White_Palace_08[right1]'], 'White_Palace_09': ['White_Palace_09[right1]'], 'White_Palace_11': ['White_Palace_11[door2]'], 'White_Palace_12': ['White_Palace_12[right1]', 'White_Palace_12[bot1]'], 'White_Palace_13': ['White_Palace_13[right1]', 'White_Palace_13[left1]', 'White_Palace_13[left2]', 'White_Palace_13[left3]'], 'White_Palace_14': ['White_Palace_14[bot1]', 'White_Palace_14[right1]'], 'White_Palace_15': ['White_Palace_15[left1]', 'White_Palace_15[right1]', 'White_Palace_15[right2]'], 'White_Palace_16': ['White_Palace_16[left1]', 'White_Palace_16[left2]'], 'White_Palace_17': ['White_Palace_17[right1]', 'White_Palace_17[bot1]'], 'White_Palace_18': ['White_Palace_18[top1]', 'White_Palace_18[right1]'], 'White_Palace_19': ['White_Palace_19[top1]', 'White_Palace_19[left1]'], 'White_Palace_20': ['White_Palace_20[bot1]']} -item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Unbreakable_Heart': {'Fragile_Heart': 1, 'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Unbreakable_Greed': {'Fragile_Greed': 1, 'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, 'Unbreakable_Strength': {'Fragile_Strength': 1, 'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'WHITEFRAGMENT': 1}, 'King_Fragment': {'WHITEFRAGMENT': 1}, 'Void_Heart': {'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}} +item_effects = {'Lurien': {'DREAMER': 1}, 'Monomon': {'DREAMER': 1}, 'Herrah': {'DREAMER': 1}, 'Dreamer': {'DREAMER': 1}, 'Mothwing_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, 'Mantis_Claw': {'LEFTCLAW': 1, 'RIGHTCLAW': 1}, 'Crystal_Heart': {'LEFTSUPERDASH': 1, 'RIGHTSUPERDASH': 1}, 'Monarch_Wings': {'WINGS': 1}, 'Shade_Cloak': {'LEFTDASH': 1, 'RIGHTDASH': 1}, "Isma's_Tear": {'ACID': 1}, 'Dream_Nail': {'DREAMNAIL': 1}, 'Dream_Gate': {'DREAMNAIL': 1}, 'Awoken_Dream_Nail': {'DREAMNAIL': 1}, 'Vengeful_Spirit': {'FIREBALL': 1, 'SPELLS': 1}, 'Shade_Soul': {'FIREBALL': 1, 'SPELLS': 1}, 'Desolate_Dive': {'QUAKE': 1, 'SPELLS': 1}, 'Descending_Dark': {'QUAKE': 1, 'SPELLS': 1}, 'Howling_Wraiths': {'SCREAM': 1, 'SPELLS': 1}, 'Abyss_Shriek': {'SCREAM': 1, 'SPELLS': 1}, 'Cyclone_Slash': {'CYCLONE': 1}, 'Focus': {'FOCUS': 1}, 'Swim': {'SWIM': 1}, 'Gathering_Swarm': {'CHARMS': 1}, 'Wayward_Compass': {'CHARMS': 1}, 'Grubsong': {'CHARMS': 1}, 'Stalwart_Shell': {'CHARMS': 1}, 'Baldur_Shell': {'CHARMS': 1}, 'Fury_of_the_Fallen': {'CHARMS': 1}, 'Quick_Focus': {'CHARMS': 1}, 'Lifeblood_Heart': {'CHARMS': 1}, 'Lifeblood_Core': {'CHARMS': 1}, "Defender's_Crest": {'CHARMS': 1}, 'Flukenest': {'CHARMS': 1}, 'Thorns_of_Agony': {'CHARMS': 1}, 'Mark_of_Pride': {'CHARMS': 1}, 'Steady_Body': {'CHARMS': 1}, 'Heavy_Blow': {'CHARMS': 1}, 'Sharp_Shadow': {'CHARMS': 1}, 'Spore_Shroom': {'CHARMS': 1}, 'Longnail': {'CHARMS': 1}, 'Shaman_Stone': {'CHARMS': 1}, 'Soul_Catcher': {'CHARMS': 1}, 'Soul_Eater': {'CHARMS': 1}, 'Glowing_Womb': {'CHARMS': 1}, 'Fragile_Heart': {'CHARMS': 1}, 'Fragile_Greed': {'CHARMS': 1}, 'Fragile_Strength': {'CHARMS': 1}, "Nailmaster's_Glory": {'CHARMS': 1}, "Joni's_Blessing": {'CHARMS': 1}, 'Shape_of_Unn': {'CHARMS': 1}, 'Hiveblood': {'CHARMS': 1}, 'Dream_Wielder': {'CHARMS': 1}, 'Dashmaster': {'CHARMS': 1}, 'Quick_Slash': {'CHARMS': 1}, 'Spell_Twister': {'CHARMS': 1}, 'Deep_Focus': {'CHARMS': 1}, "Grubberfly's_Elegy": {'CHARMS': 1}, 'Queen_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'King_Fragment': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Void_Heart': {'CHARMS': 0.5, 'WHITEFRAGMENT': 1}, 'Sprintmaster': {'CHARMS': 1}, 'Dreamshield': {'CHARMS': 1}, 'Weaversong': {'CHARMS': 1}, 'Grimmchild1': {'GRIMMCHILD': 1, 'CHARMS': 1}, 'Grimmchild2': {'GRIMMCHILD': 1, 'CHARMS': 1, 'FLAMES': 6, 'First_Grimmchild_Upgrade': 1}, 'City_Crest': {'CREST': 1}, 'Lumafly_Lantern': {'LANTERN': 1}, 'Tram_Pass': {'TRAM': 1}, 'Simple_Key': {'SIMPLE': 1}, "Shopkeeper's_Key": {'SHOPKEY': 1}, 'Elegant_Key': {'ELEGANT': 1}, 'Love_Key': {'LOVE': 1}, "King's_Brand": {'BRAND': 1}, 'Mask_Shard': {'MASKSHARDS': 1}, 'Double_Mask_Shard': {'MASKSHARDS': 2}, 'Full_Mask': {'MASKSHARDS': 4}, 'Vessel_Fragment': {'VESSELFRAGMENTS': 1}, 'Double_Vessel_Fragment': {'VESSELFRAGMENTS': 2}, 'Full_Soul_Vessel': {'VESSELFRAGMENTS': 3}, 'Charm_Notch': {'NOTCHES': 1}, 'Pale_Ore': {'PALEORE': 1}, 'Rancid_Egg': {'RANCIDEGGS': 1}, 'Whispering_Root-Crossroads': {'ESSENCE': 29}, 'Whispering_Root-Greenpath': {'ESSENCE': 44}, 'Whispering_Root-Leg_Eater': {'ESSENCE': 20}, 'Whispering_Root-Mantis_Village': {'ESSENCE': 18}, 'Whispering_Root-Deepnest': {'ESSENCE': 45}, 'Whispering_Root-Queens_Gardens': {'ESSENCE': 29}, 'Whispering_Root-Kingdoms_Edge': {'ESSENCE': 51}, 'Whispering_Root-Waterways': {'ESSENCE': 35}, 'Whispering_Root-City': {'ESSENCE': 28}, 'Whispering_Root-Resting_Grounds': {'ESSENCE': 20}, 'Whispering_Root-Spirits_Glade': {'ESSENCE': 34}, 'Whispering_Root-Crystal_Peak': {'ESSENCE': 21}, 'Whispering_Root-Howling_Cliffs': {'ESSENCE': 46}, 'Whispering_Root-Ancestral_Mound': {'ESSENCE': 42}, 'Whispering_Root-Hive': {'ESSENCE': 20}, 'Boss_Essence-Elder_Hu': {'ESSENCE': 100}, 'Boss_Essence-Xero': {'ESSENCE': 100}, 'Boss_Essence-Gorb': {'ESSENCE': 100}, 'Boss_Essence-Marmu': {'ESSENCE': 150}, 'Boss_Essence-No_Eyes': {'ESSENCE': 200}, 'Boss_Essence-Galien': {'ESSENCE': 200}, 'Boss_Essence-Markoth': {'ESSENCE': 250}, 'Boss_Essence-Failed_Champion': {'ESSENCE': 300}, 'Boss_Essence-Soul_Tyrant': {'ESSENCE': 300}, 'Boss_Essence-Lost_Kin': {'ESSENCE': 400}, 'Boss_Essence-White_Defender': {'ESSENCE': 300}, 'Boss_Essence-Grey_Prince_Zote': {'ESSENCE': 300}, 'Grub': {'GRUBS': 1}, 'Quill': {'QUILL': 1}, 'Crossroads_Stag': {'STAGS': 1}, 'Greenpath_Stag': {'STAGS': 1}, "Queen's_Station_Stag": {'STAGS': 1}, "Queen's_Gardens_Stag": {'STAGS': 1}, 'City_Storerooms_Stag': {'STAGS': 1}, "King's_Station_Stag": {'STAGS': 1}, 'Resting_Grounds_Stag': {'STAGS': 1}, 'Distant_Village_Stag': {'STAGS': 1}, 'Hidden_Station_Stag': {'STAGS': 1}, 'Stag_Nest_Stag': {'STAGS': 1}, 'Grimmkin_Flame': {'FLAMES': 1}, "Hunter's_Journal": {'JOURNAL': 1}, 'Right_Mantis_Claw': {'RIGHTCLAW': 1}, 'Left_Mantis_Claw': {'LEFTCLAW': 1}, 'Leftslash': {'LEFTSLASH': 1}, 'Rightslash': {'RIGHTSLASH': 1}, 'Upslash': {'UPSLASH': 1}, 'Downslash': {'DOWNSLASH': 1}, 'Left_Crystal_Heart': {'LEFTSUPERDASH': 1}, 'Right_Crystal_Heart': {'RIGHTSUPERDASH': 1}, 'Left_Mothwing_Cloak': {'LEFTDASH': 1}, 'Right_Mothwing_Cloak': {'RIGHTDASH': 1}} items = {'Lurien': 'Dreamer', 'Monomon': 'Dreamer', 'Herrah': 'Dreamer', 'World_Sense': 'Dreamer', 'Dreamer': 'Fake', 'Mothwing_Cloak': 'Skill', 'Mantis_Claw': 'Skill', 'Crystal_Heart': 'Skill', 'Monarch_Wings': 'Skill', 'Shade_Cloak': 'Skill', "Isma's_Tear": 'Skill', 'Dream_Nail': 'Skill', 'Dream_Gate': 'Skill', 'Awoken_Dream_Nail': 'Skill', 'Vengeful_Spirit': 'Skill', 'Shade_Soul': 'Skill', 'Desolate_Dive': 'Skill', 'Descending_Dark': 'Skill', 'Howling_Wraiths': 'Skill', 'Abyss_Shriek': 'Skill', 'Cyclone_Slash': 'Skill', 'Dash_Slash': 'Skill', 'Great_Slash': 'Skill', 'Focus': 'Focus', 'Swim': 'Swim', 'Gathering_Swarm': 'Charm', 'Wayward_Compass': 'Charm', 'Grubsong': 'Charm', 'Stalwart_Shell': 'Charm', 'Baldur_Shell': 'Charm', 'Fury_of_the_Fallen': 'Charm', 'Quick_Focus': 'Charm', 'Lifeblood_Heart': 'Charm', 'Lifeblood_Core': 'Charm', "Defender's_Crest": 'Charm', 'Flukenest': 'Charm', 'Thorns_of_Agony': 'Charm', 'Mark_of_Pride': 'Charm', 'Steady_Body': 'Charm', 'Heavy_Blow': 'Charm', 'Sharp_Shadow': 'Charm', 'Spore_Shroom': 'Charm', 'Longnail': 'Charm', 'Shaman_Stone': 'Charm', 'Soul_Catcher': 'Charm', 'Soul_Eater': 'Charm', 'Glowing_Womb': 'Charm', 'Fragile_Heart': 'Charm', 'Unbreakable_Heart': 'Charm', 'Fragile_Greed': 'Charm', 'Unbreakable_Greed': 'Charm', 'Fragile_Strength': 'Charm', 'Unbreakable_Strength': 'Charm', "Nailmaster's_Glory": 'Charm', "Joni's_Blessing": 'Charm', 'Shape_of_Unn': 'Charm', 'Hiveblood': 'Charm', 'Dream_Wielder': 'Charm', 'Dashmaster': 'Charm', 'Quick_Slash': 'Charm', 'Spell_Twister': 'Charm', 'Deep_Focus': 'Charm', "Grubberfly's_Elegy": 'Charm', 'Queen_Fragment': 'Charm', 'King_Fragment': 'Charm', 'Void_Heart': 'Charm', 'Sprintmaster': 'Charm', 'Dreamshield': 'Charm', 'Weaversong': 'Charm', 'Grimmchild1': 'Charm', 'Grimmchild2': 'Charm', 'City_Crest': 'Key', 'Lumafly_Lantern': 'Key', 'Tram_Pass': 'Key', 'Simple_Key': 'Key', "Shopkeeper's_Key": 'Key', 'Elegant_Key': 'Key', 'Love_Key': 'Key', "King's_Brand": 'Key', 'Godtuner': 'Key', "Collector's_Map": 'Key', 'Mask_Shard': 'Mask', 'Double_Mask_Shard': 'Mask', 'Full_Mask': 'Mask', 'Vessel_Fragment': 'Vessel', 'Double_Vessel_Fragment': 'Vessel', 'Full_Soul_Vessel': 'Vessel', 'Charm_Notch': 'Notch', "Salubra's_Blessing": 'Notch', 'Pale_Ore': 'Ore', 'Geo_Chest-False_Knight': 'Geo', 'Geo_Chest-Soul_Master': 'Geo', 'Geo_Chest-Watcher_Knights': 'Geo', 'Geo_Chest-Greenpath': 'Geo', 'Geo_Chest-Mantis_Lords': 'Geo', 'Geo_Chest-Resting_Grounds': 'Geo', 'Geo_Chest-Crystal_Peak': 'Geo', 'Geo_Chest-Weavers_Den': 'Geo', 'Geo_Chest-Junk_Pit_1': 'JunkPitChest', 'Geo_Chest-Junk_Pit_2': 'JunkPitChest', 'Geo_Chest-Junk_Pit_3': 'JunkPitChest', 'Geo_Chest-Junk_Pit_5': 'JunkPitChest', 'Lumafly_Escape': 'JunkPitChest', 'One_Geo': 'Fake', 'Rancid_Egg': 'Egg', "Wanderer's_Journal": 'Relic', 'Hallownest_Seal': 'Relic', "King's_Idol": 'Relic', 'Arcane_Egg': 'Relic', 'Whispering_Root-Crossroads': 'Root', 'Whispering_Root-Greenpath': 'Root', 'Whispering_Root-Leg_Eater': 'Root', 'Whispering_Root-Mantis_Village': 'Root', 'Whispering_Root-Deepnest': 'Root', 'Whispering_Root-Queens_Gardens': 'Root', 'Whispering_Root-Kingdoms_Edge': 'Root', 'Whispering_Root-Waterways': 'Root', 'Whispering_Root-City': 'Root', 'Whispering_Root-Resting_Grounds': 'Root', 'Whispering_Root-Spirits_Glade': 'Root', 'Whispering_Root-Crystal_Peak': 'Root', 'Whispering_Root-Howling_Cliffs': 'Root', 'Whispering_Root-Ancestral_Mound': 'Root', 'Whispering_Root-Hive': 'Root', 'Boss_Essence-Elder_Hu': 'DreamWarrior', 'Boss_Essence-Xero': 'DreamWarrior', 'Boss_Essence-Gorb': 'DreamWarrior', 'Boss_Essence-Marmu': 'DreamWarrior', 'Boss_Essence-No_Eyes': 'DreamWarrior', 'Boss_Essence-Galien': 'DreamWarrior', 'Boss_Essence-Markoth': 'DreamWarrior', 'Boss_Essence-Failed_Champion': 'DreamBoss', 'Boss_Essence-Soul_Tyrant': 'DreamBoss', 'Boss_Essence-Lost_Kin': 'DreamBoss', 'Boss_Essence-White_Defender': 'DreamBoss', 'Boss_Essence-Grey_Prince_Zote': 'DreamBoss', 'Grub': 'Grub', 'Mimic_Grub': 'Mimic', 'Quill': 'Map', 'Crossroads_Map': 'Map', 'Greenpath_Map': 'Map', 'Fog_Canyon_Map': 'Map', 'Fungal_Wastes_Map': 'Map', 'Deepnest_Map': 'Map', 'Ancient_Basin_Map': 'Map', "Kingdom's_Edge_Map": 'Map', 'City_of_Tears_Map': 'Map', 'Royal_Waterways_Map': 'Map', 'Howling_Cliffs_Map': 'Map', 'Crystal_Peak_Map': 'Map', "Queen's_Gardens_Map": 'Map', 'Resting_Grounds_Map': 'Map', 'Dirtmouth_Stag': 'Stag', 'Crossroads_Stag': 'Stag', 'Greenpath_Stag': 'Stag', "Queen's_Station_Stag": 'Stag', "Queen's_Gardens_Stag": 'Stag', 'City_Storerooms_Stag': 'Stag', "King's_Station_Stag": 'Stag', 'Resting_Grounds_Stag': 'Stag', 'Distant_Village_Stag': 'Stag', 'Hidden_Station_Stag': 'Stag', 'Stag_Nest_Stag': 'Stag', 'Lifeblood_Cocoon_Small': 'Cocoon', 'Lifeblood_Cocoon_Large': 'Cocoon', 'Grimmkin_Flame': 'Flame', "Hunter's_Journal": 'Journal', 'Journal_Entry-Void_Tendrils': 'Journal', 'Journal_Entry-Charged_Lumafly': 'Journal', 'Journal_Entry-Goam': 'Journal', 'Journal_Entry-Garpede': 'Journal', 'Journal_Entry-Seal_of_Binding': 'Journal', 'Elevator_Pass': 'ElevatorPass', 'Left_Mothwing_Cloak': 'SplitCloak', 'Right_Mothwing_Cloak': 'SplitCloak', 'Split_Shade_Cloak': 'SplitCloak', 'Left_Mantis_Claw': 'SplitClaw', 'Right_Mantis_Claw': 'SplitClaw', 'Leftslash': 'CursedNail', 'Rightslash': 'CursedNail', 'Upslash': 'CursedNail', 'Downslash': 'CursedNail', 'Left_Crystal_Heart': 'SplitSuperdash', 'Right_Crystal_Heart': 'SplitSuperdash', 'Geo_Rock-Default': 'Rock', 'Geo_Rock-Deepnest': 'Rock', 'Geo_Rock-Abyss': 'Rock', 'Geo_Rock-GreenPath01': 'Rock', 'Geo_Rock-Outskirts': 'Rock', 'Geo_Rock-Outskirts420': 'Rock', 'Geo_Rock-GreenPath02': 'Rock', 'Geo_Rock-Fung01': 'Rock', 'Geo_Rock-Fung02': 'Rock', 'Geo_Rock-City': 'Rock', 'Geo_Rock-Hive': 'Rock', 'Geo_Rock-Mine': 'Rock', 'Geo_Rock-Grave02': 'Rock', 'Geo_Rock-Grave01': 'Rock', 'Boss_Geo-Massive_Moss_Charger': 'Boss_Geo', 'Boss_Geo-Gorgeous_Husk': 'Boss_Geo', 'Boss_Geo-Sanctum_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Elegant_Soul_Warrior': 'Boss_Geo', 'Boss_Geo-Crystal_Guardian': 'Boss_Geo', 'Boss_Geo-Enraged_Guardian': 'Boss_Geo', 'Boss_Geo-Gruz_Mother': 'Boss_Geo', 'Boss_Geo-Vengefly_King': 'Boss_Geo', 'Soul_Refill': 'Soul', 'Soul_Totem-A': 'Soul', 'Soul_Totem-B': 'Soul', 'Soul_Totem-C': 'Soul', 'Soul_Totem-D': 'Soul', 'Soul_Totem-E': 'Soul', 'Soul_Totem-F': 'Soul', 'Soul_Totem-G': 'Soul', 'Soul_Totem-Palace': 'Soul', 'Soul_Totem-Path_of_Pain': 'Soul', 'Lore_Tablet-City_Entrance': 'Lore', 'Lore_Tablet-Pleasure_House': 'Lore', 'Lore_Tablet-Sanctum_Entrance': 'Lore', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Lore', "Lore_Tablet-Watcher's_Spire": 'Lore', 'Lore_Tablet-Archives_Upper': 'Lore', 'Lore_Tablet-Archives_Left': 'Lore', 'Lore_Tablet-Archives_Right': 'Lore', "Lore_Tablet-Pilgrim's_Way_1": 'Lore', "Lore_Tablet-Pilgrim's_Way_2": 'Lore', 'Lore_Tablet-Mantis_Outskirts': 'Lore', 'Lore_Tablet-Mantis_Village': 'Lore', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Lore', 'Lore_Tablet-Greenpath_Below_Toll': 'Lore', 'Lore_Tablet-Greenpath_Lifeblood': 'Lore', 'Lore_Tablet-Greenpath_Stag': 'Lore', 'Lore_Tablet-Greenpath_QG': 'Lore', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Lore', 'Lore_Tablet-Dung_Defender': 'Lore', 'Lore_Tablet-Spore_Shroom': 'Lore', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Lore', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Lore', 'Lore_Tablet-Fungal_Core': 'Lore', 'Lore_Tablet-Ancient_Basin': 'Lore', "Lore_Tablet-King's_Pass_Focus": 'Lore', "Lore_Tablet-King's_Pass_Fury": 'Lore', "Lore_Tablet-King's_Pass_Exit": 'Lore', 'Lore_Tablet-World_Sense': 'Lore', 'Lore_Tablet-Howling_Cliffs': 'Lore', "Lore_Tablet-Kingdom's_Edge": 'Lore', 'Lore_Tablet-Palace_Workshop': 'PalaceLore', 'Lore_Tablet-Palace_Throne': 'PalaceLore', 'Lore_Tablet-Path_of_Pain_Entrance': 'PalaceLore'} location_to_region_lookup = {'Sly_1': 'Room_shop', 'Sly_2': 'Room_shop', 'Sly_3': 'Room_shop', 'Sly_4': 'Room_shop', 'Sly_5': 'Room_shop', 'Sly_6': 'Room_shop', 'Sly_7': 'Room_shop', 'Sly_8': 'Room_shop', 'Sly_9': 'Room_shop', 'Sly_10': 'Room_shop', 'Sly_11': 'Room_shop', 'Sly_12': 'Room_shop', 'Sly_13': 'Room_shop', 'Sly_14': 'Room_shop', 'Sly_15': 'Room_shop', 'Sly_16': 'Room_shop', 'Sly_(Key)_1': 'Room_shop', 'Sly_(Key)_2': 'Room_shop', 'Sly_(Key)_3': 'Room_shop', 'Sly_(Key)_4': 'Room_shop', 'Sly_(Key)_5': 'Room_shop', 'Sly_(Key)_6': 'Room_shop', 'Sly_(Key)_7': 'Room_shop', 'Sly_(Key)_8': 'Room_shop', 'Sly_(Key)_9': 'Room_shop', 'Sly_(Key)_10': 'Room_shop', 'Sly_(Key)_11': 'Room_shop', 'Sly_(Key)_12': 'Room_shop', 'Sly_(Key)_13': 'Room_shop', 'Sly_(Key)_14': 'Room_shop', 'Sly_(Key)_15': 'Room_shop', 'Sly_(Key)_16': 'Room_shop', 'Iselda_1': 'Room_mapper', 'Iselda_2': 'Room_mapper', 'Iselda_3': 'Room_mapper', 'Iselda_4': 'Room_mapper', 'Iselda_5': 'Room_mapper', 'Iselda_6': 'Room_mapper', 'Iselda_7': 'Room_mapper', 'Iselda_8': 'Room_mapper', 'Iselda_9': 'Room_mapper', 'Iselda_10': 'Room_mapper', 'Iselda_11': 'Room_mapper', 'Iselda_12': 'Room_mapper', 'Iselda_13': 'Room_mapper', 'Iselda_14': 'Room_mapper', 'Iselda_15': 'Room_mapper', 'Iselda_16': 'Room_mapper', 'Salubra_1': 'Room_Charm_Shop', 'Salubra_2': 'Room_Charm_Shop', 'Salubra_3': 'Room_Charm_Shop', 'Salubra_4': 'Room_Charm_Shop', 'Salubra_5': 'Room_Charm_Shop', 'Salubra_6': 'Room_Charm_Shop', 'Salubra_7': 'Room_Charm_Shop', 'Salubra_8': 'Room_Charm_Shop', 'Salubra_9': 'Room_Charm_Shop', 'Salubra_10': 'Room_Charm_Shop', 'Salubra_11': 'Room_Charm_Shop', 'Salubra_12': 'Room_Charm_Shop', 'Salubra_13': 'Room_Charm_Shop', 'Salubra_14': 'Room_Charm_Shop', 'Salubra_15': 'Room_Charm_Shop', 'Salubra_16': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_1': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_2': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_3': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_4': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_5': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_6': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_7': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_8': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_9': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_10': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_11': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_12': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_13': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_14': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_15': 'Room_Charm_Shop', 'Salubra_(Requires_Charms)_16': 'Room_Charm_Shop', 'Leg_Eater_1': 'Fungus2_26', 'Leg_Eater_2': 'Fungus2_26', 'Leg_Eater_3': 'Fungus2_26', 'Leg_Eater_4': 'Fungus2_26', 'Leg_Eater_5': 'Fungus2_26', 'Leg_Eater_6': 'Fungus2_26', 'Leg_Eater_7': 'Fungus2_26', 'Leg_Eater_8': 'Fungus2_26', 'Leg_Eater_9': 'Fungus2_26', 'Leg_Eater_10': 'Fungus2_26', 'Leg_Eater_11': 'Fungus2_26', 'Leg_Eater_12': 'Fungus2_26', 'Leg_Eater_13': 'Fungus2_26', 'Leg_Eater_14': 'Fungus2_26', 'Leg_Eater_15': 'Fungus2_26', 'Leg_Eater_16': 'Fungus2_26', 'Grubfather_1': 'Crossroads_38', 'Grubfather_2': 'Crossroads_38', 'Grubfather_3': 'Crossroads_38', 'Grubfather_4': 'Crossroads_38', 'Grubfather_5': 'Crossroads_38', 'Grubfather_6': 'Crossroads_38', 'Grubfather_7': 'Crossroads_38', 'Grubfather_8': 'Crossroads_38', 'Grubfather_9': 'Crossroads_38', 'Grubfather_10': 'Crossroads_38', 'Grubfather_11': 'Crossroads_38', 'Grubfather_12': 'Crossroads_38', 'Grubfather_13': 'Crossroads_38', 'Grubfather_14': 'Crossroads_38', 'Grubfather_15': 'Crossroads_38', 'Grubfather_16': 'Crossroads_38', 'Seer_1': 'RestingGrounds_07', 'Seer_2': 'RestingGrounds_07', 'Seer_3': 'RestingGrounds_07', 'Seer_4': 'RestingGrounds_07', 'Seer_5': 'RestingGrounds_07', 'Seer_6': 'RestingGrounds_07', 'Seer_7': 'RestingGrounds_07', 'Seer_8': 'RestingGrounds_07', 'Seer_9': 'RestingGrounds_07', 'Seer_10': 'RestingGrounds_07', 'Seer_11': 'RestingGrounds_07', 'Seer_12': 'RestingGrounds_07', 'Seer_13': 'RestingGrounds_07', 'Seer_14': 'RestingGrounds_07', 'Seer_15': 'RestingGrounds_07', 'Seer_16': 'RestingGrounds_07', 'Egg_Shop_1': 'Room_Ouiji', 'Egg_Shop_2': 'Room_Ouiji', 'Egg_Shop_3': 'Room_Ouiji', 'Egg_Shop_4': 'Room_Ouiji', 'Egg_Shop_5': 'Room_Ouiji', 'Egg_Shop_6': 'Room_Ouiji', 'Egg_Shop_7': 'Room_Ouiji', 'Egg_Shop_8': 'Room_Ouiji', 'Egg_Shop_9': 'Room_Ouiji', 'Egg_Shop_10': 'Room_Ouiji', 'Egg_Shop_11': 'Room_Ouiji', 'Egg_Shop_12': 'Room_Ouiji', 'Egg_Shop_13': 'Room_Ouiji', 'Egg_Shop_14': 'Room_Ouiji', 'Egg_Shop_15': 'Room_Ouiji', 'Egg_Shop_16': 'Room_Ouiji', 'Lurien': 'Ruins2_Watcher_Room', 'Monomon': 'Fungus3_archive_02', 'Herrah': 'Deepnest_Spider_Town', 'World_Sense': 'Room_temple', 'Mothwing_Cloak': 'Fungus1_04', 'Mantis_Claw': 'Fungus2_14', 'Crystal_Heart': 'Mines_31', 'Monarch_Wings': 'Abyss_21', 'Shade_Cloak': 'Abyss_10', "Isma's_Tear": 'Waterways_13', 'Dream_Nail': 'RestingGrounds_04', 'Vengeful_Spirit': 'Crossroads_ShamanTemple', 'Shade_Soul': 'Ruins1_31b', 'Desolate_Dive': 'Ruins1_24', 'Descending_Dark': 'Mines_35', 'Howling_Wraiths': 'Room_Fungus_Shaman', 'Abyss_Shriek': 'Abyss_12', 'Cyclone_Slash': 'Room_nailmaster', 'Dash_Slash': 'Room_nailmaster_03', 'Great_Slash': 'Room_nailmaster_02', 'Focus': 'Tutorial_01', 'Baldur_Shell': 'Fungus1_28', 'Fury_of_the_Fallen': 'Tutorial_01', 'Lifeblood_Core': 'Abyss_08', "Defender's_Crest": 'Waterways_05', 'Flukenest': 'Waterways_12', 'Thorns_of_Agony': 'Fungus1_14', 'Mark_of_Pride': 'Fungus2_31', 'Sharp_Shadow': 'Deepnest_44', 'Spore_Shroom': 'Fungus2_20', 'Soul_Catcher': 'Crossroads_ShamanTemple', 'Soul_Eater': 'RestingGrounds_10', 'Glowing_Womb': 'Crossroads_22', "Nailmaster's_Glory": 'Room_shop', "Joni's_Blessing": 'Cliffs_05', 'Shape_of_Unn': 'Fungus1_Slug', 'Hiveblood': 'Hive_05', 'Dashmaster': 'Fungus2_23', 'Quick_Slash': 'Deepnest_East_14b', 'Spell_Twister': 'Ruins1_30', 'Deep_Focus': 'Mines_36', 'Queen_Fragment': 'Room_Queen', 'King_Fragment': 'White_Palace_09', 'Void_Heart': 'Abyss_15', 'Dreamshield': 'RestingGrounds_17', 'Weaversong': 'Deepnest_45_v02', 'Grimmchild': 'Grimm_Main_Tent', 'Unbreakable_Heart': 'Grimm_Divine', 'Unbreakable_Greed': 'Grimm_Divine', 'Unbreakable_Strength': 'Grimm_Divine', 'City_Crest': 'Crossroads_10', 'Tram_Pass': 'Deepnest_26b', 'Simple_Key-Basin': 'Abyss_20', 'Simple_Key-City': 'Ruins1_17', 'Simple_Key-Lurker': 'GG_Lurker', "Shopkeeper's_Key": 'Mines_11', 'Love_Key': 'Fungus3_39', "King's_Brand": 'Room_Wyrm', 'Godtuner': 'GG_Waterways', "Collector's_Map": 'Ruins2_11', 'Mask_Shard-Brooding_Mawlek': 'Crossroads_09', 'Mask_Shard-Crossroads_Goam': 'Crossroads_13', 'Mask_Shard-Stone_Sanctuary': 'Fungus1_36', "Mask_Shard-Queen's_Station": 'Fungus2_01', 'Mask_Shard-Deepnest': 'Fungus2_25', 'Mask_Shard-Waterways': 'Waterways_04b', 'Mask_Shard-Enraged_Guardian': 'Mines_32', 'Mask_Shard-Hive': 'Hive_04', 'Mask_Shard-Grey_Mourner': 'Room_Mansion', 'Mask_Shard-Bretta': 'Room_Bretta', 'Vessel_Fragment-Greenpath': 'Fungus1_13', 'Vessel_Fragment-City': 'Ruins2_09', 'Vessel_Fragment-Crossroads': 'Crossroads_37', 'Vessel_Fragment-Basin': 'Abyss_04', 'Vessel_Fragment-Deepnest': 'Deepnest_38', 'Vessel_Fragment-Stag_Nest': 'Cliffs_03', 'Charm_Notch-Shrumal_Ogres': 'Fungus2_05', 'Charm_Notch-Fog_Canyon': 'Fungus3_28', 'Charm_Notch-Colosseum': 'Room_Colosseum_01', 'Charm_Notch-Grimm': 'Grimm_Main_Tent', 'Pale_Ore-Basin': 'Abyss_17', 'Pale_Ore-Crystal_Peak': 'Mines_34', 'Pale_Ore-Nosk': 'Deepnest_32', 'Pale_Ore-Colosseum': 'Room_Colosseum_01', 'Geo_Chest-False_Knight': 'Crossroads_10', 'Geo_Chest-Soul_Master': 'Ruins1_32', 'Geo_Chest-Watcher_Knights': 'Ruins2_03', 'Geo_Chest-Greenpath': 'Fungus1_13', 'Geo_Chest-Mantis_Lords': 'Fungus2_31', 'Geo_Chest-Resting_Grounds': 'RestingGrounds_10', 'Geo_Chest-Crystal_Peak': 'Mines_37', 'Geo_Chest-Weavers_Den': 'Deepnest_45_v02', 'Geo_Chest-Junk_Pit_1': 'GG_Waterways', 'Geo_Chest-Junk_Pit_2': 'GG_Waterways', 'Geo_Chest-Junk_Pit_3': 'GG_Waterways', 'Geo_Chest-Junk_Pit_5': 'GG_Waterways', 'Lumafly_Escape-Junk_Pit_Chest_4': 'GG_Waterways', 'Rancid_Egg-Sheo': 'Fungus1_15', 'Rancid_Egg-Fungal_Core': 'Fungus2_29', "Rancid_Egg-Queen's_Gardens": 'Fungus3_34', 'Rancid_Egg-Blue_Lake': 'Crossroads_50', 'Rancid_Egg-Crystal_Peak_Dive_Entrance': 'Mines_01', 'Rancid_Egg-Crystal_Peak_Dark_Room': 'Mines_29', 'Rancid_Egg-Crystal_Peak_Tall_Room': 'Mines_20', 'Rancid_Egg-City_of_Tears_Left': 'Ruins1_05c', 'Rancid_Egg-City_of_Tears_Pleasure_House': 'Ruins_Elevator', "Rancid_Egg-Beast's_Den": 'Deepnest_Spider_Town', 'Rancid_Egg-Dark_Deepnest': 'Deepnest_39', "Rancid_Egg-Weaver's_Den": 'Deepnest_45_v02', 'Rancid_Egg-Near_Quick_Slash': 'Deepnest_East_14', "Rancid_Egg-Upper_Kingdom's_Edge": 'Deepnest_East_07', 'Rancid_Egg-Waterways_East': 'Waterways_07', 'Rancid_Egg-Waterways_Main': 'Waterways_02', 'Rancid_Egg-Waterways_West_Bluggsac': 'Waterways_04', 'Rancid_Egg-Waterways_West_Pickup': 'Waterways_04b', "Rancid_Egg-Tuk_Defender's_Crest": 'Waterways_03', "Wanderer's_Journal-Cliffs": 'Cliffs_01', "Wanderer's_Journal-Greenpath_Stag": 'Fungus1_22', "Wanderer's_Journal-Greenpath_Lower": 'Fungus1_11', "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet": 'Fungus2_04', "Wanderer's_Journal-Above_Mantis_Village": 'Fungus2_17', "Wanderer's_Journal-Crystal_Peak_Crawlers": 'Mines_20', "Wanderer's_Journal-Resting_Grounds_Catacombs": 'RestingGrounds_10', "Wanderer's_Journal-King's_Station": 'Ruins2_05', "Wanderer's_Journal-Pleasure_House": 'Ruins_Elevator', "Wanderer's_Journal-City_Storerooms": 'Ruins1_28', "Wanderer's_Journal-Ancient_Basin": 'Abyss_02', "Wanderer's_Journal-Kingdom's_Edge_Entrance": 'Deepnest_East_07', "Wanderer's_Journal-Kingdom's_Edge_Camp": 'Deepnest_East_13', "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive": 'Deepnest_East_18', 'Hallownest_Seal-Crossroads_Well': 'Crossroads_01', 'Hallownest_Seal-Greenpath': 'Fungus1_10', 'Hallownest_Seal-Fog_Canyon_West': 'Fungus3_30', 'Hallownest_Seal-Fog_Canyon_East': 'Fungus3_26', "Hallownest_Seal-Queen's_Station": 'Fungus2_34', 'Hallownest_Seal-Fungal_Wastes_Sporgs': 'Fungus2_03', 'Hallownest_Seal-Mantis_Lords': 'Fungus2_31', 'Hallownest_Seal-Resting_Grounds_Catacombs': 'RestingGrounds_10', "Hallownest_Seal-King's_Station": 'Ruins2_08', 'Hallownest_Seal-City_Rafters': 'Ruins1_03', 'Hallownest_Seal-Soul_Sanctum': 'Ruins1_32', 'Hallownest_Seal-Watcher_Knight': 'Ruins2_03', 'Hallownest_Seal-Deepnest_By_Mantis_Lords': 'Deepnest_16', "Hallownest_Seal-Beast's_Den": 'Deepnest_Spider_Town', "Hallownest_Seal-Queen's_Gardens": 'Fungus3_48', "King's_Idol-Cliffs": 'Cliffs_01', "King's_Idol-Crystal_Peak": 'Mines_30', "King's_Idol-Glade_of_Hope": 'RestingGrounds_08', "King's_Idol-Dung_Defender": 'Waterways_15', "King's_Idol-Great_Hopper": 'Deepnest_East_08', "King's_Idol-Pale_Lurker": 'GG_Lurker', "King's_Idol-Deepnest": 'Deepnest_33', 'Arcane_Egg-Lifeblood_Core': 'Abyss_08', 'Arcane_Egg-Shade_Cloak': 'Abyss_10', 'Arcane_Egg-Birthplace': 'Abyss_15', 'Whispering_Root-Crossroads': 'Crossroads_07', 'Whispering_Root-Greenpath': 'Fungus1_13', 'Whispering_Root-Leg_Eater': 'Fungus2_33', 'Whispering_Root-Mantis_Village': 'Fungus2_17', 'Whispering_Root-Deepnest': 'Deepnest_39', 'Whispering_Root-Queens_Gardens': 'Fungus3_11', 'Whispering_Root-Kingdoms_Edge': 'Deepnest_East_07', 'Whispering_Root-Waterways': 'Abyss_01', 'Whispering_Root-City': 'Ruins1_17', 'Whispering_Root-Resting_Grounds': 'RestingGrounds_05', 'Whispering_Root-Spirits_Glade': 'RestingGrounds_08', 'Whispering_Root-Crystal_Peak': 'Mines_23', 'Whispering_Root-Howling_Cliffs': 'Cliffs_01', 'Whispering_Root-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Whispering_Root-Hive': 'Hive_02', 'Boss_Essence-Elder_Hu': 'Fungus2_32', 'Boss_Essence-Xero': 'RestingGrounds_02', 'Boss_Essence-Gorb': 'Cliffs_02', 'Boss_Essence-Marmu': 'Fungus3_40', 'Boss_Essence-No_Eyes': 'Fungus1_35', 'Boss_Essence-Galien': 'Deepnest_40', 'Boss_Essence-Markoth': 'Deepnest_East_10', 'Boss_Essence-Failed_Champion': 'Crossroads_10', 'Boss_Essence-Soul_Tyrant': 'Ruins1_24', 'Boss_Essence-Lost_Kin': 'Abyss_19', 'Boss_Essence-White_Defender': 'Waterways_15', 'Boss_Essence-Grey_Prince_Zote': 'Room_Bretta', 'Grub-Crossroads_Acid': 'Crossroads_35', 'Grub-Crossroads_Center': 'Crossroads_05', 'Grub-Crossroads_Stag': 'Crossroads_03', 'Grub-Crossroads_Spike': 'Crossroads_31', 'Grub-Crossroads_Guarded': 'Crossroads_48', 'Grub-Greenpath_Cornifer': 'Fungus1_06', 'Grub-Greenpath_Journal': 'Fungus1_07', 'Grub-Greenpath_MMC': 'Fungus1_13', 'Grub-Greenpath_Stag': 'Fungus1_21', 'Grub-Fog_Canyon': 'Fungus3_47', 'Grub-Fungal_Bouncy': 'Fungus2_18', 'Grub-Fungal_Spore_Shroom': 'Fungus2_20', 'Grub-Deepnest_Mimic': 'Deepnest_36', 'Grub-Deepnest_Nosk': 'Deepnest_31', 'Grub-Deepnest_Spike': 'Deepnest_03', 'Grub-Dark_Deepnest': 'Deepnest_39', "Grub-Beast's_Den": 'Deepnest_Spider_Town', "Grub-Kingdom's_Edge_Oro": 'Deepnest_East_14', "Grub-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Grub-Hive_External': 'Hive_03', 'Grub-Hive_Internal': 'Hive_04', 'Grub-Basin_Requires_Wings': 'Abyss_19', 'Grub-Basin_Requires_Dive': 'Abyss_17', 'Grub-Waterways_Main': 'Waterways_04', "Grub-Isma's_Grove": 'Waterways_13', 'Grub-Waterways_Requires_Tram': 'Waterways_14', 'Grub-City_of_Tears_Left': 'Ruins1_05', 'Grub-Soul_Sanctum': 'Ruins1_32', "Grub-Watcher's_Spire": 'Ruins2_03', 'Grub-City_of_Tears_Guarded': 'Ruins_House_01', "Grub-King's_Station": 'Ruins2_07', 'Grub-Resting_Grounds': 'RestingGrounds_10', 'Grub-Crystal_Peak_Below_Chest': 'Mines_04', 'Grub-Crystallized_Mound': 'Mines_35', 'Grub-Crystal_Peak_Spike': 'Mines_03', 'Grub-Crystal_Peak_Mimic': 'Mines_16', 'Grub-Crystal_Peak_Crushers': 'Mines_19', 'Grub-Crystal_Heart': 'Mines_31', 'Grub-Hallownest_Crown': 'Mines_24', 'Grub-Howling_Cliffs': 'Fungus1_28', "Grub-Queen's_Gardens_Stag": 'Fungus3_10', "Grub-Queen's_Gardens_Marmu": 'Fungus3_48', "Grub-Queen's_Gardens_Top": 'Fungus3_22', 'Grub-Collector_1': 'Ruins2_11', 'Grub-Collector_2': 'Ruins2_11', 'Grub-Collector_3': 'Ruins2_11', 'Mimic_Grub-Deepnest_1': 'Deepnest_36', 'Mimic_Grub-Deepnest_2': 'Deepnest_36', 'Mimic_Grub-Deepnest_3': 'Deepnest_36', 'Mimic_Grub-Crystal_Peak': 'Mines_16', 'Crossroads_Map': 'Crossroads_33', 'Greenpath_Map': 'Fungus1_06', 'Fog_Canyon_Map': 'Fungus3_25', 'Fungal_Wastes_Map': 'Fungus2_18', 'Deepnest_Map-Upper': 'Deepnest_01b', 'Deepnest_Map-Right': 'Fungus2_25', 'Ancient_Basin_Map': 'Abyss_04', "Kingdom's_Edge_Map": 'Deepnest_East_03', 'City_of_Tears_Map': 'Ruins1_31', 'Royal_Waterways_Map': 'Waterways_09', 'Howling_Cliffs_Map': 'Cliffs_01', 'Crystal_Peak_Map': 'Mines_30', "Queen's_Gardens_Map": 'Fungus1_24', 'Resting_Grounds_Map': 'RestingGrounds_09', 'Dirtmouth_Stag': 'Room_Town_Stag_Station', 'Crossroads_Stag': 'Crossroads_47', 'Greenpath_Stag': 'Fungus1_16_alt', "Queen's_Station_Stag": 'Fungus2_02', "Queen's_Gardens_Stag": 'Fungus3_40', 'City_Storerooms_Stag': 'Ruins1_29', "King's_Station_Stag": 'Ruins2_08', 'Resting_Grounds_Stag': 'RestingGrounds_09', 'Distant_Village_Stag': 'Deepnest_09', 'Hidden_Station_Stag': 'Abyss_22', 'Stag_Nest_Stag': 'Cliffs_03', "Lifeblood_Cocoon-King's_Pass": 'Tutorial_01', 'Lifeblood_Cocoon-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Lifeblood_Cocoon-Greenpath': 'Fungus1_32', 'Lifeblood_Cocoon-Fog_Canyon_West': 'Fungus3_30', 'Lifeblood_Cocoon-Mantis_Village': 'Fungus2_15', 'Lifeblood_Cocoon-Failed_Tramway': 'Deepnest_26', 'Lifeblood_Cocoon-Galien': 'Deepnest_40', "Lifeblood_Cocoon-Kingdom's_Edge": 'Deepnest_East_15', 'Grimmkin_Flame-City_Storerooms': 'Ruins1_28', 'Grimmkin_Flame-Greenpath': 'Fungus1_10', 'Grimmkin_Flame-Crystal_Peak': 'Mines_10', "Grimmkin_Flame-King's_Pass": 'Tutorial_01', 'Grimmkin_Flame-Resting_Grounds': 'RestingGrounds_06', "Grimmkin_Flame-Kingdom's_Edge": 'Deepnest_East_03', 'Grimmkin_Flame-Fungal_Core': 'Fungus2_30', 'Grimmkin_Flame-Ancient_Basin': 'Abyss_02', 'Grimmkin_Flame-Hive': 'Hive_03', 'Grimmkin_Flame-Brumm': 'Room_spider_small', "Hunter's_Journal": 'Fungus1_08', 'Journal_Entry-Void_Tendrils': 'Abyss_09', 'Journal_Entry-Charged_Lumafly': 'Fungus3_archive_02', 'Journal_Entry-Goam': 'Crossroads_52', 'Journal_Entry-Garpede': 'Deepnest_44', 'Journal_Entry-Seal_of_Binding': 'White_Palace_20', 'Elevator_Pass': 'Crossroads_49b', 'Split_Mothwing_Cloak': 'Fungus1_04', 'Left_Mantis_Claw': 'Fungus2_14', 'Right_Mantis_Claw': 'Fungus2_14', 'Leftslash': 'Tutorial_01', 'Rightslash': 'Tutorial_01', 'Upslash': 'Tutorial_01', 'Split_Crystal_Heart': 'Mines_31', 'Geo_Rock-Broken_Elevator_1': 'Abyss_01', 'Geo_Rock-Broken_Elevator_2': 'Abyss_01', 'Geo_Rock-Broken_Elevator_3': 'Abyss_01', 'Geo_Rock-Broken_Bridge_Upper': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower': 'Abyss_02', 'Geo_Rock-Broken_Bridge_Lower_Dupe': 'Abyss_02', 'Geo_Rock-Abyss_1': 'Abyss_06_Core', 'Geo_Rock-Abyss_2': 'Abyss_06_Core', 'Geo_Rock-Abyss_3': 'Abyss_06_Core', 'Geo_Rock-Basin_Tunnel': 'Abyss_18', 'Geo_Rock-Basin_Grub': 'Abyss_19', 'Geo_Rock-Basin_Before_Broken_Vessel': 'Abyss_19', 'Geo_Rock-Cliffs_Main_1': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_2': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_3': 'Cliffs_01', 'Geo_Rock-Cliffs_Main_4': 'Cliffs_01', 'Geo_Rock-Below_Gorb_Dupe': 'Cliffs_02', 'Geo_Rock-Below_Gorb': 'Cliffs_02', 'Geo_Rock-Crossroads_Well': 'Crossroads_01', 'Geo_Rock-Crossroads_Center_Grub': 'Crossroads_05', 'Geo_Rock-Crossroads_Root': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_1': 'Crossroads_07', 'Geo_Rock-Crossroads_Root_Dupe_2': 'Crossroads_07', 'Geo_Rock-Crossroads_Aspid_Arena': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2': 'Crossroads_08', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden': 'Crossroads_08', 'Geo_Rock-Crossroads_Above_False_Knight': 'Crossroads_10', 'Geo_Rock-Crossroads_Before_Acid_Grub': 'Crossroads_12', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard': 'Crossroads_13', 'Geo_Rock-Crossroads_Above_Lever': 'Crossroads_16', 'Geo_Rock-Crossroads_Before_Fungal': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2': 'Crossroads_18', 'Geo_Rock-Crossroads_Before_Shops': 'Crossroads_19', 'Geo_Rock-Crossroads_Before_Glowing_Womb': 'Crossroads_21', 'Geo_Rock-Crossroads_Above_Tram': 'Crossroads_27', 'Geo_Rock-Crossroads_Above_Mawlek': 'Crossroads_36', 'Geo_Rock-Crossroads_Vessel_Fragment': 'Crossroads_37', 'Geo_Rock-Crossroads_Goam_Alcove': 'Crossroads_42', 'Geo_Rock-Crossroads_Goam_Damage_Boost': 'Crossroads_42', 'Geo_Rock-Crossroads_Tram': 'Crossroads_46', 'Geo_Rock-Crossroads_Goam_Journal': 'Crossroads_52', 'Geo_Rock-Crossroads_Goam_Journal_Dupe': 'Crossroads_52', 'Geo_Rock-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree': 'Crossroads_ShamanTemple', 'Geo_Rock-Ancestral_Mound_Tree_Dupe': 'Crossroads_ShamanTemple', 'Geo_Rock-Moss_Prophet': 'Deepnest_01', 'Geo_Rock-Moss_Prophet_Dupe': 'Deepnest_01', 'Geo_Rock-Deepnest_Below_Mimics': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Mimics_Dupe': 'Deepnest_02', 'Geo_Rock-Deepnest_Below_Spike_Grub': 'Deepnest_03', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe': 'Deepnest_03', 'Geo_Rock-Deepnest_Spike_Grub_Right': 'Deepnest_03', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2': 'Deepnest_16', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3': 'Deepnest_16', 'Geo_Rock-Deepnest_Nosk_1': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_2': 'Deepnest_31', 'Geo_Rock-Deepnest_Nosk_3': 'Deepnest_31', 'Geo_Rock-Deepnest_Above_Galien': 'Deepnest_35', 'Geo_Rock-Deepnest_Galien_Spike': 'Deepnest_35', 'Geo_Rock-Deepnest_Garpede_1': 'Deepnest_37', 'Geo_Rock-Deepnest_Garpede_2': 'Deepnest_37', 'Geo_Rock-Dark_Deepnest_Above_Grub_1': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Above_Grub_2': 'Deepnest_39', 'Geo_Rock-Dark_Deepnest_Bottom_Left': 'Deepnest_39', 'Geo_Rock-Above_Mask_Maker_1': 'Deepnest_43', 'Geo_Rock-Above_Mask_Maker_2': 'Deepnest_43', "Geo_Rock-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_2": 'Deepnest_East_01', "Geo_Rock-Lower_Kingdom's_Edge_3": 'Deepnest_East_02', "Geo_Rock-Lower_Kingdom's_Edge_Dive": 'Deepnest_East_02', "Geo_Rock-Kingdom's_Edge_Below_Bardoon": 'Deepnest_East_04', "Geo_Rock-Kingdom's_Edge_Oro_Far_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left": 'Deepnest_East_06', "Geo_Rock-Kingdom's_Edge_Above_Root": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Above_Tower": 'Deepnest_East_07', "Geo_Rock-Kingdom's_Edge_Below_Colosseum": 'Deepnest_East_08', "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Kingdom's_Edge_420_Geo_Rock": 'Deepnest_East_17', "Geo_Rock-Beast's_Den_Above_Trilobite": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Herrah": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Below_Egg_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_Bottom_Dupe": 'Deepnest_Spider_Town', "Geo_Rock-Beast's_Den_After_Herrah": 'Deepnest_Spider_Town', 'Geo_Rock-Greenpath_Entrance': 'Fungus1_01', 'Geo_Rock-Greenpath_Waterfall': 'Fungus1_01b', 'Geo_Rock-Greenpath_Below_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Skip_Squit': 'Fungus1_02', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe': 'Fungus1_03', 'Geo_Rock-Greenpath_Second_Skip_Lower': 'Fungus1_03', 'Geo_Rock-Greenpath_Below_Hornet': 'Fungus1_04', 'Geo_Rock-Greenpath_Above_Thorns': 'Fungus1_05', "Geo_Rock-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Geo_Rock-Greenpath_Acid_Bridge': 'Fungus1_10', 'Geo_Rock-Greenpath_After_MMC_Hidden': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC': 'Fungus1_12', 'Geo_Rock-Greenpath_After_MMC_Dupe': 'Fungus1_12', 'Geo_Rock-Greenpath_Obbles_Fool_Eater': 'Fungus1_19', 'Geo_Rock-Greenpath_Moss_Knights': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1': 'Fungus1_21', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2': 'Fungus1_21', 'Geo_Rock-Greenpath_Below_Stag': 'Fungus1_22', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater': 'Fungus1_22', 'Geo_Rock-Baldur_Shell_Top_Left': 'Fungus1_28', 'Geo_Rock-Baldur_Shell_Alcove': 'Fungus1_28', 'Geo_Rock-Greenpath_MMC': 'Fungus1_29', 'Geo_Rock-Greenpath_Below_Toll': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden': 'Fungus1_31', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe': 'Fungus1_31', 'Geo_Rock-Fungal_Below_Shrumal_Ogres': 'Fungus2_04', 'Geo_Rock-Fungal_Above_Cloth': 'Fungus2_08', 'Geo_Rock-Fungal_After_Cloth': 'Fungus2_10', "Geo_Rock-Fungal_Below_Pilgrim's_Way": 'Fungus2_11', "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe": 'Fungus2_11', 'Geo_Rock-Mantis_Outskirts_Guarded': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe': 'Fungus2_13', 'Geo_Rock-Mantis_Outskirts_Alcove': 'Fungus2_13', 'Geo_Rock-Mantis_Village_After_Lever': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Below_Lore': 'Fungus2_14', 'Geo_Rock-Mantis_Village_Above_Lever': 'Fungus2_14', 'Geo_Rock-Above_Mantis_Lords_1': 'Fungus2_15', 'Geo_Rock-Above_Mantis_Lords_2': 'Fungus2_15', 'Geo_Rock-Fungal_After_Bouncy_Grub': 'Fungus2_18', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe': 'Fungus2_18', 'Geo_Rock-Fungal_Bouncy_Grub_Lever': 'Fungus2_18', 'Geo_Rock-Fungal_After_Cornifer': 'Fungus2_18', 'Geo_Rock-Fungal_Above_City_Entrance': 'Fungus2_21', 'Geo_Rock-Deepnest_By_Mantis_Lords_1': 'Fungus2_25', 'Geo_Rock-Deepnest_By_Mantis_Lords_2': 'Fungus2_25', 'Geo_Rock-Deepnest_Lower_Cornifer': 'Fungus2_25', 'Geo_Rock-Fungal_Core_Entrance': 'Fungus2_29', 'Geo_Rock-Fungal_Core_Hidden': 'Fungus2_30', 'Geo_Rock-Fungal_Core_Above_Elder': 'Fungus2_30', "Geo_Rock-Queen's_Gardens_Acid_Entrance": 'Fungus3_03', "Geo_Rock-Queen's_Gardens_Below_Stag": 'Fungus3_10', 'Geo_Rock-Fog_Canyon_East': 'Fungus3_26', 'Geo_Rock-Love_Key': 'Fungus3_39', 'Geo_Rock-Love_Key_Dupe': 'Fungus3_39', "Geo_Rock-Queen's_Gardens_Above_Marmu": 'Fungus3_48', 'Geo_Rock-Pale_Lurker': 'GG_Lurker', 'Geo_Rock-Godhome_Pipeway': 'GG_Pipeway', 'Geo_Rock-Hive_Entrance': 'Hive_01', 'Geo_Rock-Hive_Outside_Bench': 'Hive_02', 'Geo_Rock-Hive_Below_Root': 'Hive_02', 'Geo_Rock-Hive_After_Root': 'Hive_02', 'Geo_Rock-Hive_Below_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash': 'Hive_03', 'Geo_Rock-Hive_Stash_Dupe': 'Hive_03', 'Geo_Rock-Hive_Below_Grub': 'Hive_04', 'Geo_Rock-Hive_Above_Mask': 'Hive_04', 'Geo_Rock-Crystal_Peak_Lower_Middle': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1': 'Mines_02', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2': 'Mines_02', 'Geo_Rock-Crystal_Peak_Before_Dark_Room': 'Mines_04', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe': 'Mines_04', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub': 'Mines_05', 'Geo_Rock-Crystal_Peak_Mimic_Grub': 'Mines_16', 'Geo_Rock-Crystal_Peak_Dive_Egg': 'Mines_20', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe': 'Mines_20', 'Geo_Rock-Crystal_Peak_Conga_Line': 'Mines_20', 'Geo_Rock-Hallownest_Crown_Dive': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Dive_Dupe': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1': 'Mines_25', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2': 'Mines_25', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart': 'Mines_31', 'Geo_Rock-Crystal_Peak_Entrance': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1': 'Mines_33', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2': 'Mines_33', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower': 'Mines_37', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher': 'Mines_37', 'Geo_Rock-Resting_Grounds_Catacombs_Grub': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe': 'RestingGrounds_10', 'Geo_Rock-Resting_Grounds_Catacombs_Left': 'RestingGrounds_10', 'Geo_Rock-Overgrown_Mound': 'Room_Fungus_Shaman', 'Geo_Rock-Fluke_Hermit_Dupe': 'Room_GG_Shortcut', 'Geo_Rock-Fluke_Hermit': 'Room_GG_Shortcut', 'Geo_Rock-Pleasure_House': 'Ruins_Elevator', 'Geo_Rock-City_of_Tears_Quirrel': 'Ruins1_03', 'Geo_Rock-City_of_Tears_Lemm': 'Ruins1_05b', 'Geo_Rock-City_of_Tears_Above_Lemm': 'Ruins1_05c', 'Geo_Rock-Soul_Sanctum': 'Ruins1_32', "Geo_Rock-Watcher's_Spire": 'Ruins2_01', "Geo_Rock-Above_King's_Station": 'Ruins2_05', "Geo_Rock-King's_Station": 'Ruins2_06', "Geo_Rock-King's_Pass_Left": 'Tutorial_01', "Geo_Rock-King's_Pass_Below_Fury": 'Tutorial_01', "Geo_Rock-King's_Pass_Hidden": 'Tutorial_01', "Geo_Rock-King's_Pass_Collapse": 'Tutorial_01', "Geo_Rock-King's_Pass_Above_Fury": 'Tutorial_01', 'Geo_Rock-Waterways_Tuk': 'Waterways_01', 'Geo_Rock-Waterways_Tuk_Alcove': 'Waterways_01', 'Geo_Rock-Waterways_Left': 'Waterways_04b', 'Geo_Rock-Waterways_East': 'Waterways_07', 'Geo_Rock-Waterways_Flukemarm': 'Waterways_08', 'Boss_Geo-Massive_Moss_Charger': 'Fungus1_29', 'Boss_Geo-Gorgeous_Husk': 'Ruins_House_02', 'Boss_Geo-Sanctum_Soul_Warrior': 'Ruins1_23', 'Boss_Geo-Elegant_Soul_Warrior': 'Ruins1_31b', 'Boss_Geo-Crystal_Guardian': 'Mines_18', 'Boss_Geo-Enraged_Guardian': 'Mines_32', 'Boss_Geo-Gruz_Mother': 'Crossroads_04', 'Boss_Geo-Vengefly_King': 'Fungus1_20_v02', 'Soul_Totem-Basin': 'Abyss_04', 'Soul_Totem-Cliffs_Main': 'Cliffs_01', 'Soul_Totem-Cliffs_Gorb': 'Cliffs_02', "Soul_Totem-Cliffs_Joni's": 'Cliffs_04', 'Soul_Totem-Crossroads_Goam_Journal': 'Crossroads_18', 'Soul_Totem-Crossroads_Shops': 'Crossroads_19', 'Soul_Totem-Crossroads_Mawlek_Upper': 'Crossroads_25', 'Soul_Totem-Crossroads_Acid': 'Crossroads_35', 'Soul_Totem-Crossroads_Mawlek_Lower': 'Crossroads_36', 'Soul_Totem-Crossroads_Myla': 'Crossroads_45', 'Soul_Totem-Ancestral_Mound': 'Crossroads_ShamanTemple', 'Soul_Totem-Distant_Village': 'Deepnest_10', 'Soul_Totem-Deepnest_Vessel': 'Deepnest_38', 'Soul_Totem-Mask_Maker': 'Deepnest_42', "Soul_Totem-Lower_Kingdom's_Edge_1": 'Deepnest_East_01', "Soul_Totem-Lower_Kingdom's_Edge_2": 'Deepnest_East_02', "Soul_Totem-Upper_Kingdom's_Edge": 'Deepnest_East_07', "Soul_Totem-Kingdom's_Edge_Camp": 'Deepnest_East_11', 'Soul_Totem-Oro_Dive_2': 'Deepnest_East_14', 'Soul_Totem-Oro_Dive_1': 'Deepnest_East_14', 'Soul_Totem-Oro': 'Deepnest_East_16', 'Soul_Totem-420_Geo_Rock': 'Deepnest_East_17', "Soul_Totem-Beast's_Den": 'Deepnest_Spider_Town', "Soul_Totem-Greenpath_Hunter's_Journal": 'Fungus1_07', 'Soul_Totem-Greenpath_MMC': 'Fungus1_29', 'Soul_Totem-Greenpath_Below_Toll': 'Fungus1_30', "Soul_Totem-Before_Pilgrim's_Way": 'Fungus2_10', "Soul_Totem-Pilgrim's_Way": 'Fungus2_21', 'Soul_Totem-Fungal_Core': 'Fungus2_29', "Soul_Totem-Top_Left_Queen's_Gardens": 'Fungus3_21', 'Soul_Totem-Below_Marmu': 'Fungus3_40', 'Soul_Totem-Upper_Crystal_Peak': 'Mines_20', 'Soul_Totem-Hallownest_Crown': 'Mines_25', 'Soul_Totem-Outside_Crystallized_Mound': 'Mines_28', 'Soul_Totem-Crystal_Heart_1': 'Mines_31', 'Soul_Totem-Crystal_Heart_2': 'Mines_31', 'Soul_Totem-Crystallized_Mound': 'Mines_35', 'Soul_Totem-Resting_Grounds': 'RestingGrounds_05', 'Soul_Totem-Below_Xero': 'RestingGrounds_06', 'Soul_Totem-Sanctum_Below_Soul_Master': 'Ruins1_24', 'Soul_Totem-Sanctum_Below_Chest': 'Ruins1_32', 'Soul_Totem-Sanctum_Above_Grub': 'Ruins1_32', 'Soul_Totem-Waterways_Entrance': 'Waterways_01', 'Soul_Totem-Top_Left_Waterways': 'Waterways_04b', 'Soul_Totem-Waterways_East': 'Waterways_07', 'Soul_Totem-Waterways_Flukemarm': 'Waterways_08', 'Soul_Totem-White_Palace_Entrance': 'White_Palace_02', 'Soul_Totem-White_Palace_Hub': 'White_Palace_03_hub', 'Soul_Totem-White_Palace_Left': 'White_Palace_04', 'Soul_Totem-White_Palace_Final': 'White_Palace_09', 'Soul_Totem-White_Palace_Right': 'White_Palace_15', 'Soul_Totem-Path_of_Pain_Below_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Left_of_Lever': 'White_Palace_17', 'Soul_Totem-Path_of_Pain_Entrance': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Second': 'White_Palace_18', 'Soul_Totem-Path_of_Pain_Hidden': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Below_Thornskip': 'White_Palace_19', 'Soul_Totem-Path_of_Pain_Final': 'White_Palace_20', 'Soul_Totem-Pale_Lurker': 'GG_Lurker', 'Lore_Tablet-City_Entrance': 'Ruins1_02', 'Lore_Tablet-Pleasure_House': 'Ruins_Elevator', 'Lore_Tablet-Sanctum_Entrance': 'Ruins1_23', 'Lore_Tablet-Sanctum_Past_Soul_Master': 'Ruins1_32', "Lore_Tablet-Watcher's_Spire": 'Ruins2_Watcher_Room', 'Lore_Tablet-Archives_Upper': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Left': 'Fungus3_archive_02', 'Lore_Tablet-Archives_Right': 'Fungus3_archive_02', "Lore_Tablet-Pilgrim's_Way_1": 'Crossroads_11_alt', "Lore_Tablet-Pilgrim's_Way_2": 'Fungus2_21', 'Lore_Tablet-Mantis_Outskirts': 'Fungus2_12', 'Lore_Tablet-Mantis_Village': 'Fungus2_14', 'Lore_Tablet-Greenpath_Upper_Hidden': 'Fungus1_17', 'Lore_Tablet-Greenpath_Below_Toll': 'Fungus1_30', 'Lore_Tablet-Greenpath_Lifeblood': 'Fungus1_32', 'Lore_Tablet-Greenpath_Stag': 'Fungus1_21', 'Lore_Tablet-Greenpath_QG': 'Fungus1_13', 'Lore_Tablet-Greenpath_Lower_Hidden': 'Fungus1_19', 'Lore_Tablet-Dung_Defender': 'Waterways_07', 'Lore_Tablet-Spore_Shroom': 'Fungus2_20', 'Lore_Tablet-Fungal_Wastes_Hidden': 'Fungus2_07', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres': 'Fungus2_04', 'Lore_Tablet-Fungal_Core': 'Fungus2_30', 'Lore_Tablet-Ancient_Basin': 'Abyss_06_Core', "Lore_Tablet-King's_Pass_Focus": 'Tutorial_01', "Lore_Tablet-King's_Pass_Fury": 'Tutorial_01', "Lore_Tablet-King's_Pass_Exit": 'Tutorial_01', 'Lore_Tablet-World_Sense': 'Room_temple', 'Lore_Tablet-Howling_Cliffs': 'Cliffs_01', "Lore_Tablet-Kingdom's_Edge": 'Deepnest_East_17', 'Lore_Tablet-Palace_Workshop': 'White_Palace_08', 'Lore_Tablet-Palace_Throne': 'White_Palace_09', 'Lore_Tablet-Path_of_Pain_Entrance': 'White_Palace_18'} locations = ['Sly_1', 'Sly_2', 'Sly_3', 'Sly_4', 'Sly_5', 'Sly_6', 'Sly_7', 'Sly_8', 'Sly_9', 'Sly_10', 'Sly_11', 'Sly_12', 'Sly_13', 'Sly_14', 'Sly_15', 'Sly_16', 'Sly_(Key)_1', 'Sly_(Key)_2', 'Sly_(Key)_3', 'Sly_(Key)_4', 'Sly_(Key)_5', 'Sly_(Key)_6', 'Sly_(Key)_7', 'Sly_(Key)_8', 'Sly_(Key)_9', 'Sly_(Key)_10', 'Sly_(Key)_11', 'Sly_(Key)_12', 'Sly_(Key)_13', 'Sly_(Key)_14', 'Sly_(Key)_15', 'Sly_(Key)_16', 'Iselda_1', 'Iselda_2', 'Iselda_3', 'Iselda_4', 'Iselda_5', 'Iselda_6', 'Iselda_7', 'Iselda_8', 'Iselda_9', 'Iselda_10', 'Iselda_11', 'Iselda_12', 'Iselda_13', 'Iselda_14', 'Iselda_15', 'Iselda_16', 'Salubra_1', 'Salubra_2', 'Salubra_3', 'Salubra_4', 'Salubra_5', 'Salubra_6', 'Salubra_7', 'Salubra_8', 'Salubra_9', 'Salubra_10', 'Salubra_11', 'Salubra_12', 'Salubra_13', 'Salubra_14', 'Salubra_15', 'Salubra_16', 'Salubra_(Requires_Charms)_1', 'Salubra_(Requires_Charms)_2', 'Salubra_(Requires_Charms)_3', 'Salubra_(Requires_Charms)_4', 'Salubra_(Requires_Charms)_5', 'Salubra_(Requires_Charms)_6', 'Salubra_(Requires_Charms)_7', 'Salubra_(Requires_Charms)_8', 'Salubra_(Requires_Charms)_9', 'Salubra_(Requires_Charms)_10', 'Salubra_(Requires_Charms)_11', 'Salubra_(Requires_Charms)_12', 'Salubra_(Requires_Charms)_13', 'Salubra_(Requires_Charms)_14', 'Salubra_(Requires_Charms)_15', 'Salubra_(Requires_Charms)_16', 'Leg_Eater_1', 'Leg_Eater_2', 'Leg_Eater_3', 'Leg_Eater_4', 'Leg_Eater_5', 'Leg_Eater_6', 'Leg_Eater_7', 'Leg_Eater_8', 'Leg_Eater_9', 'Leg_Eater_10', 'Leg_Eater_11', 'Leg_Eater_12', 'Leg_Eater_13', 'Leg_Eater_14', 'Leg_Eater_15', 'Leg_Eater_16', 'Grubfather_1', 'Grubfather_2', 'Grubfather_3', 'Grubfather_4', 'Grubfather_5', 'Grubfather_6', 'Grubfather_7', 'Grubfather_8', 'Grubfather_9', 'Grubfather_10', 'Grubfather_11', 'Grubfather_12', 'Grubfather_13', 'Grubfather_14', 'Grubfather_15', 'Grubfather_16', 'Seer_1', 'Seer_2', 'Seer_3', 'Seer_4', 'Seer_5', 'Seer_6', 'Seer_7', 'Seer_8', 'Seer_9', 'Seer_10', 'Seer_11', 'Seer_12', 'Seer_13', 'Seer_14', 'Seer_15', 'Seer_16', 'Egg_Shop_1', 'Egg_Shop_2', 'Egg_Shop_3', 'Egg_Shop_4', 'Egg_Shop_5', 'Egg_Shop_6', 'Egg_Shop_7', 'Egg_Shop_8', 'Egg_Shop_9', 'Egg_Shop_10', 'Egg_Shop_11', 'Egg_Shop_12', 'Egg_Shop_13', 'Egg_Shop_14', 'Egg_Shop_15', 'Egg_Shop_16', 'Lurien', 'Monomon', 'Herrah', 'World_Sense', 'Mothwing_Cloak', 'Mantis_Claw', 'Crystal_Heart', 'Monarch_Wings', 'Shade_Cloak', "Isma's_Tear", 'Dream_Nail', 'Vengeful_Spirit', 'Shade_Soul', 'Desolate_Dive', 'Descending_Dark', 'Howling_Wraiths', 'Abyss_Shriek', 'Cyclone_Slash', 'Dash_Slash', 'Great_Slash', 'Focus', 'Baldur_Shell', 'Fury_of_the_Fallen', 'Lifeblood_Core', "Defender's_Crest", 'Flukenest', 'Thorns_of_Agony', 'Mark_of_Pride', 'Sharp_Shadow', 'Spore_Shroom', 'Soul_Catcher', 'Soul_Eater', 'Glowing_Womb', "Nailmaster's_Glory", "Joni's_Blessing", 'Shape_of_Unn', 'Hiveblood', 'Dashmaster', 'Quick_Slash', 'Spell_Twister', 'Deep_Focus', 'Queen_Fragment', 'King_Fragment', 'Void_Heart', 'Dreamshield', 'Weaversong', 'Grimmchild', 'Unbreakable_Heart', 'Unbreakable_Greed', 'Unbreakable_Strength', 'City_Crest', 'Tram_Pass', 'Simple_Key-Basin', 'Simple_Key-City', 'Simple_Key-Lurker', "Shopkeeper's_Key", 'Love_Key', "King's_Brand", 'Godtuner', "Collector's_Map", 'Mask_Shard-Brooding_Mawlek', 'Mask_Shard-Crossroads_Goam', 'Mask_Shard-Stone_Sanctuary', "Mask_Shard-Queen's_Station", 'Mask_Shard-Deepnest', 'Mask_Shard-Waterways', 'Mask_Shard-Enraged_Guardian', 'Mask_Shard-Hive', 'Mask_Shard-Grey_Mourner', 'Mask_Shard-Bretta', 'Vessel_Fragment-Greenpath', 'Vessel_Fragment-City', 'Vessel_Fragment-Crossroads', 'Vessel_Fragment-Basin', 'Vessel_Fragment-Deepnest', 'Vessel_Fragment-Stag_Nest', 'Charm_Notch-Shrumal_Ogres', 'Charm_Notch-Fog_Canyon', 'Charm_Notch-Colosseum', 'Charm_Notch-Grimm', 'Pale_Ore-Basin', 'Pale_Ore-Crystal_Peak', 'Pale_Ore-Nosk', 'Pale_Ore-Colosseum', 'Geo_Chest-False_Knight', 'Geo_Chest-Soul_Master', 'Geo_Chest-Watcher_Knights', 'Geo_Chest-Greenpath', 'Geo_Chest-Mantis_Lords', 'Geo_Chest-Resting_Grounds', 'Geo_Chest-Crystal_Peak', 'Geo_Chest-Weavers_Den', 'Geo_Chest-Junk_Pit_1', 'Geo_Chest-Junk_Pit_2', 'Geo_Chest-Junk_Pit_3', 'Geo_Chest-Junk_Pit_5', 'Lumafly_Escape-Junk_Pit_Chest_4', 'Rancid_Egg-Sheo', 'Rancid_Egg-Fungal_Core', "Rancid_Egg-Queen's_Gardens", 'Rancid_Egg-Blue_Lake', 'Rancid_Egg-Crystal_Peak_Dive_Entrance', 'Rancid_Egg-Crystal_Peak_Dark_Room', 'Rancid_Egg-Crystal_Peak_Tall_Room', 'Rancid_Egg-City_of_Tears_Left', 'Rancid_Egg-City_of_Tears_Pleasure_House', "Rancid_Egg-Beast's_Den", 'Rancid_Egg-Dark_Deepnest', "Rancid_Egg-Weaver's_Den", 'Rancid_Egg-Near_Quick_Slash', "Rancid_Egg-Upper_Kingdom's_Edge", 'Rancid_Egg-Waterways_East', 'Rancid_Egg-Waterways_Main', 'Rancid_Egg-Waterways_West_Bluggsac', 'Rancid_Egg-Waterways_West_Pickup', "Rancid_Egg-Tuk_Defender's_Crest", "Wanderer's_Journal-Cliffs", "Wanderer's_Journal-Greenpath_Stag", "Wanderer's_Journal-Greenpath_Lower", "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet", "Wanderer's_Journal-Above_Mantis_Village", "Wanderer's_Journal-Crystal_Peak_Crawlers", "Wanderer's_Journal-Resting_Grounds_Catacombs", "Wanderer's_Journal-King's_Station", "Wanderer's_Journal-Pleasure_House", "Wanderer's_Journal-City_Storerooms", "Wanderer's_Journal-Ancient_Basin", "Wanderer's_Journal-Kingdom's_Edge_Entrance", "Wanderer's_Journal-Kingdom's_Edge_Camp", "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive", 'Hallownest_Seal-Crossroads_Well', 'Hallownest_Seal-Greenpath', 'Hallownest_Seal-Fog_Canyon_West', 'Hallownest_Seal-Fog_Canyon_East', "Hallownest_Seal-Queen's_Station", 'Hallownest_Seal-Fungal_Wastes_Sporgs', 'Hallownest_Seal-Mantis_Lords', 'Hallownest_Seal-Resting_Grounds_Catacombs', "Hallownest_Seal-King's_Station", 'Hallownest_Seal-City_Rafters', 'Hallownest_Seal-Soul_Sanctum', 'Hallownest_Seal-Watcher_Knight', 'Hallownest_Seal-Deepnest_By_Mantis_Lords', "Hallownest_Seal-Beast's_Den", "Hallownest_Seal-Queen's_Gardens", "King's_Idol-Cliffs", "King's_Idol-Crystal_Peak", "King's_Idol-Glade_of_Hope", "King's_Idol-Dung_Defender", "King's_Idol-Great_Hopper", "King's_Idol-Pale_Lurker", "King's_Idol-Deepnest", 'Arcane_Egg-Lifeblood_Core', 'Arcane_Egg-Shade_Cloak', 'Arcane_Egg-Birthplace', 'Whispering_Root-Crossroads', 'Whispering_Root-Greenpath', 'Whispering_Root-Leg_Eater', 'Whispering_Root-Mantis_Village', 'Whispering_Root-Deepnest', 'Whispering_Root-Queens_Gardens', 'Whispering_Root-Kingdoms_Edge', 'Whispering_Root-Waterways', 'Whispering_Root-City', 'Whispering_Root-Resting_Grounds', 'Whispering_Root-Spirits_Glade', 'Whispering_Root-Crystal_Peak', 'Whispering_Root-Howling_Cliffs', 'Whispering_Root-Ancestral_Mound', 'Whispering_Root-Hive', 'Boss_Essence-Elder_Hu', 'Boss_Essence-Xero', 'Boss_Essence-Gorb', 'Boss_Essence-Marmu', 'Boss_Essence-No_Eyes', 'Boss_Essence-Galien', 'Boss_Essence-Markoth', 'Boss_Essence-Failed_Champion', 'Boss_Essence-Soul_Tyrant', 'Boss_Essence-Lost_Kin', 'Boss_Essence-White_Defender', 'Boss_Essence-Grey_Prince_Zote', 'Grub-Crossroads_Acid', 'Grub-Crossroads_Center', 'Grub-Crossroads_Stag', 'Grub-Crossroads_Spike', 'Grub-Crossroads_Guarded', 'Grub-Greenpath_Cornifer', 'Grub-Greenpath_Journal', 'Grub-Greenpath_MMC', 'Grub-Greenpath_Stag', 'Grub-Fog_Canyon', 'Grub-Fungal_Bouncy', 'Grub-Fungal_Spore_Shroom', 'Grub-Deepnest_Mimic', 'Grub-Deepnest_Nosk', 'Grub-Deepnest_Spike', 'Grub-Dark_Deepnest', "Grub-Beast's_Den", "Grub-Kingdom's_Edge_Oro", "Grub-Kingdom's_Edge_Camp", 'Grub-Hive_External', 'Grub-Hive_Internal', 'Grub-Basin_Requires_Wings', 'Grub-Basin_Requires_Dive', 'Grub-Waterways_Main', "Grub-Isma's_Grove", 'Grub-Waterways_Requires_Tram', 'Grub-City_of_Tears_Left', 'Grub-Soul_Sanctum', "Grub-Watcher's_Spire", 'Grub-City_of_Tears_Guarded', "Grub-King's_Station", 'Grub-Resting_Grounds', 'Grub-Crystal_Peak_Below_Chest', 'Grub-Crystallized_Mound', 'Grub-Crystal_Peak_Spike', 'Grub-Crystal_Peak_Mimic', 'Grub-Crystal_Peak_Crushers', 'Grub-Crystal_Heart', 'Grub-Hallownest_Crown', 'Grub-Howling_Cliffs', "Grub-Queen's_Gardens_Stag", "Grub-Queen's_Gardens_Marmu", "Grub-Queen's_Gardens_Top", 'Grub-Collector_1', 'Grub-Collector_2', 'Grub-Collector_3', 'Mimic_Grub-Deepnest_1', 'Mimic_Grub-Deepnest_2', 'Mimic_Grub-Deepnest_3', 'Mimic_Grub-Crystal_Peak', 'Crossroads_Map', 'Greenpath_Map', 'Fog_Canyon_Map', 'Fungal_Wastes_Map', 'Deepnest_Map-Upper', 'Deepnest_Map-Right', 'Ancient_Basin_Map', "Kingdom's_Edge_Map", 'City_of_Tears_Map', 'Royal_Waterways_Map', 'Howling_Cliffs_Map', 'Crystal_Peak_Map', "Queen's_Gardens_Map", 'Resting_Grounds_Map', 'Dirtmouth_Stag', 'Crossroads_Stag', 'Greenpath_Stag', "Queen's_Station_Stag", "Queen's_Gardens_Stag", 'City_Storerooms_Stag', "King's_Station_Stag", 'Resting_Grounds_Stag', 'Distant_Village_Stag', 'Hidden_Station_Stag', 'Stag_Nest_Stag', "Lifeblood_Cocoon-King's_Pass", 'Lifeblood_Cocoon-Ancestral_Mound', 'Lifeblood_Cocoon-Greenpath', 'Lifeblood_Cocoon-Fog_Canyon_West', 'Lifeblood_Cocoon-Mantis_Village', 'Lifeblood_Cocoon-Failed_Tramway', 'Lifeblood_Cocoon-Galien', "Lifeblood_Cocoon-Kingdom's_Edge", 'Grimmkin_Flame-City_Storerooms', 'Grimmkin_Flame-Greenpath', 'Grimmkin_Flame-Crystal_Peak', "Grimmkin_Flame-King's_Pass", 'Grimmkin_Flame-Resting_Grounds', "Grimmkin_Flame-Kingdom's_Edge", 'Grimmkin_Flame-Fungal_Core', 'Grimmkin_Flame-Ancient_Basin', 'Grimmkin_Flame-Hive', 'Grimmkin_Flame-Brumm', "Hunter's_Journal", 'Journal_Entry-Void_Tendrils', 'Journal_Entry-Charged_Lumafly', 'Journal_Entry-Goam', 'Journal_Entry-Garpede', 'Journal_Entry-Seal_of_Binding', 'Elevator_Pass', 'Split_Mothwing_Cloak', 'Left_Mantis_Claw', 'Right_Mantis_Claw', 'Leftslash', 'Rightslash', 'Upslash', 'Split_Crystal_Heart', 'Geo_Rock-Broken_Elevator_1', 'Geo_Rock-Broken_Elevator_2', 'Geo_Rock-Broken_Elevator_3', 'Geo_Rock-Broken_Bridge_Upper', 'Geo_Rock-Broken_Bridge_Lower', 'Geo_Rock-Broken_Bridge_Lower_Dupe', 'Geo_Rock-Abyss_1', 'Geo_Rock-Abyss_2', 'Geo_Rock-Abyss_3', 'Geo_Rock-Basin_Tunnel', 'Geo_Rock-Basin_Grub', 'Geo_Rock-Basin_Before_Broken_Vessel', 'Geo_Rock-Cliffs_Main_1', 'Geo_Rock-Cliffs_Main_2', 'Geo_Rock-Cliffs_Main_3', 'Geo_Rock-Cliffs_Main_4', 'Geo_Rock-Below_Gorb_Dupe', 'Geo_Rock-Below_Gorb', 'Geo_Rock-Crossroads_Well', 'Geo_Rock-Crossroads_Center_Grub', 'Geo_Rock-Crossroads_Root', 'Geo_Rock-Crossroads_Root_Dupe_1', 'Geo_Rock-Crossroads_Root_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_1', 'Geo_Rock-Crossroads_Aspid_Arena_Dupe_2', 'Geo_Rock-Crossroads_Aspid_Arena_Hidden', 'Geo_Rock-Crossroads_Above_False_Knight', 'Geo_Rock-Crossroads_Before_Acid_Grub', 'Geo_Rock-Crossroads_Below_Goam_Mask_Shard', 'Geo_Rock-Crossroads_After_Goam_Mask_Shard', 'Geo_Rock-Crossroads_Above_Lever', 'Geo_Rock-Crossroads_Before_Fungal', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_1', 'Geo_Rock-Crossroads_Before_Fungal_Dupe_2', 'Geo_Rock-Crossroads_Before_Shops', 'Geo_Rock-Crossroads_Before_Glowing_Womb', 'Geo_Rock-Crossroads_Above_Tram', 'Geo_Rock-Crossroads_Above_Mawlek', 'Geo_Rock-Crossroads_Vessel_Fragment', 'Geo_Rock-Crossroads_Goam_Alcove', 'Geo_Rock-Crossroads_Goam_Damage_Boost', 'Geo_Rock-Crossroads_Tram', 'Geo_Rock-Crossroads_Goam_Journal', 'Geo_Rock-Crossroads_Goam_Journal_Dupe', 'Geo_Rock-Ancestral_Mound', 'Geo_Rock-Ancestral_Mound_Dupe', 'Geo_Rock-Ancestral_Mound_Tree', 'Geo_Rock-Ancestral_Mound_Tree_Dupe', 'Geo_Rock-Moss_Prophet', 'Geo_Rock-Moss_Prophet_Dupe', 'Geo_Rock-Deepnest_Below_Mimics', 'Geo_Rock-Deepnest_Below_Mimics_Dupe', 'Geo_Rock-Deepnest_Below_Spike_Grub', 'Geo_Rock-Deepnest_Below_Spike_Grub_Dupe', 'Geo_Rock-Deepnest_Spike_Grub_Right', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo', 'Geo_Rock-Deepnest_By_Mantis_Lords_Garpede_Pogo_Dupe', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_2', 'Geo_Rock-Deepnest_By_Mantis_Lords_Requires_Claw_3', 'Geo_Rock-Deepnest_Nosk_1', 'Geo_Rock-Deepnest_Nosk_2', 'Geo_Rock-Deepnest_Nosk_3', 'Geo_Rock-Deepnest_Above_Galien', 'Geo_Rock-Deepnest_Galien_Spike', 'Geo_Rock-Deepnest_Garpede_1', 'Geo_Rock-Deepnest_Garpede_2', 'Geo_Rock-Dark_Deepnest_Above_Grub_1', 'Geo_Rock-Dark_Deepnest_Above_Grub_2', 'Geo_Rock-Dark_Deepnest_Bottom_Left', 'Geo_Rock-Above_Mask_Maker_1', 'Geo_Rock-Above_Mask_Maker_2', "Geo_Rock-Lower_Kingdom's_Edge_1", "Geo_Rock-Lower_Kingdom's_Edge_2", "Geo_Rock-Lower_Kingdom's_Edge_3", "Geo_Rock-Lower_Kingdom's_Edge_Dive", "Geo_Rock-Kingdom's_Edge_Below_Bardoon", "Geo_Rock-Kingdom's_Edge_Oro_Far_Left", "Geo_Rock-Kingdom's_Edge_Oro_Middle_Left", "Geo_Rock-Kingdom's_Edge_Above_Root", "Geo_Rock-Kingdom's_Edge_Above_Tower", "Geo_Rock-Kingdom's_Edge_Below_Colosseum", "Geo_Rock-Kingdom's_Edge_Above_420_Geo_Rock", "Geo_Rock-Kingdom's_Edge_420_Geo_Rock", "Geo_Rock-Beast's_Den_Above_Trilobite", "Geo_Rock-Beast's_Den_Above_Trilobite_Dupe", "Geo_Rock-Beast's_Den_Below_Herrah", "Geo_Rock-Beast's_Den_Below_Egg", "Geo_Rock-Beast's_Den_Below_Egg_Dupe", "Geo_Rock-Beast's_Den_Bottom", "Geo_Rock-Beast's_Den_Bottom_Dupe", "Geo_Rock-Beast's_Den_After_Herrah", 'Geo_Rock-Greenpath_Entrance', 'Geo_Rock-Greenpath_Waterfall', 'Geo_Rock-Greenpath_Below_Skip_Squit', 'Geo_Rock-Greenpath_Skip_Squit', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater', 'Geo_Rock-Greenpath_Second_Skip_Fool_Eater_Dupe', 'Geo_Rock-Greenpath_Second_Skip_Lower', 'Geo_Rock-Greenpath_Below_Hornet', 'Geo_Rock-Greenpath_Above_Thorns', "Geo_Rock-Greenpath_Hunter's_Journal", 'Geo_Rock-Greenpath_Acid_Bridge', 'Geo_Rock-Greenpath_After_MMC_Hidden', 'Geo_Rock-Greenpath_After_MMC', 'Geo_Rock-Greenpath_After_MMC_Dupe', 'Geo_Rock-Greenpath_Obbles_Fool_Eater', 'Geo_Rock-Greenpath_Moss_Knights', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_1', 'Geo_Rock-Greenpath_Moss_Knights_Dupe_2', 'Geo_Rock-Greenpath_Below_Stag', 'Geo_Rock-Greenpath_Below_Stag_Fool_Eater', 'Geo_Rock-Baldur_Shell_Top_Left', 'Geo_Rock-Baldur_Shell_Alcove', 'Geo_Rock-Greenpath_MMC', 'Geo_Rock-Greenpath_Below_Toll', 'Geo_Rock-Greenpath_Toll_Hidden', 'Geo_Rock-Greenpath_Toll_Hidden_Dupe', 'Geo_Rock-Fungal_Below_Shrumal_Ogres', 'Geo_Rock-Fungal_Above_Cloth', 'Geo_Rock-Fungal_After_Cloth', "Geo_Rock-Fungal_Below_Pilgrim's_Way", "Geo_Rock-Fungal_Below_Pilgrim's_Way_Dupe", 'Geo_Rock-Mantis_Outskirts_Guarded', 'Geo_Rock-Mantis_Outskirts_Guarded_Dupe', 'Geo_Rock-Mantis_Outskirts_Alcove', 'Geo_Rock-Mantis_Village_After_Lever', 'Geo_Rock-Mantis_Village_Above_Claw', 'Geo_Rock-Mantis_Village_Above_Claw_Dupe', 'Geo_Rock-Mantis_Village_Below_Lore', 'Geo_Rock-Mantis_Village_Above_Lever', 'Geo_Rock-Above_Mantis_Lords_1', 'Geo_Rock-Above_Mantis_Lords_2', 'Geo_Rock-Fungal_After_Bouncy_Grub', 'Geo_Rock-Fungal_After_Bouncy_Grub_Dupe', 'Geo_Rock-Fungal_Bouncy_Grub_Lever', 'Geo_Rock-Fungal_After_Cornifer', 'Geo_Rock-Fungal_Above_City_Entrance', 'Geo_Rock-Deepnest_By_Mantis_Lords_1', 'Geo_Rock-Deepnest_By_Mantis_Lords_2', 'Geo_Rock-Deepnest_Lower_Cornifer', 'Geo_Rock-Fungal_Core_Entrance', 'Geo_Rock-Fungal_Core_Hidden', 'Geo_Rock-Fungal_Core_Above_Elder', "Geo_Rock-Queen's_Gardens_Acid_Entrance", "Geo_Rock-Queen's_Gardens_Below_Stag", 'Geo_Rock-Fog_Canyon_East', 'Geo_Rock-Love_Key', 'Geo_Rock-Love_Key_Dupe', "Geo_Rock-Queen's_Gardens_Above_Marmu", 'Geo_Rock-Pale_Lurker', 'Geo_Rock-Godhome_Pipeway', 'Geo_Rock-Hive_Entrance', 'Geo_Rock-Hive_Outside_Bench', 'Geo_Rock-Hive_Below_Root', 'Geo_Rock-Hive_After_Root', 'Geo_Rock-Hive_Below_Stash', 'Geo_Rock-Hive_Stash', 'Geo_Rock-Hive_Stash_Dupe', 'Geo_Rock-Hive_Below_Grub', 'Geo_Rock-Hive_Above_Mask', 'Geo_Rock-Crystal_Peak_Lower_Middle', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_1', 'Geo_Rock-Crystal_Peak_Lower_Conveyer_2', 'Geo_Rock-Crystal_Peak_Before_Dark_Room', 'Geo_Rock-Crystal_Peak_Before_Dark_Room_Dupe', 'Geo_Rock-Crystal_Peak_Above_Spike_Grub', 'Geo_Rock-Crystal_Peak_Mimic_Grub', 'Geo_Rock-Crystal_Peak_Dive_Egg', 'Geo_Rock-Crystal_Peak_Dive_Egg_Dupe', 'Geo_Rock-Crystal_Peak_Conga_Line', 'Geo_Rock-Hallownest_Crown_Dive', 'Geo_Rock-Hallownest_Crown_Dive_Dupe', 'Geo_Rock-Hallownest_Crown_Hidden', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_1', 'Geo_Rock-Hallownest_Crown_Hidden_Dupe_2', 'Geo_Rock-Crystal_Peak_Before_Crystal_Heart', 'Geo_Rock-Crystal_Peak_Entrance', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_1', 'Geo_Rock-Crystal_Peak_Entrance_Dupe_2', 'Geo_Rock-Crystal_Peak_Above_Crushers_Lower', 'Geo_Rock-Crystal_Peak_Above_Crushers_Higher', 'Geo_Rock-Resting_Grounds_Catacombs_Grub', 'Geo_Rock-Resting_Grounds_Catacombs_Left_Dupe', 'Geo_Rock-Resting_Grounds_Catacombs_Left', 'Geo_Rock-Overgrown_Mound', 'Geo_Rock-Fluke_Hermit_Dupe', 'Geo_Rock-Fluke_Hermit', 'Geo_Rock-Pleasure_House', 'Geo_Rock-City_of_Tears_Quirrel', 'Geo_Rock-City_of_Tears_Lemm', 'Geo_Rock-City_of_Tears_Above_Lemm', 'Geo_Rock-Soul_Sanctum', "Geo_Rock-Watcher's_Spire", "Geo_Rock-Above_King's_Station", "Geo_Rock-King's_Station", "Geo_Rock-King's_Pass_Left", "Geo_Rock-King's_Pass_Below_Fury", "Geo_Rock-King's_Pass_Hidden", "Geo_Rock-King's_Pass_Collapse", "Geo_Rock-King's_Pass_Above_Fury", 'Geo_Rock-Waterways_Tuk', 'Geo_Rock-Waterways_Tuk_Alcove', 'Geo_Rock-Waterways_Left', 'Geo_Rock-Waterways_East', 'Geo_Rock-Waterways_Flukemarm', 'Boss_Geo-Massive_Moss_Charger', 'Boss_Geo-Gorgeous_Husk', 'Boss_Geo-Sanctum_Soul_Warrior', 'Boss_Geo-Elegant_Soul_Warrior', 'Boss_Geo-Crystal_Guardian', 'Boss_Geo-Enraged_Guardian', 'Boss_Geo-Gruz_Mother', 'Boss_Geo-Vengefly_King', 'Soul_Totem-Basin', 'Soul_Totem-Cliffs_Main', 'Soul_Totem-Cliffs_Gorb', "Soul_Totem-Cliffs_Joni's", 'Soul_Totem-Crossroads_Goam_Journal', 'Soul_Totem-Crossroads_Shops', 'Soul_Totem-Crossroads_Mawlek_Upper', 'Soul_Totem-Crossroads_Acid', 'Soul_Totem-Crossroads_Mawlek_Lower', 'Soul_Totem-Crossroads_Myla', 'Soul_Totem-Ancestral_Mound', 'Soul_Totem-Distant_Village', 'Soul_Totem-Deepnest_Vessel', 'Soul_Totem-Mask_Maker', "Soul_Totem-Lower_Kingdom's_Edge_1", "Soul_Totem-Lower_Kingdom's_Edge_2", "Soul_Totem-Upper_Kingdom's_Edge", "Soul_Totem-Kingdom's_Edge_Camp", 'Soul_Totem-Oro_Dive_2', 'Soul_Totem-Oro_Dive_1', 'Soul_Totem-Oro', 'Soul_Totem-420_Geo_Rock', "Soul_Totem-Beast's_Den", "Soul_Totem-Greenpath_Hunter's_Journal", 'Soul_Totem-Greenpath_MMC', 'Soul_Totem-Greenpath_Below_Toll', "Soul_Totem-Before_Pilgrim's_Way", "Soul_Totem-Pilgrim's_Way", 'Soul_Totem-Fungal_Core', "Soul_Totem-Top_Left_Queen's_Gardens", 'Soul_Totem-Below_Marmu', 'Soul_Totem-Upper_Crystal_Peak', 'Soul_Totem-Hallownest_Crown', 'Soul_Totem-Outside_Crystallized_Mound', 'Soul_Totem-Crystal_Heart_1', 'Soul_Totem-Crystal_Heart_2', 'Soul_Totem-Crystallized_Mound', 'Soul_Totem-Resting_Grounds', 'Soul_Totem-Below_Xero', 'Soul_Totem-Sanctum_Below_Soul_Master', 'Soul_Totem-Sanctum_Below_Chest', 'Soul_Totem-Sanctum_Above_Grub', 'Soul_Totem-Waterways_Entrance', 'Soul_Totem-Top_Left_Waterways', 'Soul_Totem-Waterways_East', 'Soul_Totem-Waterways_Flukemarm', 'Soul_Totem-White_Palace_Entrance', 'Soul_Totem-White_Palace_Hub', 'Soul_Totem-White_Palace_Left', 'Soul_Totem-White_Palace_Final', 'Soul_Totem-White_Palace_Right', 'Soul_Totem-Path_of_Pain_Below_Lever', 'Soul_Totem-Path_of_Pain_Left_of_Lever', 'Soul_Totem-Path_of_Pain_Entrance', 'Soul_Totem-Path_of_Pain_Second', 'Soul_Totem-Path_of_Pain_Hidden', 'Soul_Totem-Path_of_Pain_Below_Thornskip', 'Soul_Totem-Path_of_Pain_Final', 'Soul_Totem-Pale_Lurker', 'Lore_Tablet-City_Entrance', 'Lore_Tablet-Pleasure_House', 'Lore_Tablet-Sanctum_Entrance', 'Lore_Tablet-Sanctum_Past_Soul_Master', "Lore_Tablet-Watcher's_Spire", 'Lore_Tablet-Archives_Upper', 'Lore_Tablet-Archives_Left', 'Lore_Tablet-Archives_Right', "Lore_Tablet-Pilgrim's_Way_1", "Lore_Tablet-Pilgrim's_Way_2", 'Lore_Tablet-Mantis_Outskirts', 'Lore_Tablet-Mantis_Village', 'Lore_Tablet-Greenpath_Upper_Hidden', 'Lore_Tablet-Greenpath_Below_Toll', 'Lore_Tablet-Greenpath_Lifeblood', 'Lore_Tablet-Greenpath_Stag', 'Lore_Tablet-Greenpath_QG', 'Lore_Tablet-Greenpath_Lower_Hidden', 'Lore_Tablet-Dung_Defender', 'Lore_Tablet-Spore_Shroom', 'Lore_Tablet-Fungal_Wastes_Hidden', 'Lore_Tablet-Fungal_Wastes_Below_Shrumal_Ogres', 'Lore_Tablet-Fungal_Core', 'Lore_Tablet-Ancient_Basin', "Lore_Tablet-King's_Pass_Focus", "Lore_Tablet-King's_Pass_Fury", "Lore_Tablet-King's_Pass_Exit", 'Lore_Tablet-World_Sense', 'Lore_Tablet-Howling_Cliffs', "Lore_Tablet-Kingdom's_Edge", 'Lore_Tablet-Palace_Workshop', 'Lore_Tablet-Palace_Throne', 'Lore_Tablet-Path_of_Pain_Entrance'] diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 544e710741b4..513d85257b97 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -821,7 +821,8 @@ async def verifyItems(self): def finishedGame(ctx: KH2Context, message): if ctx.kh2slotdata['FinalXemnas'] == 1: - if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked: + if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \ + & 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0: ctx.final_xemnas = True # three proofs if ctx.kh2slotdata['Goal'] == 0: diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index 3e656b418bfc..cb3d7c8d85ed 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -2,22 +2,7 @@ from BaseClasses import Item from .Names import ItemName - - -class KH2Item(Item): - game: str = "Kingdom Hearts 2" - - -class ItemData(typing.NamedTuple): - quantity: int = 0 - kh2id: int = 0 - # Save+ mem addr - memaddr: int = 0 - # some items have bitmasks. if bitmask>0 bitor to give item else - bitmask: int = 0 - # if ability then - ability: bool = False - +from .Subclasses import ItemData # 0x130000 Reports_Table = { @@ -209,7 +194,7 @@ class ItemData(typing.NamedTuple): ItemName.GrandRibbon: ItemData(1, 157, 0x35D4), } Usefull_Table = { - ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per + ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696), ItemName.UnknownDisk: ItemData(1, 462, 0x365F), @@ -349,7 +334,7 @@ class ItemData(typing.NamedTuple): Wincon_Table = { ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item - ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), + # ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14 # ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster } diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 9d7d948443cd..100e971e7dff 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1,19 +1,9 @@ import typing from BaseClasses import Location -from .Names import LocationName, ItemName - - -class KH2Location(Location): - game: str = "Kingdom Hearts 2" - - -class LocationData(typing.NamedTuple): - locid: int - yml: str - charName: str = "Sora" - charNumber: int = 1 - +from .Names import LocationName, ItemName, RegionName +from .Subclasses import LocationData +from .Regions import KH2REGIONS # data's addrcheck sys3 addr obtained roomid bit index is eventid LoD_Checks = { @@ -541,7 +531,7 @@ class LocationData(typing.NamedTuple): LocationName.Xemnas1: LocationData(26, "Double Get Bonus"), LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"), LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"), - LocationName.FinalXemnas: LocationData(71, "Get Bonus"), + # LocationName.FinalXemnas: LocationData(71, "Get Bonus"), LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"), } @@ -806,74 +796,75 @@ class LocationData(typing.NamedTuple): } event_location_to_item = { - LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, - LocationName.McpEventLocation: ItemName.McpEvent, + LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, + LocationName.McpEventLocation: ItemName.McpEvent, # LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent, - LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, - LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, - LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, - LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, - LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, - LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, - LocationName.CerberusEventLocation: ItemName.CerberusEvent, - LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, - LocationName.HydraEventLocation: ItemName.HydraEvent, + LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, + LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, + LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, + LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, + LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, + LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, + LocationName.CerberusEventLocation: ItemName.CerberusEvent, + LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, + LocationName.HydraEventLocation: ItemName.HydraEvent, LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent, - LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, - LocationName.HadesEventLocation: ItemName.HadesEvent, + LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, + LocationName.HadesEventLocation: ItemName.HadesEvent, # LocationName.ASZexionEventLocation: ItemName.ASZexionEvent, - LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, - LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, - LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, + LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, + LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, + LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, # LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation, - LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, - LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, - LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, - LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, + LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, + LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, + LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, + LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, # LocationName.ASVexenEventLocation: ItemName.ASVexenEvent, - LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, - LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, - LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, - LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, - LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, - LocationName.RoxasEventLocation: ItemName.RoxasEvent, - LocationName.XigbarEventLocation: ItemName.XigbarEvent, - LocationName.LuxordEventLocation: ItemName.LuxordEvent, - LocationName.SaixEventLocation: ItemName.SaixEvent, - LocationName.XemnasEventLocation: ItemName.XemnasEvent, - LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, - LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, + LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, + LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, + LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, + LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, + LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, + LocationName.RoxasEventLocation: ItemName.RoxasEvent, + LocationName.XigbarEventLocation: ItemName.XigbarEvent, + LocationName.LuxordEventLocation: ItemName.LuxordEvent, + LocationName.SaixEventLocation: ItemName.SaixEvent, + LocationName.XemnasEventLocation: ItemName.XemnasEvent, + LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, + LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, # LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent, - LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, - LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, - LocationName.BeastEventLocation: ItemName.BeastEvent, - LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, - LocationName.XaldinEventLocation: ItemName.XaldinEvent, - LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, - LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, - LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, + LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, + LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, + LocationName.BeastEventLocation: ItemName.BeastEvent, + LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, + LocationName.XaldinEventLocation: ItemName.XaldinEvent, + LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, + LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, + LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, # LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent, - LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, - LocationName.ScarEventLocation: ItemName.ScarEvent, - LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, - LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, - LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, - LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, - LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, - LocationName.SephiEventLocation: ItemName.SephiEvent, - LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, - LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, - LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, - LocationName.TransportEventLocation: ItemName.TransportEvent, - LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, - LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, + LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, + LocationName.ScarEventLocation: ItemName.ScarEvent, + LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, + LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, + LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, + LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, + LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, + LocationName.SephiEventLocation: ItemName.SephiEvent, + LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, + LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, + LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, + LocationName.TransportEventLocation: ItemName.TransportEvent, + LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, + LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, # LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent, - LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, - LocationName.TerraEventLocation: ItemName.TerraEvent, - LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, - LocationName.Axel1EventLocation: ItemName.Axel1Event, - LocationName.Axel2EventLocation: ItemName.Axel2Event, - LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, + LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, + LocationName.TerraEventLocation: ItemName.TerraEvent, + LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, + LocationName.Axel1EventLocation: ItemName.Axel1Event, + LocationName.Axel2EventLocation: ItemName.Axel2Event, + LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, + LocationName.FinalXemnasEventLocation: ItemName.Victory, } all_weapon_slot = { LocationName.FAKESlot, @@ -1361,3 +1352,9 @@ class LocationData(typing.NamedTuple): location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest" } } + +location_groups: typing.Dict[str, list] +location_groups = { + Region_Name: [loc for loc in Region_Locs if "Event" not in loc] + for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs and "Event" not in Region_Locs[0] +} diff --git a/worlds/kh2/Logic.py b/worlds/kh2/Logic.py index 1f13aa5f029c..1e6c403106d1 100644 --- a/worlds/kh2/Logic.py +++ b/worlds/kh2/Logic.py @@ -606,11 +606,11 @@ ItemName.LimitForm: 1, } final_leveling_access = { - LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.RoxasEventLocation, LocationName.GrimReaper2, LocationName.Xaldin, LocationName.StormRider, - LocationName.SunsetTerraceAbilityRing + LocationName.UndergroundConcourseMythrilGem } multi_form_region_access = { diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 7ba7c0082d17..b7caf7437007 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -2,7 +2,7 @@ from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool -from worlds.kh2 import default_itempool_option +from . import default_itempool_option class SoraEXP(Range): diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 6dd8313107fe..235500ec89e4 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1,9 +1,11 @@ import typing from BaseClasses import MultiWorld, Region +from . import Locations -from .Locations import KH2Location, event_location_to_item -from . import LocationName, RegionName, Events_Table +from .Subclasses import KH2Location +from .Names import LocationName, RegionName +from .Items import Events_Table KH2REGIONS: typing.Dict[str, typing.List[str]] = { "Menu": [], @@ -788,7 +790,7 @@ LocationName.ArmoredXemnas2EventLocation ], RegionName.FinalXemnas: [ - LocationName.FinalXemnas + LocationName.FinalXemnasEventLocation ], RegionName.DataXemnas: [ LocationName.XemnasDataPowerBoost, @@ -1020,7 +1022,8 @@ def create_regions(self): multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in KH2REGIONS.items()] # fill the event locations with events - for location, item in event_location_to_item.items(): + + for location, item in Locations.event_location_to_item.items(): multiworld.get_location(location, player).place_locked_item( multiworld.worlds[player].create_event_item(item)) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 7c5551dbd563..1124f8109c54 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -1,7 +1,7 @@ from typing import Dict, Callable, TYPE_CHECKING from BaseClasses import CollectionState -from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table +from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks from .Names import LocationName, ItemName, RegionName from worlds.generic.Rules import add_rule, forbid_items, add_item_rule @@ -83,6 +83,8 @@ def hundred_acre_unlocked(self, state: CollectionState, amount) -> bool: return state.has(ItemName.TornPages, self.player, amount) def level_locking_unlock(self, state: CollectionState, amount): + if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player): + return True return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]]) def summon_levels_unlocked(self, state: CollectionState, amount) -> bool: @@ -224,7 +226,7 @@ def __init__(self, kh2world: KH2World) -> None: RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), RegionName.Ag: lambda state: self.ag_unlocked(state, 1), - RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state), RegionName.Bc: lambda state: self.bc_unlocked(state, 1), RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), @@ -266,9 +268,11 @@ def set_kh2_rules(self) -> None: add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) elif location.name in Donald_Checks: add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) + else: + add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) + final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) if self.multiworld.FinalXemnas[self.player]: @@ -417,7 +421,7 @@ def __init__(self, world: KH2World) -> None: RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state), RegionName.OldPete: lambda state: self.get_old_pete_rules(), RegionName.FuturePete: lambda state: self.get_future_pete_rules(state), - RegionName.Terra: lambda state: self.get_terra_rules(state), + RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player), RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state), RegionName.Barbosa: lambda state: self.get_barbosa_rules(state), RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(), diff --git a/worlds/kh2/Subclasses.py b/worlds/kh2/Subclasses.py new file mode 100644 index 000000000000..79f52c41c02a --- /dev/null +++ b/worlds/kh2/Subclasses.py @@ -0,0 +1,29 @@ +import typing + +from BaseClasses import Location, Item + + +class KH2Location(Location): + game: str = "Kingdom Hearts 2" + + +class LocationData(typing.NamedTuple): + locid: int + yml: str + charName: str = "Sora" + charNumber: int = 1 + + +class KH2Item(Item): + game: str = "Kingdom Hearts 2" + + +class ItemData(typing.NamedTuple): + quantity: int = 0 + kh2id: int = 0 + # Save+ mem addr + memaddr: int = 0 + # some items have bitmasks. if bitmask>0 bitor to give item else + bitmask: int = 0 + # if ability then + ability: bool = False diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 2bddbd5ec30e..d02614d3802e 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -12,6 +12,7 @@ from .Options import KingdomHearts2Options from .Regions import create_regions, connect_regions from .Rules import * +from .Subclasses import KH2Item def launch_client(): @@ -49,7 +50,9 @@ class KH2World(World): for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)} location_name_to_id = {item: location for location, item in enumerate(all_locations.keys(), 0x130000)} + item_name_groups = item_groups + location_name_groups = location_groups visitlocking_dict: Dict[str, int] plando_locations: Dict[str, str] @@ -253,11 +256,8 @@ def generate_early(self) -> None: self.goofy_gen_early() self.keyblade_gen_early() - if self.multiworld.FinalXemnas[self.player]: - self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory - else: - self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name - self.total_locations -= 1 + # final xemnas isn't a location anymore + # self.total_locations -= 1 if self.options.WeaponSlotStartHint: for location in all_weapon_slot: diff --git a/worlds/ladx/LADXR/utils.py b/worlds/ladx/LADXR/utils.py index fcf1d2bb56e7..5f8b2685550d 100644 --- a/worlds/ladx/LADXR/utils.py +++ b/worlds/ladx/LADXR/utils.py @@ -146,7 +146,7 @@ def setReplacementName(key: str, value: str) -> None: def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes: instr = instr.format(**_NAMES) - s = instr.encode("ascii") + s = instr.encode("ascii", errors="replace") s = s.replace(b"'", b"^") def padLine(line: bytes) -> bytes: @@ -169,7 +169,7 @@ def padLine(line: bytes) -> bytes: if result_line: result += padLine(result_line) if ask is not None: - askbytes = ask.encode("ascii") + askbytes = ask.encode("ascii", errors="replace") result = result.rstrip() while len(result) % 32 != 16: result += b' ' diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 691891c0b350..117242208be2 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -399,6 +399,26 @@ class Palette(Choice): option_pink = 4 option_inverted = 5 +class Music(Choice, LADXROption): + """ + [Vanilla] Regular Music + [Shuffled] Shuffled Music + [Off] No music + """ + ladxr_name = "music" + option_vanilla = 0 + option_shuffled = 1 + option_off = 2 + + + def to_ladxr_option(self, all_options): + s = "" + if self.value == self.option_shuffled: + s = "random" + elif self.value == self.option_off: + s = "off" + return self.ladxr_name, s + class WarpImprovements(DefaultOffToggle): """ [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. @@ -444,6 +464,7 @@ class AdditionalWarpPoints(DefaultOffToggle): 'shuffle_maps': ShuffleMaps, 'shuffle_compasses': ShuffleCompasses, 'shuffle_stone_beaks': ShuffleStoneBeaks, + 'music': Music, 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md index 9f453c146de3..32e46a4b3354 100644 --- a/worlds/landstalker/docs/landstalker_setup_en.md +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings -and export a config file from them. +The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows +you to easily configure your personal settings ## How-to-play diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 088967445007..2f9354193250 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -82,9 +82,8 @@ def create_items(self): skips = int(non_traps * skip_percentage / 100.0) non_skips = non_traps - skips - filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] for i in range(0, non_skips): - pool.append(self.create_item(filler_list[i % len(filler_list)])) + pool.append(self.create_item(self.get_filler_item_name())) for i in range(0, skips): pool.append(self.create_item("Puzzle Skip")) @@ -130,3 +129,7 @@ def fill_slot_data(self): slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data + + def get_filler_item_name(self) -> str: + filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] + return self.random.choice(filler_list) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index ea5886fea00e..1a149f2db9f0 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -278,10 +278,10 @@ tag: forbid check: True achievement: The Seeker - BEAR: + BEAR (1): id: Heteronym Room/Panel_bear_bear tag: midwhite - MINE: + MINE (1): id: Heteronym Room/Panel_mine_mine tag: double midwhite subtag: left @@ -297,7 +297,7 @@ DOES: id: Heteronym Room/Panel_does_does tag: midwhite - MOBILE: + MOBILE (1): id: Heteronym Room/Panel_mobile_mobile tag: double midwhite subtag: left @@ -399,8 +399,7 @@ door: Crossroads Entrance The Tenacious: door: Tenacious Entrance - Warts Straw Area: - door: Symmetry Door + Near Far Area: True Hedge Maze: door: Shortcut to Hedge Maze Orange Tower First Floor: @@ -427,14 +426,6 @@ id: Palindrome Room/Panel_slaughter_laughter colors: red tag: midred - NEAR: - id: Symmetry Room/Panel_near_far - colors: black - tag: botblack - FAR: - id: Symmetry Room/Panel_far_near - colors: black - tag: botblack TRACE: id: Maze Room/Panel_trace_trace tag: midwhite @@ -477,14 +468,6 @@ group: Entrances to The Tenacious panels: - SLAUGHTER - Symmetry Door: - id: - - Symmetry Room Area Doors/Door_near_far - - Symmetry Room Area Doors/Door_far_near - group: Symmetry Doors - panels: - - NEAR - - FAR Shortcut to Hedge Maze: id: Maze Area Doors/Door_trace_trace group: Hedge Maze Doors @@ -548,7 +531,7 @@ id: Lingo Room/Panel_shortcut colors: yellow tag: midyellow - PILGRIMAGE: + PILGRIM: id: Lingo Room/Panel_pilgrim colors: blue tag: midblue @@ -569,7 +552,7 @@ Exit: event: True panels: - - PILGRIMAGE + - PILGRIM Pilgrim Room: entrances: The Seeker: @@ -755,7 +738,7 @@ panels: - TURN - room: Orange Tower Fourth Floor - panel: RUNT + panel: RUNT (1) Words Sword Door: id: - Shuffle Room Area Doors/Door_words_shuffle_3 @@ -962,11 +945,36 @@ - LEVEL (White) - RACECAR (White) - SOLOS (White) + Near Far Area: + entrances: + Hub Room: True + Warts Straw Area: + door: Door + panels: + NEAR: + id: Symmetry Room/Panel_near_far + colors: black + tag: botblack + FAR: + id: Symmetry Room/Panel_far_near + colors: black + tag: botblack + doors: + Door: + id: + - Symmetry Room Area Doors/Door_near_far + - Symmetry Room Area Doors/Door_far_near + group: Symmetry Doors + item_name: Symmetry Room - Near Far Door + location_name: Symmetry Room - NEAR, FAR + panels: + - NEAR + - FAR Warts Straw Area: entrances: - Hub Room: - room: Hub Room - door: Symmetry Door + Near Far Area: + room: Near Far Area + door: Door Leaf Feel Area: door: Door panels: @@ -984,6 +992,8 @@ - Symmetry Room Area Doors/Door_warts_straw - Symmetry Room Area Doors/Door_straw_warts group: Symmetry Doors + item_name: Symmetry Room - Warts Straw Door + location_name: Symmetry Room - WARTS, STRAW panels: - WARTS - STRAW @@ -1009,6 +1019,8 @@ - Symmetry Room Area Doors/Door_leaf_feel - Symmetry Room Area Doors/Door_feel_leaf group: Symmetry Doors + item_name: Symmetry Room - Leaf Feel Door + location_name: Symmetry Room - LEAF, FEEL panels: - LEAF - FEEL @@ -1118,7 +1130,13 @@ id: Cross Room/Panel_north_missing colors: green tag: forbid - required_room: Outside The Bold + required_panel: + - room: Outside The Bold + panel: SOUND + - room: Outside The Bold + panel: YEAST + - room: Outside The Bold + panel: WET DIAMONDS: id: Cross Room/Panel_diamonds_missing colors: green @@ -1160,7 +1178,7 @@ group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: PURPLE Hallway Door: id: Red Blue Purple Room Area Doors/Door_room_2 @@ -1430,7 +1448,7 @@ entrances: The Perceptive: True panels: - NAPS: + SPAN: id: Naps Room/Panel_naps_span colors: black tag: midblack @@ -1456,7 +1474,7 @@ location_name: The Fearless - First Floor Puzzles group: Fearless Doors panels: - - NAPS + - SPAN - TEAM - TEEM - IMPATIENT @@ -1558,11 +1576,11 @@ required_door: door: Stairs achievement: The Observant - BACK: + FOUR (1): id: Look Room/Panel_four_back colors: green tag: forbid - SIDE: + FOUR (2): id: Look Room/Panel_four_side colors: green tag: forbid @@ -1572,87 +1590,87 @@ hunt: True required_door: door: Backside Door - STAIRS: + SIX: id: Look Room/Panel_six_stairs colors: green tag: forbid - WAYS: + FOUR (3): id: Look Room/Panel_four_ways colors: green tag: forbid - "ON": + TWO (1): id: Look Room/Panel_two_on colors: green tag: forbid - UP: + TWO (2): id: Look Room/Panel_two_up colors: green tag: forbid - SWIMS: + FIVE: id: Look Room/Panel_five_swims colors: green tag: forbid - UPSTAIRS: + BELOW (1): id: Look Room/Panel_eight_upstairs colors: green tag: forbid required_door: door: Stairs - TOIL: + BLUE: id: Look Room/Panel_blue_toil colors: green tag: forbid required_door: door: Stairs - STOP: + BELOW (2): id: Look Room/Panel_four_stop colors: green tag: forbid required_door: door: Stairs - TOP: + MINT (1): id: Look Room/Panel_aqua_top colors: green tag: forbid required_door: door: Stairs - HI: + ESACREWOL: id: Look Room/Panel_blue_hi colors: green tag: forbid required_door: door: Stairs - HI (2): + EULB: id: Look Room/Panel_blue_hi2 colors: green tag: forbid required_door: door: Stairs - "31": + NUMBERS (1): id: Look Room/Panel_numbers_31 colors: green tag: forbid required_door: door: Stairs - "52": + NUMBERS (2): id: Look Room/Panel_numbers_52 colors: green tag: forbid required_door: door: Stairs - OIL: + MINT (2): id: Look Room/Panel_aqua_oil colors: green tag: forbid required_door: door: Stairs - BACKSIDE (GREEN): + GREEN (1): id: Look Room/Panel_eight_backside colors: green tag: forbid required_door: door: Stairs - SIDEWAYS: + GREEN (2): id: Look Room/Panel_eight_sideways colors: green tag: forbid @@ -1663,13 +1681,13 @@ id: Maze Area Doors/Door_backside group: Backside Doors panels: - - BACK - - SIDE + - FOUR (1) + - FOUR (2) Stairs: id: Maze Area Doors/Door_stairs group: Observant Doors panels: - - STAIRS + - SIX The Incomparable: entrances: The Observant: True # Assuming that access to The Observant includes access to the right entrance @@ -1951,7 +1969,7 @@ group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: RED Rhyme Room Entrance: id: Double Room Area Doors/Door_room_entry_stairs2 @@ -1969,9 +1987,9 @@ - Color Arrow Room Doors/Door_orange_hider_1 - Color Arrow Room Doors/Door_orange_hider_2 - Color Arrow Room Doors/Door_orange_hider_3 - location_name: Color Hunt - RED and YELLOW - group: Champion's Rest - Color Barriers - item_name: Champion's Rest - Orange Barrier + location_name: Color Barriers - RED and YELLOW + group: Color Hunt Barriers + item_name: Color Hunt - Orange Barrier panels: - RED - room: Directional Gallery @@ -1999,7 +2017,7 @@ Courtyard: True Roof: True # through the sunwarp panels: - RUNT: + RUNT (1): id: Shuffle Room/Panel_turn_runt2 colors: yellow tag: midyellow @@ -2213,6 +2231,7 @@ - Master Room Doors/Door_master_down - Master Room Doors/Door_master_down2 skip_location: True + item_name: Mastery panels: - THE MASTER Mastery Panels: @@ -2229,25 +2248,25 @@ panel: MASTERY - room: Hedge Maze panel: MASTERY (1) - - room: Roof - panel: MASTERY (1) - - room: Roof - panel: MASTERY (2) + - room: Behind A Smile + panel: MASTERY + - room: Sixteen Colorful Squares + panel: MASTERY - MASTERY - room: Hedge Maze panel: MASTERY (2) - - room: Roof - panel: MASTERY (3) - - room: Roof - panel: MASTERY (4) - - room: Roof - panel: MASTERY (5) + - room: Among Treetops + panel: MASTERY + - room: Horizon's Edge + panel: MASTERY + - room: Beneath The Lookout + panel: MASTERY - room: Elements Area panel: MASTERY - room: Pilgrim Antechamber panel: MASTERY - - room: Roof - panel: MASTERY (6) + - room: Rooftop Staircase + panel: MASTERY paintings: - id: map_painting2 orientation: north @@ -2259,52 +2278,75 @@ Crossroads: room: Crossroads door: Roof Access + Behind A Smile: + entrances: + Roof: True panels: - MASTERY (1): + MASTERY: id: Master Room/Panel_mastery_mastery6 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - MASTERY (2): + STAIRCASE: + id: Open Areas/Panel_staircase + tag: midwhite + Sixteen Colorful Squares: + entrances: + Roof: True + panels: + MASTERY: id: Master Room/Panel_mastery_mastery7 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - MASTERY (3): + Among Treetops: + entrances: + Roof: True + panels: + MASTERY: id: Master Room/Panel_mastery_mastery10 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - MASTERY (4): + Horizon's Edge: + entrances: + Roof: True + panels: + MASTERY: id: Master Room/Panel_mastery_mastery11 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - MASTERY (5): + Beneath The Lookout: + entrances: + Roof: True + panels: + MASTERY: id: Master Room/Panel_mastery_mastery12 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - MASTERY (6): + Rooftop Staircase: + entrances: + Roof: True + panels: + MASTERY: id: Master Room/Panel_mastery_mastery15 tag: midwhite hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery - STAIRCASE: - id: Open Areas/Panel_staircase - tag: midwhite Orange Tower Basement: entrances: Orange Tower Sixth Floor: @@ -2376,7 +2418,7 @@ group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: GREEN paintings: - id: flower_painting_7 @@ -2626,21 +2668,13 @@ tag: forbid doors: Progress Door: - id: - - Doorway Room Doors/Door_gray - - Doorway Room Doors/Door_gray2 # See comment below + id: Doorway Room Doors/Door_gray item_name: The Colorful - Gray Door location_name: The Colorful - Gray group: Colorful Doors panels: - OBSTACLE The Colorful: - # The set of required_doors in the achievement panel should prevent - # generation from asking you to solve The Colorful before opening all of the - # doors. Access from the roof is included so that the painting here could be - # an entrance. The client will have to be hardcoded to not open the door to - # the achievement until all of the doors are open, whether by solving the - # panels or through receiving items. entrances: The Colorful (Gray): room: The Colorful (Gray) @@ -2651,31 +2685,53 @@ id: Countdown Panels/Panel_colorful_colorful check: True tag: forbid - required_door: + required_panel: - room: The Colorful (White) - door: Progress Door + panel: BEGIN - room: The Colorful (Black) - door: Progress Door + panel: FOUND - room: The Colorful (Red) - door: Progress Door + panel: LOAF - room: The Colorful (Yellow) - door: Progress Door + panel: CREAM - room: The Colorful (Blue) - door: Progress Door + panel: SUN - room: The Colorful (Purple) - door: Progress Door + panel: SPOON - room: The Colorful (Orange) - door: Progress Door + panel: LETTERS - room: The Colorful (Green) - door: Progress Door + panel: WALLS - room: The Colorful (Brown) - door: Progress Door + panel: IRON - room: The Colorful (Gray) - door: Progress Door + panel: OBSTACLE achievement: The Colorful paintings: - id: arrows_painting_12 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 Welcome Back Area: entrances: Starting Room: @@ -2762,6 +2818,7 @@ door: Exit Eight Alcove: door: Eight Door + The Optimistic: True panels: SEVEN (1): id: Backside Room/Panel_seven_seven_5 @@ -2811,21 +2868,11 @@ id: Rhyme Room/Panel_locked_knocked colors: purple tag: midpurp - BACKSIDE: - id: Backside Room/Panel_backside_1 - tag: midwhite - The Optimistic: - id: Countdown Panels/Panel_optimistic_optimistic - check: True - tag: forbid - required_door: - door: Backsides - achievement: The Optimistic - PAST: + PAST (1): id: Shuffle Room/Panel_past_present colors: brown tag: botbrown - FUTURE: + FUTURE (1): id: Shuffle Room/Panel_future_present colors: - brown @@ -2871,14 +2918,14 @@ group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: BLUE Orange Barrier: id: Color Arrow Room Doors/Door_orange_3 group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: ORANGE Initiated Entrance: id: Red Blue Purple Room Area Doors/Door_locked_knocked @@ -2890,9 +2937,9 @@ # containing region. Green Barrier: id: Color Arrow Room Doors/Door_green_hider_1 - location_name: Color Hunt - BLUE and YELLOW - item_name: Champion's Rest - Green Barrier - group: Champion's Rest - Color Barriers + location_name: Color Barriers - BLUE and YELLOW + item_name: Color Hunt - Green Barrier + group: Color Hunt Barriers panels: - BLUE - room: Directional Gallery @@ -2902,9 +2949,9 @@ - Color Arrow Room Doors/Door_purple_hider_1 - Color Arrow Room Doors/Door_purple_hider_2 - Color Arrow Room Doors/Door_purple_hider_3 - location_name: Color Hunt - RED and BLUE - item_name: Champion's Rest - Purple Barrier - group: Champion's Rest - Color Barriers + location_name: Color Barriers - RED and BLUE + item_name: Color Hunt - Purple Barrier + group: Color Hunt Barriers panels: - BLUE - room: Orange Tower Third Floor @@ -2914,7 +2961,7 @@ - Color Arrow Room Doors/Door_all_hider_1 - Color Arrow Room Doors/Door_all_hider_2 - Color Arrow Room Doors/Door_all_hider_3 - location_name: Color Hunt - GREEN, ORANGE and PURPLE + location_name: Color Barriers - GREEN, ORANGE and PURPLE item_name: Champion's Rest - Entrance panels: - ORANGE @@ -2922,17 +2969,6 @@ panel: GREEN - room: Outside The Agreeable panel: PURPLE - Backsides: - event: True - panels: - - room: The Observant - panel: BACKSIDE - - room: Yellow Backside Area - panel: BACKSIDE - - room: Directional Gallery - panel: BACKSIDE - - room: The Bearer - panel: BACKSIDE Eight Door: id: Red Blue Purple Room Area Doors/Door_a_strands2 skip_location: True @@ -3042,6 +3078,28 @@ id: Rhyme Room/Panel_bed_dead colors: purple tag: toppurp + The Optimistic: + entrances: + Outside The Initiated: True + panels: + BACKSIDE: + id: Backside Room/Panel_backside_1 + tag: midwhite + Achievement: + id: Countdown Panels/Panel_optimistic_optimistic + check: True + tag: forbid + required_panel: + - panel: BACKSIDE + - room: The Observant + panel: BACKSIDE + - room: Yellow Backside Area + panel: BACKSIDE + - room: Directional Gallery + panel: BACKSIDE + - room: The Bearer + panel: BACKSIDE + achievement: The Optimistic The Traveled: entrances: Hub Room: @@ -3142,7 +3200,7 @@ Outside The Undeterred: True Crossroads: True Hedge Maze: True - Outside The Initiated: True # backside + The Optimistic: True # backside Directional Gallery: True # backside Yellow Backside Area: True The Bearer: @@ -3154,12 +3212,12 @@ Outside The Bold: entrances: Color Hallways: True - Champion's Rest: - room: Champion's Rest + Color Hunt: + room: Color Hunt door: Shortcut to The Steady The Bearer: room: The Bearer - door: Shortcut to The Bold + door: Entrance Directional Gallery: # There is a painting warp here from the Directional Gallery, but it # only appears when the sixes are revealed. It could be its own item if @@ -3230,7 +3288,7 @@ tag: midwhite required_door: door: Stargazer Door - MOUTH: + SOUND: id: Cross Room/Panel_mouth_south colors: purple tag: midpurp @@ -3574,7 +3632,7 @@ id: Blue Room/Panel_bone_skeleton colors: blue tag: botblue - EYE: + EYE (1): id: Blue Room/Panel_mouth_face colors: blue tag: double botblue @@ -3980,7 +4038,7 @@ group: Color Hunt Barriers skip_location: True panels: - - room: Champion's Rest + - room: Color Hunt panel: YELLOW paintings: - id: smile_painting_7 @@ -3998,12 +4056,15 @@ orientation: south - id: cherry_painting orientation: east - Champion's Rest: + Color Hunt: entrances: Outside The Bold: door: Shortcut to The Steady Orange Tower Fourth Floor: True # sunwarp Roof: True # through ceiling of sunwarp + Champion's Rest: + room: Outside The Initiated + door: Entrance panels: EXIT: id: Rock Room/Panel_red_red @@ -4044,11 +4105,28 @@ required_door: room: Orange Tower Third Floor door: Orange Barrier - YOU: - id: Color Arrow Room/Panel_you + doors: + Shortcut to The Steady: + id: Rock Room Doors/Door_hint + panels: + - EXIT + paintings: + - id: arrows_painting_7 + orientation: east + - id: fruitbowl_painting3 + orientation: west + enter_only: True required_door: room: Outside The Initiated door: Entrance + Champion's Rest: + entrances: + Color Hunt: + room: Outside The Initiated + door: Entrance + panels: + YOU: + id: Color Arrow Room/Panel_you check: True colors: gray tag: forbid @@ -4056,53 +4134,24 @@ id: Color Arrow Room/Panel_me colors: gray tag: forbid - required_door: - room: Outside The Initiated - door: Entrance SECRET BLUE: # Pretend this and the other two are white, because they are snipes. # TODO: Extract them and randomize them? id: Color Arrow Room/Panel_secret_blue tag: forbid - required_door: - room: Outside The Initiated - door: Entrance SECRET YELLOW: id: Color Arrow Room/Panel_secret_yellow tag: forbid - required_door: - room: Outside The Initiated - door: Entrance SECRET RED: id: Color Arrow Room/Panel_secret_red tag: forbid - required_door: - room: Outside The Initiated - door: Entrance - doors: - Shortcut to The Steady: - id: Rock Room Doors/Door_hint - panels: - - EXIT paintings: - - id: arrows_painting_7 - orientation: east - - id: fruitbowl_painting3 - orientation: west - enter_only: True - required_door: - room: Outside The Initiated - door: Entrance - id: colors_painting orientation: south - enter_only: True - required_door: - room: Outside The Initiated - door: Entrance The Bearer: entrances: Outside The Bold: - door: Shortcut to The Bold + door: Entrance Orange Tower Fifth Floor: room: Art Gallery door: Exit @@ -4179,7 +4228,7 @@ - yellow tag: mid red yellow doors: - Shortcut to The Bold: + Entrance: id: Red Blue Purple Room Area Doors/Door_middle_middle panels: - MIDDLE @@ -4202,9 +4251,6 @@ SIX: id: Backside Room/Panel_six_six_5 tag: midwhite - colors: - - red - - yellow hunt: True required_door: room: Number Hunt @@ -4280,9 +4326,6 @@ SIX: id: Backside Room/Panel_six_six_6 tag: midwhite - colors: - - red - - yellow hunt: True required_door: room: Number Hunt @@ -4314,21 +4357,21 @@ door: Side Area Shortcut Roof: True panels: - SNOW: + SMILE: id: Cross Room/Panel_smile_lime colors: - red - yellow tag: mid yellow red - SMILE: + required_panel: + room: The Bearer (North) + panel: WARTS + SNOW: id: Cross Room/Panel_snow_won colors: - red - yellow tag: mid red yellow - required_panel: - room: The Bearer (North) - panel: WARTS doors: Side Area Shortcut: event: True @@ -4404,9 +4447,14 @@ colors: blue tag: forbid required_panel: - room: The Bearer (West) - panel: SMILE - required_room: Outside The Bold + - room: The Bearer (West) + panel: SMILE + - room: Outside The Bold + panel: SOUND + - room: Outside The Bold + panel: YEAST + - room: Outside The Bold + panel: WET Cross Tower (South): entrances: # No roof access The Bearer (North): @@ -6117,7 +6165,7 @@ id: Painting Room/Panel_our_four colors: blue tag: midblue - ONE ROAD MANY TURNS: + ORDER: id: Painting Room/Panel_order_onepathmanyturns tag: forbid colors: @@ -6172,8 +6220,9 @@ Exit: id: Tower Room Area Doors/Door_painting_exit include_reduce: True + item_name: Orange Tower Fifth Floor - Quadruple Intersection panels: - - ONE ROAD MANY TURNS + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6191,7 +6240,6 @@ - Third Floor - Fourth Floor - Fifth Floor - - Exit Art Gallery (Second Floor): entrances: Art Gallery: @@ -6666,7 +6714,7 @@ - room: Rhyme Room (Target) panel: PISTOL - room: Rhyme Room (Target) - panel: QUARTZ + panel: GEM Rhyme Room (Target): entrances: Rhyme Room (Smiley): # one-way @@ -6694,7 +6742,7 @@ tag: syn rhyme subtag: top link: rhyme CRYSTAL - QUARTZ: + GEM: id: Double Room/Panel_crystal_syn colors: purple tag: syn rhyme @@ -6719,7 +6767,7 @@ group: Rhyme Room Doors panels: - PISTOL - - QUARTZ + - GEM - INNOVATIVE (Top) - INNOVATIVE (Bottom) paintings: @@ -6777,22 +6825,18 @@ id: Panel Room/Panel_room_floor_5 colors: gray tag: forbid - FLOOR (7): + FLOOR (6): id: Panel Room/Panel_room_floor_7 colors: gray tag: forbid - FLOOR (8): + FLOOR (7): id: Panel Room/Panel_room_floor_8 colors: gray tag: forbid - FLOOR (9): + FLOOR (8): id: Panel Room/Panel_room_floor_9 colors: gray tag: forbid - FLOOR (10): - id: Panel Room/Panel_room_floor_10 - colors: gray - tag: forbid CEILING (1): id: Panel Room/Panel_room_ceiling_1 colors: gray @@ -6813,6 +6857,10 @@ id: Panel Room/Panel_room_ceiling_5 colors: gray tag: forbid + CEILING (6): + id: Panel Room/Panel_room_floor_10 + colors: gray + tag: forbid WALL (1): id: Panel Room/Panel_room_wall_1 colors: gray @@ -6918,10 +6966,12 @@ MASTERY: id: Master Room/Panel_mastery_mastery tag: midwhite - colors: gray required_door: room: Orange Tower Seventh Floor door: Mastery + required_panel: + room: Room Room + panel: WALL (2) doors: Excavation: event: True @@ -7071,7 +7121,7 @@ id: Hangry Room/Panel_red_top_3 colors: red tag: topred - FLUMMOXED: + FLUSTERED: id: Hangry Room/Panel_red_top_4 colors: red tag: topred @@ -7574,7 +7624,7 @@ - black - blue tag: chain mid black blue - BREAD: + CHEESE: id: Challenge Room/Panel_bread_mold colors: brown tag: double botbrown @@ -7621,7 +7671,7 @@ id: Challenge Room/Panel_double_anagram_5 colors: yellow tag: midyellow - FACTS: + FACTS (Chain): id: Challenge Room/Panel_facts colors: - red @@ -7631,18 +7681,18 @@ id: Challenge Room/Panel_facts2 colors: red tag: forbid - FACTS (3): + FACTS (2): id: Challenge Room/Panel_facts3 tag: forbid - FACTS (4): + FACTS (3): id: Challenge Room/Panel_facts4 colors: blue tag: forbid - FACTS (5): + FACTS (4): id: Challenge Room/Panel_facts5 colors: blue tag: forbid - FACTS (6): + FACTS (5): id: Challenge Room/Panel_facts6 colors: blue tag: forbid diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 3239f21854c4..4cad94855512 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -31,12 +31,12 @@ panels: LIES: 444408 The Seeker: Achievement: 444409 - BEAR: 444410 - MINE: 444411 + BEAR (1): 444410 + MINE (1): 444411 MINE (2): 444412 BOW: 444413 DOES: 444414 - MOBILE: 444415 + MOBILE (1): 444415 MOBILE (2): 444416 DESERT: 444417 DESSERT: 444418 @@ -57,8 +57,6 @@ panels: Hub Room: ORDER: 444432 SLAUGHTER: 444433 - NEAR: 444434 - FAR: 444435 TRACE: 444436 RAT: 444437 OPEN: 444438 @@ -72,7 +70,7 @@ panels: EIGHT: 444445 Pilgrim Antechamber: HOT CRUST: 444446 - PILGRIMAGE: 444447 + PILGRIM: 444447 MASTERY: 444448 Pilgrim Room: THIS: 444449 @@ -123,6 +121,9 @@ panels: RACECAR (White): 444489 SOLOS (White): 444490 Achievement: 444491 + Near Far Area: + NEAR: 444434 + FAR: 444435 Warts Straw Area: WARTS: 444492 STRAW: 444493 @@ -187,7 +188,7 @@ panels: Achievement: 444546 GAZE: 444547 The Fearless (First Floor): - NAPS: 444548 + SPAN: 444548 TEAM: 444549 TEEM: 444550 IMPATIENT: 444551 @@ -208,25 +209,25 @@ panels: EVEN: 444564 The Observant: Achievement: 444565 - BACK: 444566 - SIDE: 444567 + FOUR (1): 444566 + FOUR (2): 444567 BACKSIDE: 444568 - STAIRS: 444569 - WAYS: 444570 - 'ON': 444571 - UP: 444572 - SWIMS: 444573 - UPSTAIRS: 444574 - TOIL: 444575 - STOP: 444576 - TOP: 444577 - HI: 444578 - HI (2): 444579 - '31': 444580 - '52': 444581 - OIL: 444582 - BACKSIDE (GREEN): 444583 - SIDEWAYS: 444584 + SIX: 444569 + FOUR (3): 444570 + TWO (1): 444571 + TWO (2): 444572 + FIVE: 444573 + BELOW (1): 444574 + BLUE: 444575 + BELOW (2): 444576 + MINT (1): 444577 + ESACREWOL: 444578 + EULB: 444579 + NUMBERS (1): 444580 + NUMBERS (2): 444581 + MINT (2): 444582 + GREEN (1): 444583 + GREEN (2): 444584 The Incomparable: Achievement: 444585 A (One): 444586 @@ -254,7 +255,7 @@ panels: RED: 444605 DEER + WREN: 444606 Orange Tower Fourth Floor: - RUNT: 444607 + RUNT (1): 444607 RUNT (2): 444608 LEARNS + UNSEW: 444609 HOT CRUSTS: 444610 @@ -279,14 +280,19 @@ panels: THE END: 444620 THE MASTER: 444621 MASTERY: 444622 - Roof: - MASTERY (1): 444623 - MASTERY (2): 444624 - MASTERY (3): 444625 - MASTERY (4): 444626 - MASTERY (5): 444627 - MASTERY (6): 444628 + Behind A Smile: + MASTERY: 444623 STAIRCASE: 444629 + Sixteen Colorful Squares: + MASTERY: 444624 + Among Treetops: + MASTERY: 444625 + Horizon's Edge: + MASTERY: 444626 + Beneath The Lookout: + MASTERY: 444627 + Rooftop Staircase: + MASTERY: 444628 Orange Tower Basement: MASTERY: 444630 THE LIBRARY: 444631 @@ -341,16 +347,17 @@ panels: ORANGE: 444663 UNCOVER: 444664 OXEN: 444665 - BACKSIDE: 444666 - The Optimistic: 444667 - PAST: 444668 - FUTURE: 444669 + PAST (1): 444668 + FUTURE (1): 444669 FUTURE (2): 444670 PAST (2): 444671 PRESENT: 444672 SMILE: 444673 ANGERED: 444674 VOTE: 444675 + The Optimistic: + BACKSIDE: 444666 + Achievement: 444667 The Initiated: Achievement: 444676 DAUGHTER: 444677 @@ -400,7 +407,7 @@ panels: ZEN: 444719 SON: 444720 STARGAZER: 444721 - MOUTH: 444722 + SOUND: 444722 YEAST: 444723 WET: 444724 The Bold: @@ -442,7 +449,7 @@ panels: The Undeterred: Achievement: 444759 BONE: 444760 - EYE: 444761 + EYE (1): 444761 MOUTH: 444762 IRIS: 444763 EYE (2): 444764 @@ -489,7 +496,7 @@ panels: WINDWARD: 444803 LIGHT: 444804 REWIND: 444805 - Champion's Rest: + Color Hunt: EXIT: 444806 HUES: 444807 RED: 444808 @@ -498,6 +505,7 @@ panels: GREEN: 444811 PURPLE: 444812 ORANGE: 444813 + Champion's Rest: YOU: 444814 ME: 444815 SECRET BLUE: 444816 @@ -523,8 +531,8 @@ panels: TENT: 444832 BOWL: 444833 The Bearer (West): - SNOW: 444834 - SMILE: 444835 + SMILE: 444834 + SNOW: 444835 Bearer Side Area: SHORTCUT: 444836 POTS: 444837 @@ -719,7 +727,7 @@ panels: TRUSTWORTHY: 444978 FREE: 444979 OUR: 444980 - ONE ROAD MANY TURNS: 444981 + ORDER: 444981 Art Gallery (Second Floor): HOUSE: 444982 PATH: 444983 @@ -777,7 +785,7 @@ panels: WILD: 445028 KID: 445029 PISTOL: 445030 - QUARTZ: 445031 + GEM: 445031 INNOVATIVE (Top): 445032 INNOVATIVE (Bottom): 445033 Room Room: @@ -791,15 +799,15 @@ panels: FLOOR (3): 445041 FLOOR (4): 445042 FLOOR (5): 445043 - FLOOR (7): 445044 - FLOOR (8): 445045 - FLOOR (9): 445046 - FLOOR (10): 445047 + FLOOR (6): 445044 + FLOOR (7): 445045 + FLOOR (8): 445046 CEILING (1): 445048 CEILING (2): 445049 CEILING (3): 445050 CEILING (4): 445051 CEILING (5): 445052 + CEILING (6): 445047 WALL (1): 445053 WALL (2): 445054 WALL (3): 445055 @@ -847,7 +855,7 @@ panels: PANDEMIC (1): 445100 TRINITY: 445101 CHEMISTRY: 445102 - FLUMMOXED: 445103 + FLUSTERED: 445103 PANDEMIC (2): 445104 COUNTERCLOCKWISE: 445105 FEARLESS: 445106 @@ -933,7 +941,7 @@ panels: CORNER: 445182 STRAWBERRIES: 445183 GRUB: 445184 - BREAD: 445185 + CHEESE: 445185 COLOR: 445186 WRITER: 445187 '02759': 445188 @@ -944,12 +952,12 @@ panels: DUCK LOGO: 445193 AVIAN GREEN: 445194 FEVER TEAR: 445195 - FACTS: 445196 + FACTS (Chain): 445196 FACTS (1): 445197 - FACTS (3): 445198 - FACTS (4): 445199 - FACTS (5): 445200 - FACTS (6): 445201 + FACTS (2): 445198 + FACTS (3): 445199 + FACTS (4): 445200 + FACTS (5): 445201 LAPEL SHEEP: 445202 doors: Starting Room: @@ -979,9 +987,6 @@ doors: Tenacious Entrance: item: 444426 location: 444433 - Symmetry Door: - item: 444428 - location: 445204 Shortcut to Hedge Maze: item: 444430 location: 444436 @@ -1038,6 +1043,10 @@ doors: location: 445210 White Palindromes: location: 445211 + Near Far Area: + Door: + item: 444428 + location: 445204 Warts Straw Area: Door: item: 444451 @@ -1286,12 +1295,12 @@ doors: location: 445246 Yellow Barrier: item: 444538 - Champion's Rest: + Color Hunt: Shortcut to The Steady: item: 444539 location: 444806 The Bearer: - Shortcut to The Bold: + Entrance: item: 444540 location: 444820 Backside Door: @@ -1442,7 +1451,6 @@ door_groups: Fearless Doors: 444469 Backside Doors: 444473 Orange Tower First Floor - Shortcuts: 444484 - Champion's Rest - Color Barriers: 444489 Welcome Back Doors: 444492 Colorful Doors: 444498 Directional Gallery Doors: 444531 @@ -1452,3 +1460,4 @@ progression: Progressive Fearless: 444470 Progressive Orange Tower: 444482 Progressive Art Gallery: 444563 + Progressive Colorful: 444580 diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index af24570f278e..9f8bf5615592 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -24,10 +24,6 @@ def should_include(self, world: "LingoWorld") -> bool: return world.options.shuffle_colors > 0 elif self.mode == "doors": return world.options.shuffle_doors != ShuffleDoors.option_none - elif self.mode == "orange tower": - # door shuffle is on and tower isn't progressive - return world.options.shuffle_doors != ShuffleDoors.option_none \ - and not world.options.progressive_orange_tower elif self.mode == "complex door": return world.options.shuffle_doors == ShuffleDoors.option_complex elif self.mode == "door group": @@ -68,10 +64,7 @@ def load_item_data(): door_groups.setdefault(door.group, []).extend(door.door_ids) if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]: - if room_name == "Orange Tower": - door_mode = "orange tower" - else: - door_mode = "special" + door_mode = "special" ALL_ITEM_TABLE[door.item_name] = \ ItemData(get_door_item_id(room_name, door_name), diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index c00208621f9e..ed1426450eb7 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool class ShuffleDoors(Choice): @@ -21,6 +21,13 @@ class ProgressiveOrangeTower(DefaultOnToggle): display_name = "Progressive Orange Tower" +class ProgressiveColorful(DefaultOnToggle): + """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. + If 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 access to them. + If on, there are ten progressive items, which open up the sequence from White forward.""" + display_name = "Progressive Colorful" + + class LocationChecks(Choice): """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for achievement panels and a small handful of other panels. @@ -117,6 +124,7 @@ class DeathLink(Toggle): class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors progressive_orange_tower: ProgressiveOrangeTower + progressive_colorful: ProgressiveColorful location_checks: LocationChecks shuffle_colors: ShuffleColors shuffle_panels: ShufflePanels @@ -128,3 +136,4 @@ class LingoOptions(PerGameCommonOptions): trap_percentage: TrapPercentage puzzle_skip_percentage: PuzzleSkipPercentage death_link: DeathLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index fa497c59bd45..0ae303518cf1 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from .items import ALL_ITEM_TABLE @@ -36,6 +37,27 @@ class PlayerLocation(NamedTuple): access: AccessRequirements +class ProgressiveItemBehavior(Enum): + DISABLE = 1 + SPLIT = 2 + PROGRESSIVE = 3 + + +def should_split_progression(progression_name: str, world: "LingoWorld") -> ProgressiveItemBehavior: + if progression_name == "Progressive Orange Tower": + if world.options.progressive_orange_tower: + return ProgressiveItemBehavior.PROGRESSIVE + else: + return ProgressiveItemBehavior.SPLIT + elif progression_name == "Progressive Colorful": + if world.options.progressive_colorful: + return ProgressiveItemBehavior.PROGRESSIVE + else: + return ProgressiveItemBehavior.SPLIT + + return ProgressiveItemBehavior.PROGRESSIVE + + class LingoPlayerLogic: """ Defines logic after a player's options have been applied @@ -83,9 +105,13 @@ def set_door_item(self, room: str, door: str, item: str): 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]: - if room_name == "Orange Tower" and not world.options.progressive_orange_tower: + progression_name = PROGRESSION_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) - else: + 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 self.set_door_item(room_name, door_data.name, progressive_item_name) self.real_items.append(progressive_item_name) @@ -195,9 +221,8 @@ def __init__(self, world: "LingoWorld"): ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], - ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], - ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], - ["Outside The Agreeable", "Tenacious Entrance"] + ["Color Hunt", "Shortcut to The Steady"], ["The Bearer", "Entrance"], ["Art Gallery", "Exit"], + ["The Tenacious", "Shortcut to Hub Room"], ["Outside The Agreeable", "Tenacious Entrance"] ] pilgrimage_reqs = AccessRequirements() for door in fake_pilgrimage: @@ -223,7 +248,7 @@ def __init__(self, world: "LingoWorld"): "kind of logic error.") if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ - and not early_color_hallways is False: + and not early_color_hallways: # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index 8edc7ce6ccef..081d6743a5f2 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -96,7 +96,7 @@ def test_item(self): self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance", @@ -105,7 +105,7 @@ def test_item(self): self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery") @@ -115,7 +115,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect(progressive_gallery_room[1]) @@ -123,7 +123,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect(progressive_gallery_room[2]) @@ -131,7 +131,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect(progressive_gallery_room[3]) @@ -139,15 +139,15 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) - self.collect(progressive_gallery_room[4]) + self.collect_by_name("Orange Tower Fifth Floor - Quadruple Intersection") self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.can_reach_location("Art Gallery - ORDER")) self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) @@ -162,7 +162,7 @@ def test_item(self): self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect_by_name("Yellow") @@ -170,7 +170,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect_by_name("Brown") @@ -178,7 +178,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect_by_name("Blue") @@ -186,7 +186,7 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.can_reach_location("Art Gallery - ORDER")) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) self.collect_by_name(["Orange", "Gray"]) @@ -194,5 +194,5 @@ def test_item(self): self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) - self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.can_reach_location("Art Gallery - ORDER")) self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index 3ac49dc220ce..96ed9fcd66b9 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -8,7 +8,8 @@ require 'yaml' configpath = ARGV[0] -mappath = ARGV[1] +idspath = ARGV[1] +mappath = ARGV[2] panels = Set["Countdown Panels/Panel_1234567890_wanderlust"] doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"] @@ -46,6 +47,8 @@ non_counting = 0 +ids = YAML.load_file(idspath) + config = YAML.load_file(configpath) config.each do |room_name, room| configured_rooms.add(room_name) @@ -162,6 +165,10 @@ unless bad_subdirectives.empty? then puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" end + + unless ids.include?("panels") and ids["panels"].include?(room_name) and ids["panels"][room_name].include?(panel_name) + puts "#{room_name} - #{panel_name} :::: Panel is missing a location ID" + end end (room["doors"] || {}).each do |door_name, door| @@ -229,6 +236,18 @@ unless bad_subdirectives.empty? then puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" end + + unless door["skip_item"] or door["event"] + unless ids.include?("doors") and ids["doors"].include?(room_name) and ids["doors"][room_name].include?(door_name) and ids["doors"][room_name][door_name].include?("item") + puts "#{room_name} - #{door_name} :::: Door is missing an item ID" + end + end + + unless door["skip_location"] or door["event"] + unless ids.include?("doors") and ids["doors"].include?(room_name) and ids["doors"][room_name].include?(door_name) and ids["doors"][room_name][door_name].include?("location") + puts "#{room_name} - #{door_name} :::: Door is missing a location ID" + end + end end (room["paintings"] || []).each do |painting| @@ -281,6 +300,10 @@ mentioned_doors.add("#{room_name} - #{door}") end end + + unless ids.include?("progression") and ids["progression"].include?(progression_name) + puts "#{room_name} - #{progression_name} :::: Progression is missing an item ID" + end end end @@ -303,6 +326,10 @@ if num == 1 then puts "Door group \"#{group}\" only has one door in it" end + + unless ids.include?("door_groups") and ids["door_groups"].include?(group) + puts "#{group} :::: Door group is missing an item ID" + end end slashed_rooms = configured_rooms.select do |room| diff --git a/worlds/messenger/test/__init__.py b/worlds/messenger/test/__init__.py index 7ab1e11781da..f3fcd4ae2d60 100644 --- a/worlds/messenger/test/__init__.py +++ b/worlds/messenger/test/__init__.py @@ -1,6 +1,7 @@ from test.TestBase import WorldTestBase +from .. import MessengerWorld class MessengerTestBase(WorldTestBase): game = "The Messenger" - player: int = 1 + world: MessengerWorld diff --git a/worlds/messenger/test/test_locations.py b/worlds/messenger/test/test_locations.py index 0c330be4bd3a..627d58c29061 100644 --- a/worlds/messenger/test/test_locations.py +++ b/worlds/messenger/test/test_locations.py @@ -12,5 +12,5 @@ def run_default_tests(self) -> bool: return False def test_locations_exist(self) -> None: - for location in self.multiworld.worlds[1].location_name_to_id: + for location in self.world.location_name_to_id: self.assertIsInstance(self.multiworld.get_location(location, self.player), MessengerLocation) diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index afb1b32b88e3..ee7e82d6cdbe 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -17,7 +17,7 @@ def test_shop_rules(self) -> None: self.assertFalse(self.can_reach_location(loc)) def test_shop_prices(self) -> None: - prices: Dict[str, int] = self.multiworld.worlds[self.player].shop_prices + prices: Dict[str, int] = self.world.shop_prices for loc, price in prices.items(): with self.subTest("prices", loc=loc): self.assertLessEqual(price, self.multiworld.get_location(f"The Shop - {loc}", self.player).cost) @@ -51,7 +51,7 @@ class ShopCostMinTest(ShopCostTest): } def test_shop_rules(self) -> None: - if self.multiworld.worlds[self.player].total_shards: + if self.world.total_shards: super().test_shop_rules() else: for loc in SHOP_ITEMS: @@ -85,7 +85,7 @@ def test_costs(self) -> None: with self.subTest("has cost", loc=loc): self.assertFalse(self.can_reach_location(loc)) - prices = self.multiworld.worlds[self.player].shop_prices + prices = self.world.shop_prices for loc, price in prices.items(): with self.subTest("prices", loc=loc): if loc == "Karuta Plates": @@ -98,7 +98,7 @@ def test_costs(self) -> None: self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS) self.assertEqual(len(prices), len(SHOP_ITEMS)) - figures = self.multiworld.worlds[self.player].figurine_prices + figures = self.world.figurine_prices for loc, price in figures.items(): with self.subTest("figure prices", loc=loc): if loc == "Barmath'azel Figurine": diff --git a/worlds/messenger/test/test_shop_chest.py b/worlds/messenger/test/test_shop_chest.py index a34fa0fb96c0..f2030c63de99 100644 --- a/worlds/messenger/test/test_shop_chest.py +++ b/worlds/messenger/test/test_shop_chest.py @@ -41,8 +41,8 @@ class HalfSealsRequired(MessengerTestBase): def test_seals_amount(self) -> None: """Should have 45 power seals in the item pool and half that required""" self.assertEqual(self.multiworld.total_seals[self.player], 45) - self.assertEqual(self.multiworld.worlds[self.player].total_seals, 45) - self.assertEqual(self.multiworld.worlds[self.player].required_seals, 22) + self.assertEqual(self.world.total_seals, 45) + self.assertEqual(self.world.required_seals, 22) total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] @@ -60,8 +60,8 @@ class ThirtyThirtySeals(MessengerTestBase): def test_seals_amount(self) -> None: """Should have 30 power seals in the pool and 33 percent of that required.""" self.assertEqual(self.multiworld.total_seals[self.player], 30) - self.assertEqual(self.multiworld.worlds[self.player].total_seals, 30) - self.assertEqual(self.multiworld.worlds[self.player].required_seals, 10) + self.assertEqual(self.world.total_seals, 30) + self.assertEqual(self.world.required_seals, 10) total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] @@ -78,7 +78,7 @@ class MaxSealsNoShards(MessengerTestBase): def test_seals_amount(self) -> None: """Should set total seals to 70 since shards aren't shuffled.""" self.assertEqual(self.multiworld.total_seals[self.player], 85) - self.assertEqual(self.multiworld.worlds[self.player].total_seals, 70) + self.assertEqual(self.world.total_seals, 70) class MaxSealsWithShards(MessengerTestBase): @@ -91,8 +91,8 @@ class MaxSealsWithShards(MessengerTestBase): def test_seals_amount(self) -> None: """Should have 85 seals in the pool with all required and be a valid seed.""" self.assertEqual(self.multiworld.total_seals[self.player], 85) - self.assertEqual(self.multiworld.worlds[self.player].total_seals, 85) - self.assertEqual(self.multiworld.worlds[self.player].required_seals, 85) + self.assertEqual(self.world.total_seals, 85) + self.assertEqual(self.world.required_seals, 85) total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 55523542d7df..cc4cc71ce33f 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -26,7 +26,8 @@ class MuseDashCollections: # MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings. # "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026. "Miku in Museland", # Paid DLC not included in Muse Plus - "MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024. + "Rin Len's Mirrorland", # Paid DLC not included in Muse Plus + "MSR Anthology", # Now no longer available. ] DIFF_OVERRIDES: List[str] = [ @@ -34,6 +35,7 @@ class MuseDashCollections: "Rush-Hour", "Find this Month's Featured Playlist", "PeroPero in the Universe", + "umpopoff" ] album_items: Dict[str, AlbumData] = {} @@ -81,11 +83,22 @@ def __init__(self) -> None: steamer_mode = sections[3] == "True" if song_name in self.DIFF_OVERRIDES: - # Note: These difficulties may not actually be representative of these songs. - # The game does not provide these difficulties so they have to be filled in. - diff_of_easy = 4 - diff_of_hard = 7 - diff_of_master = 10 + # These songs use non-standard difficulty values. Which are being overriden with standard values. + # But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value. + if sections[4] != '0': + diff_of_easy = 4 + else: + diff_of_easy = None + + if sections[5] != '0': + diff_of_hard = 7 + else: + diff_of_hard = None + + if sections[6] != '0': + diff_of_master = 10 + else: + diff_of_master = None else: diff_of_easy = self.parse_song_difficulty(sections[4]) diff_of_hard = self.parse_song_difficulty(sections[5]) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 54a0124474c6..ce5929bfd00d 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11| Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11| Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7| Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8| -Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7| +Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11 Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8| Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10| Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9| @@ -484,7 +484,7 @@ Hand in Hand|66-1|Miku in Museland|False|1|3|6| Cynical Night Plan|66-2|Miku in Museland|False|4|6|8| God-ish|66-3|Miku in Museland|False|4|7|10| Darling Dance|66-4|Miku in Museland|False|4|7|9| -Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10| +Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|11 The Vampire|66-6|Miku in Museland|False|4|6|9| Future Eve|66-7|Miku in Museland|False|4|8|11| Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10| @@ -501,4 +501,32 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9| Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11 Swing Edge|68-3|Gambler's Tricks|True|4|8|10| Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11 -Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| \ No newline at end of file +Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| +Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8| +YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9| +Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9| +INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10| +Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9| +HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10| +Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11 +Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8| +Twilight Poems|43-44|MD Plus Project|True|3|6|8| +All My Friends feat. RANASOL|43-45|MD Plus Project|True|4|7|9| +Heartache|43-46|MD Plus Project|True|5|7|10| +Blue Lemonade|43-47|MD Plus Project|True|3|6|8| +Haunted Dance|43-48|MD Plus Project|False|6|9|11| +Hey Vincent.|43-49|MD Plus Project|True|6|8|10| +Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9| +Narcissism Angel|43-51|MD Plus Project|True|1|3|6| +AlterLuna|43-52|MD Plus Project|True|6|8|11| +Niki Tousen|43-53|MD Plus Project|True|6|8|10|11 +Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9| +Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10| +Iya Iya Iya|70-2|Rin Len's Mirrorland|False|2|4|7| +Nee Nee Nee|70-3|Rin Len's Mirrorland|False|4|6|8| +Chaotic Love Revolution|70-4|Rin Len's Mirrorland|False|4|6|8| +Dance of the Corpses|70-5|Rin Len's Mirrorland|False|2|5|8| +Bitter Choco Decoration|70-6|Rin Len's Mirrorland|False|3|6|9| +Dance Robot Dance|70-7|Rin Len's Mirrorland|False|4|7|10| +Sweet Devil|70-8|Rin Len's Mirrorland|False|5|7|9| +Someday'z Coming|70-9|Rin Len's Mirrorland|False|5|7|9| \ No newline at end of file diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 3fe28187fae6..26ad5ff5d967 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -36,7 +36,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 500 # Note will probably not reach this high if any other settings are done. + range_end = 528 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index a68fd2853def..af2d4cc207da 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -328,5 +328,6 @@ def fill_slot_data(self): "victoryLocation": self.victory_song_name, "deathLink": self.options.death_link.value, "musicSheetWinCount": self.get_music_sheet_win_count(), - "gradeNeeded": self.options.grade_needed.value + "gradeNeeded": self.options.grade_needed.value, + "hasFiller": True, } diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index 01420347af15..af3469aa080f 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -66,5 +66,11 @@ def test_songs_have_difficulty(self) -> None: for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: song = muse_dash_world.md_collection.song_items[song_name] - self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, + # umpopoff is a one time weird song. Its currently the only song in the game + # with non-standard difficulties and also doesn't have 3 or more difficulties. + if song_name == 'umpopoff': + self.assertTrue(song.easy is None and song.hard is not None and song.master is None, + f"Song '{song_name}' difficulty not set when it should be.") + else: + self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None, f"Song '{song_name}' difficulty not set when it should be.") diff --git a/worlds/noita/Events.py b/worlds/noita/Events.py deleted file mode 100644 index e759d38c6c7a..000000000000 --- a/worlds/noita/Events.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Dict - -from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region -from . import Items, Locations - - -def create_event(player: int, name: str) -> Item: - return Items.NoitaItem(name, ItemClassification.progression, None, player) - - -def create_location(player: int, name: str, region: Region) -> Location: - return Locations.NoitaLocation(player, name, None, region) - - -def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location: - region = multiworld.get_region(region_name, player) - - new_location = create_location(player, item, region) - new_location.place_locked_item(create_event(player, item)) - - region.locations.append(new_location) - return new_location - - -def create_all_events(multiworld: MultiWorld, player: int) -> None: - for region, event in event_locks.items(): - create_locked_location_event(multiworld, player, region, event) - - multiworld.completion_condition[player] = lambda state: state.has("Victory", player) - - -# Maps region names to event names -event_locks: Dict[str, str] = { - "The Work": "Victory", - "Mines": "Portal to Holy Mountain 1", - "Coal Pits": "Portal to Holy Mountain 2", - "Snowy Depths": "Portal to Holy Mountain 3", - "Hiisi Base": "Portal to Holy Mountain 4", - "Underground Jungle": "Portal to Holy Mountain 5", - "The Vault": "Portal to Holy Mountain 6", - "Temple of the Art": "Portal to Holy Mountain 7", -} diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py deleted file mode 100644 index 8190b80dc710..000000000000 --- a/worlds/noita/Rules.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import List, NamedTuple, Set - -from BaseClasses import CollectionState, MultiWorld -from . import Items, Locations -from .Options import BossesAsChecks, VictoryCondition -from worlds.generic import Rules as GenericRules - - -class EntranceLock(NamedTuple): - source: str - destination: str - event: str - items_needed: int - - -entrance_locks: List[EntranceLock] = [ - EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1), - EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2), - EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3), - EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4), - EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5), - EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6), - EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7), -] - - -holy_mountain_regions: List[str] = [ - "Coal Pits Holy Mountain", - "Snowy Depths Holy Mountain", - "Hiisi Base Holy Mountain", - "Underground Jungle Holy Mountain", - "Vault Holy Mountain", - "Temple of the Art Holy Mountain", - "Laboratory Holy Mountain", -] - - -wand_tiers: List[str] = [ - "Wand (Tier 1)", # Coal Pits - "Wand (Tier 2)", # Snowy Depths - "Wand (Tier 3)", # Hiisi Base - "Wand (Tier 4)", # Underground Jungle - "Wand (Tier 5)", # The Vault - "Wand (Tier 6)", # Temple of the Art -] - -items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", - "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", - "Powder Pouch"] - -perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) - - -# ---------------- -# Helper Functions -# ---------------- - - -def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: - return sum(state.count(perk, player) for perk in perk_list) >= amount - - -def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: - return state.count("Orb", player) >= amount - - -def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int): - location = multiworld.get_location(location_name, player) - GenericRules.forbid_items_for_player(location, items, player) - - -# ---------------- -# Rule Functions -# ---------------- - - -# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them) -def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None: - for location_name in Locations.location_name_to_id.keys(): - if "Shop Item" in location_name: - forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player) - - -# Prevent high tier wands from appearing in early Holy Mountain shops -def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None: - for i, region_name in enumerate(holy_mountain_regions): - wands_to_forbid = wand_tiers[i+1:] - - locations_in_region = Locations.location_region_mapping[region_name].keys() - for location_name in locations_in_region: - forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) - - # Prevent high tier wands from appearing in the Secret shop - wands_to_forbid = wand_tiers[3:] - locations_in_region = Locations.location_region_mapping["Secret Shop"].keys() - for location_name in locations_in_region: - forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) - - -def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None: - for lock in entrance_locks: - location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player) - GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player)) - - -def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None: - victory_condition = multiworld.victory_condition[player].value - for lock in entrance_locks: - location = multiworld.get_location(lock.event, player) - - if victory_condition == VictoryCondition.option_greed_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) - ) - elif victory_condition == VictoryCondition.option_pure_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) and - has_orb_count(state, player, items_needed) - ) - elif victory_condition == VictoryCondition.option_peaceful_ending: - location.access_rule = lambda state, items_needed=lock.items_needed: ( - has_perk_count(state, player, items_needed//2) and - has_orb_count(state, player, items_needed * 3) - ) - - -def biome_unlock_conditions(multiworld: MultiWorld, player: int): - lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances - magical_entrances = multiworld.get_region("Magical Temple", player).entrances - wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances - for entrance in lukki_entrances: - entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\ - state.has("All-Seeing Eye Perk", player) - for entrance in magical_entrances: - entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player) - for entrance in wizard_entrances: - entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player) - - -def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: - victory_condition = multiworld.victory_condition[player].value - victory_location = multiworld.get_location("Victory", player) - - if victory_condition == VictoryCondition.option_pure_ending: - victory_location.access_rule = lambda state: has_orb_count(state, player, 11) - elif victory_condition == VictoryCondition.option_peaceful_ending: - victory_location.access_rule = lambda state: has_orb_count(state, player, 33) - - -# ---------------- -# Main Function -# ---------------- - - -def create_all_rules(multiworld: MultiWorld, player: int) -> None: - if multiworld.players > 1: - ban_items_from_shops(multiworld, player) - ban_early_high_tier_wands(multiworld, player) - lock_holy_mountains_into_spheres(multiworld, player) - holy_mountain_unlock_conditions(multiworld, player) - biome_unlock_conditions(multiworld, player) - victory_unlock_conditions(multiworld, player) - - # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) - if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses: - forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 792b90e3f551..b8f8e4ae8346 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -1,6 +1,8 @@ from BaseClasses import Item, Tutorial from worlds.AutoWorld import WebWorld, World -from . import Events, Items, Locations, Options, Regions, Rules +from typing import Dict, Any +from . import events, items, locations, regions, rules +from .options import NoitaOptions class NoitaWeb(WebWorld): @@ -24,13 +26,14 @@ class NoitaWorld(World): """ game = "Noita" - option_definitions = Options.noita_options + options: NoitaOptions + options_dataclass = NoitaOptions - item_name_to_id = Items.item_name_to_id - location_name_to_id = Locations.location_name_to_id + item_name_to_id = items.item_name_to_id + location_name_to_id = locations.location_name_to_id - item_name_groups = Items.item_name_groups - location_name_groups = Locations.location_name_groups + item_name_groups = items.item_name_groups + location_name_groups = locations.location_name_groups data_version = 2 web = NoitaWeb() @@ -40,21 +43,21 @@ def generate_early(self): raise Exception("Noita yaml's slot name has invalid character(s).") # Returned items will be sent over to the client - def fill_slot_data(self): - return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests", + "pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price") def create_regions(self) -> None: - Regions.create_all_regions_and_connections(self.multiworld, self.player) - Events.create_all_events(self.multiworld, self.player) + regions.create_all_regions_and_connections(self) def create_item(self, name: str) -> Item: - return Items.create_item(self.player, name) + return items.create_item(self.player, name) def create_items(self) -> None: - Items.create_all_items(self.multiworld, self.player) + items.create_all_items(self) def set_rules(self) -> None: - Rules.create_all_rules(self.multiworld, self.player) + rules.create_all_rules(self) def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(Items.filler_items) + return self.random.choice(items.filler_items) diff --git a/worlds/noita/events.py b/worlds/noita/events.py new file mode 100644 index 000000000000..4ec04e98b457 --- /dev/null +++ b/worlds/noita/events.py @@ -0,0 +1,43 @@ +from typing import Dict, TYPE_CHECKING +from BaseClasses import Item, ItemClassification, Location, Region +from . import items, locations + +if TYPE_CHECKING: + from . import NoitaWorld + + +def create_event(player: int, name: str) -> Item: + return items.NoitaItem(name, ItemClassification.progression, None, player) + + +def create_location(player: int, name: str, region: Region) -> Location: + return locations.NoitaLocation(player, name, None, region) + + +def create_locked_location_event(player: int, region: Region, item: str) -> Location: + new_location = create_location(player, item, region) + new_location.place_locked_item(create_event(player, item)) + + region.locations.append(new_location) + return new_location + + +def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None: + for region_name, event in event_locks.items(): + region = created_regions[region_name] + create_locked_location_event(world.player, region, event) + + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + + +# Maps region names to event names +event_locks: Dict[str, str] = { + "The Work": "Victory", + "Mines": "Portal to Holy Mountain 1", + "Coal Pits": "Portal to Holy Mountain 2", + "Snowy Depths": "Portal to Holy Mountain 3", + "Hiisi Base": "Portal to Holy Mountain 4", + "Underground Jungle": "Portal to Holy Mountain 5", + "The Vault": "Portal to Holy Mountain 6", + "Temple of the Art": "Portal to Holy Mountain 7", +} diff --git a/worlds/noita/Items.py b/worlds/noita/items.py similarity index 82% rename from worlds/noita/Items.py rename to worlds/noita/items.py index c859a8039494..6b662fbee692 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/items.py @@ -1,9 +1,14 @@ import itertools from collections import Counter -from typing import Dict, List, NamedTuple, Set +from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING -from BaseClasses import Item, ItemClassification, MultiWorld -from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs +from BaseClasses import Item, ItemClassification +from .options import BossesAsChecks, VictoryCondition, ExtraOrbs + +if TYPE_CHECKING: + from . import NoitaWorld +else: + NoitaWorld = object class ItemData(NamedTuple): @@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]: return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else [] -def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]: +def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]: filler_pool = weights.copy() - if multiworld.bad_effects[player].value == 0: + if not world.options.bad_effects: del filler_pool["Trap"] - return multiworld.random.choices(population=list(filler_pool.keys()), - weights=list(filler_pool.values()), - k=count) + return world.random.choices(population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=count) -def create_all_items(multiworld: MultiWorld, player: int) -> None: - locations_to_fill = len(multiworld.get_unfilled_locations(player)) +def create_all_items(world: NoitaWorld) -> None: + player = world.player + locations_to_fill = len(world.multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() - + create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player]) - + create_spatial_awareness_item(multiworld.bosses_as_checks[player]) - + create_kantele(multiworld.victory_condition[player]) + + create_orb_items(world.options.victory_condition, world.options.extra_orbs) + + create_spatial_awareness_item(world.options.bosses_as_checks) + + create_kantele(world.options.victory_condition) ) # if there's not enough shop-allowed items in the pool, we can encounter gen issues # 39 is the number of shop-valid items we need to guarantee if len(itempool) < 39: - itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool)) + itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool)) # this is so that it passes tests and gens if you have minimal locations and only one player - if multiworld.players == 1: - for location in multiworld.get_unfilled_locations(player): + if world.multiworld.players == 1: + for location in world.multiworld.get_unfilled_locations(player): if "Shop Item" in location.name: location.item = create_item(player, itempool.pop()) - locations_to_fill = len(multiworld.get_unfilled_locations(player)) + locations_to_fill = len(world.multiworld.get_unfilled_locations(player)) - itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) - multiworld.itempool += [create_item(player, name) for name in itempool] + itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool)) + world.multiworld.itempool += [create_item(player, name) for name in itempool] # 110000 - 110032 diff --git a/worlds/noita/Locations.py b/worlds/noita/locations.py similarity index 94% rename from worlds/noita/Locations.py rename to worlds/noita/locations.py index 7c27d699ccba..afe16c54e4b2 100644 --- a/worlds/noita/Locations.py +++ b/worlds/noita/locations.py @@ -201,11 +201,10 @@ class LocationFlag(IntEnum): } -# Iterating the hidden chest and pedestal locations here to avoid clutter above -def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]: - if locinfo.ltype in ["chest", "pedestal"]: - return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)} - return {locname: locinfo.id} +def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]: + if amt == 1: + return {location_name: base_id} + return {f"{location_name} {i+1}": base_id + i for i in range(amt)} location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(), @@ -215,9 +214,11 @@ def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, for location_group in location_region_mapping.values(): for locname, locinfo in location_group.items(): - location_name_to_id.update(generate_location_entries(locname, locinfo)) - if locinfo.ltype in ["chest", "pedestal"]: - for i in range(20): - location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}") - else: - location_name_groups[locinfo.ltype].add(locname) + # Iterating the hidden chest and pedestal locations here to avoid clutter above + amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1 + entries = make_location_range(locname, locinfo.id, amount) + + location_name_to_id.update(entries) + location_name_groups[locinfo.ltype].update(entries.keys()) + +shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name} diff --git a/worlds/noita/Options.py b/worlds/noita/options.py similarity index 87% rename from worlds/noita/Options.py rename to worlds/noita/options.py index 0b54597f364d..7d987571a589 100644 --- a/worlds/noita/Options.py +++ b/worlds/noita/options.py @@ -1,5 +1,5 @@ -from typing import Dict -from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool +from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions +from dataclasses import dataclass class PathOption(Choice): @@ -99,16 +99,16 @@ class ShopPrice(Choice): default = 100 -noita_options: Dict[str, AssembleOptions] = { - "start_inventory_from_pool": StartInventoryPool, - "death_link": DeathLink, - "bad_effects": Traps, - "victory_condition": VictoryCondition, - "path_option": PathOption, - "hidden_chests": HiddenChests, - "pedestal_checks": PedestalChecks, - "orbs_as_checks": OrbsAsChecks, - "bosses_as_checks": BossesAsChecks, - "extra_orbs": ExtraOrbs, - "shop_price": ShopPrice, -} +@dataclass +class NoitaOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + death_link: DeathLink + bad_effects: Traps + victory_condition: VictoryCondition + path_option: PathOption + hidden_chests: HiddenChests + pedestal_checks: PedestalChecks + orbs_as_checks: OrbsAsChecks + bosses_as_checks: BossesAsChecks + extra_orbs: ExtraOrbs + shop_price: ShopPrice diff --git a/worlds/noita/Regions.py b/worlds/noita/regions.py similarity index 61% rename from worlds/noita/Regions.py rename to worlds/noita/regions.py index 561d483b4865..6a9c86772381 100644 --- a/worlds/noita/Regions.py +++ b/worlds/noita/regions.py @@ -1,48 +1,43 @@ # Regions are areas in your game that you travel to. -from typing import Dict, Set, List +from typing import Dict, List, TYPE_CHECKING -from BaseClasses import Entrance, MultiWorld, Region -from . import Locations +from BaseClasses import Entrance, Region +from . import locations +from .events import create_all_events +if TYPE_CHECKING: + from . import NoitaWorld -def add_location(player: int, loc_name: str, id: int, region: Region) -> None: - location = Locations.NoitaLocation(player, loc_name, id, region) - region.locations.append(location) - -def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None: - locations = Locations.location_region_mapping.get(region.name, {}) - for location_name, location_data in locations.items(): +def create_locations(world: "NoitaWorld", region: Region) -> None: + locs = locations.location_region_mapping.get(region.name, {}) + for location_name, location_data in locs.items(): location_type = location_data.ltype flag = location_data.flag - opt_orbs = multiworld.orbs_as_checks[player].value - opt_bosses = multiworld.bosses_as_checks[player].value - opt_paths = multiworld.path_option[player].value - opt_num_chests = multiworld.hidden_chests[player].value - opt_num_pedestals = multiworld.pedestal_checks[player].value + is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks + is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks + amount = 0 + if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: + amount = 1 + elif location_type == "chest" and flag <= world.options.path_option: + amount = world.options.hidden_chests.value + elif location_type == "pedestal" and flag <= world.options.path_option: + amount = world.options.pedestal_checks.value - is_orb_allowed = location_type == "orb" and flag <= opt_orbs - is_boss_allowed = location_type == "boss" and flag <= opt_bosses - if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: - add_location(player, location_name, location_data.id, region) - elif location_type == "chest" and flag <= opt_paths: - for i in range(opt_num_chests): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) - elif location_type == "pedestal" and flag <= opt_paths: - for i in range(opt_num_pedestals): - add_location(player, f"{location_name} {i+1}", location_data.id + i, region) + region.add_locations(locations.make_location_range(location_name, location_data.id, amount), + locations.NoitaLocation) # Creates a new Region with the locations found in `location_region_mapping` and adds them to the world. -def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region: - new_region = Region(region_name, player, multiworld) - add_locations(multiworld, player, new_region) +def create_region(world: "NoitaWorld", region_name: str) -> Region: + new_region = Region(region_name, world.player, world.multiworld) + create_locations(world, new_region) return new_region -def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]: - return {name: create_region(multiworld, player, name) for name in noita_regions} +def create_regions(world: "NoitaWorld") -> Dict[str, Region]: + return {name: create_region(world, name) for name in noita_regions} # An "Entrance" is really just a connection between two regions @@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None: # Creates all regions and connections. Called from NoitaWorld. -def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None: - created_regions = create_regions(multiworld, player) - create_connections(player, created_regions) +def create_all_regions_and_connections(world: "NoitaWorld") -> None: + created_regions = create_regions(world) + create_connections(world.player, created_regions) + create_all_events(world, created_regions) - multiworld.regions += created_regions.values() + world.multiworld.regions += created_regions.values() # Oh, what a tangled web we weave diff --git a/worlds/noita/rules.py b/worlds/noita/rules.py new file mode 100644 index 000000000000..95039bee4635 --- /dev/null +++ b/worlds/noita/rules.py @@ -0,0 +1,172 @@ +from typing import List, NamedTuple, Set, TYPE_CHECKING + +from BaseClasses import CollectionState +from . import items, locations +from .options import BossesAsChecks, VictoryCondition +from worlds.generic import Rules as GenericRules + +if TYPE_CHECKING: + from . import NoitaWorld + + +class EntranceLock(NamedTuple): + source: str + destination: str + event: str + items_needed: int + + +entrance_locks: List[EntranceLock] = [ + EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1), + EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2), + EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3), + EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4), + EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5), + EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6), + EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7), +] + + +holy_mountain_regions: List[str] = [ + "Coal Pits Holy Mountain", + "Snowy Depths Holy Mountain", + "Hiisi Base Holy Mountain", + "Underground Jungle Holy Mountain", + "Vault Holy Mountain", + "Temple of the Art Holy Mountain", + "Laboratory Holy Mountain", +] + + +wand_tiers: List[str] = [ + "Wand (Tier 1)", # Coal Pits + "Wand (Tier 2)", # Snowy Depths + "Wand (Tier 3)", # Hiisi Base + "Wand (Tier 4)", # Underground Jungle + "Wand (Tier 5)", # The Vault + "Wand (Tier 6)", # Temple of the Art +] + + +items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", + "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", + "Powder Pouch"} + +perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys())) + + +# ---------------- +# Helper Functions +# ---------------- + + +def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: + return sum(state.count(perk, player) for perk in perk_list) >= amount + + +def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: + return state.count("Orb", player) >= amount + + +def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]): + for shop_location in shop_locations: + location = world.multiworld.get_location(shop_location, world.player) + GenericRules.forbid_items_for_player(location, forbidden_items, world.player) + + +# ---------------- +# Rule Functions +# ---------------- + + +# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them) +# def ban_items_from_shops(world: "NoitaWorld") -> None: +# for location_name in Locations.location_name_to_id.keys(): +# if "Shop Item" in location_name: +# forbid_items_at_location(world, location_name, items_hidden_from_shops) +def ban_items_from_shops(world: "NoitaWorld") -> None: + forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops) + + +# Prevent high tier wands from appearing in early Holy Mountain shops +def ban_early_high_tier_wands(world: "NoitaWorld") -> None: + for i, region_name in enumerate(holy_mountain_regions): + wands_to_forbid = set(wand_tiers[i+1:]) + + locations_in_region = set(locations.location_region_mapping[region_name].keys()) + forbid_items_at_locations(world, locations_in_region, wands_to_forbid) + + # Prevent high tier wands from appearing in the Secret shop + wands_to_forbid = set(wand_tiers[3:]) + locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys()) + forbid_items_at_locations(world, locations_in_region, wands_to_forbid) + + +def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None: + for lock in entrance_locks: + location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player) + GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player)) + + +def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None: + victory_condition = world.options.victory_condition.value + for lock in entrance_locks: + location = world.multiworld.get_location(lock.event, world.player) + + if victory_condition == VictoryCondition.option_greed_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) + ) + elif victory_condition == VictoryCondition.option_pure_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) and + has_orb_count(state, world.player, items_needed) + ) + elif victory_condition == VictoryCondition.option_peaceful_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, world.player, items_needed//2) and + has_orb_count(state, world.player, items_needed * 3) + ) + + +def biome_unlock_conditions(world: "NoitaWorld"): + lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances + magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances + wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances + for entrance in lukki_entrances: + entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\ + state.has("All-Seeing Eye Perk", world.player) + for entrance in magical_entrances: + entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player) + for entrance in wizard_entrances: + entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player) + + +def victory_unlock_conditions(world: "NoitaWorld") -> None: + victory_condition = world.options.victory_condition.value + victory_location = world.multiworld.get_location("Victory", world.player) + + if victory_condition == VictoryCondition.option_pure_ending: + victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11) + elif victory_condition == VictoryCondition.option_peaceful_ending: + victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33) + + +# ---------------- +# Main Function +# ---------------- + + +def create_all_rules(world: "NoitaWorld") -> None: + if world.multiworld.players > 1: + ban_items_from_shops(world) + ban_early_high_tier_wands(world) + lock_holy_mountains_into_spheres(world) + holy_mountain_unlock_conditions(world) + biome_unlock_conditions(world) + victory_unlock_conditions(world) + + # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) + if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses: + toveri = world.multiworld.get_location("Toveri", world.player) + GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 120027e29dfa..2543cdc715c7 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1271,7 +1271,7 @@ class LogicTricks(OptionList): https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py """ display_name = "Logic Tricks" - valid_keys = frozenset(normalized_name_tricks) + valid_keys = tuple(normalized_name_tricks.keys()) valid_keys_casefold = True diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index e9c889d6f653..eb9c41f0b032 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -118,7 +118,16 @@ class OOTWeb(WebWorld): ["TheLynk"] ) - tutorials = [setup, setup_es, setup_fr] + setup_de = Tutorial( + setup.tutorial_name, + setup.description, + "Deutsch", + "setup_de.md", + "setup/de", + ["Held_der_Zeit"] + ) + + tutorials = [setup, setup_es, setup_fr, setup_de] class OOTWorld(World): diff --git a/worlds/oot/docs/MultiWorld-Room_oot.png b/worlds/oot/docs/MultiWorld-Room_oot.png new file mode 100644 index 000000000000..f0f224e5e1af Binary files /dev/null and b/worlds/oot/docs/MultiWorld-Room_oot.png differ diff --git a/worlds/oot/docs/de_Ocarina of Time.md b/worlds/oot/docs/de_Ocarina of Time.md new file mode 100644 index 000000000000..4d9fd2ea14bd --- /dev/null +++ b/worlds/oot/docs/de_Ocarina of Time.md @@ -0,0 +1,41 @@ +# The Legend of Zelda: Ocarina of Time + +## Wo ist die Seite für die Einstellungen? + +Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um +eine YAML-Datei zu konfigurieren und zu exportieren. + +## Was macht der Randomizer in diesem Spiel? + +Items, welche der Spieler für gewöhnlich im Verlauf des Spiels erhalten würde, wurden umhergemischt. Die Logik bleit +bestehen, damit ist das Spiel immer durchspielbar. Doch weil die Items durch das ganze Spiel gemischt wurden, müssen + manche Bereiche früher bescuht werden, als man es in Vanilla tun würde. +Eine Liste von implementierter Logik, die unoffensichtlich erscheinen kann, kann +[hier (Englisch)](https://wiki.ootrandomizer.com/index.php?title=Logic) gefunden werden. + +## Welche Items und Bereiche werden gemischt? + +Alle ausrüstbare und sammelbare Gegenstände, sowie Munition können gemischt werden. Und alle Bereiche, die einen +dieser Items enthalten könnten, haben (sehr wahrscheinlich) ihren Inhalt verändert. Goldene Skulltulas können ebenfalls +dazugezählt werden, je nach Wunsch des Spielers. + +## Welche Items können in sich in der Welt eines anderen Spielers befinden? + +Jedes dieser Items, die gemicht werden können, können in einer Multiworld auch in der Welt eines anderen Spielers +fallen. Es ist jedoch möglich ausgewählte Items auf deine eigene Welt zu beschränken. + +## Wie sieht ein Item einer anderen Welt in OoT aus? + +Items, die zu einer anderen Welt gehören, werden repräsentiert durch Zelda's Brief. + +## Was passiert, wenn der Spieler ein Item erhält? + +Sobald der Spieler ein Item erhält, wird Link das Item über seinen Kopf halten und der ganzen Welt präsentieren. +Gut für's Geschäft! + +## Einzigartige Lokale Befehle + +Die folgenden Befehle stehen nur im OoTClient, um mit Archipelago zu spielen, zur Verfügung: + +- `/n64` Überprüffe den Verbindungsstatus deiner N64 +- `/deathlink` Schalte den "Deathlink" des Clients um. Überschreibt die zuvor konfigurierten Einstellungen. diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md new file mode 100644 index 000000000000..92c3150a7d2f --- /dev/null +++ b/worlds/oot/docs/setup_de.md @@ -0,0 +1,108 @@ +# Setup Anleitung für Ocarina of Time: Archipelago Edition + +## WICHTIG + +Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux. + +## Benötigte Software + +- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen. + - Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden. + - Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über + den obrigen Link gefunden werden. +- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert + werden kann. +- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!) + +## Konfigurieren von BizHawk + +Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen: + +- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu + `"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren. + **ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und** + **wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die** + **Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.** +- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann + den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal + abstürzen. +- **(Optional)** Unter `Config > Customize` kannst du die Haken in den "Run in background" + (Laufe weiter im Hintergrund) und "Accept background input" (akzeptiere Tastendruck im Hintergrund) Kästchen setzen. + Dies erlaubt dir das Spiel im Hintergrund weiter zu spielen, selbst wenn ein anderes Fenster aktiv ist. (Nützlich bei + mehreren oder eher großen Bildschrimen/Monitoren.) +- Unter `Config > Hotkeys` sind viele Hotkeys, die mit oft genuten Tasten belegt worden sind. Es wird empfohlen die + meisten (oder alle) Hotkeys zu deaktivieren. Dies kann schnell mit `Esc` erledigt werden. +- Wird mit einem Kontroller gespielt, bei der Tastenbelegung (bei einem Laufendem Spiel, unter + `Config > Controllers...`), deaktiviere "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" und gehe stattdessen in + den Reiter `Analog Controls` um den Stick zu belegen, da sonst Probleme beim Zielen auftreten (mit dem Bogen oder + ähnliches). Y-Axis ist für Oben und Unten, und die X-Axis ist für Links und Rechts. +- Unter `N64` setze einen Haken bei "Use Expansion Slot" (Benutze Erweiterungs-Slot). Dies wird benötigt damit + savestates/schnellspeichern funktioniert. (Das N64-Menü taucht nur **nach** dem laden einer N64-ROM auf.) + +Es wird sehr empfohlen N64 Rom-Erweiterungen (\*.n64, \*.z64) mit dem Emuhawk - welcher zuvor installiert wurde - zu +verknüpfen. +Um dies zu tun, muss eine beliebige N64 Rom aufgefunden werden, welche in deinem Besitz ist, diese Rechtsklicken und +dann auf "Öffnen mit..." gehen. Gehe dann auf "Andere App auswählen" und suche nach deinen BizHawk-Ordner, in der +sich der Emulator befindet, und wähle dann `EmuHawk.exe` **(NICHT "DiscoHawk.exe"!)** aus. + +Eine Alternative BizHawk Setup Anleitung (auf Englisch), sowie weitere Hilfe bei Problemen kann +[hier](https://wiki.ootrandomizer.com/index.php?title=Bizhawk) gefunden werden. + +## Erstelle eine YAML-Datei + +### Was ist eine YAML-Datei und Warum brauch ich eine? + +Eine YAML-Datie enthält einen Satz an einstellbaren Optionen, die dem Generator mitteilen, wie +dein Spiel generiert werden soll. In einer Multiworld stellt jeder Spieler eine eigene YAML-Datei zur Verfügung. Dies +erlaubt jeden Spieler eine personalisierte Erfahrung nach derem Geschmack. Damit kann auch jeder Spieler in einer +Multiworld (des gleichen Spiels) völlig unterschiedliche Einstellungen haben. + +Für weitere Informationen, besuche die allgemeine Anleitung zum Erstellen einer +YAML-Datei: [Archipelago Setup Anleitung](/tutorial/Archipelago/setup/en) + +### Woher bekomme ich eine YAML-Datei? + +Die Seite für die Spielereinstellungen auf dieser Website erlaubt es dir deine persönlichen Einstellungen nach +vorlieben zu konfigurieren und eine YAML-Datei zu exportieren. +Seite für die Spielereinstellungen: +[Seite für die Spielereinstellungen von Ocarina of Time](/games/Ocarina%20of%20Time/player-options) + +### Überprüfen deiner YAML-Datei + +Wenn du deine YAML-Datei überprüfen möchtest, um sicher zu gehen, dass sie funktioniert, kannst du dies auf der +YAML-Überprüfungsseite tun. +YAML-Überprüfungsseite: [YAML-Überprüfungsseite](/check) + +## Beitreten einer Multiworld + +### Erhalte deinen OoT-Patch + +(Der folgende Prozess ist bei den meisten ROM-basierenden Spielen sehr ähnlich.) + +Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem Host abzugeben. Ist dies getan, +erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle +teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren +und klicke dann auf `Download APZ5 File...`. +![Screenshot of a Multiworld Room with an Ocarina of Time Player](/static/generated/docs/Ocarina%20of%20Time/MultiWorld-room_oot.png) + +Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen +deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch +(sofern das "Öffnen mit..." ausgewählt wurde). + +### Verbinde zum Multiserver + +Sind einmal der Client und der Emulator gestartet, müssen sie nur noch miteinander verbunden werden. Gehe dazu in +deinen Archipelago-Ordner, dann zu `data/lua`, und füge das `connector_oot.lua` Script per Drag&Drop (ziehen und +fallen lassen) auf das EmuHawk-Fenster. (Alternativ kannst du die Lua-Konsole manuell öffnen, gehe dazu auf +`Script > Open Script` und durchsuche die Ordner nach `data/lua/connector_oot.lua`.) + +Um den Client mit dem Multiserver zu verbinden, füge einfach `:` in das Textfeld ganz oben im +Client ein und drücke Enter oder "Connect" (verbinden). Wird ein Passwort benötigt, musst du es danach unten in das +Textfeld (für den Chat und Befehle) eingeben. +Alternativ kannst du auch in dem unterem Textfeld den folgenden Befehl schreiben: +`/connect : [Passwort]` (wie die Adresse und der Port lautet steht in dem Raum, oder wird von deinem +Host an dich weitergegeben.) +Beispiel: `/connect archipelago.gg:12345 Passw123` + +Du bist nun bereit für dein Zeitreise-Abenteuer in Hyrule. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index b7730fbdf785..95e549a32ef0 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -7,7 +7,7 @@ import os from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar -from BaseClasses import ItemClassification, MultiWorld, Tutorial +from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType from Fill import FillError, fill_restrictive from Options import Toggle import settings @@ -20,7 +20,7 @@ offset_item_value) from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_with_tags) -from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, +from .options import (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility, HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions) from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type @@ -146,6 +146,60 @@ def create_regions(self) -> None: self.multiworld.regions.extend(regions.values()) + # Exclude locations which are always locked behind the player's goal + def exclude_locations(location_names: List[str]): + for location_name in location_names: + try: + self.multiworld.get_location(location_name, + self.player).progress_type = LocationProgressType.EXCLUDED + except KeyError: + continue # Location not in multiworld + + if self.options.goal == Goal.option_champion: + # Always required to beat champion before receiving this + exclude_locations([ + "Littleroot Town - S.S. Ticket from Norman" + ]) + + # S.S. Ticket requires beating champion, so ferry is not accessible until after goal + if not self.options.enable_ferry: + exclude_locations([ + "SS Tidal - Hidden Item in Lower Deck Trash Can", + "SS Tidal - TM49 from Thief" + ]) + + # Construction workers don't move until champion is defeated + if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value: + exclude_locations([ + "Safari Zone NE - Hidden Item North", + "Safari Zone NE - Hidden Item East", + "Safari Zone NE - Item on Ledge", + "Safari Zone SE - Hidden Item in South Grass 1", + "Safari Zone SE - Hidden Item in South Grass 2", + "Safari Zone SE - Item in Grass" + ]) + elif self.options.goal == Goal.option_norman: + # If the player sets their options such that Surf or the Balance + # Badge is vanilla, a very large number of locations become + # "post-Norman". Similarly, access to the E4 may require you to + # defeat Norman as an event or to get his badge, making postgame + # locations inaccessible. Detecting these situations isn't trivial + # and excluding all locations requiring Surf would be a bad idea. + # So for now we just won't touch it and blame the user for + # constructing their options in this way. Players usually expect + # to only partially complete their world when playing this goal + # anyway. + + # Locations which are directly unlocked by defeating Norman. + exclude_locations([ + "Petalburg Gym - Balance Badge", + "Petalburg Gym - TM42 from Norman", + "Petalburg City - HM03 from Wally's Uncle", + "Dewford Town - TM36 from Sludge Bomb Man", + "Mauville City - Basement Key from Wattson", + "Mauville City - TM24 from Wattson" + ]) + def create_items(self) -> None: item_locations: List[PokemonEmeraldLocation] = [ location @@ -225,6 +279,7 @@ def create_items(self) -> None: def refresh_tm_choices() -> None: fill_item_candidates_by_category["TM"] = all_tm_choices.copy() self.random.shuffle(fill_item_candidates_by_category["TM"]) + refresh_tm_choices() # Create items for item in default_itempool: @@ -275,6 +330,7 @@ def convert_unrandomized_items_to_events(tag: str) -> None: for location in locations: if location.tags is not None and tag in location.tags: 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: @@ -311,6 +367,12 @@ def pre_fill(self) -> None: } badge_items.sort(key=lambda item: badge_priority.get(item.name, 0)) + # Un-exclude badge locations, since we need to put progression items on them + for location in badge_locations: + location.progress_type = LocationProgressType.DEFAULT \ + if location.progress_type == LocationProgressType.EXCLUDED \ + else location.progress_type + collection_state = self.multiworld.get_all_state(False) if self.hm_shuffle_info is not None: for _, item in self.hm_shuffle_info: @@ -355,6 +417,12 @@ def pre_fill(self) -> None: } hm_items.sort(key=lambda item: hm_priority.get(item.name, 0)) + # Un-exclude HM locations, since we need to put progression items on them + for location in hm_locations: + location.progress_type = LocationProgressType.DEFAULT \ + if location.progress_type == LocationProgressType.EXCLUDED \ + else location.progress_type + collection_state = self.multiworld.get_all_state(False) # In specific very constrained conditions, fill_restrictive may run diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index bfe5be754585..3d842ecbac98 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -114,8 +114,10 @@ def create_location_label_to_id_map() -> Dict[str, int]: "Littleroot Town - S.S. Ticket from Norman", "SS Tidal - Hidden Item in Lower Deck Trash Can", "SS Tidal - TM49 from Thief", + "Safari Zone NE - Item on Ledge", "Safari Zone NE - Hidden Item North", "Safari Zone NE - Hidden Item East", + "Safari Zone SE - Item in Grass", "Safari Zone SE - Hidden Item in South Grass 1", "Safari Zone SE - Hidden Item in South Grass 2", } diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 5a94a8b5ff26..169ff1d59f1e 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -353,7 +353,9 @@ def pre_fill(self) -> None: location.show_in_spoiler = False def intervene(move, test_state): - if self.multiworld.randomize_wild_pokemon[self.player]: + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] + if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons: accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if loc.type == "Wild Encounter"] @@ -363,8 +365,6 @@ def number_of_zones(mon): zones.add(loc.name.split(" - ")[0]) return len(zones) - move_bit = pow(2, poke_data.hm_moves.index(move) + 2) - viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] placed_mons = [slot.item.name for slot in accessible_slots] if self.multiworld.area_1_to_1_mapping[self.player]: diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index 7424cc8ddff6..9e2689bccc37 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -10,7 +10,7 @@ logger = logging.getLogger("Client") -BANK_EXCHANGE_RATE = 100000000 +BANK_EXCHANGE_RATE = 50000000 DATA_LOCATIONS = { "ItemIndex": (0x1A6E, 0x02), diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index b584869f41b9..24cad13252b1 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -42,7 +42,7 @@ def __init__(self, item_id, classification, groups): "Repel": ItemData(30, ItemClassification.filler, ["Consumables"]), "Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils", "Key Items"]), "Fire Stone": ItemData(32, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), - "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones" "Key Items"]), + "Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), "Water Stone": ItemData(34, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]), "HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]), "Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]), diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py index 3dbd2047debd..83b7b62d2977 100644 --- a/worlds/sc2wol/Client.py +++ b/worlds/sc2wol/Client.py @@ -3,7 +3,6 @@ import asyncio import copy import ctypes -import json import logging import multiprocessing import os.path @@ -15,6 +14,7 @@ import zipfile import io import random +import concurrent.futures from pathlib import Path # CommonClient import first to trigger ModuleUpdater @@ -42,6 +42,7 @@ from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart from MultiServer import mark_raw +pool = concurrent.futures.ThreadPoolExecutor(1) loop = asyncio.get_event_loop_policy().new_event_loop() nest_asyncio.apply(loop) max_bonus: int = 13 @@ -210,6 +211,11 @@ def _cmd_set_path(self, path: str = '') -> bool: def _cmd_download_data(self) -> bool: """Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files.""" + pool.submit(self._download_data) + return True + + @staticmethod + def _download_data() -> bool: if "SC2PATH" not in os.environ: check_game_install_path() @@ -220,7 +226,7 @@ def _cmd_download_data(self) -> bool: metadata = None tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION, - metadata=metadata, force_download=True) + metadata=metadata, force_download=True) if tempzip != '': try: diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 4e1058fecfc8..57488ff33314 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -151,14 +151,14 @@ def get_rules_lookup(player: int): "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player), "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player), "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player), - "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) + "Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player)) }, "elevators": { - "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) - and state.has("Key for Office Elevator", player))), + "Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)) + and state.has("Key for Office Elevator", player)), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) - and state.has("Key for Three Floor Elevator", player))) + "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) + and state.has("Key for Three Floor Elevator", player)) }, "lightning": { "Ixupi Captured Lightning": lambda state: lightning_capturable(state, player) diff --git a/worlds/shivers/data/excluded_locations.json b/worlds/shivers/data/excluded_locations.json index 6ed625077af8..a37285eb1d29 100644 --- a/worlds/shivers/data/excluded_locations.json +++ b/worlds/shivers/data/excluded_locations.json @@ -42,7 +42,7 @@ "Information Plaque: Aliens (UFO)" ], "elevators": [ - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Bedroom Elevator", "Puzzle Solved Three Floor Elevator" ], diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json index 7d031b886bff..fdf8ed69d1e5 100644 --- a/worlds/shivers/data/locations.json +++ b/worlds/shivers/data/locations.json @@ -110,7 +110,7 @@ "Information Plaque: Astronomical Construction (UFO)", "Information Plaque: Guillotine (Torture)", "Information Plaque: Aliens (UFO)", - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Bedroom Elevator", "Puzzle Solved Three Floor Elevator", "Ixupi Captured Lightning" @@ -129,7 +129,7 @@ "Ixupi Captured Sand", "Ixupi Captured Metal", "Ixupi Captured Lightning", - "Puzzle Solved Underground Elevator", + "Puzzle Solved Office Elevator", "Puzzle Solved Three Floor Elevator", "Puzzle Hint Found: Combo Lock in Mailbox", "Puzzle Hint Found: Orange Symbol", diff --git a/worlds/sm64ex/Items.py b/worlds/sm64ex/Items.py index 5b429a23cdc3..546f1abd316b 100644 --- a/worlds/sm64ex/Items.py +++ b/worlds/sm64ex/Items.py @@ -16,6 +16,21 @@ class SM64Item(Item): "1Up Mushroom": 3626184 } +action_item_table = { + "Double Jump": 3626185, + "Triple Jump": 3626186, + "Long Jump": 3626187, + "Backflip": 3626188, + "Side Flip": 3626189, + "Wall Kick": 3626190, + "Dive": 3626191, + "Ground Pound": 3626192, + "Kick": 3626193, + "Climb": 3626194, + "Ledge Grab": 3626195 +} + + cannon_item_table = { "Cannon Unlock BoB": 3626200, "Cannon Unlock WF": 3626201, @@ -29,4 +44,4 @@ class SM64Item(Item): "Cannon Unlock RR": 3626214 } -item_table = {**generic_item_table, **cannon_item_table} \ No newline at end of file +item_table = {**generic_item_table, **action_item_table, **cannon_item_table} \ No newline at end of file diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 8a10f3edea55..d9a877df2b37 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,9 +1,10 @@ import typing from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice - +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""" + """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything. + Removes 15 locations from the pool.""" display_name = "Enable 100 Coin Stars" @@ -13,56 +14,63 @@ class StrictCapRequirements(DefaultOnToggle): class StrictCannonRequirements(DefaultOnToggle): - """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy - Checks are enabled""" + """If disabled, Stars that expect cannons may have to be acquired without them. + Has no effect if Buddy Checks and Move Randomizer are disabled""" display_name = "Strict Cannon Requirements" class FirstBowserStarDoorCost(Range): - """How many stars are required at the Star Door to Bowser in the Dark World""" + """What percent of the total stars are required at the Star Door to Bowser in the Dark World""" + display_name = "First Star Door Cost %" range_start = 0 - range_end = 50 - default = 8 + range_end = 40 + default = 7 class BasementStarDoorCost(Range): - """How many stars are required at the Star Door in the Basement""" + """What percent of the total stars are required at the Star Door in the Basement""" + display_name = "Basement Star Door %" range_start = 0 - range_end = 70 - default = 30 + range_end = 50 + default = 25 class SecondFloorStarDoorCost(Range): - """How many stars are required to access the third floor""" + """What percent of the total stars are required to access the third floor""" + display_name = 'Second Floor Star Door %' range_start = 0 - range_end = 90 - default = 50 + range_end = 70 + default = 42 class MIPS1Cost(Range): - """How many stars are required to spawn MIPS the first time""" + """What percent of the total stars are required to spawn MIPS the first time""" + display_name = "MIPS 1 Star %" range_start = 0 - range_end = 40 - default = 15 + range_end = 35 + default = 12 class MIPS2Cost(Range): - """How many stars are required to spawn MIPS the second time.""" + """What percent of the total stars are required to spawn MIPS the second time.""" + display_name = "MIPS 2 Star %" range_start = 0 - range_end = 80 - default = 50 + range_end = 70 + default = 42 class StarsToFinish(Range): - """How many stars are required at the infinite stairs""" - display_name = "Endless Stairs Stars" + """What percent of the total stars are required at the infinite stairs""" + display_name = "Endless Stairs Star %" range_start = 0 - range_end = 100 - default = 70 + range_end = 90 + default = 58 class AmountOfStars(Range): - """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" + """How many stars exist. + If there aren't enough locations to hold the given total, the total will be reduced.""" + display_name = "Total Power Stars" range_start = 35 range_end = 120 default = 120 @@ -83,11 +91,13 @@ class BuddyChecks(Toggle): class ExclamationBoxes(Choice): - """Include 1Up Exclamation Boxes during randomization""" + """Include 1Up Exclamation Boxes during randomization. + Adds 29 locations to the pool.""" display_name = "Randomize 1Up !-Blocks" option_Off = 0 option_1Ups_Only = 1 + class CompletionType(Choice): """Set goal for game completion""" display_name = "Completion Goal" @@ -96,17 +106,32 @@ class CompletionType(Choice): class ProgressiveKeys(DefaultOnToggle): - """Keys will first grant you access to the Basement, then to the Secound Floor""" + """Keys will first grant you access to the Basement, then to the Second Floor""" display_name = "Progressive Keys" +class StrictMoveRequirements(DefaultOnToggle): + """If disabled, Stars that expect certain moves may have to be acquired without them. Only makes a difference + if Move Randomization is enabled""" + display_name = "Strict Move Requirements" + +def getMoveRandomizerOption(action: str): + class MoveRandomizerOption(Toggle): + """Mario is unable to perform this action until a corresponding item is picked up. + This option is incompatible with builds using a 'nomoverando' branch.""" + display_name = f"Randomize {action}" + return MoveRandomizerOption + sm64_options: typing.Dict[str, type(Option)] = { "AreaRandomizer": AreaRandomizer, + "BuddyChecks": BuddyChecks, + "ExclamationBoxes": ExclamationBoxes, "ProgressiveKeys": ProgressiveKeys, "EnableCoinStars": EnableCoinStars, - "AmountOfStars": AmountOfStars, "StrictCapRequirements": StrictCapRequirements, "StrictCannonRequirements": StrictCannonRequirements, + "StrictMoveRequirements": StrictMoveRequirements, + "AmountOfStars": AmountOfStars, "FirstBowserStarDoorCost": FirstBowserStarDoorCost, "BasementStarDoorCost": BasementStarDoorCost, "SecondFloorStarDoorCost": SecondFloorStarDoorCost, @@ -114,7 +139,10 @@ class ProgressiveKeys(DefaultOnToggle): "MIPS2Cost": MIPS2Cost, "StarsToFinish": StarsToFinish, "death_link": DeathLink, - "BuddyChecks": BuddyChecks, - "ExclamationBoxes": ExclamationBoxes, - "CompletionType" : CompletionType, + "CompletionType": CompletionType, } + +for action in action_item_table: + # HACK: Disable randomization of double jump + if action == 'Double Jump': continue + sm64_options[f"MoveRandomizer{action.replace(' ','')}"] = getMoveRandomizerOption(action) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d426804c30f3..c04b862fa757 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -1,5 +1,4 @@ import typing - from enum import Enum from BaseClasses import MultiWorld, Region, Entrance, Location @@ -8,7 +7,8 @@ locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \ locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ - locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table + locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table + class SM64Levels(int, Enum): BOB_OMB_BATTLEFIELD = 91 @@ -55,7 +55,7 @@ class SM64Levels(int, Enum): SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock", SM64Levels.RAINBOW_RIDE: "Rainbow Ride" } -sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } +sm64_paintings_to_level = {painting: level for (level, painting) in sm64_level_to_paintings.items() } # sm64secrets is a dict of secret areas, same format as sm64paintings sm64_level_to_secrets: typing.Dict[SM64Levels, str] = { @@ -68,152 +68,163 @@ class SM64Levels(int, Enum): SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea", SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow" } -sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } +sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secrets.items() } -sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level } -sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets } +sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level } +sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets } def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", player, world, "Castle Area") - create_default_locs(regSS, locSS_table, player) + create_default_locs(regSS, locSS_table) world.regions.append(regSS) regBoB = create_region("Bob-omb Battlefield", player, world) - create_default_locs(regBoB, locBoB_table, player) + create_locs(regBoB, "BoB: Big Bob-Omb on the Summit", "BoB: Footrace with Koopa The Quick", + "BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy") + create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins") if (world.EnableCoinStars[player].value): - regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB)) - world.regions.append(regBoB) + create_locs(regBoB, "BoB: 100 Coins") regWhomp = create_region("Whomp's Fortress", player, world) - create_default_locs(regWhomp, locWhomp_table, player) + create_locs(regWhomp, "WF: Chip Off Whomp's Block", "WF: Shoot into the Wild Blue", "WF: Red Coins on the Floating Isle", + "WF: Fall onto the Caged Island", "WF: Blast Away the Wall") + create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy") if (world.EnableCoinStars[player].value): - regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp)) - world.regions.append(regWhomp) + create_locs(regWhomp, "WF: 100 Coins") regJRB = create_region("Jolly Roger Bay", player, world) - create_default_locs(regJRB, locJRB_table, player) + create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave", + "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy") + jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat") if (world.EnableCoinStars[player].value): - regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB)) - world.regions.append(regJRB) + create_locs(jrb_upper, "JRB: 100 Coins") regCCM = create_region("Cool, Cool Mountain", player, world) - create_default_locs(regCCM, locCCM_table, player) + create_default_locs(regCCM, locCCM_table) if (world.EnableCoinStars[player].value): - regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM)) - world.regions.append(regCCM) + create_locs(regCCM, "CCM: 100 Coins") regBBH = create_region("Big Boo's Haunt", player, world) - create_default_locs(regBBH, locBBH_table, player) + create_locs(regBBH, "BBH: Go on a Ghost Hunt", "BBH: Ride Big Boo's Merry-Go-Round", + "BBH: Secret of the Haunted Books", "BBH: Seek the 8 Red Coins") + bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room") + create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion") if (world.EnableCoinStars[player].value): - regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH)) - world.regions.append(regBBH) + create_locs(regBBH, "BBH: 100 Coins") regPSS = create_region("The Princess's Secret Slide", player, world) - create_default_locs(regPSS, locPSS_table, player) - world.regions.append(regPSS) + create_default_locs(regPSS, locPSS_table) regSA = create_region("The Secret Aquarium", player, world) - create_default_locs(regSA, locSA_table, player) - world.regions.append(regSA) + create_default_locs(regSA, locSA_table) regTotWC = create_region("Tower of the Wing Cap", player, world) - create_default_locs(regTotWC, locTotWC_table, player) - world.regions.append(regTotWC) + create_default_locs(regTotWC, locTotWC_table) regBitDW = create_region("Bowser in the Dark World", player, world) - create_default_locs(regBitDW, locBitDW_table, player) - world.regions.append(regBitDW) + create_default_locs(regBitDW, locBitDW_table) - regBasement = create_region("Basement", player, world) - world.regions.append(regBasement) + create_region("Basement", player, world) regHMC = create_region("Hazy Maze Cave", player, world) - create_default_locs(regHMC, locHMC_table, player) + create_locs(regHMC, "HMC: Swimming Beast in the Cavern", "HMC: Metal-Head Mario Can Move!", + "HMC: Watch for Rolling Rocks", "HMC: Navigating the Toxic Maze","HMC: 1Up Block Past Rolling Rocks") + hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins") + create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit") if (world.EnableCoinStars[player].value): - regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC)) - world.regions.append(regHMC) + create_locs(hmc_red_coin_area, "HMC: 100 Coins") regLLL = create_region("Lethal Lava Land", player, world) - create_default_locs(regLLL, locLLL_table, player) + create_locs(regLLL, "LLL: Boil the Big Bully", "LLL: Bully the Bullies", + "LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling") + create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano") if (world.EnableCoinStars[player].value): - regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL)) - world.regions.append(regLLL) + create_locs(regLLL, "LLL: 100 Coins") regSSL = create_region("Shifting Sand Land", player, world) - create_default_locs(regSSL, locSSL_table, player) + create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid", "SSL: Inside the Ancient Pyramid", + "SSL: Free Flying for 8 Red Coins", "SSL: Bob-omb Buddy", + "SSL: 1Up Block Outside Pyramid", "SSL: 1Up Block Pyramid Left Path", "SSL: 1Up Block Pyramid Back") + create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle") if (world.EnableCoinStars[player].value): - regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL)) - world.regions.append(regSSL) + create_locs(regSSL, "SSL: 100 Coins") regDDD = create_region("Dire, Dire Docks", player, world) - create_default_locs(regDDD, locDDD_table, player) + create_locs(regDDD, "DDD: Board Bowser's Sub", "DDD: Chests in the Current", "DDD: Through the Jet Stream", + "DDD: The Manta Ray's Reward", "DDD: Collect the Caps...") + ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins") if (world.EnableCoinStars[player].value): - regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD)) - world.regions.append(regDDD) + create_locs(ddd_moving_poles, "DDD: 100 Coins") regCotMC = create_region("Cavern of the Metal Cap", player, world) - create_default_locs(regCotMC, locCotMC_table, player) - world.regions.append(regCotMC) + create_default_locs(regCotMC, locCotMC_table) regVCutM = create_region("Vanish Cap under the Moat", player, world) - create_default_locs(regVCutM, locVCutM_table, player) - world.regions.append(regVCutM) + create_default_locs(regVCutM, locVCutM_table) regBitFS = create_region("Bowser in the Fire Sea", player, world) - create_default_locs(regBitFS, locBitFS_table, player) - world.regions.append(regBitFS) + create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys()) - regFloor2 = create_region("Second Floor", player, world) - world.regions.append(regFloor2) + create_region("Second Floor", player, world) regSL = create_region("Snowman's Land", player, world) - create_default_locs(regSL, locSL_table, player) + create_default_locs(regSL, locSL_table) if (world.EnableCoinStars[player].value): - regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL)) - world.regions.append(regSL) + create_locs(regSL, "SL: 100 Coins") regWDW = create_region("Wet-Dry World", player, world) - create_default_locs(regWDW, locWDW_table, player) + create_locs(regWDW, "WDW: Express Elevator--Hurry Up!") + wdw_top = create_subregion(regWDW, "WDW: Top", "WDW: Shocking Arrow Lifts!", "WDW: Top o' the Town", + "WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy") + create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown") if (world.EnableCoinStars[player].value): - regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW)) - world.regions.append(regWDW) + create_locs(wdw_top, "WDW: 100 Coins") regTTM = create_region("Tall, Tall Mountain", player, world) - create_default_locs(regTTM, locTTM_table, player) + ttm_middle = create_subregion(regTTM, "TTM: Middle", "TTM: Scary 'Shrooms, Red Coins", "TTM: Blast to the Lonely Mushroom", + "TTM: Bob-omb Buddy", "TTM: 1Up Block on Red Mushroom") + ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage", + "TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge") if (world.EnableCoinStars[player].value): - regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM)) - world.regions.append(regTTM) - - regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world) - create_default_locs(regTHIT, locTHI_table, player) + create_locs(ttm_top, "TTM: 100 Coins") + + create_region("Tiny-Huge Island (Huge)", player, world) + create_region("Tiny-Huge Island (Tiny)", player, world) + regTHI = create_region("Tiny-Huge Island", player, world) + create_locs(regTHI, "THI: The Tip Top of the Huge Island", "THI: 1Up Block THI Small near Start") + thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick", + "THI: Five Itty Bitty Secrets", "THI: Wiggler's Red Coins", "THI: Bob-omb Buddy", + "THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area") + thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm") if (world.EnableCoinStars[player].value): - regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT)) - world.regions.append(regTHIT) - regTHIH = create_region("Tiny-Huge Island (Huge)", player, world) - world.regions.append(regTHIH) + create_locs(thi_large_top, "THI: 100 Coins") regFloor3 = create_region("Third Floor", player, world) world.regions.append(regFloor3) regTTC = create_region("Tick Tock Clock", player, world) - create_default_locs(regTTC, locTTC_table, player) + create_locs(regTTC, "TTC: Stop Time for Red Coins") + ttc_lower = create_subregion(regTTC, "TTC: Lower", "TTC: Roll into the Cage", "TTC: Get a Hand", "TTC: 1Up Block Midway Up") + ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums") + ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top") if (world.EnableCoinStars[player].value): - regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC)) - world.regions.append(regTTC) + create_locs(ttc_top, "TTC: 100 Coins") regRR = create_region("Rainbow Ride", player, world) - create_default_locs(regRR, locRR_table, player) + create_locs(regRR, "RR: Swingin' in the Breeze", "RR: Tricky Triangles!", + "RR: 1Up Block Top of Red Coin Maze", "RR: 1Up Block Under Fly Guy", "RR: Bob-omb Buddy") + rr_maze = create_subregion(regRR, "RR: Maze", "RR: Coins Amassed in a Maze") + create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow") + create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky") if (world.EnableCoinStars[player].value): - regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR)) - world.regions.append(regRR) + create_locs(rr_maze, "RR: 100 Coins") regWMotR = create_region("Wing Mario over the Rainbow", player, world) - create_default_locs(regWMotR, locWMotR_table, player) - world.regions.append(regWMotR) + create_default_locs(regWMotR, locWMotR_table) regBitS = create_region("Bowser in the Sky", player, world) - create_default_locs(regBitS, locBitS_table, player) - world.regions.append(regBitS) + create_locs(regBitS, "Bowser in the Sky 1Up Block") + create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins") def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): @@ -227,9 +238,30 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru sourceRegion.exits.append(connection) connection.connect(targetRegion) + def create_region(name: str, player: int, world: MultiWorld) -> Region: - return Region(name, player, world) + region = Region(name, player, world) + world.regions.append(region) + return region + + +def create_subregion(source_region: Region, name: str, *locs: str) -> Region: + region = Region(name, source_region.player, source_region.multiworld) + connection = Entrance(source_region.player, name, source_region) + source_region.exits.append(connection) + connection.connect(region) + source_region.multiworld.regions.append(region) + create_locs(region, *locs) + return region + + +def set_subregion_access_rule(world, player, region_name: str, rule): + world.get_entrance(world, player, region_name).access_rule = rule + + +def create_default_locs(reg: Region, default_locs: dict): + create_locs(reg, *default_locs.keys()) + -def create_default_locs(reg: Region, locs, player): - reg_names = [name for name, id in locs.items()] - reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs] +def create_locs(reg: Region, *locs: str): + reg.locations += [SM64Location(reg.player, loc_name, location_table[loc_name], reg) for loc_name in locs] diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index fedd5b7a6ebd..f2b8e0bcdf2d 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,36 +1,59 @@ -from ..generic.Rules import add_rule -from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances +from typing import Callable, Union, Dict, Set -def shuffle_dict_keys(world, obj: dict) -> dict: - keys = list(obj.keys()) - values = list(obj.values()) +from BaseClasses import MultiWorld +from ..generic.Rules import add_rule, set_rule +from .Locations import location_table +from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\ +sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances +from .Items import action_item_table + +def shuffle_dict_keys(world, dictionary: dict) -> dict: + keys = list(dictionary.keys()) + values = list(dictionary.values()) world.random.shuffle(keys) - return dict(zip(keys,values)) + return dict(zip(keys, values)) -def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, - swapdict: dict, world): +def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_regions: Set[str], + swapdict: Dict[SM64Levels, str], world): if entrance_map[entrance] in invalid_regions: # Unlucky :C - replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items() + replacement_regions = [(rand_entrance, rand_region) for rand_entrance, rand_region in swapdict.items() if rand_region not in invalid_regions] - rand_region, rand_entrance = world.random.choice(replacement_regions) + rand_entrance, rand_region = world.random.choice(replacement_regions) old_dest = entrance_map[entrance] entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest - swapdict[rand_region] = entrance - swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region + swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest + swapdict.pop(entrance) -def set_rules(world, player: int, area_connections: dict): +def set_rules(world, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() + valid_move_randomizer_start_courses = [ + "Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain", + "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land", + "Dire, Dire Docks", "Snowman's Land" + ] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) + # If not shuffling later, ensure a valid start course on move randomizer + if world.AreaRandomizer[player].value < 3 and move_rando_bitvec > 0: + swapdict = randomized_level_to_paintings.copy() + invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses} + fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world) + fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world) + if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) - randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } + randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool - randomized_entrances = shuffle_dict_keys(world,randomized_entrances) - swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() } + randomized_entrances = shuffle_dict_keys(world, randomized_entrances) # Guarantee first entrance is a course - fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) + swapdict = randomized_entrances.copy() + if move_rando_bitvec == 0: + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) + else: + invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses} + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world) + fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world) # Guarantee BITFS is not mapped to DDD fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world) # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. @@ -43,27 +66,34 @@ def set_rules(world, player: int, area_connections: dict): # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} - + + rf = RuleFactory(world, player, move_rando_bitvec) + connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1)) connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3)) connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12)) connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1)) - connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, randomized_entrances_s["Jolly Roger Bay"], randomized_entrances_s["The Secret Aquarium"], + rf.build_rule("SF/BF | TJ & LG | MOVELESS & TJ")) connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10)) - connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) + connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], + lambda state: state.has("Power Star", player, star_costs["FirstBowserDoorCost"])) connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"]) connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"]) connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) + connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"])) 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"]) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + 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)) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) @@ -72,66 +102,127 @@ def set_rules(world, player: int, area_connections: dict): connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"]) connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"]) connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"]) - connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)") - connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)") + connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island") + connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island") - connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) + connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"])) connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"]) connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"]) connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"]) - connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) + connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"])) - #Special Rules for some Locations - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player)) - add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player)) + # Course Rules + # Bob-omb Battlefield + rf.assign_rule("BoB: Island", "CANN | CANNLESS & WC & TJ | CAPLESS & CANNLESS & LJ") + rf.assign_rule("BoB: Mario Wings to the Sky", "CANN & WC | CAPLESS & CANN") + rf.assign_rule("BoB: Behind Chain Chomp's Gate", "GP | MOVELESS") + # Whomp's Fortress + rf.assign_rule("WF: Tower", "{{WF: Chip Off Whomp's Block}}") + rf.assign_rule("WF: Chip Off Whomp's Block", "GP") + rf.assign_rule("WF: Shoot into the Wild Blue", "WK & TJ/SF | CANN") + rf.assign_rule("WF: Fall onto the Caged Island", "CL & {WF: Tower} | MOVELESS & TJ | MOVELESS & LJ | MOVELESS & CANN") + rf.assign_rule("WF: Blast Away the Wall", "CANN | CANNLESS & LG") + # Jolly Roger Bay + rf.assign_rule("JRB: Upper", "TJ/BF/SF/WK | MOVELESS & LG") + rf.assign_rule("JRB: Red Coins on the Ship Afloat", "CL/CANN/TJ/BF/WK") + rf.assign_rule("JRB: Blast to the Stone Pillar", "CANN+CL | CANNLESS & MOVELESS | CANN & MOVELESS") + rf.assign_rule("JRB: Through the Jet Stream", "MC | CAPLESS") + # Cool, Cool Mountain + rf.assign_rule("CCM: Wall Kicks Will Work", "TJ/WK & CANN | CANNLESS & TJ/WK | MOVELESS") + # Big Boo's Haunt + rf.assign_rule("BBH: Third Floor", "WK+LG | MOVELESS & WK") + rf.assign_rule("BBH: Roof", "LJ | MOVELESS") + rf.assign_rule("BBH: Secret of the Haunted Books", "KK | MOVELESS") + rf.assign_rule("BBH: Seek the 8 Red Coins", "BF/WK/TJ/SF") + rf.assign_rule("BBH: Eye to Eye in the Secret Room", "VC") + # Haze Maze Cave + rf.assign_rule("HMC: Red Coin Area", "CL & WK/LG/BF/SF/TJ | MOVELESS & WK") + rf.assign_rule("HMC: Pit Islands", "TJ+CL | MOVELESS & WK & TJ/LJ | MOVELESS & WK+SF+LG") + rf.assign_rule("HMC: Metal-Head Mario Can Move!", "LJ+MC | CAPLESS & LJ+TJ | CAPLESS & MOVELESS & LJ/TJ/WK") + rf.assign_rule("HMC: Navigating the Toxic Maze", "WK/SF/BF/TJ") + rf.assign_rule("HMC: Watch for Rolling Rocks", "WK") + # Lethal Lava Land + rf.assign_rule("LLL: Upper Volcano", "CL") + # Shifting Sand Land + rf.assign_rule("SSL: Upper Pyramid", "CL & TJ/BF/SF/LG | MOVELESS") + rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ/SF/BF & TJ+WC | TJ/SF/BF & CAPLESS | MOVELESS") + # Dire, Dire Docks + rf.assign_rule("DDD: Moving Poles", "CL & {{Bowser in the Fire Sea Key}} | TJ+DV+LG+WK & MOVELESS") + rf.assign_rule("DDD: Through the Jet Stream", "MC | CAPLESS") + rf.assign_rule("DDD: Collect the Caps...", "VC+MC | CAPLESS & VC") + # Snowman's Land + rf.assign_rule("SL: Snowman's Big Head", "BF/SF/CANN/TJ") + rf.assign_rule("SL: In the Deep Freeze", "WK/SF/LG/BF/CANN/TJ") + rf.assign_rule("SL: Into the Igloo", "VC & TJ/SF/BF/WK/LG | MOVELESS & VC") + # Wet-Dry World + rf.assign_rule("WDW: Top", "WK/TJ/SF/BF | MOVELESS") + rf.assign_rule("WDW: Downtown", "NAR & LG & TJ/SF/BF | CANN | MOVELESS & TJ+DV") + rf.assign_rule("WDW: Go to Town for Red Coins", "WK | MOVELESS & TJ") + rf.assign_rule("WDW: Quick Race Through Downtown!", "VC & WK/BF | VC & TJ+LG | MOVELESS & VC & TJ") + rf.assign_rule("WDW: Bob-omb Buddy", "TJ | SF+LG | NAR & BF/SF") + # Tall, Tall Mountain + rf.assign_rule("TTM: Top", "MOVELESS & TJ | LJ/DV & LG/KK | MOVELESS & WK & SF/LG | MOVELESS & KK/DV") + rf.assign_rule("TTM: Blast to the Lonely Mushroom", "CANN | CANNLESS & LJ | MOVELESS & CANNLESS") + # Tiny-Huge Island + rf.assign_rule("THI: Pipes", "NAR | LJ/TJ/DV/LG | MOVELESS & BF/SF/KK") + rf.assign_rule("THI: Large Top", "NAR | LJ/TJ/DV | MOVELESS") + rf.assign_rule("THI: Wiggler's Red Coins", "WK") + rf.assign_rule("THI: Make Wiggler Squirm", "GP | MOVELESS & DV") + # Tick Tock Clock + rf.assign_rule("TTC: Lower", "LG/TJ/SF/BF/WK") + rf.assign_rule("TTC: Upper", "CL | SF+WK") + rf.assign_rule("TTC: Top", "CL | SF+WK") + rf.assign_rule("TTC: Stomp on the Thwomp", "LG & TJ/SF/BF") + rf.assign_rule("TTC: Stop Time for Red Coins", "NAR | {TTC: Lower}") + # Rainbow Ride + rf.assign_rule("RR: Maze", "WK | LJ & SF/BF/TJ | MOVELESS & LG/TJ") + rf.assign_rule("RR: Bob-omb Buddy", "WK | MOVELESS & LG") + rf.assign_rule("RR: Swingin' in the Breeze", "LG/TJ/BF/SF") + rf.assign_rule("RR: Tricky Triangles!", "LG/TJ/BF/SF") + rf.assign_rule("RR: Cruiser", "WK/SF/BF/LG/TJ") + rf.assign_rule("RR: House", "TJ/SF/BF/LG") + rf.assign_rule("RR: Somewhere Over the Rainbow", "CANN") + # Cavern of the Metal Cap + rf.assign_rule("Cavern of the Metal Cap Red Coins", "MC | CAPLESS") + # Vanish Cap Under the Moat + rf.assign_rule("Vanish Cap Under the Moat Switch", "WK/TJ/BF/SF/LG | MOVELESS") + rf.assign_rule("Vanish Cap Under the Moat Red Coins", "TJ/BF/SF/LG/WK & VC | CAPLESS & WK") + # Bowser in the Fire Sea + rf.assign_rule("BitFS: Upper", "CL") + rf.assign_rule("Bowser in the Fire Sea Red Coins", "LG/WK") + rf.assign_rule("Bowser in the Fire Sea 1Up Block Near Poles", "LG/WK") + # Wing Mario Over the Rainbow + rf.assign_rule("Wing Mario Over the Rainbow Red Coins", "TJ+WC") + rf.assign_rule("Wing Mario Over the Rainbow 1Up Block", "TJ+WC") + # Bowser in the Sky + rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG") + # 100 Coin Stars if world.EnableCoinStars[player]: - add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player)) - add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player)) - - if world.AreaRandomizer[player] or world.StrictCannonRequirements[player]: - # If area rando is on, it may not be possible to modify WDW's starting water level, - # which would make it impossible to reach downtown area without the cannon. - add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player)) - add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player)) - add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player)) - - if world.StrictCapRequirements[player]: - add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("HMC: Metal-Head Mario Can Move!", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("JRB: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("SSL: Free Flying for 8 Red Coins", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("DDD: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Metal Cap", player)) - add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.has("Vanish Cap", player)) - add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.has("Metal Cap", player)) - if world.StrictCannonRequirements[player]: - add_rule(world.get_location("WF: Blast Away the Wall", player), lambda state: state.has("Cannon Unlock WF", player)) - add_rule(world.get_location("JRB: Blast to the Stone Pillar", player), lambda state: state.has("Cannon Unlock JRB", player)) - add_rule(world.get_location("CCM: Wall Kicks Will Work", player), lambda state: state.has("Cannon Unlock CCM", player)) - add_rule(world.get_location("TTM: Blast to the Lonely Mushroom", player), lambda state: state.has("Cannon Unlock TTM", player)) - if world.StrictCapRequirements[player] and world.StrictCannonRequirements[player]: - # Ability to reach the floating island. Need some of those coins to get 100 coin star as well. - add_rule(world.get_location("BoB: Find the 8 Red Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - add_rule(world.get_location("BoB: Shoot to the Island in the Sky", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - if world.EnableCoinStars[player]: - add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) - - #Rules for Secret Stars - add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player)) - add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player)) + rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ") + rf.assign_rule("WF: 100 Coins", "GP | MOVELESS") + rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}") + rf.assign_rule("HMC: 100 Coins", "GP") + rf.assign_rule("SSL: 100 Coins", "{SSL: Upper Pyramid} | GP") + rf.assign_rule("DDD: 100 Coins", "GP") + rf.assign_rule("SL: 100 Coins", "VC | MOVELESS") + rf.assign_rule("WDW: 100 Coins", "GP | {WDW: Downtown}") + rf.assign_rule("TTC: 100 Coins", "GP") + rf.assign_rule("THI: 100 Coins", "GP") + rf.assign_rule("RR: 100 Coins", "GP & WK") + # Castle Stars add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) - if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value: - (world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value) - add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) - add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) + if star_costs["MIPS1Cost"] > star_costs["MIPS2Cost"]: + (star_costs["MIPS2Cost"], star_costs["MIPS1Cost"]) = (star_costs["MIPS1Cost"], star_costs["MIPS2Cost"]) + rf.assign_rule("MIPS 1", "DV | MOVELESS") + rf.assign_rule("MIPS 2", "DV | MOVELESS") + add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS1Cost"])) + add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS2Cost"])) + + world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) if world.CompletionType[player] == "last_bowser_stage": world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) @@ -139,3 +230,145 @@ def set_rules(world, player: int, area_connections: dict): world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ state.can_reach("Bowser in the Fire Sea", 'Region', player) and \ state.can_reach("Bowser in the Sky", 'Region', player) + + +class RuleFactory: + + world: MultiWorld + player: int + move_rando_bitvec: bool + area_randomizer: bool + capless: bool + cannonless: bool + moveless: bool + + token_table = { + "TJ": "Triple Jump", + "LJ": "Long Jump", + "BF": "Backflip", + "SF": "Side Flip", + "WK": "Wall Kick", + "DV": "Dive", + "GP": "Ground Pound", + "KK": "Kick", + "CL": "Climb", + "LG": "Ledge Grab", + "WC": "Wing Cap", + "MC": "Metal Cap", + "VC": "Vanish Cap" + } + + class SM64LogicException(Exception): + pass + + def __init__(self, world, player, move_rando_bitvec): + self.world = world + self.player = player + self.move_rando_bitvec = move_rando_bitvec + self.area_randomizer = world.AreaRandomizer[player].value > 0 + self.capless = not world.StrictCapRequirements[player] + self.cannonless = not world.StrictCannonRequirements[player] + self.moveless = not world.StrictMoveRequirements[player] or not move_rando_bitvec > 0 + + def assign_rule(self, target_name: str, rule_expr: str): + target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player) + cannon_name = "Cannon Unlock " + target_name.split(':')[0] + try: + rule = self.build_rule(rule_expr, cannon_name) + except RuleFactory.SM64LogicException as exception: + raise RuleFactory.SM64LogicException( + f"Error generating rule for {target_name} using rule expression {rule_expr}: {exception}") + if rule: + set_rule(target, rule) + + def build_rule(self, rule_expr: str, cannon_name: str = '') -> Callable: + expressions = rule_expr.split(" | ") + rules = [] + for expression in expressions: + or_clause = self.combine_and_clauses(expression, cannon_name) + if or_clause is True: + return None + if or_clause is not False: + rules.append(or_clause) + if rules: + if len(rules) == 1: + return rules[0] + else: + return lambda state: any(rule(state) for rule in rules) + else: + return None + + def combine_and_clauses(self, rule_expr: str, cannon_name: str) -> Union[Callable, bool]: + expressions = rule_expr.split(" & ") + rules = [] + for expression in expressions: + and_clause = self.make_lambda(expression, cannon_name) + if and_clause is False: + return False + if and_clause is not True: + rules.append(and_clause) + if rules: + if len(rules) == 1: + return rules[0] + return lambda state: all(rule(state) for rule in rules) + else: + return True + + def make_lambda(self, expression: str, cannon_name: str) -> Union[Callable, bool]: + if '+' in expression: + tokens = expression.split('+') + items = set() + for token in tokens: + item = self.parse_token(token, cannon_name) + if item is True: + continue + if item is False: + return False + items.add(item) + if items: + return lambda state: state.has_all(items, self.player) + else: + return True + if '/' in expression: + tokens = expression.split('/') + items = set() + for token in tokens: + item = self.parse_token(token, cannon_name) + if item is True: + return True + if item is False: + continue + items.add(item) + if items: + return lambda state: state.has_any(items, self.player) + else: + return False + if '{{' in expression: + return lambda state: state.can_reach(expression[2:-2], "Location", self.player) + if '{' in expression: + return lambda state: state.can_reach(expression[1:-1], "Region", self.player) + item = self.parse_token(expression, cannon_name) + if item in (True, False): + return item + return lambda state: state.has(item, self.player) + + def parse_token(self, token: str, cannon_name: str) -> Union[str, bool]: + if token == "CANN": + return cannon_name + if token == "CAPLESS": + return self.capless + if token == "CANNLESS": + return self.cannonless + if token == "MOVELESS": + return self.moveless + if token == "NAR": + return not self.area_randomizer + item = self.token_table.get(token, None) + if not item: + raise Exception(f"Invalid token: '{item}'") + if item in action_item_table: + if self.move_rando_bitvec & (1 << (action_item_table[item] - action_item_table['Double Jump'])) == 0: + # This action item is not randomized. + return True + return item + diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index ab7409a324c3..e54a4b7a9103 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -1,7 +1,7 @@ import typing import os import json -from .Items import item_table, cannon_item_table, SM64Item +from .Items import item_table, action_item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules @@ -35,14 +35,44 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 8 + data_version = 9 required_client_version = (0, 3, 5) area_connections: typing.Dict[int, int] option_definitions = sm64_options + number_of_stars: int + move_rando_bitvec: int + filler_count: int + star_costs: typing.Dict[str, int] + def generate_early(self): + max_stars = 120 + if (not self.multiworld.EnableCoinStars[self.player].value): + max_stars -= 15 + self.move_rando_bitvec = 0 + for action, itemid in action_item_table.items(): + # HACK: Disable randomization of double jump + if action == 'Double Jump': continue + if getattr(self.multiworld, f"MoveRandomizer{action.replace(' ','')}")[self.player].value: + max_stars -= 1 + self.move_rando_bitvec |= (1 << (itemid - action_item_table['Double Jump'])) + if (self.multiworld.ExclamationBoxes[self.player].value > 0): + max_stars += 29 + self.number_of_stars = min(self.multiworld.AmountOfStars[self.player].value, max_stars) + self.filler_count = max_stars - self.number_of_stars + self.star_costs = { + 'FirstBowserDoorCost': round(self.multiworld.FirstBowserStarDoorCost[self.player].value * self.number_of_stars / 100), + 'BasementDoorCost': round(self.multiworld.BasementStarDoorCost[self.player].value * self.number_of_stars / 100), + 'SecondFloorDoorCost': round(self.multiworld.SecondFloorStarDoorCost[self.player].value * self.number_of_stars / 100), + 'MIPS1Cost': round(self.multiworld.MIPS1Cost[self.player].value * self.number_of_stars / 100), + 'MIPS2Cost': round(self.multiworld.MIPS2Cost[self.player].value * self.number_of_stars / 100), + 'StarsToFinish': round(self.multiworld.StarsToFinish[self.player].value * self.number_of_stars / 100) + } + # Nudge MIPS 1 to match vanilla on default percentage + if self.number_of_stars == 120 and self.multiworld.MIPS1Cost[self.player].value == 12: + self.star_costs['MIPS1Cost'] = 15 self.topology_present = self.multiworld.AreaRandomizer[self.player].value def create_regions(self): @@ -50,7 +80,7 @@ def create_regions(self): def set_rules(self): self.area_connections = {} - set_rules(self.multiworld, self.player, self.area_connections) + set_rules(self.multiworld, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec) if self.topology_present: # Write area_connections to spoiler log for entrance, destination in self.area_connections.items(): @@ -72,31 +102,29 @@ def create_item(self, name: str) -> Item: return item def create_items(self): - starcount = self.multiworld.AmountOfStars[self.player].value - if (not self.multiworld.EnableCoinStars[self.player].value): - starcount = max(35,self.multiworld.AmountOfStars[self.player].value-15) - starcount = max(starcount, self.multiworld.FirstBowserStarDoorCost[self.player].value, - self.multiworld.BasementStarDoorCost[self.player].value, self.multiworld.SecondFloorStarDoorCost[self.player].value, - self.multiworld.MIPS1Cost[self.player].value, self.multiworld.MIPS2Cost[self.player].value, - self.multiworld.StarsToFinish[self.player].value) - self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,starcount)] - self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.multiworld.EnableCoinStars[self.player].value else 0))] - + # 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)] + # Keys if (not self.multiworld.ProgressiveKeys[self.player].value): key1 = self.create_item("Basement Key") key2 = self.create_item("Second Floor Key") self.multiworld.itempool += [key1, key2] else: self.multiworld.itempool += [self.create_item("Progressive Key") for i in range(0,2)] - - wingcap = self.create_item("Wing Cap") - metalcap = self.create_item("Metal Cap") - vanishcap = self.create_item("Vanish Cap") - self.multiworld.itempool += [wingcap, metalcap, vanishcap] - + # Caps + self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]] + # Cannons if (self.multiworld.BuddyChecks[self.player].value): self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()] - else: + # Moves + self.multiworld.itempool += [self.create_item(action) + for action, itemid in action_item_table.items() + if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])] + + def generate_basic(self): + if not (self.multiworld.BuddyChecks[self.player].value): self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB")) self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF")) self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB")) @@ -108,9 +136,7 @@ def create_items(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.multiworld.ExclamationBoxes[self.player].value > 0): - self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)] - else: + if (self.multiworld.ExclamationBoxes[self.player].value == 0): 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")) @@ -147,14 +173,10 @@ def get_filler_item_name(self) -> str: def fill_slot_data(self): return { "AreaRando": self.area_connections, - "FirstBowserDoorCost": self.multiworld.FirstBowserStarDoorCost[self.player].value, - "BasementDoorCost": self.multiworld.BasementStarDoorCost[self.player].value, - "SecondFloorDoorCost": self.multiworld.SecondFloorStarDoorCost[self.player].value, - "MIPS1Cost": self.multiworld.MIPS1Cost[self.player].value, - "MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value, - "StarsToFinish": self.multiworld.StarsToFinish[self.player].value, + "MoveRandoVec": self.move_rando_bitvec, "DeathLink": self.multiworld.death_link[self.player].value, - "CompletionType" : self.multiworld.CompletionType[self.player].value, + "CompletionType": self.multiworld.CompletionType[self.player].value, + **self.star_costs } def generate_output(self, output_directory: str): diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 859cf234eb95..b07aa850c31d 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -40,6 +40,7 @@ async def validate_rom(self, ctx): if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM": return False + ctx.smz3_new_message_queue = rom_name[7] in b"1234567890" ctx.game = self.game ctx.items_handling = 0b101 # local items and remote start inventory @@ -53,6 +54,22 @@ async def game_watcher(self, ctx): if ctx.server is None or ctx.slot is None: # not successfully connected to a multiworld server, cannot process the game sending items return + + send_progress_addr_ptr_offset = 0x680 + send_progress_size = 8 + send_progress_message_byte_offset = 4 + send_progress_addr_table_offset = 0x700 + recv_progress_addr_ptr_offset = 0x600 + recv_progress_size = 4 + recv_progress_addr_table_offset = 0x602 + if ctx.smz3_new_message_queue: + send_progress_addr_ptr_offset = 0xD3C + send_progress_size = 2 + send_progress_message_byte_offset = 0 + send_progress_addr_table_offset = 0xDA0 + recv_progress_addr_ptr_offset = 0xD36 + recv_progress_size = 2 + recv_progress_addr_table_offset = 0xD38 currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2) if (currentGame is not None): @@ -69,7 +86,7 @@ async def game_watcher(self, ctx): ctx.finished_game = True return - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, 4) if data is None: return @@ -77,14 +94,14 @@ async def game_watcher(self, ctx): recv_item = data[2] | (data[3] << 8) while (recv_index < recv_item): - item_address = recv_index * 2 - message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2) - is_z3_item = ((message[1] & 0x80) != 0) - masked_part = (message[1] & 0x7F) if is_z3_item else message[1] - item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) + item_address = recv_index * send_progress_size + message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_table_offset + item_address, send_progress_size) + is_z3_item = ((message[send_progress_message_byte_offset+1] & 0x80) != 0) + masked_part = (message[send_progress_message_byte_offset+1] & 0x7F) if is_z3_item else message[send_progress_message_byte_offset+1] + item_index = ((message[send_progress_message_byte_offset] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) recv_index += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from .TotalSMZ3.Location import locations_start_id from . import convertLocSMZ3IDToAPID @@ -95,7 +112,7 @@ async def game_watcher(self, ctx): snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_ptr_offset, 4) if data is None: return @@ -107,9 +124,12 @@ async def game_watcher(self, ctx): item_id = item.item - items_start_id player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id])) + snes_buffered_write(ctx, + SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * recv_progress_size, + bytes([player_id, item_id]) if ctx.smz3_new_message_queue else + bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF])) item_out_ptr += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index c137442d9bd0..27fd8dcc3535 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -616,7 +616,8 @@ def WriteGameTitle(self): "H" if self.myWorld.Config.SMLogic == Config.SMLogic.Hard else \ "X" - self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21] + from Utils import __version__ + self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{__version__.replace('.', '')[0:3]}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21] self.patches.append((Snes(0x00FFC0), bytearray(self.title, 'utf8'))) self.patches.append((Snes(0x80FFC0), bytearray(self.title, 'utf8'))) diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py deleted file mode 100644 index fe5339c955b9..000000000000 --- a/worlds/soe/Logic.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Protocol, Set - -from BaseClasses import MultiWorld -from worlds.AutoWorld import LogicMixin -from . import pyevermizer -from .Options import EnergyCore, OutOfBounds, SequenceBreaks - -# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? - -# TODO: resolve/flatten/expand rules to get rid of recursion below where possible -# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) -rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] -# Logic.items are all items and extra items excluding non-progression items and duplicates -item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) - if item.name not in item_names and not item_names.add(item.name)] - - -class LogicProtocol(Protocol): - def has(self, name: str, player: int) -> bool: ... - def count(self, name: str, player: int) -> int: ... - def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... - def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... - - -# when this module is loaded, this mixin will extend BaseClasses.CollectionState -class SecretOfEvermoreLogic(LogicMixin): - def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: - """ - Returns reached count of one of evermizer's progress steps based on collected items. - i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP - """ - n = 0 - for item in items: - for pvd in item.provides: - if pvd[1] == progress: - if self.has(item.name, player): - n += self.count(item.name, player) * pvd[0] - if n >= max_count > 0: - return n - for rule in rules: - for pvd in rule.provides: - if pvd[1] == progress and pvd[0] > 0: - has = True - for req in rule.requires: - if not self.soe_has(req[1], world, player, req[0]): - has = False - break - if has: - n += pvd[0] - if n >= max_count > 0: - return n - return n - - def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: - """ - Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE - """ - if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call - w = world.worlds[player] - if w.energy_core == EnergyCore.option_fragments: - progress = pyevermizer.P_CORE_FRAGMENT - count = w.required_fragments - elif progress == pyevermizer.P_ALLOW_OOB: - if world.worlds[player].out_of_bounds == OutOfBounds.option_logic: - return True - elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: - if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic: - return True - return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index d02a8d02ee97..bbe018da5329 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -4,18 +4,23 @@ import threading import typing +# from . import pyevermizer # as part of the source tree +import pyevermizer # from package + import settings +from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial +from Utils import output_path from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule -from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial -from Utils import output_path +from .logic import SoEPlayerLogic +from .options import Difficulty, EnergyCore, SoEOptions +from .patch import SoEDeltaPatch, get_base_rom_path -import pyevermizer # from package -# from . import pyevermizer # as part of the source tree +if typing.TYPE_CHECKING: + from BaseClasses import MultiWorld, CollectionState + +__all__ = ["pyevermizer", "SoEWorld"] -from . import Logic # load logic mixin -from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments -from .Patch import SoEDeltaPatch, get_base_rom_path """ In evermizer: @@ -24,17 +29,17 @@ For most items this is their vanilla location (i.e. CHECK_GOURD, number). Items have `provides`, which give the actual progression -instead of providing multiple events per item, we iterate through them in Logic.py +instead of providing multiple events per item, we iterate through them in logic.py e.g. Found any weapon Locations have `requires` and `provides`. Requirements have to be converted to (access) rules for AP e.g. Chest locked behind having a weapon -Provides could be events, but instead we iterate through the entire logic in Logic.py +Provides could be events, but instead we iterate through the entire logic in logic.py e.g. NPC available after fighting a Boss Rules are special locations that don't have a physical location -instead of implementing virtual locations and virtual items, we simply use them in Logic.py +instead of implementing virtual locations and virtual items, we simply use them in logic.py e.g. 2DEs+Wheel+Gauge = Rocket Rules and Locations live on the same logic tree returned by pyevermizer.get_logic() @@ -76,7 +81,7 @@ # item helpers _ingredients = ( 'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron', - 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet', + 'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion', 'Ash', 'Acorn' ) _other_items = ( @@ -84,8 +89,8 @@ ) -def _match_item_name(item, substr: str) -> bool: - sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name +def _match_item_name(item: pyevermizer.Item, substr: str) -> bool: + sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name return sub == substr or sub == substr+'s' @@ -156,10 +161,11 @@ class RomFile(settings.SNESRomPath): class SoEWorld(World): """ Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a - space station where the final boss must be defeated. + space station where the final boss must be defeated. """ - game: str = "Secret of Evermore" - option_definitions = soe_options + game: typing.ClassVar[str] = "Secret of Evermore" + options_dataclass = SoEOptions + options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False data_version = 4 @@ -170,31 +176,21 @@ class SoEWorld(World): location_name_to_id, location_id_to_raw = _get_location_mapping() item_name_groups = _get_item_grouping() - trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')] - + logic: SoEPlayerLogic evermizer_seed: int connect_name: str - energy_core: int - sequence_breaks: int - out_of_bounds: int - available_fragments: int - required_fragments: int _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] - def __init__(self, *args, **kwargs): + def __init__(self, multiworld: "MultiWorld", player: int): self.connect_name_available_event = threading.Event() - super(SoEWorld, self).__init__(*args, **kwargs) + super(SoEWorld, self).__init__(multiworld, player) def generate_early(self) -> None: - # store option values that change logic - self.energy_core = self.multiworld.energy_core[self.player].value - self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value - self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value - self.required_fragments = self.multiworld.required_fragments[self.player].value - if self.required_fragments > self.multiworld.available_fragments[self.player].value: - self.multiworld.available_fragments[self.player].value = self.required_fragments - self.available_fragments = self.multiworld.available_fragments[self.player].value + # create logic from options + if self.options.required_fragments.value > self.options.available_fragments.value: + self.options.available_fragments.value = self.options.required_fragments.value + self.logic = SoEPlayerLogic(self.player, self.options) def create_event(self, event: str) -> Item: return SoEItem(event, ItemClassification.progression, None, self.player) @@ -214,20 +210,20 @@ def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item: return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player) @classmethod - def stage_assert_generate(cls, multiworld): + def stage_assert_generate(cls, _: "MultiWorld") -> None: rom_file = get_base_rom_path() if not os.path.exists(rom_file): raise FileNotFoundError(rom_file) - def create_regions(self): + def create_regions(self) -> None: # exclude 'hidden' on easy - max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256 + max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256 # TODO: generate *some* regions from locations' requirements? menu = Region('Menu', self.player, self.multiworld) self.multiworld.regions += [menu] - def get_sphere_index(evermizer_loc): + def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int: """Returns 0, 1 or 2 for locations in spheres 1, 2, 3+""" if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON: return 2 @@ -252,18 +248,18 @@ def get_sphere_index(evermizer_loc): # mark some as excluded based on numbers above for trash_sphere, fills in trash_fills.items(): for typ, counts in fills.items(): - count = counts[self.multiworld.difficulty[self.player].value] - for location in self.multiworld.random.sample(spheres[trash_sphere][typ], count): + count = counts[self.options.difficulty.value] + for location in self.random.sample(spheres[trash_sphere][typ], count): assert location.name != "Energy Core #285", "Error in sphere generation" location.progress_type = LocationProgressType.EXCLUDED - def sphere1_blocked_items_rule(item): + def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool: if isinstance(item, SoEItem): # disable certain items in sphere 1 if item.name in {"Gauge", "Wheel"}: return False # and some more for non-easy, non-mystery - if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery): + if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery): if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}: return False return True @@ -273,13 +269,13 @@ def sphere1_blocked_items_rule(item): add_item_rule(location, sphere1_blocked_items_rule) # make some logically late(r) bosses priority locations to increase complexity - if self.multiworld.difficulty[self.player] == Difficulty.option_mystery: - late_count = self.multiworld.random.randint(0, 2) + if self.options.difficulty == Difficulty.option_mystery: + late_count = self.random.randint(0, 2) else: - late_count = self.multiworld.difficulty[self.player].value + late_count = self.options.difficulty.value late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala", "Mungola", "Lightning Storm", "Magmar", "Volcano Viper") - late_locations = self.multiworld.random.sample(late_bosses, late_count) + late_locations = self.random.sample(late_bosses, late_count) # add locations to the world for sphere in spheres.values(): @@ -293,17 +289,17 @@ def sphere1_blocked_items_rule(item): menu.connect(ingame, "New Game") self.multiworld.regions += [ingame] - def create_items(self): + def create_items(self) -> None: # add regular items to the pool exclusions: typing.List[str] = [] - if self.energy_core != EnergyCore.option_shuffle: + if self.options.energy_core != EnergyCore.option_shuffle: exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions))) # remove one pair of wings that will be placed in generate_basic items.remove(self.create_item("Wings")) - def is_ingredient(item): + def is_ingredient(item: pyevermizer.Item) -> bool: for ingredient in _ingredients: if _match_item_name(item, ingredient): return True @@ -311,84 +307,72 @@ def is_ingredient(item): # add energy core fragments to the pool ingredients = [n for n, item in enumerate(items) if is_ingredient(item)] - if self.energy_core == EnergyCore.option_fragments: + if self.options.energy_core == EnergyCore.option_fragments: items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core - for _ in range(self.available_fragments - 1): + for _ in range(self.options.available_fragments - 1): if len(ingredients) < 1: break # out of ingredients to replace - r = self.multiworld.random.choice(ingredients) + r = self.random.choice(ingredients) ingredients.remove(r) items[r] = self.create_item("Energy Core Fragment") # add traps to the pool - trap_count = self.multiworld.trap_count[self.player].value - trap_chances = {} - trap_names = {} + trap_count = self.options.trap_count.value + trap_names: typing.List[str] = [] + trap_weights: typing.List[int] = [] if trap_count > 0: - for trap_type in self.trap_types: - trap_option = getattr(self.multiworld, f'trap_chance_{trap_type}')[self.player] - trap_chances[trap_type] = trap_option.value - trap_names[trap_type] = trap_option.item_name - trap_chances_total = sum(trap_chances.values()) - if trap_chances_total == 0: - for trap_type in trap_chances: - trap_chances[trap_type] = 1 - trap_chances_total = len(trap_chances) + for trap_option in self.options.trap_chances: + trap_names.append(trap_option.item_name) + trap_weights.append(trap_option.value) + if sum(trap_weights) == 0: + trap_weights = [1 for _ in trap_weights] def create_trap() -> Item: - v = self.multiworld.random.randrange(trap_chances_total) - for t, c in trap_chances.items(): - if v < c: - return self.create_item(trap_names[t]) - v -= c - assert False, "Bug in create_trap" + return self.create_item(self.random.choices(trap_names, trap_weights)[0]) for _ in range(trap_count): if len(ingredients) < 1: break # out of ingredients to replace - r = self.multiworld.random.choice(ingredients) + r = self.random.choice(ingredients) ingredients.remove(r) items[r] = create_trap() self.multiworld.itempool += items - def set_rules(self): + def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player) # set Done from goal option once we have multiple goals set_rule(self.multiworld.get_location('Done', self.player), - lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player)) + lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS)) set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True) for loc in _locations: location = self.multiworld.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]: - def rule(state) -> bool: + def rule(state: "CollectionState") -> bool: for count, progress in requires: - if not state.soe_has(progress, self.multiworld, self.player, count): + if not self.logic.has(state, progress, count): return False return True return rule - def make_item_type_limit_rule(self, item_type: int): - return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type - - def generate_basic(self): + def generate_basic(self) -> None: # place Victory event self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory')) # place wings in halls NE to avoid softlock - wings_location = self.multiworld.random.choice(self._halls_ne_chest_names) + wings_location = self.random.choice(self._halls_ne_chest_names) wings_item = self.create_item('Wings') self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item) # place energy core at vanilla location for vanilla mode - if self.energy_core == EnergyCore.option_vanilla: + if self.options.energy_core == EnergyCore.option_vanilla: energy_core = self.create_item('Energy Core') self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core) # generate stuff for later - self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? + self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando? - def generate_output(self, output_directory: str): + def generate_output(self, output_directory: str) -> None: player_name = self.multiworld.get_player_name(self.player) self.connect_name = player_name[:32] while len(self.connect_name.encode('utf-8')) > 32: @@ -397,24 +381,21 @@ def generate_output(self, output_directory: str): placement_file = "" out_file = "" try: - money = self.multiworld.money_modifier[self.player].value - exp = self.multiworld.exp_modifier[self.player].value + money = self.options.money_modifier.value + exp = self.options.exp_modifier.value switches: typing.List[str] = [] - if self.multiworld.death_link[self.player].value: + if self.options.death_link.value: switches.append("--death-link") - if self.energy_core == EnergyCore.option_fragments: - switches.extend(('--available-fragments', str(self.available_fragments), - '--required-fragments', str(self.required_fragments))) + if self.options.energy_core == EnergyCore.option_fragments: + switches.extend(('--available-fragments', str(self.options.available_fragments.value), + '--required-fragments', str(self.options.required_fragments.value))) rom_file = get_base_rom_path() out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player)) out_file = out_base + '.sfc' placement_file = out_base + '.txt' patch_file = out_base + '.apsoe' flags = 'l' # spoiler log - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] - if hasattr(option, 'to_flag'): - flags += option.to_flag() + flags += self.options.flags with open(placement_file, "wb") as f: # generate placement file for location in self.multiworld.get_locations(self.player): @@ -448,7 +429,7 @@ def generate_output(self, output_directory: str): except FileNotFoundError: pass - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None: # wait for self.connect_name to be available. self.connect_name_available_event.wait() # we skip in case of error, so that the original error in the output thread is the one that gets raised @@ -457,7 +438,7 @@ def modify_multidata(self, multidata: dict): multidata["connect_names"][self.connect_name] = payload def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"])) + return self.random.choice(list(self.item_name_groups["Ingredients"])) class SoEItem(Item): diff --git a/worlds/soe/logic.py b/worlds/soe/logic.py new file mode 100644 index 000000000000..ee81c76e58de --- /dev/null +++ b/worlds/soe/logic.py @@ -0,0 +1,85 @@ +import typing +from typing import Callable, Set + +from . import pyevermizer +from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions + +if typing.TYPE_CHECKING: + from BaseClasses import CollectionState + +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? + +# TODO: resolve/flatten/expand rules to get rid of recursion below where possible +# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) +rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +# Logic.items are all items and extra items excluding non-progression items and duplicates +item_names: Set[str] = set() +items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) + if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value] + + +class SoEPlayerLogic: + __slots__ = "player", "out_of_bounds", "sequence_breaks", "has" + player: int + out_of_bounds: bool + sequence_breaks: bool + + has: Callable[..., bool] + """ + Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE + """ + + def __init__(self, player: int, options: "SoEOptions"): + self.player = player + self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic + self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic + + if options.energy_core == EnergyCore.option_fragments: + # override logic for energy core fragments + required_fragments = options.required_fragments.value + + def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool: + if progress == pyevermizer.P_ENERGY_CORE: + progress = pyevermizer.P_CORE_FRAGMENT + count = required_fragments + return self._has(state, progress, count) + + self.has = fragmented_has + else: + # default (energy core) logic + self.has = self._has + + def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int: + """ + Returns reached count of one of evermizer's progress steps based on collected items. + i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP + """ + n = 0 + for item in items: + for pvd in item.provides: + if pvd[1] == progress: + if state.has(item.name, self.player): + n += state.count(item.name, self.player) * pvd[0] + if n >= max_count > 0: + return n + for rule in rules: + for pvd in rule.provides: + if pvd[1] == progress and pvd[0] > 0: + has = True + for req in rule.requires: + if not self.has(state, req[1], req[0]): + has = False + break + if has: + n += pvd[0] + if n >= max_count > 0: + return n + return n + + def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool: + """Default implementation of has""" + if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB: + return True + if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: + return True + return self._count(state, progress, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/options.py similarity index 70% rename from worlds/soe/Options.py rename to worlds/soe/options.py index 3de2de34ac67..cb9e9bb6de23 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/options.py @@ -1,16 +1,18 @@ -import typing +from dataclasses import dataclass, fields +from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol -from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing +from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ + ProgressionBalancing, Range, Toggle # typing boilerplate -class FlagsProtocol(typing.Protocol): +class FlagsProtocol(Protocol): value: int default: int - flags: typing.List[str] + flags: List[str] -class FlagProtocol(typing.Protocol): +class FlagProtocol(Protocol): value: int default: int flag: str @@ -18,7 +20,7 @@ class FlagProtocol(typing.Protocol): # meta options class EvermizerFlags: - flags: typing.List[str] + flags: List[str] def to_flag(self: FlagsProtocol) -> str: return self.flags[self.value] @@ -200,13 +202,13 @@ class TrapCount(Range): # more meta options class ItemChanceMeta(AssembleOptions): - def __new__(mcs, name, bases, attrs): + def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta": if 'item_name' in attrs: attrs["display_name"] = f"{attrs['item_name']} Chance" attrs["range_start"] = 0 attrs["range_end"] = 100 - - return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + return cast(ItemChanceMeta, cls) class TrapChance(Range, metaclass=ItemChanceMeta): @@ -247,33 +249,52 @@ class SoEProgressionBalancing(ProgressionBalancing): special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} -soe_options: typing.Dict[str, AssembleOptions] = { - "difficulty": Difficulty, - "energy_core": EnergyCore, - "required_fragments": RequiredFragments, - "available_fragments": AvailableFragments, - "money_modifier": MoneyModifier, - "exp_modifier": ExpModifier, - "sequence_breaks": SequenceBreaks, - "out_of_bounds": OutOfBounds, - "fix_cheats": FixCheats, - "fix_infinite_ammo": FixInfiniteAmmo, - "fix_atlas_glitch": FixAtlasGlitch, - "fix_wings_glitch": FixWingsGlitch, - "shorter_dialogs": ShorterDialogs, - "short_boss_rush": ShortBossRush, - "ingredienizer": Ingredienizer, - "sniffamizer": Sniffamizer, - "callbeadamizer": Callbeadamizer, - "musicmizer": Musicmizer, - "doggomizer": Doggomizer, - "turdo_mode": TurdoMode, - "death_link": DeathLink, - "trap_count": TrapCount, - "trap_chance_quake": TrapChanceQuake, - "trap_chance_poison": TrapChancePoison, - "trap_chance_confound": TrapChanceConfound, - "trap_chance_hud": TrapChanceHUD, - "trap_chance_ohko": TrapChanceOHKO, - "progression_balancing": SoEProgressionBalancing, -} +# noinspection SpellCheckingInspection +@dataclass +class SoEOptions(PerGameCommonOptions): + difficulty: Difficulty + energy_core: EnergyCore + required_fragments: RequiredFragments + available_fragments: AvailableFragments + money_modifier: MoneyModifier + exp_modifier: ExpModifier + sequence_breaks: SequenceBreaks + out_of_bounds: OutOfBounds + fix_cheats: FixCheats + fix_infinite_ammo: FixInfiniteAmmo + fix_atlas_glitch: FixAtlasGlitch + fix_wings_glitch: FixWingsGlitch + shorter_dialogs: ShorterDialogs + short_boss_rush: ShortBossRush + ingredienizer: Ingredienizer + sniffamizer: Sniffamizer + callbeadamizer: Callbeadamizer + musicmizer: Musicmizer + doggomizer: Doggomizer + turdo_mode: TurdoMode + death_link: DeathLink + trap_count: TrapCount + trap_chance_quake: TrapChanceQuake + trap_chance_poison: TrapChancePoison + trap_chance_confound: TrapChanceConfound + trap_chance_hud: TrapChanceHUD + trap_chance_ohko: TrapChanceOHKO + progression_balancing: SoEProgressionBalancing + + @property + def trap_chances(self) -> Iterator[TrapChance]: + for field in fields(self): + option = getattr(self, field.name) + if isinstance(option, TrapChance): + yield option + + @property + def flags(self) -> str: + flags = '' + for field in fields(self): + option = getattr(self, field.name) + if isinstance(option, (EvermizerFlag, EvermizerFlags)): + assert isinstance(option, Option) + # noinspection PyUnresolvedReferences + flags += option.to_flag() + return flags diff --git a/worlds/soe/Patch.py b/worlds/soe/patch.py similarity index 86% rename from worlds/soe/Patch.py rename to worlds/soe/patch.py index f4de5d06ead1..a322de2af65f 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/patch.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import BinaryIO, Optional import Utils from worlds.Files import APDeltaPatch @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: return file_name -def read_rom(stream, strip_header=True) -> bytes: +def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes: """Reads rom into bytearray and optionally strips off any smc header""" data = stream.read() if strip_header and len(data) % 0x400 == 0x200: @@ -40,5 +40,5 @@ def read_rom(stream, strip_header=True) -> bytes: if __name__ == '__main__': import sys - print('Please use ../../Patch.py', file=sys.stderr) + print('Please use ../../patch.py', file=sys.stderr) sys.exit(1) diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 27d38605aae4..1ab852163053 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from typing import Iterable @@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase): game = "Secret of Evermore" def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), - satisfied=True) -> None: + satisfied: bool = True) -> None: """ Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True. Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True @@ -18,3 +18,14 @@ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: for location in unreachable: self.assertFalse(self.can_reach_location(location), f"{location} is reachable but shouldn't be") + + def testRocketPartsExist(self) -> None: + """Tests that rocket parts exist and are unique""" + self.assertEqual(len(self.get_items_by_name("Gauge")), 1) + self.assertEqual(len(self.get_items_by_name("Wheel")), 1) + diamond_eyes = self.get_items_by_name("Diamond Eye") + self.assertEqual(len(diamond_eyes), 3) + # verify diamond eyes are individual items + self.assertFalse(diamond_eyes[0] is diamond_eyes[1]) + self.assertFalse(diamond_eyes[0] is diamond_eyes[2]) + self.assertFalse(diamond_eyes[1] is diamond_eyes[2]) diff --git a/worlds/soe/test/test_access.py b/worlds/soe/test/test_access.py index c7da7b889627..f1d6ee993b34 100644 --- a/worlds/soe/test/test_access.py +++ b/worlds/soe/test/test_access.py @@ -4,10 +4,10 @@ class AccessTest(SoETestBase): @staticmethod - def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]): + def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]: return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers] - def testBronzeAxe(self): + def test_bronze_axe(self) -> None: gourds = { "Pyramid bottom": (118, 121, 122, 123, 124, 125), "Pyramid top": (140,) @@ -16,7 +16,7 @@ def testBronzeAxe(self): items = [["Bronze Axe"]] self.assertAccessDependency(locations, items) - def testBronzeSpearPlus(self): + def test_bronze_spear_plus(self) -> None: locations = ["Megataur"] items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]] self.assertAccessDependency(locations, items) diff --git a/worlds/soe/test/test_goal.py b/worlds/soe/test/test_goal.py index d127d3899869..bb64b8eca759 100644 --- a/worlds/soe/test/test_goal.py +++ b/worlds/soe/test/test_goal.py @@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase): "required_fragments": 20, } - def testFragments(self): + def test_fragments(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) # 0 fragments fragments = self.get_items_by_name("Energy Core Fragment") @@ -24,11 +24,11 @@ def testFragments(self): self.assertEqual(self.count("Energy Core Fragment"), 21) self.assertBeatable(True) - def testNoWeapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"]) self.assertBeatable(False) - def testNoRocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"]) self.assertBeatable(False) @@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase): "energy_core": "shuffle", } - def testCore(self): + def test_core(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]) self.assertBeatable(False) self.collect_by_name(["Energy Core"]) self.assertBeatable(True) - def testNoWeapon(self): + def test_no_weapon(self) -> None: self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"]) self.assertBeatable(False) - def testNoRocket(self): + def test_no_rocket(self) -> None: self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"]) self.assertBeatable(False) diff --git a/worlds/soe/test/test_item_mapping.py b/worlds/soe/test/test_item_mapping.py new file mode 100644 index 000000000000..7df05837c78b --- /dev/null +++ b/worlds/soe/test/test_item_mapping.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from .. import SoEWorld + + +class TestMapping(TestCase): + def test_atlas_medallion_name_group(self) -> None: + """ + Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in item groups. + """ + self.assertIn("Any Atlas Medallion", SoEWorld.item_name_groups) + + def test_atlas_medallion_name_items(self) -> None: + """ + Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in items. + """ + found_medallion = False + for name in SoEWorld.item_name_to_id: + self.assertNotIn("Atlas Amulet", name, "Expected Atlas Medallion, not Amulet") + if "Atlas Medallion" in name: + found_medallion = True + self.assertTrue(found_medallion, "Did not find Atlas Medallion in items") diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py index 27e00cd3e764..3c1a2829de8e 100644 --- a/worlds/soe/test/test_oob.py +++ b/worlds/soe/test/test_oob.py @@ -6,7 +6,7 @@ class OoBTest(SoETestBase): """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} - def testOoBAccess(self): + def test_oob_access(self) -> None: in_logic = self.options["out_of_bounds"] == "logic" # some locations that just need a weapon + OoB @@ -37,7 +37,7 @@ def testOoBAccess(self): self.collect_by_name("Diamond Eye") self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) - def testOoBGoal(self): + def test_oob_goal(self) -> None: # still need Energy Core with OoB if sequence breaks are not in logic for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: self.collect_by_name(item) diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py index 4248f9b47d97..2da8c9242cb9 100644 --- a/worlds/soe/test/test_sequence_breaks.py +++ b/worlds/soe/test/test_sequence_breaks.py @@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase): """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} - def testSequenceBreaksAccess(self): + def test_sequence_breaks_access(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # some locations that just need any weapon + sequence break @@ -30,7 +30,7 @@ def testSequenceBreaksAccess(self): self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead self.assertEqual(self.can_reach_location("Escape"), in_logic) - def testSequenceBreaksGoal(self): + def test_sequence_breaks_goal(self) -> None: in_logic = self.options["sequence_breaks"] == "logic" # don't need Energy Core with sequence breaks in logic diff --git a/worlds/soe/test/test_traps.py b/worlds/soe/test/test_traps.py new file mode 100644 index 000000000000..7babd4522b30 --- /dev/null +++ b/worlds/soe/test/test_traps.py @@ -0,0 +1,56 @@ +import typing +from dataclasses import fields + +from . import SoETestBase +from ..options import SoEOptions + +if typing.TYPE_CHECKING: + from .. import SoEWorld + + +class Bases: + # class in class to avoid running tests for TrapTest class + class TrapTestBase(SoETestBase): + """Test base for trap tests""" + option_name_to_item_name = { + # filtering by name here validates that there is no confusion between name and type + field.name: field.type.item_name for field in fields(SoEOptions) if field.name.startswith("trap_chance_") + } + + def test_dataclass(self) -> None: + """Test that the dataclass helper property returns the expected sequence""" + self.assertGreater(len(self.option_name_to_item_name), 0, "Expected more than 0 trap types") + world: "SoEWorld" = typing.cast("SoEWorld", self.multiworld.worlds[1]) + item_name_to_rolled_option = {option.item_name: option for option in world.options.trap_chances} + # compare that all fields are present - that is property in dataclass and selector code in test line up + self.assertEqual(sorted(self.option_name_to_item_name.values()), sorted(item_name_to_rolled_option), + "field names probably do not match field types") + # sanity check that chances are correctly set and returned by property + for option_name, item_name in self.option_name_to_item_name.items(): + self.assertEqual(item_name_to_rolled_option[item_name].value, + self.options.get(option_name, item_name_to_rolled_option[item_name].default)) + + def test_trap_count(self) -> None: + """Test that total trap count is correct""" + self.assertEqual(self.options["trap_count"], + len(self.get_items_by_name(self.option_name_to_item_name.values()))) + + +class TestTrapAllZeroChance(Bases.TrapTestBase): + """Tests all zero chances still gives traps if trap_count is set.""" + options: typing.Dict[str, typing.Any] = { + "trap_count": 1, + **{name: 0 for name in Bases.TrapTestBase.option_name_to_item_name} + } + + +class TestTrapNoConfound(Bases.TrapTestBase): + """Tests that one zero chance does not give that trap.""" + options: typing.Dict[str, typing.Any] = { + "trap_count": 99, + "trap_chance_confound": 0, + } + + def test_no_confound_trap(self) -> None: + self.assertEqual(self.option_name_to_item_name["trap_chance_confound"], "Confound Trap") + self.assertEqual(len(self.get_items_by_name("Confound Trap")), 0) diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index a880a40b971a..04ba9c15c3c1 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -124,6 +124,6 @@ List of supported mods: ## Multiplayer -You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. +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. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 68c7fb9af6a0..d8f0e16b1017 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -84,4 +84,4 @@ See the [Supported mods documentation](https://github.com/agilbert1412/StardewAr ### Multiplayer -You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-terms plans to support that feature. \ No newline at end of file +You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index f56dec39a1f0..88aa13f31471 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp logic.received("Bus Repair").simplify()) MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player), logic.received(Wallet.skull_key).simplify()) + MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player), + logic.received("Club Card").simplify()) for floor in range(25, 200 + 25, 25): MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player), logic.can_mine_to_skull_cavern_floor(floor).simplify()) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 456598edecef..5b90e99722df 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -94,17 +94,17 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() final_starting_weapons = [weapon for weapon in starting_weapons - if weapon not in world.multiworld.non_local_items[world.player]] + if weapon not in world.options.non_local_items] if not final_starting_weapons: final_starting_weapons = starting_weapons starting_weapon = random.choice(final_starting_weapons) - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: + if world.options.StartingPosition == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon - elif world.multiworld.StartingPosition[world.player] in \ + elif world.options.StartingPosition in \ [StartingPosition.option_unsafe, StartingPosition.option_dangerous]: - if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous: + if world.options.StartingPosition == StartingPosition.option_dangerous: for location in dangerous_weapon_locations: - if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + if world.options.ExpandedPool or "Drop" not in location: start_weapon_locations.append(location) placed_items[random.choice(start_weapon_locations)] = starting_weapon else: @@ -115,7 +115,7 @@ def get_pool_core(world): # Triforce Fragments fragment = "Triforce Fragment" - if world.multiworld.ExpandedPool[world.player]: + if world.options.ExpandedPool: possible_level_locations = [location for location in all_level_locations if location not in level_locations[8]] else: @@ -125,15 +125,15 @@ def get_pool_core(world): if location in possible_level_locations: possible_level_locations.remove(location) for level in range(1, 9): - if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: + if world.options.TriforceLocations == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment - elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons: + elif world.options.TriforceLocations == TriforceLocations.option_dungeons: placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment else: pool.append(fragment) # Level 9 junk fill - if world.multiworld.ExpandedPool[world.player] > 0: + 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())) @@ -142,7 +142,7 @@ def get_pool_core(world): # Finish Pool final_pool = basic_pool - if world.multiworld.ExpandedPool[world.player]: + 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) diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py index 96bd3e296dca..58a50ec35929 100644 --- a/worlds/tloz/Options.py +++ b/worlds/tloz/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Option, DefaultOnToggle, Choice +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions class ExpandedPool(DefaultOnToggle): @@ -32,9 +33,8 @@ class StartingPosition(Choice): option_dangerous = 2 option_very_dangerous = 3 - -tloz_options: typing.Dict[str, type(Option)] = { - "ExpandedPool": ExpandedPool, - "TriforceLocations": TriforceLocations, - "StartingPosition": StartingPosition -} +@dataclass +class TlozOptions(PerGameCommonOptions): + ExpandedPool: ExpandedPool + TriforceLocations: TriforceLocations + StartingPosition: StartingPosition diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 12bf466bce99..b94002f25da2 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -11,6 +11,7 @@ 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): @@ -23,7 +24,7 @@ def set_rules(tloz_world: "TLoZWorld"): # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons for i, level in enumerate(tloz_world.levels[1:10]): for location in level.locations: - if world.StartingPosition[player] < StartingPosition.option_dangerous \ + if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: add_rule(world.get_location(location.name, player), lambda state: state.has_group("weapons", player)) @@ -66,7 +67,7 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) add_rule(world.get_location("Level 7 Boss", player), lambda state: state.has("Recorder", player)) - if world.ExpandedPool[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), @@ -75,13 +76,13 @@ def set_rules(tloz_world: "TLoZWorld"): lambda state: state.has("Recorder", player)) for location in food_locations: - if world.ExpandedPool[player] or "Drop" not in location: + if options.ExpandedPool or "Drop" not in location: add_rule(world.get_location(location, player), lambda state: state.has("Food", player)) add_rule(world.get_location("Level 8 Item (Magical Key)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) @@ -106,13 +107,13 @@ def set_rules(tloz_world: "TLoZWorld"): for location in stepladder_locations: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) - if world.ExpandedPool[player]: + if options.ExpandedPool: for location in stepladder_locations_expanded: add_rule(world.get_location(location, player), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one - if world.ExpandedPool[player]: + if options.ExpandedPool: add_rule(world.get_location("Take Any Item Left", player), lambda state: state.has_group("candles", player) or state.has("Raft", player)) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 6e8927c4e7b9..259bfe204716 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -13,7 +13,7 @@ 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 -from .Options import tloz_options +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 from worlds.AutoWorld import World, WebWorld @@ -63,7 +63,8 @@ class TLoZWorld(World): This randomizer shuffles all the items in the game around, leading to a new adventure every time. """ - option_definitions = tloz_options + options_dataclass = TlozOptions + options: TlozOptions settings: typing.ClassVar[TLoZSettings] game = "The Legend of Zelda" topology_present = False @@ -132,7 +133,7 @@ def create_regions(self): for i, level in enumerate(level_locations): for location in level: - if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + if self.options.ExpandedPool or "Drop" not in location: self.levels[i + 1].locations.append( self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) @@ -144,7 +145,7 @@ def create_regions(self): self.levels[level].locations.append(boss_event) for location in major_locations: - if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + if self.options.ExpandedPool or "Take Any" not in location: overworld.locations.append( self.create_location(location, self.location_name_to_id[location], overworld)) @@ -311,7 +312,7 @@ def get_filler_item_name(self) -> str: return self.multiworld.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: - if self.multiworld.ExpandedPool[self.player]: + if self.options.ExpandedPool: take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py new file mode 100644 index 000000000000..fb04570f22ca --- /dev/null +++ b/worlds/tunic/__init__.py @@ -0,0 +1,269 @@ +from typing import Dict, List, Any + +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification +from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names +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 .options import TunicOptions +from worlds.AutoWorld import WebWorld, World +from decimal import Decimal, ROUND_HALF_UP + + +class TunicWeb(WebWorld): + tutorials = [ + Tutorial( + tutorial_name="Multiworld Setup Guide", + description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.", + language="English", + file_name="setup_en.md", + link="setup/en", + authors=["SilentDestroyer"] + ) + ] + theme = "grassFlowers" + game = "TUNIC" + + +class TunicItem(Item): + game: str = "TUNIC" + + +class TunicLocation(Location): + game: str = "TUNIC" + + +class TunicWorld(World): + """ + Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game + about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will + confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox! + """ + game = "TUNIC" + web = TunicWeb() + + data_version = 2 + options: TunicOptions + options_dataclass = TunicOptions + item_name_groups = item_name_groups + location_name_groups = location_name_groups + + item_name_to_id = item_name_to_id + location_name_to_id = location_name_to_id + + ability_unlocks: Dict[str, int] + slot_data_items: List[TunicItem] + tunic_portal_pairs: Dict[str, str] + er_portal_hints: Dict[int, str] + + def generate_early(self) -> None: + # Universal tracker stuff, shouldn't do anything in standard gen + if hasattr(self.multiworld, "re_gen_passthrough"): + if "TUNIC" in self.multiworld.re_gen_passthrough: + passthrough = self.multiworld.re_gen_passthrough["TUNIC"] + self.options.start_with_sword.value = passthrough["start_with_sword"] + 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.lanternless.value = passthrough["lanternless"] + self.options.maskless.value = passthrough["maskless"] + self.options.hexagon_quest.value = passthrough["hexagon_quest"] + self.options.entrance_rando.value = passthrough["entrance_rando"] + + if self.options.start_with_sword and "Sword" not in self.options.start_inventory: + self.options.start_inventory.value["Sword"] = 1 + + def create_item(self, name: str) -> TunicItem: + item_data = item_table[name] + return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + + def create_items(self) -> None: + keys_behind_bosses = self.options.keys_behind_bosses + hexagon_quest = self.options.hexagon_quest + sword_progression = self.options.sword_progression + + tunic_items: List[TunicItem] = [] + self.slot_data_items = [] + + items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} + + for money_fool in fool_tiers[self.options.fool_traps]: + items_to_create["Fool Trap"] += items_to_create[money_fool] + items_to_create[money_fool] = 0 + + if sword_progression: + items_to_create["Stick"] = 0 + items_to_create["Sword"] = 0 + else: + items_to_create["Sword Upgrade"] = 0 + + 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) + elif self.options.laurels_location == "10_coins": + self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).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) + items_to_create["Hero's Laurels"] = 0 + + if keys_behind_bosses: + for rgb_hexagon, location in hexagon_locations.items(): + hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon) + self.multiworld.get_location(location, self.player).place_locked_item(hex_item) + self.slot_data_items.append(hex_item) + items_to_create[rgb_hexagon] = 0 + items_to_create[gold_hexagon] -= 3 + + if hexagon_quest: + # Calculate number of hexagons in item pool + hexagon_goal = self.options.hexagon_goal + extra_hexagons = self.options.extra_hexagon_percentage + items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) + + # Replace pages and normal hexagons with filler + for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): + items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item] + items_to_create[replaced_item] = 0 + + # Filler items that are still in the item pool to swap out + available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and + item_table[filler].classification == ItemClassification.filler] + + # Remove filler to make room for extra hexagons + for i in range(0, items_to_create[gold_hexagon]): + fill = self.random.choice(available_filler) + items_to_create[fill] -= 1 + if items_to_create[fill] == 0: + available_filler.remove(fill) + + 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) + items_to_create["Scavenger Mask"] = 0 + + if self.options.lanternless: + mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) + tunic_items.append(mask_item) + 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) + + self.multiworld.itempool += tunic_items + + def create_regions(self) -> None: + self.tunic_portal_pairs = {} + self.er_portal_hints = {} + self.ability_unlocks = randomize_ability_unlocks(self.random, self.options) + + # stuff for universal tracker support, can be ignored for standard gen + if hasattr(self.multiworld, "re_gen_passthrough"): + if "TUNIC" in self.multiworld.re_gen_passthrough: + passthrough = self.multiworld.re_gen_passthrough["TUNIC"] + self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"] + self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"] + self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"] + + if self.options.entrance_rando: + portal_pairs, portal_hints = create_er_regions(self) + for portal1, portal2 in portal_pairs.items(): + self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() + + self.er_portal_hints = portal_hints + + else: + 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.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) + location = TunicLocation(self.player, location_name, location_id, region) + region.locations.append(location) + + victory_region = self.multiworld.get_region("Spirit Arena", self.player) + 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: + set_er_location_rules(self, self.ability_unlocks) + else: + set_region_rules(self, self.ability_unlocks) + set_location_rules(self, self.ability_unlocks) + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_items) + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.options.entrance_rando: + hint_data[self.player] = self.er_portal_hints + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "seed": self.random.randint(0, 2147483647), + "start_with_sword": self.options.start_with_sword.value, + "keys_behind_bosses": self.options.keys_behind_bosses.value, + "sword_progression": self.options.sword_progression.value, + "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, + "lanternless": self.options.lanternless.value, + "maskless": self.options.maskless.value, + "entrance_rando": self.options.entrance_rando.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 + } + + 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]) + + 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]): + slot_data[start_item].extend(["Your Pocket", self.player]) + + for plando_item in self.multiworld.plando_items[self.player]: + if plando_item["from_pool"]: + items_to_find = set() + for item_type in [key for key in ["item", "items"] if key in plando_item]: + for item in plando_item[item_type]: + items_to_find.add(item) + for item in items_to_find: + 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]) + + return slot_data + + # for the universal tracker, doesn't get called in standard gen + @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 + return slot_data diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md new file mode 100644 index 000000000000..e957f9eafaf5 --- /dev/null +++ b/worlds/tunic/docs/en_TUNIC.md @@ -0,0 +1,64 @@ +# TUNIC + +## 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. + +## I haven't played TUNIC before. + +**Play vanilla first.** It is **_heavily discouraged_** to play this randomizer before playing the vanilla game. +It is recommended that you achieve both endings in the vanilla game before playing the randomizer. + +## What does randomization do to this game? + +In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics, +and other unique items are shuffled.
+ +Ability shuffling is an option available from the options page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), +preventing them from being used until they are unlocked.
+ +Enemy randomization and other options are also available and can be turned on in the client mod. + +## What is the goal of TUNIC when randomized? +The standard goal is the same as the vanilla game, which is to find the three hexagon keys, at which point you may either Take Your +Rightful Place or seek another path and Share Your Wisdom. + +Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal +being to find the required amount of them and then Share Your Wisdom. + +## What items from TUNIC can appear in another player's world? +Every item has a chance to appear in another player's world. + +## How many checks are in TUNIC? +There are 302 checks located across the world of TUNIC. + +## What do items from other worlds look like in TUNIC? +Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a +chest with the original chest texture for that item. + +Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with +a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression. + +## Is there a tracker pack? +There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty. + +There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon. + +## What should I know regarding logic? +- Nighttime is not considered in logic. Every check in the game is obtainable during the day. +- 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 Entrance Rando specifically: +- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. +- The West Garden fuse can be activated from below. +- You can pray at the tree at the exterior of the Library. +- The elevators in the Rooted Ziggurat only go down. +- The portal in the trophy room of the Old House is active from the start. +- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing. + +## What item groups are there? +Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, ice rod, and progressive sword. + +## What location groups are there? +Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group. diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md new file mode 100644 index 000000000000..3c13331fe5f1 --- /dev/null +++ b/worlds/tunic/docs/setup_en.md @@ -0,0 +1,65 @@ +# TUNIC Setup Guide + +## Installation + +### Required Software + +- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported) +- [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip) +- [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest) + +### Optional Software +- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest) (For use with EmoTracker/PopTracker) +- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest) + +### Find Your Relevant Game Directories + +Find your TUNIC game installation directory: + +- **Steam**: Right click TUNIC in your Steam Library, then *Manage → Browse local files*.
+ - **Steam Deck**: Hold down the power button, tap "Switch to Desktop", then launch Steam from Desktop Mode to access the above option. +- **PC Game Pass**: In the Xbox PC app, go to the TUNIC game page from your library, click the [...] button next to "Play", then +*Manage → Files → Browse...*
+- **Other platforms**: Follow a similar pattern of steps as above to locate your specific game directory. + +### Install BepInEx + +BepInEx is a general purpose framework for modding Unity games, and is used by the TUNIC Randomizer. + +Download [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip). + +If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html). + +Extract the contents of the BepInEx .zip file into your TUNIC game directory:
+- **Steam**: Steam\steamapps\common\TUNIC
+- **PC Game Pass**: XboxGames\Tunic\Content
+- **Other platforms**: Place into the same folder that the Tunic_Data/Secret Legend_Data folder is found. + +Launch the game once and close it to finish the BepInEx installation. + +### Install The TUNIC Randomizer Archipelago Client Mod + +Download the latest release of the [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest). + +The downloaded .zip will contain a folder called `Tunic Archipelago`. + +Copy the `Tunic Archipelago` folder into `BepInEx/plugins` in your TUNIC game installation directory. +The filepath to the mod should look like `BepInEx/plugins/Tunic Archipelago/TunicArchipelago.dll`
+ +Launch the game, and if everything was installed correctly you should see `Randomizer + Archipelago Mod Ver. x.y.z` in the top left corner of the title screen! + +## Configure Archipelago Options + +### Configure Your YAML File + +Visit the [TUNIC options page](/games/Tunic/player-options) to generate a YAML with your selected options. + +### Configure Your Mod Settings +Launch the game and click the button labeled `Open AP Config` on the Title Screen. +In the menu that opens, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room. + +Once you've input your information, click on Close. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`. + +An error message will display if the game fails to connect to the server. + +Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization! diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py new file mode 100644 index 000000000000..d76af1133906 --- /dev/null +++ b/worlds/tunic/er_data.py @@ -0,0 +1,1008 @@ +from typing import Dict, NamedTuple, List, Tuple +from enum import IntEnum + + +class Portal(NamedTuple): + name: str # human-readable name + region: str # AP region + destination: str # vanilla destination scene and tag + + def scene(self) -> str: # the actual scene name in Tunic + return tunic_er_regions[self.region].game_scene + + def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod + return self.scene() + ", " + self.destination + + +portal_mapping: List[Portal] = [ + Portal(name="Stick House Entrance", region="Overworld", + destination="Sword Cave_"), + Portal(name="Windmill Entrance", region="Overworld", + destination="Windmill_"), + Portal(name="Well Ladder Entrance", region="Overworld", + destination="Sewer_entrance"), + Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail", + destination="Sewer_west_aqueduct"), + Portal(name="Old House Door Entrance", region="Overworld Old House Door", + destination="Overworld Interiors_house"), + Portal(name="Old House Waterfall Entrance", region="Overworld", + destination="Overworld Interiors_under_checkpoint"), + Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail", + destination="Furnace_gyro_upper_north"), + Portal(name="Entrance to Furnace under Windmill", region="Overworld", + destination="Furnace_gyro_upper_east"), + Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace", + destination="Furnace_gyro_west"), + Portal(name="Entrance to Furnace from Beach", region="Overworld", + destination="Furnace_gyro_lower"), + Portal(name="Caustic Light Cave Entrance", region="Overworld", + destination="Overworld Cave_"), + Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry", + destination="Swamp Redux 2_wall"), + Portal(name="Swamp Lower Entrance", region="Overworld", + destination="Swamp Redux 2_conduit"), + Portal(name="Ruined Passage Not-Door Entrance", region="Overworld", + destination="Ruins Passage_east"), + Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door", + destination="Ruins Passage_west"), + Portal(name="Atoll Upper Entrance", region="Overworld", + destination="Atoll Redux_upper"), + Portal(name="Atoll Lower Entrance", region="Overworld", + destination="Atoll Redux_lower"), + Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry", + destination="ShopSpecial_"), + Portal(name="Maze Cave Entrance", region="Overworld", + destination="Maze Room_"), + Portal(name="West Garden Entrance near Belltower", region="Overworld Belltower", + destination="Archipelagos Redux_upper"), + Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", + destination="Archipelagos Redux_lower"), + Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry", + destination="Archipelagos Redux_lowest"), + Portal(name="Temple Door Entrance", region="Overworld Temple Door", + destination="Temple_main"), + Portal(name="Temple Rafters Entrance", region="Overworld", + destination="Temple_rafters"), + Portal(name="Ruined Shop Entrance", region="Overworld", + destination="Ruined Shop_"), + Portal(name="Patrol Cave Entrance", region="Overworld", + destination="PatrolCave_"), + Portal(name="Hourglass Cave Entrance", region="Overworld", + destination="Town Basement_beach"), + Portal(name="Changing Room Entrance", region="Overworld", + destination="Changing Room_"), + Portal(name="Cube Cave Entrance", region="Overworld", + destination="CubeRoom_"), + Portal(name="Stairs from Overworld to Mountain", region="Overworld", + destination="Mountain_"), + Portal(name="Overworld to Fortress", region="Overworld", + destination="Fortress Courtyard_"), + Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door", + destination="Town_FiligreeRoom_"), + Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door", + destination="EastFiligreeCache_"), + Portal(name="Overworld to Quarry Connector", region="Overworld", + destination="Darkwoods Tunnel_"), + Portal(name="Dark Tomb Main Entrance", region="Overworld", + destination="Crypt Redux_"), + Portal(name="Overworld to Forest Belltower", region="Overworld", + destination="Forest Belltower_"), + Portal(name="Town to Far Shore", region="Overworld Town Portal", + destination="Transit_teleporter_town"), + Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal", + destination="Transit_teleporter_starting island"), + Portal(name="Secret Gathering Place Entrance", region="Overworld", + destination="Waterfall_"), + + Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place", + destination="Overworld Redux_"), + + Portal(name="Windmill Exit", region="Windmill", + destination="Overworld Redux_"), + Portal(name="Windmill Shop", region="Windmill", + destination="Shop_"), + + Portal(name="Old House Door Exit", region="Old House Front", + destination="Overworld Redux_house"), + Portal(name="Old House to Glyph Tower", region="Old House Front", + destination="g_elements_"), + Portal(name="Old House Waterfall Exit", region="Old House Back", + destination="Overworld Redux_under_checkpoint"), + + Portal(name="Glyph Tower Exit", region="Relic Tower", + destination="Overworld Interiors_"), + + Portal(name="Changing Room Exit", region="Changing Room", + destination="Overworld Redux_"), + + Portal(name="Fountain HC Room Exit", region="Fountain Cross Room", + destination="Overworld Redux_"), + + Portal(name="Cube Cave Exit", region="Cube Cave", + destination="Overworld Redux_"), + + Portal(name="Guard Patrol Cave Exit", region="Patrol Cave", + destination="Overworld Redux_"), + + Portal(name="Ruined Shop Exit", region="Ruined Shop", + destination="Overworld Redux_"), + + Portal(name="Furnace Exit towards Well", region="Furnace Fuse", + destination="Overworld Redux_gyro_upper_north"), + Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path", + destination="Crypt Redux_"), + Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path", + destination="Overworld Redux_gyro_west"), + Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_lower"), + Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_upper_east"), + + Portal(name="Stick House Exit", region="Stick House", + destination="Overworld Redux_"), + + Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage", + destination="Overworld Redux_east"), + Portal(name="Ruined Passage Door Exit", region="Ruined Passage", + destination="Overworld Redux_west"), + + Portal(name="Southeast HC Room Exit", region="Southeast Cross Room", + destination="Overworld Redux_"), + + Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave", + destination="Overworld Redux_"), + + Portal(name="Maze Cave Exit", region="Maze Cave", + destination="Overworld Redux_"), + + Portal(name="Hourglass Cave Exit", region="Hourglass Cave", + destination="Overworld Redux_beach"), + + Portal(name="Special Shop Exit", region="Special Shop", + destination="Overworld Redux_"), + + Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters", + destination="Overworld Redux_rafters"), + Portal(name="Temple Door Exit", region="Sealed Temple", + destination="Overworld Redux_main"), + + Portal(name="Well Ladder Exit", region="Beneath the Well Front", + destination="Overworld Redux_entrance"), + Portal(name="Well to Well Boss", region="Beneath the Well Back", + destination="Sewer_Boss_"), + Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", + destination="Overworld Redux_west_aqueduct"), + + Portal(name="Well Boss to Well", region="Well Boss", + destination="Sewer_"), + Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", + destination="Crypt Redux_"), + + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", + destination="Overworld Redux_"), + Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", + destination="Furnace_"), + Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", + destination="Sewer_Boss_"), + + Portal(name="West Garden Exit near Hero's Grave", region="West Garden", + destination="Overworld Redux_lower"), + Portal(name="West Garden to Magic Dagger House", region="West Garden", + destination="archipelagos_house_"), + Portal(name="West Garden Exit after Boss", region="West Garden after Boss", + destination="Overworld Redux_upper"), + Portal(name="West Garden Shop", region="West Garden", + destination="Shop_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit", + destination="Overworld Redux_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="West Garden to Far Shore", region="West Garden Portal", + destination="Transit_teleporter_archipelagos_teleporter"), + + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", + destination="Archipelagos Redux_"), + + Portal(name="Atoll Upper Exit", region="Ruined Atoll", + destination="Overworld Redux_upper"), + Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", + destination="Overworld Redux_lower"), + Portal(name="Atoll Shop", region="Ruined Atoll", + destination="Shop_"), + Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", + destination="Transit_teleporter_atoll"), + Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Portal", + destination="Library Exterior_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll", + destination="Frog Stairs_eye"), + Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", + destination="Frog Stairs_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog's Domain Entry", + destination="Atoll Redux_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog's Domain Entry", + destination="Atoll Redux_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog's Domain Entry", + destination="frog cave main_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog's Domain Entry", + destination="frog cave main_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain", + destination="Frog Stairs_Entrance"), + Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", + destination="Frog Stairs_Exit"), + + Portal(name="Library Exterior Tree", region="Library Exterior Tree", + destination="Atoll Redux_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder", + destination="Library Hall_"), + + Portal(name="Library Hall Bookshelf Exit", region="Library Hall", + destination="Library Exterior_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall", + destination="Library Rotunda_"), + + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda", + destination="Library Hall_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda", + destination="Library Lab_"), + + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", + destination="Library Rotunda_"), + Portal(name="Library to Far Shore", region="Library Portal", + destination="Transit_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab", + destination="Library Arena_"), + + Portal(name="Librarian Arena Exit", region="Library Arena", + destination="Library Lab_"), + + Portal(name="Forest to Belltower", region="East Forest", + destination="Forest Belltower_"), + Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", + destination="East Forest Redux Laddercave_lower"), + Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", + destination="East Forest Redux Laddercave_gate"), + Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", + destination="East Forest Redux Laddercave_upper"), + Portal(name="Forest to Far Shore", region="East Forest Portal", + destination="Transit_teleporter_forest teleporter"), + Portal(name="Forest Guard House 2 Lower Entrance", region="East Forest", + destination="East Forest Redux Interior_lower"), + Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", + destination="East Forest Redux Interior_upper"), + Portal(name="Forest Grave Path Lower Entrance", region="East Forest", + destination="Sword Access_lower"), + Portal(name="Forest Grave Path Upper Entrance", region="East Forest", + destination="Sword Access_upper"), + + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", + destination="East Forest Redux_upper"), + Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", + destination="East Forest Redux_lower"), + Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", + destination="East Forest Redux_gate"), + Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", + destination="Forest Boss Room_"), + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Guard House 2 Lower Exit", region="Guard House 2", + destination="East Forest Redux_lower"), + Portal(name="Guard House 2 Upper Exit", region="Guard House 2", + destination="East Forest Redux_upper"), + + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", + destination="East Forest Redux Laddercave_"), + Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", + destination="Forest Belltower_"), + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + destination="Overworld Redux_"), + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room_"), + + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", + destination="Fortress Reliquary_Lower"), + Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", + destination="Fortress Reliquary_Upper"), + Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", + destination="Fortress Main_Big Door"), + Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", + destination="Fortress East_"), + Portal(name="Fortress Courtyard to Beneath the Earth", region="Fortress Exterior near cave", + destination="Fortress Basement_"), + Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", + destination="Forest Belltower_"), + Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", + destination="Overworld Redux_"), + Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", + destination="Shop_"), + + Portal(name="Beneath the Earth to Fortress Interior", region="Beneath the Vault Back", + destination="Fortress Main_"), + Portal(name="Beneath the Earth to Fortress Courtyard", region="Beneath the Vault Front", + destination="Fortress Courtyard_"), + + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", + destination="Fortress Courtyard_Big Door"), + Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", + destination="Fortress Basement_"), + Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", + destination="Fortress Arena_"), + Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", + destination="Shop_"), + Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", + destination="Fortress East_upper"), + Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", + destination="Fortress East_lower"), + + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", + destination="Fortress Main_lower"), + Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", + destination="Fortress Courtyard_"), + Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", + destination="Fortress Main_upper"), + + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", + destination="Fortress Courtyard_Lower"), + Portal(name="Fortress Hero's Grave", region="Fortress Grave Path", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", + destination="Fortress Courtyard_Upper"), + Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance", + destination="Dusty_"), + + Portal(name="Dusty Exit", region="Fortress Leaf Piles", + destination="Fortress Reliquary_"), + + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", + destination="Fortress Main_"), + Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", + destination="Transit_teleporter_spidertank"), + + Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", + destination="Mountaintop_"), + Portal(name="Mountain to Quarry", region="Lower Mountain", + destination="Quarry Redux_"), + Portal(name="Mountain to Overworld", region="Lower Mountain", + destination="Overworld Redux_"), + + Portal(name="Top of the Mountain Exit", region="Top of the Mountain", + destination="Mountain_"), + + Portal(name="Quarry Connector to Overworld", region="Quarry Connector", + destination="Overworld Redux_"), + Portal(name="Quarry Connector to Quarry", region="Quarry Connector", + destination="Quarry Redux_"), + + Portal(name="Quarry to Overworld Exit", region="Quarry Entry", + destination="Darkwoods Tunnel_"), + Portal(name="Quarry Shop", region="Quarry Entry", + destination="Shop_"), + Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry", + destination="Monastery_front"), + Portal(name="Quarry to Monastery Back", region="Monastery Rope", + destination="Monastery_back"), + Portal(name="Quarry to Mountain", region="Quarry Back", + destination="Mountain_"), + Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door", + destination="ziggurat2020_0_"), + Portal(name="Quarry to Far Shore", region="Quarry Portal", + destination="Transit_teleporter_quarry teleporter"), + + Portal(name="Monastery Rear Exit", region="Monastery Back", + destination="Quarry Redux_back"), + Portal(name="Monastery Front Exit", region="Monastery Front", + destination="Quarry Redux_front"), + Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry", + destination="Quarry Redux_"), + + Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry", + destination="ziggurat2020_0_"), + Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back", + destination="ziggurat2020_2_"), + + Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", + destination="ziggurat2020_3_"), + + Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front", + destination="ziggurat2020_2_"), + Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", + destination="ziggurat2020_FTRoom_"), + + Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", + destination="ziggurat2020_3_"), + Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal", + destination="Transit_teleporter_ziggurat teleporter"), + + Portal(name="Swamp Lower Exit", region="Swamp", + destination="Overworld Redux_conduit"), + Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance", + destination="Cathedral Redux_main"), + Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room", + destination="Cathedral Redux_secret"), + Portal(name="Swamp to Gauntlet", region="Back of Swamp", + destination="Cathedral Arena_"), + Portal(name="Swamp Shop", region="Swamp", + destination="Shop_"), + Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area", + destination="Overworld Redux_wall"), + Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Cathedral Main Exit", region="Cathedral", + destination="Swamp Redux 2_main"), + Portal(name="Cathedral Elevator", region="Cathedral", + destination="Cathedral Arena_"), + Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", + destination="Swamp Redux 2_secret"), + + Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit", + destination="Swamp Redux 2_"), + Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint", + destination="Cathedral Redux_"), + Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint", + destination="Shop_"), + + Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress", + destination="Fortress Reliquary_teleporter_relic plinth"), + Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry", + destination="Monastery_teleporter_relic plinth"), + Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden", + destination="Archipelagos Redux_teleporter_relic plinth"), + Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest", + destination="Sword Access_teleporter_relic plinth"), + Portal(name="Hero's Grave to Library", region="Hero Relic - Library", + destination="Library Hall_teleporter_relic plinth"), + Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp", + destination="Swamp Redux 2_teleporter_relic plinth"), + + Portal(name="Far Shore to West Garden", region="Far Shore to West Garden", + destination="Archipelagos Redux_teleporter_archipelagos_teleporter"), + Portal(name="Far Shore to Library", region="Far Shore to Library", + destination="Library Lab_teleporter_library teleporter"), + Portal(name="Far Shore to Quarry", region="Far Shore to Quarry", + destination="Quarry Redux_teleporter_quarry teleporter"), + Portal(name="Far Shore to East Forest", region="Far Shore to East Forest", + destination="East Forest Redux_teleporter_forest teleporter"), + Portal(name="Far Shore to Fortress", region="Far Shore to Fortress", + destination="Fortress Arena_teleporter_spidertank"), + Portal(name="Far Shore to Atoll", region="Far Shore", + destination="Atoll Redux_teleporter_atoll"), + Portal(name="Far Shore to Ziggurat", region="Far Shore", + destination="ziggurat2020_FTRoom_teleporter_ziggurat teleporter"), + Portal(name="Far Shore to Heir", region="Far Shore", + destination="Spirit Arena_teleporter_spirit arena"), + Portal(name="Far Shore to Town", region="Far Shore", + destination="Overworld Redux_teleporter_town"), + Portal(name="Far Shore to Spawn", region="Far Shore to Spawn", + destination="Overworld Redux_teleporter_starting island"), + + Portal(name="Heir Arena Exit", region="Spirit Arena", + destination="Transit_teleporter_spirit arena"), + + Portal(name="Purgatory Bottom Exit", region="Purgatory", + destination="Purgatory_bottom"), + Portal(name="Purgatory Top Exit", region="Purgatory", + destination="Purgatory_top"), +] + + +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 + hint: int = 0 # what kind of hint text you should have + + +class DeadEnd(IntEnum): + free = 0 # not a dead end + all_cats = 1 # dead end in every logic category + restricted = 2 # dead end only in restricted + # there's no dead ends that are only in unrestricted + + +class Hint(IntEnum): + none = 0 # big areas, empty hallways, etc. + region = 1 # at least one of the portals must not be a dead end + scene = 2 # multiple regions in the scene, so using region could mean no valid hints + special = 3 # for if there's a weird case of specific regions being viable + + +# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all +tunic_er_regions: Dict[str, RegionInfo] = { + "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld": RegionInfo("Overworld Redux"), + "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest + "Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot + "Overworld Special Shop Entry": RegionInfo("Overworld Redux"), # special shop entry spot + "Overworld West Garden Laurels Entry": RegionInfo("Overworld Redux"), # west garden laurels entry + "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region), + "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway + "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"), + "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Town Portal": RegionInfo("Overworld Redux"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux"), + "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Windmill": RegionInfo("Windmill"), + "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door + "Old House Front": RegionInfo("Overworld Interiors"), # part with the bedroom + "Relic Tower": RegionInfo("g_elements", dead_end=DeadEnd.all_cats), + "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace + "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder + "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden + "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Passage": RegionInfo("Ruins Passage", hint=Hint.region), + "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Sealed Temple": RegionInfo("Temple", hint=Hint.scene), + "Sealed Temple Rafters": RegionInfo("Temple", hint=Hint.scene), + "Forest Belltower Upper": RegionInfo("Forest Belltower", hint=Hint.region), + "Forest Belltower Main": RegionInfo("Forest Belltower"), + "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"), + "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), + "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), + "Guard House 2": RegionInfo("East Forest Redux Interior"), + "Forest Boss Room": RegionInfo("Forest Boss Room"), + "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"), + "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits + "Dark Tomb Main": RegionInfo("Crypt Redux"), + "Dark Tomb Dark Exit": RegionInfo("Crypt Redux"), + "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"), # can laurels backwards + "Well Boss": RegionInfo("Sewer_Boss"), # can walk through (with bombs at least) + "Beneath the Well Front": RegionInfo("Sewer"), + "Beneath the Well Main": RegionInfo("Sewer"), + "Beneath the Well Back": RegionInfo("Sewer"), + "West Garden": RegionInfo("Archipelagos Redux"), + "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats, hint=Hint.region), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, hint=Hint.special), + "West Garden Laurels Exit": RegionInfo("Archipelagos Redux"), + "West Garden after Boss": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave": RegionInfo("Archipelagos Redux"), + "Ruined Atoll": RegionInfo("Atoll Redux"), + "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), + "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux"), + "Frog's Domain Entry": RegionInfo("Frog Stairs"), + "Frog's Domain": RegionInfo("frog cave main", hint=Hint.region), + "Frog's Domain Back": RegionInfo("frog cave main", hint=Hint.scene), + "Library Exterior Tree": RegionInfo("Library Exterior"), + "Library Exterior Ladder": RegionInfo("Library Exterior"), + "Library Hall": RegionInfo("Library Hall"), + "Library Hero's Grave": RegionInfo("Library Hall"), + "Library Rotunda": RegionInfo("Library Rotunda"), + "Library Lab": RegionInfo("Library Lab"), + "Library Lab Lower": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab"), + "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), + "Fortress Exterior from Overworld": RegionInfo("Fortress Courtyard"), + "Fortress Exterior near cave": RegionInfo("Fortress Courtyard"), # where the shop and beneath the earth entry are + "Fortress Courtyard": RegionInfo("Fortress Courtyard"), + "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"), + "Beneath the Vault Front": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla entry point + "Beneath the Vault Back": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla exit point + "Eastern Vault Fortress": RegionInfo("Fortress Main"), + "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 Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted, hint=Hint.region), + "Fortress Grave Path Dusty Entrance": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave": RegionInfo("Fortress Reliquary"), + "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Arena": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Lower Mountain": RegionInfo("Mountain"), + "Lower Mountain Stairs": RegionInfo("Mountain"), + "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Quarry Connector": RegionInfo("Darkwoods Tunnel"), + "Quarry Entry": RegionInfo("Quarry Redux"), + "Quarry": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Back": RegionInfo("Quarry Redux"), + "Quarry Monastery Entry": RegionInfo("Quarry Redux"), + "Monastery Front": RegionInfo("Monastery"), + "Monastery Back": RegionInfo("Monastery"), + "Monastery Hero's Grave": RegionInfo("Monastery"), + "Monastery Rope": RegionInfo("Quarry Redux"), + "Lower Quarry": RegionInfo("Quarry Redux"), + "Lower Quarry Zig Door": RegionInfo("Quarry Redux"), + "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), + "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), + "Rooted Ziggurat Upper Front": RegionInfo("ziggurat2020_1"), + "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 Back": RegionInfo("ziggurat2020_3"), # the boss side + "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"), + "Swamp": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Main Entrance": RegionInfo("Swamp Redux 2"), + "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance + "Swamp Hero's Grave": RegionInfo("Swamp Redux 2"), + "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse + "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet Exit": RegionInfo("Cathedral Arena"), + "Far Shore": RegionInfo("Transit"), + "Far Shore to Spawn": RegionInfo("Transit"), + "Far Shore to East Forest": RegionInfo("Transit"), + "Far Shore to Quarry": RegionInfo("Transit"), + "Far Shore to Fortress": RegionInfo("Transit"), + "Far Shore to Library": RegionInfo("Transit"), + "Far Shore to West Garden": RegionInfo("Transit"), + "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Purgatory": RegionInfo("Purgatory"), + "Shop Entrance 1": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 2": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 3": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 4": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 5": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 6": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats) +} + + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways: Dict[str, str] = { + "Overworld Redux, Furnace_gyro_west": "Overworld Redux, Archipelagos Redux_lower", + "Overworld Redux, Furnace_gyro_upper_north": "Overworld Redux, Sewer_west_aqueduct", + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper: Dict[str, str] = {} +for p1, p2 in hallways.items(): + hallway_helper[p1] = p2 + hallway_helper[p2] = p1 + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways_ur: Dict[str, str] = { + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper_ur: Dict[str, str] = {} +for p1, p2 in hallways_ur.items(): + hallway_helper_ur[p1] = p2 + hallway_helper_ur[p2] = p1 + + +# the key is the region you have, the value is the regions you get for having that region +# this is mostly so we don't have to do something overly complex to get this information +dependent_regions_restricted: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"): + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal"], + ("Old House Front",): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Well Boss",): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"], + ("West Garden Portal", "West Garden Portal Item"): ["West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Courtyard Upper",): + ["Fortress Courtyard Upper", "Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper",): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress",): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front",): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Monastery Back", "Monastery Hero's Grave"): + ["Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave", "Swamp", + "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { + # can use ladder storage to get to the well rail + ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", + "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", + "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", + "Overworld Spawn Portal", "Overworld Well to Furnace Rail"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + # can use laurels, ice grapple, or ladder storage to traverse + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + # can ice grapple from portal area to the rest, and vice versa + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + # can use ice grapple or ladder storage to get from any ladder to upper + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + # can ice grapple up + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + # can ice grapple down + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + # can use ladder storage at any of the Quarry ladders to get to Monastery Rope + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope", "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py new file mode 100644 index 000000000000..ebc563c3da50 --- /dev/null +++ b/worlds/tunic/er_rules.py @@ -0,0 +1,1004 @@ +from typing import Dict, 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 BaseClasses import Region + +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +icebolt = "Pages 52-53 (Icebolt)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], + portal_pairs: Dict[Portal, Portal]) -> None: + player = world.player + options = world.options + + 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)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Belltower"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Belltower"].connect( + connecting_region=regions["Overworld"]) + + # nmg: can laurels through the ruined passage door + 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)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Swamp Upper Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Swamp Upper Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Special Shop Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Special Shop Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld West Garden Laurels Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld West Garden Laurels Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + # 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)) + + # not including ice grapple through this because it's very tedious to get an enemy here + regions["Overworld"].connect( + connecting_region=regions["Overworld Southeast Cross Door"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Overworld Southeast Cross Door"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # 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)) + regions["Overworld Fountain Cross Door"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Town Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + 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)) + regions["Overworld Spawn Portal"].connect( + connecting_region=regions["Overworld"]) + + # nmg: ice grapple through temple door + regions["Overworld"].connect( + connecting_region=regions["Overworld Temple Door"], + name="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)) + + # Overworld side areas + regions["Old House Front"].connect( + connecting_region=regions["Old House Back"]) + # nmg: laurels through the gate + regions["Old House Back"].connect( + connecting_region=regions["Old House Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Sealed Temple"].connect( + connecting_region=regions["Sealed Temple Rafters"]) + regions["Sealed Temple Rafters"].connect( + connecting_region=regions["Sealed Temple"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + + # East Forest + regions["Forest Belltower Upper"].connect( + connecting_region=regions["Forest Belltower Main"]) + + regions["Forest Belltower Main"].connect( + connecting_region=regions["Forest Belltower Lower"]) + + # nmg: 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)) + 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)) + + regions["East Forest"].connect( + connecting_region=regions["East Forest Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["East Forest Portal"].connect( + connecting_region=regions["East Forest"]) + + regions["Guard House 1 East"].connect( + connecting_region=regions["Guard House 1 West"]) + regions["Guard House 1 West"].connect( + connecting_region=regions["Guard House 1 East"], + rule=lambda state: state.has(laurels, player)) + + # nmg: 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)) + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path Upper"], + rule=lambda state: state.has(laurels, player)) + + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + # nmg: 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)) + + 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)) + regions["Forest Hero's Grave"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + + # Beneath the Well and Dark Tomb + 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)) + 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)) + + regions["Beneath the Well Back"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + regions["Beneath the Well Main"].connect( + connecting_region=regions["Beneath the Well Back"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + + regions["Well Boss"].connect( + connecting_region=regions["Dark Tomb Checkpoint"]) + # nmg: can laurels through the gate + regions["Dark Tomb Checkpoint"].connect( + connecting_region=regions["Well Boss"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Dark Tomb Entry Point"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Entry Point"], + rule=lambda state: has_lantern(state, player, options)) + + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Dark Exit"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Dark Exit"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + + # West Garden + regions["West Garden Laurels Exit"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden"].connect( + connecting_region=regions["West Garden Laurels Exit"], + 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( + connecting_region=regions["West Garden after Boss"], + rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + + regions["West Garden"].connect( + connecting_region=regions["West Garden Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["West Garden Hero's Grave"].connect( + connecting_region=regions["West Garden"]) + + regions["West Garden 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)) + + # nmg: 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 Portal Item"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # Atoll and Frog's Domain + # nmg: 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)) + 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 Frog Mouth"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Ruined Atoll Frog Mouth"].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 Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Ruined Atoll Portal"].connect( + connecting_region=regions["Ruined Atoll"]) + + regions["Frog's Domain"].connect( + connecting_region=regions["Frog's Domain Back"], + rule=lambda state: state.has(grapple, player)) + + # Library + regions["Library Exterior Tree"].connect( + connecting_region=regions["Library Exterior Ladder"], + rule=lambda state: state.has(grapple, player) or state.has(laurels, player)) + regions["Library Exterior Ladder"].connect( + connecting_region=regions["Library Exterior Tree"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + and (state.has(grapple, player) or state.has(laurels, player))) + + regions["Library Hall"].connect( + connecting_region=regions["Library Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Hero's Grave"].connect( + connecting_region=regions["Library Hall"]) + + regions["Library Lab Lower"].connect( + connecting_region=regions["Library Lab"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Library Lab"].connect( + connecting_region=regions["Library Lab Lower"], + rule=lambda state: state.has(laurels, player)) + + regions["Library Lab"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab"]) + + # Eastern Vault Fortress + regions["Fortress Exterior from East Forest"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Exterior from East Forest"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + 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)) + + regions["Fortress Courtyard"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player)) + # nmg: can ice grapple an enemy in the courtyard + 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)) + + 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)) + + regions["Fortress Courtyard Upper"].connect( + connecting_region=regions["Fortress Exterior from Overworld"]) + + regions["Beneath the Vault Front"].connect( + connecting_region=regions["Beneath the Vault Back"], + rule=lambda state: has_lantern(state, player, options)) + regions["Beneath the Vault Back"].connect( + connecting_region=regions["Beneath the Vault Front"]) + + 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)) + + # 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"], + name="Fortress to 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)) + regions["Eastern Vault Fortress Gold Door"].connect( + connecting_region=regions["Eastern Vault Fortress"], + name="Gold Door to Fortress", + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance"], + rule=lambda state: state.has(laurels, player)) + regions["Fortress Grave Path Dusty Entrance"].connect( + connecting_region=regions["Fortress Grave Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Fortress Hero's Grave"].connect( + connecting_region=regions["Fortress Grave Path"]) + + # 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)) + + regions["Fortress Arena"].connect( + connecting_region=regions["Fortress Arena Portal"], + name="Fortress Arena to Fortress Portal", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Fortress Arena Portal"].connect( + connecting_region=regions["Fortress Arena"]) + + # Quarry + regions["Lower Mountain"].connect( + connecting_region=regions["Lower Mountain Stairs"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Lower Mountain Stairs"].connect( + connecting_region=regions["Lower Mountain"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + regions["Quarry Entry"].connect( + connecting_region=regions["Quarry Portal"], + name="Quarry to Quarry Portal", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Quarry Portal"].connect( + connecting_region=regions["Quarry Entry"]) + + 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( + 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( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Monastery Entry"]) + + regions["Quarry Monastery Entry"].connect( + connecting_region=regions["Quarry Back"], + rule=lambda state: state.has(laurels, player)) + regions["Quarry Back"].connect( + connecting_region=regions["Quarry Monastery Entry"], + rule=lambda state: state.has(laurels, player)) + + regions["Monastery Rope"].connect( + connecting_region=regions["Quarry Back"]) + + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry"], + rule=lambda state: has_mask(state, player, options)) + + # nmg: bring a scav over, then ice grapple through the door + regions["Lower Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + name="Quarry to Zig Door", + rule=lambda state: state.has("Activate Quarry Fuse", player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Monastery Front"].connect( + connecting_region=regions["Monastery Back"]) + # nmg: can laurels through the gate + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Monastery Hero's Grave"].connect( + connecting_region=regions["Monastery Back"]) + + # Ziggurat + regions["Rooted Ziggurat Upper Entry"].connect( + connecting_region=regions["Rooted Ziggurat Upper Front"]) + + 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( + connecting_region=regions["Rooted Ziggurat Upper Front"], + rule=lambda state: state.has(laurels, player)) + + regions["Rooted Ziggurat Middle Top"].connect( + connecting_region=regions["Rooted Ziggurat Middle Bottom"]) + + regions["Rooted Ziggurat Lower Front"].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 + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"], + rule=lambda state: state.has(laurels, player) or can_ladder_storage(state, player, options)) + + 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)) + regions["Rooted Ziggurat Portal Room Entrance"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"]) + + regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room Exit"], + name="Zig 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)) + + # Swamp and Cathedral + # nmg: ice grapple through cathedral door, can do it both ways + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Main Entrance"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Swamp to Cathedral Main Entrance"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Treasure Room"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Swamp to Cathedral Treasure Room"].connect( + connecting_region=regions["Swamp"]) + + regions["Back of Swamp"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: state.has(laurels, player)) + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Back of Swamp"], + rule=lambda state: state.has(laurels, player)) + + # nmg: can ice grapple down while you're on the pillars + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: state.has(laurels, player) + and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Back of Swamp"].connect( + connecting_region=regions["Swamp Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Swamp Hero's Grave"].connect( + connecting_region=regions["Back of Swamp"]) + + regions["Cathedral Gauntlet Checkpoint"].connect( + connecting_region=regions["Cathedral Gauntlet"]) + + regions["Cathedral Gauntlet"].connect( + connecting_region=regions["Cathedral Gauntlet Exit"], + rule=lambda state: state.has(laurels, player)) + regions["Cathedral Gauntlet Exit"].connect( + connecting_region=regions["Cathedral Gauntlet"], + rule=lambda state: state.has(laurels, player)) + + # Far Shore + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Spawn"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to Spawn"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to East Forest"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to East Forest"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to West Garden"], + name="Far Shore to West Garden", + rule=lambda state: state.has("Activate West Garden Fuse", player)) + regions["Far Shore to West Garden"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Quarry"], + name="Far Shore to Quarry", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Far Shore to Quarry"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Fortress"], + name="Far Shore to Fortress", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Far Shore to Fortress"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Library"], + name="Far Shore to Library", + rule=lambda state: state.has("Activate Library Fuse", player)) + regions["Far Shore to Library"].connect( + connecting_region=regions["Far Shore"]) + + # Misc + regions["Shop Entrance 1"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 2"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 3"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 4"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 5"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 6"].connect( + connecting_region=regions["Shop"]) + + 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}, 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": + def get_paired_region(portal_sd: str) -> str: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal2.region + if portal2.scene_destination() == portal_sd: + return portal1.region + raise Exception("no matches found in get_paired_region") + + # The upper Swamp entrance + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Swamp Redux 2_wall")], + rule=lambda state: has_stick(state, player)) + # Western Furnace entrance, next to the sign that leads to West Garden + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_west")], + rule=lambda state: has_stick(state, player)) + # Upper West Garden entry, by the belltower + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden entry by the Furnace + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lower")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels entrance, by the beach + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lowest")], + rule=lambda state: has_stick(state, player)) + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Sewer_west_aqueduct")], + rule=lambda state: has_stick(state, player)) + # Well rail, east side. Need some height from the temple stairs + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + + # Furnace ladder to the fuse entrance + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to Dark Tomb + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Crypt Redux_")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to the West Garden connector + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_west")], + rule=lambda state: has_stick(state, player)) + + # West Garden exit after Garden Knight + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels exit + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_lowest")], + rule=lambda state: has_stick(state, player)) + + # Frog mouth entrance + regions["Ruined Atoll"].connect( + regions[get_paired_region("Atoll Redux, Frog Stairs_mouth")], + rule=lambda state: has_stick(state, player)) + + # Entrance by the dancing fox holy cross spot + regions["East Forest"].connect( + regions[get_paired_region("East Forest Redux, East Forest Redux Laddercave_upper")], + rule=lambda state: has_stick(state, player)) + + # From the west side of guard house 1 to the east side + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, East Forest Redux_gate")], + rule=lambda state: has_stick(state, player)) + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, Forest Boss Room_")], + rule=lambda state: has_stick(state, player)) + + # Upper exit from the Forest Grave Path, use ls at the ladder by the gate switch + regions["Forest Grave Path Main"].connect( + regions[get_paired_region("Sword Access, East Forest Redux_upper")], + rule=lambda state: has_stick(state, player)) + + # Fortress exterior shop, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the east side of the area + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the Beneath the Vault entrance ladder + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # ls at the ladder, need to gain a little height to get up the stairs + regions["Lower Mountain"].connect( + regions[get_paired_region("Mountain, Mountaintop_")], + rule=lambda state: has_stick(state, player)) + + # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword + regions["Quarry Monastery Entry"].connect( + regions[get_paired_region("Quarry Redux, Monastery_back")], + rule=lambda state: has_stick(state, player)) + + # Swamp to Gauntlet + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Arena_")], + rule=lambda state: has_stick(state, player)) + # Swamp to Overworld upper + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_wall")], + rule=lambda state: has_stick(state, player)) + # Ladder by the hero grave + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_conduit")], + rule=lambda state: has_stick(state, player)) + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Shop_")], + rule=lambda state: has_stick(state, player)) + # Need to put the cathedral HC code mid-flight + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Redux_secret")], + rule=lambda state: has_stick(state, player) + and has_ability(state, player, holy_cross, options, ability_unlocks)) + + +def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + player = world.player + multiworld = world.multiworld + options = world.options + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), 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), + 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)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + 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), + 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), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + 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), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + 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))) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + 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)) + set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + lambda state: state.has(laurels, player)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, player)) + + # Eastern Vault Fortress + set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + lambda state: state.has(vault_key, 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(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), + lambda state: has_lantern(state, player, options)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + + # Ziggurat + set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + lambda state: has_sword(state, player) or state.has(fire_wand, player)) + 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)) + + # Bosses + set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + 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), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + lambda state: has_sword(state, player)) + + # Hero's Grave and Far Shore + set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + 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), + lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) + set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + lambda state: state.has("Activate Beneath the Vault Fuse", player)) + set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + 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), + 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)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py new file mode 100644 index 000000000000..d2b854f5df0e --- /dev/null +++ b/worlds/tunic/er_scripts.py @@ -0,0 +1,571 @@ +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, hallway_helper, hallway_helper_ur, \ + dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur +from .er_rules import set_er_region_rules +from worlds.generic import PlandoConnection + +if TYPE_CHECKING: + from . import TunicWorld + + +class TunicERItem(Item): + game: str = "TUNIC" + + +class TunicERLocation(Location): + game: str = "TUNIC" + + +def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]: + regions: Dict[str, Region] = {} + portal_pairs: Dict[Portal, Portal] = pair_portals(world) + logic_rules = world.options.logic_rules + + # 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) + + # check if a portal leads to a hallway. if it does, update the hint text accordingly + def hint_helper(portal: Portal, hint_string: str = "") -> str: + # start by setting it as the name of the portal, for the case we're not using the hallway helper + if hint_string == "": + hint_string = portal.name + + # unrestricted has fewer hallways, like the well rail + if logic_rules == "unrestricted": + hallways = hallway_helper_ur + else: + hallways = hallway_helper + + if portal.scene_destination() in hallways: + # if we have a hallway, we want the region rather than the portal name + if hint_string == portal.name: + hint_string = portal.region + # library exterior is two regions, we just want to fix up the name + if hint_string in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_string = "Library Exterior" + + # search through the list for the other end of the hallway + for portala, portalb in portal_pairs.items(): + if portala.scene_destination() == hallways[portal.scene_destination()]: + # if we find that we have a chain of hallways, do recursion + if portalb.scene_destination() in hallways: + hint_region = portalb.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portalb, hint_string) + else: + # if we didn't find a chain, get the portal name for the end of the chain + hint_string = portalb.name + " then " + hint_string + return hint_string + # and then the same thing for the other portal, since we have to check each separately + if portalb.scene_destination() == hallways[portal.scene_destination()]: + if portala.scene_destination() in hallways: + hint_region = portala.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portala, hint_string) + else: + hint_string = portala.name + " then " + hint_string + return hint_string + return hint_string + + # create our regions, give them hint text if they're in a spot where it makes sense to + # we're limiting which ones get hints so that it still gets that ER feel with a little less BS + for region_name, region_data in tunic_er_regions.items(): + hint_text = "error" + if region_data.hint == 1: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == region_name: + hint_text = hint_helper(portal2) + break + if portal2.region == region_name: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 2: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal2) + break + if portal2.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 3: + # west garden portal item is at a dead end in restricted, otherwise just in west garden + if region_name == "West Garden Portal Item": + if world.options.logic_rules: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal2) + break + if portal2.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == "West Garden Portal": + hint_text = hint_helper(portal2) + break + if portal2.region == "West Garden Portal": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + regions[region_name] = Region(region_name, world.player, world.multiworld) + + set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) + + er_hint_data: Dict[int, str] = {} + 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) + if region.name == region.hint_text: + continue + er_hint_data[location.address] = region.hint_text + + create_randomized_entrances(portal_pairs, regions) + + for region in regions.values(): + world.multiworld.regions.append(region) + + place_event_items(world, regions) + + victory_region = regions["Spirit Arena Victory"] + victory_location = TunicERLocation(world.player, "The Heir", None, victory_region) + victory_location.place_locked_item(TunicERItem("Victory", ItemClassification.progression, None, world.player)) + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + victory_region.locations.append(victory_location) + + portals_and_hints = (portal_pairs, er_hint_data) + + return portals_and_hints + + +tunic_events: Dict[str, str] = { + "Eastern Bell": "Forest Belltower Upper", + "Western Bell": "Overworld Belltower", + "Furnace Fuse": "Furnace Fuse", + "South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld", + "Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper", + "Beneath the Vault Fuse": "Beneath the Vault Back", + "Eastern Vault West Fuses": "Eastern Vault Fortress", + "Eastern Vault East Fuse": "Eastern Vault Fortress", + "Quarry Connector Fuse": "Quarry Connector", + "Quarry Fuse": "Quarry", + "Ziggurat Fuse": "Rooted Ziggurat Lower Back", + "West Garden Fuse": "West Garden", + "Library Fuse": "Library Lab", +} + + +def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: + for event_name, region_name in tunic_events.items(): + region = regions[region_name] + location = TunicERLocation(world.player, event_name, None, region) + if event_name.endswith("Bell"): + location.place_locked_item( + TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player)) + else: + location.place_locked_item( + TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player)) + region.locations.append(location) + + +# 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 + portal_pairs: Dict[Portal, Portal] = {} + dead_ends: List[Portal] = [] + two_plus: List[Portal] = [] + plando_connections: List[PlandoConnection] = [] + fixed_shop = False + logic_rules = world.options.logic_rules.value + + if not logic_rules: + dependent_regions = dependent_regions_restricted + elif logic_rules == 1: + dependent_regions = dependent_regions_nmg + else: + dependent_regions = dependent_regions_ur + + # create separate lists for dead ends and non-dead ends + if logic_rules: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end == 1: + dead_ends.append(portal) + else: + two_plus.append(portal) + else: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end: + dead_ends.append(portal) + else: + two_plus.append(portal) + + connected_regions: Set[str] = set() + # make better start region stuff when/if implementing random start + start_region = "Overworld" + connected_regions.update(add_dependent_regions(start_region, logic_rules)) + + # universal tracker support stuff, don't need to care about region dependency + if hasattr(world.multiworld, "re_gen_passthrough"): + if "TUNIC" in world.multiworld.re_gen_passthrough: + # universal tracker stuff, won't do anything in normal gen + for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items(): + portal_name1 = "" + portal_name2 = "" + + # skip this if 10 fairies laurels location is on, it can be handled normally + if portal1 == "Overworld Redux, Waterfall_" and portal2 == "Waterfall, Overworld Redux_" \ + and world.options.laurels_location == "10_fairies": + continue + + for portal in portal_mapping: + if portal.scene_destination() == portal1: + portal_name1 = portal.name + # connected_regions.update(add_dependent_regions(portal.region, logic_rules)) + if portal.scene_destination() == portal2: + portal_name2 = portal.name + # connected_regions.update(add_dependent_regions(portal.region, logic_rules)) + # shops have special handling + if not portal_name2 and portal2 == "Shop, Previous Region_": + portal_name2 = "Shop Portal" + plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) + + if plando_connections: + portal_pairs, dependent_regions, dead_ends, two_plus = \ + create_plando_connections(plando_connections, dependent_regions, dead_ends, two_plus) + + # if we have plando connections, our connected regions may change somewhat + while True: + test1 = len(connected_regions) + for region in connected_regions.copy(): + connected_regions.update(add_dependent_regions(region, logic_rules)) + test2 = len(connected_regions) + if test1 == test2: + break + + # need to plando fairy cave, or it could end up laurels locked + # fix this later to be random after adding some item logic to dependent regions + if world.options.laurels_location == "10_fairies": + portal1 = None + portal2 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Waterfall_": + portal1 = portal + break + for portal in dead_ends: + if portal.scene_destination() == "Waterfall, Overworld Redux_": + portal2 = portal + break + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + dead_ends.remove(portal2) + + if world.options.fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): + fixed_shop = True + portal1 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Windmill_": + portal1 = portal + break + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance 2", destination="Previous Region_") + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + + # we want to start by making sure every region is accessible + non_dead_end_regions = set() + for region_name, region_info in tunic_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: + non_dead_end_regions.add(region_name) + + world.random.shuffle(two_plus) + check_success = 0 + portal1 = None + portal2 = None + while len(connected_regions) < len(non_dead_end_regions): + # find a portal in an inaccessible region + if check_success == 0: + for portal in two_plus: + if portal.region in connected_regions: + # if there's risk of self-locking, start over + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal1 = portal + two_plus.remove(portal) + check_success = 1 + break + + # then we find a portal in a connected region + if check_success == 1: + for portal in two_plus: + if portal.region not in connected_regions: + # if there's risk of self-locking, shuffle and try again + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal2 = portal + two_plus.remove(portal) + check_success = 2 + break + + # once we have both portals, connect them and add the new region(s) to connected_regions + if check_success == 2: + connected_regions.update(add_dependent_regions(portal2.region, logic_rules)) + portal_pairs[portal1] = portal2 + check_success = 0 + world.random.shuffle(two_plus) + + # add 6 shops, connect them to unique scenes + # this is due to a limitation in Tunic -- you wrong warp if there's multiple shops + shop_scenes: Set[str] = set() + shop_count = 6 + + if fixed_shop: + shop_count = 1 + shop_scenes.add("Overworld Redux") + + # for universal tracker, we want to skip shop gen + if hasattr(world.multiworld, "re_gen_passthrough"): + if "TUNIC" in world.multiworld.re_gen_passthrough: + 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 + if portal1 is None: + raise Exception("Too many shops in the pool, or something else went wrong") + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {i + 1}", destination="Previous Region_") + portal_pairs[portal1] = portal2 + + # connect dead ends to random non-dead ends + # none of the key events are in dead ends, so we don't need to do gate_before_switch + while len(dead_ends) > 0: + portal1 = two_plus.pop() + portal2 = dead_ends.pop() + portal_pairs[portal1] = portal2 + + # then randomly connect the remaining portals to each other + # every region is accessible, so gate_before_switch is not necessary + while len(two_plus) > 1: + portal1 = two_plus.pop() + portal2 = two_plus.pop() + portal_pairs[portal1] = portal2 + + if len(two_plus) == 1: + raise Exception("two plus had an odd number of portals, investigate this. last portal is " + two_plus[0].name) + + return portal_pairs + + +# loop through our list of paired portals and make two-way connections +def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None: + for portal1, portal2 in portal_pairs.items(): + region1 = regions[portal1.region] + region2 = regions[portal2.region] + region1.connect(region2, f"{portal1.name} -> {portal2.name}") + # prevent the logic from thinking you can get to any shop-connected region from the shop + if portal2.name != "Shop": + region2.connect(region1, f"{portal2.name} -> {portal1.name}") + + +# loop through the static connections, return regions you can reach from this region +# todo: refactor to take region_name and dependent_regions +def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]: + region_set = set() + if not logic_rules: + regions_to_add = dependent_regions_restricted + elif logic_rules == 1: + regions_to_add = dependent_regions_nmg + else: + regions_to_add = dependent_regions_ur + for origin_regions, destination_regions in regions_to_add.items(): + if region_name in origin_regions: + # if you matched something in the first set, you get the regions in its paired set + region_set.update(destination_regions) + return region_set + # if you didn't match anything in the first sets, just gives you the region + region_set = {region_name} + return region_set + + +# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are +# doing this ensures the keys will not be locked behind the event-locked portal +def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: + # the western belltower cannot be locked since you can access it with laurels + # so we only need to make sure the forest belltower isn't locked + if check_portal.scene_destination() == "Overworld Redux, Temple_main": + i = 0 + for portal in two_plus: + if portal.region == "Forest Belltower Upper": + i += 1 + break + if i == 1: + return True + + # fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard + elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_": + i = j = k = 0 + for portal in two_plus: + if portal.region == "Fortress Courtyard Upper": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 2 or j == 2 or k == 5: + return True + + # fortress teleporter needs only the left fuses + elif check_portal.scene_destination() in ["Fortress Arena, Transit_teleporter_spidertank", + "Transit, Fortress Arena_teleporter_spidertank"]: + i = j = k = 0 + for portal in two_plus: + if portal.scene() == "Fortress Courtyard": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 8 or j == 2 or k == 5: + return True + + # Cathedral door needs Overworld and the front of Swamp + # Overworld is currently guaranteed, so no need to check it + elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main": + i = 0 + for portal in two_plus: + if portal.region == "Swamp": + i += 1 + if i == 4: + return True + + # Zig portal room exit needs Zig 3 to be accessible to hit the fuse + elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_": + i = 0 + for portal in two_plus: + if portal.scene() == "ziggurat2020_3": + i += 1 + if i == 2: + return True + + # Quarry teleporter needs you to hit the Darkwoods fuse + # Since it's physically in Quarry, we don't need to check for it + elif check_portal.scene_destination() in ["Quarry Redux, Transit_teleporter_quarry teleporter", + "Quarry Redux, ziggurat2020_0_"]: + i = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if i == 2: + return True + + # Same as above, but Quarry isn't guaranteed here + elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter": + i = j = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if portal.scene() == "Quarry Redux": + j += 1 + if i == 2 or j == 7: + return True + + # Need Library fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Library Lab": + i += 1 + if i == 3: + return True + + # Need West Garden fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Archipelagos Redux": + i += 1 + if i == 6: + return True + + # false means you're good to place the portal + return False + + +# this is for making the connections themselves +def create_plando_connections(plando_connections: List[PlandoConnection], + dependent_regions: Dict[Tuple[str, ...], List[str]], dead_ends: List[Portal], + two_plus: List[Portal]) \ + -> Tuple[Dict[Portal, Portal], Dict[Tuple[str, ...], List[str]], List[Portal], List[Portal]]: + + portal_pairs: Dict[Portal, Portal] = {} + shop_num = 1 + for connection in plando_connections: + p_entrance = connection.entrance + p_exit = connection.exit + + portal1 = None + portal2 = None + + # search two_plus for both at once + for portal in two_plus: + if p_entrance == portal.name: + portal1 = portal + if p_exit == portal.name: + portal2 = portal + + # search dead_ends individually since we can't really remove items from two_plus during the loop + if not portal1: + for portal in dead_ends: + if p_entrance == portal.name: + portal1 = portal + break + dead_ends.remove(portal1) + else: + two_plus.remove(portal1) + + if not portal2: + for portal in dead_ends: + if p_exit == portal.name: + portal2 = portal + break + if p_exit == "Shop Portal": + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {shop_num}", destination="Previous Region_") + shop_num += 1 + else: + dead_ends.remove(portal2) + else: + two_plus.remove(portal2) + + if not portal1: + raise Exception("could not find entrance named " + p_entrance + " for Tunic player's plando") + if not portal2: + raise Exception("could not find entrance named " + p_exit + " for Tunic player's plando") + + portal_pairs[portal1] = portal2 + + # update dependent regions based on the plando'd connections, to make sure the portals connect well, logically + for origins, destinations in dependent_regions.items(): + if portal1.region in origins: + destinations.append(portal2.region) + if portal2.region in origins: + destinations.append(portal1.region) + + return portal_pairs, dependent_regions, dead_ends, two_plus diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py new file mode 100644 index 000000000000..547a0ffb816f --- /dev/null +++ b/worlds/tunic/items.py @@ -0,0 +1,215 @@ +from itertools import groupby +from typing import Dict, List, Set, NamedTuple +from BaseClasses import ItemClassification + + +class TunicItemData(NamedTuple): + classification: ItemClassification + quantity_in_item_pool: int + item_id_offset: int + item_group: str = "" + + +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, "potions"), + "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"), + "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.useful, 1, 51, "hero relics"), + "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"), + "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"), + "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"), + "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"), + "Hero Relic - SP": TunicItemData(ItemClassification.useful, 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, "fool"), + "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"), +} + +fool_tiers: List[List[str]] = [ + [], + ["Money x1", "Money x10", "Money x15", "Money x16"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], +] + +slot_data_item_names = [ + "Stick", + "Sword", + "Sword Upgrade", + "Magic Dagger", + "Magic Wand", + "Magic Orb", + "Hero's Laurels", + "Lantern", + "Gun", + "Scavenger Mask", + "Shield", + "Dath Stone", + "Hourglass", + "Old House Key", + "Fortress Vault Key", + "Hero Relic - ATT", + "Hero Relic - DEF", + "Hero Relic - POTION", + "Hero Relic - HP", + "Hero Relic - SP", + "Hero Relic - MP", + "Pages 24-25 (Prayer)", + "Pages 42-43 (Holy Cross)", + "Pages 52-53 (Icebolt)", + "Red Questagon", + "Green Questagon", + "Blue Questagon", + "Gold Questagon", +] + +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] + + +def get_item_group(item_name: str) -> str: + return item_table[item_name].item_group + + +item_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != "" +} + +# extra groups for the purpose of aliasing items +extra_groups: Dict[str, Set[str]] = { + "laurels": {"Hero's Laurels"}, + "orb": {"Magic Orb"}, + "dagger": {"Magic Dagger"}, + "magic rod": {"Magic Wand"}, + "holy cross": {"Pages 42-43 (Holy Cross)"}, + "prayer": {"Pages 24-25 (Prayer)"}, + "icebolt": {"Pages 52-53 (Icebolt)"}, + "ice rod": {"Pages 52-53 (Icebolt)"}, + "melee weapons": {"Stick", "Sword", "Sword Upgrade"}, + "progressive sword": {"Sword Upgrade"}, + "abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"}, + "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"} +} + +item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py new file mode 100644 index 000000000000..1501fb7da24d --- /dev/null +++ b/worlds/tunic/locations.py @@ -0,0 +1,337 @@ +from typing import Dict, NamedTuple, Set +from itertools import groupby + + +class TunicLocationData(NamedTuple): + region: str + er_region: str # entrance rando region + location_group: str = "region" + + +location_base_id = 509342400 + +location_table: Dict[str, TunicLocationData] = { + "Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Tentacle Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Entryway] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Save Room] Upper Floor Chest 1": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Save Room] Upper Floor Chest 2": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Second Room] Underwater Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "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("Beneath the Well", "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"), + "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 - 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 Main"), + "Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"), + "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", "holy cross"), + "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "East Forest", "holy cross"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East 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"), + "East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"), + "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"), + "East Forest - Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"), + "East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"), + "Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"), + "Forest Belltower - After Guard Captain": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", "holy cross"), + "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"), + "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"), + "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"), + "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"), + "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"), + "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"), + "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", "holy cross"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "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"), + "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 Front"), + "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 - 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"), + "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena"), + "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", "holy cross"), + "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Behind Chalkboard by Fuse": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"), + "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"), + "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"), + "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"), + "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "Fortress Courtyard - Near Fuse": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Below Walkway": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Page Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "West Furnace - Lantern Pickup": TunicLocationData("Overworld", "Furnace Fuse"), + "Maze Cave - Maze Room Chest": TunicLocationData("Overworld", "Maze Cave"), + "Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"), + "Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"), + "Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "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] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"), + "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"), + "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"), + "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"), + "Shop - Potion 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Potion 2": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 2": TunicLocationData("Overworld", "Shop", "shop"), + "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"), + "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"), + "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"), + "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"), + "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"), + "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn"), + "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", "holy cross"), + "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", "holy cross"), + "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", "holy cross"), + "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", "holy cross"), + "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", "holy cross"), + "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", "holy cross"), + "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", "holy cross"), + "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave", "holy cross"), + "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", "holy cross"), + "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", "holy cross"), + "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", "holy cross"), + "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", "holy cross"), + "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", "holy cross"), + "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "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] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), + "Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"), + "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"), + "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"), + "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "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", "Lower Quarry"), + "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 - 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 - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back"), + "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [East] Locked Room Upper Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"), + "Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"), + "Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"), + "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"), + "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"), + "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp"), + "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", "holy cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", "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", "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] After Garden Knight": TunicLocationData("West Garden", "West Garden after Boss"), + "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), + "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 House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), + "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), +} + +hexagon_locations: Dict[str, str] = { + "Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup", + "Green Questagon": "Librarian - Hexagon Green", + "Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue", +} + +location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)} + + +def get_loc_group(location_name: str) -> str: + loc_group = location_table[location_name].location_group + if loc_group == "region": + # set loc_group as the region name. Typically, location groups are lowercase + loc_group = location_table[location_name].region.lower() + return loc_group + + +location_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(location_table, key=get_loc_group), get_loc_group) +} diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py new file mode 100644 index 000000000000..f4790da36729 --- /dev/null +++ b/worlds/tunic/options.py @@ -0,0 +1,150 @@ +from dataclasses import dataclass + +from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, PerGameCommonOptions + + +class SwordProgression(DefaultOnToggle): + """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new + swords with increased range and attack power.""" + internal_name = "sword_progression" + display_name = "Sword Progression" + + +class StartWithSword(Toggle): + """Start with a sword in the player's inventory. Does not count towards Sword Progression.""" + internal_name = "start_with_sword" + display_name = "Start With Sword" + + +class KeysBehindBosses(Toggle): + """Places the three hexagon keys behind their respective boss fight in your world.""" + internal_name = "keys_behind_bosses" + display_name = "Keys Behind Bosses" + + +class AbilityShuffling(Toggle): + """Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found. + If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required + Hexagon goal amount. + *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other + player-facing codes. + """ + internal_name = "ability_shuffling" + display_name = "Ability Shuffling" + + +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. + *Special Shop is not in logic without the Hero's Laurels due to soft lock potential. + *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. + When enabled, the Lantern is marked as Useful instead of Progression.""" + internal_name = "lanternless" + display_name = "Lanternless" + + +class Maskless(Toggle): + """Choose whether you require the Scavenger's Mask for Lower Quarry. + When enabled, the Scavenger's Mask is marked as Useful instead of Progression.""" + internal_name = "maskless" + display_name = "Maskless" + + +class FoolTraps(Choice): + """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative + effects to the player.""" + internal_name = "fool_traps" + display_name = "Fool Traps" + option_off = 0 + option_normal = 1 + option_double = 2 + option_onslaught = 3 + default = 1 + + +class HexagonQuest(Toggle): + """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed + after collecting the required number of them.""" + internal_name = "hexagon_quest" + display_name = "Hexagon Quest" + + +class HexagonGoal(Range): + """How many Gold Questagons are required to complete the game on Hexagon Quest.""" + internal_name = "hexagon_goal" + display_name = "Gold Hexagons Required" + range_start = 15 + range_end = 50 + default = 20 + + +class ExtraHexagonPercentage(Range): + """How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.""" + internal_name = "extra_hexagon_percentage" + display_name = "Percentage of Extra Gold Hexagons" + range_start = 0 + range_end = 100 + default = 50 + + +class EntranceRando(Toggle): + """Randomize the connections between scenes. + A small, very lost fox on a big adventure.""" + internal_name = "entrance_rando" + display_name = "Entrance Rando" + + +class FixedShop(Toggle): + """Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool. + Has no effect if Entrance Rando is not enabled.""" + internal_name = "fixed_shop" + display_name = "ER Fixed Shop" + + +class LaurelsLocation(Choice): + """Force the Hero's Laurels to be placed at a location in your world. + For if you want to avoid or specify early or late Laurels. + If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance.""" + internal_name = "laurels_location" + display_name = "Laurels Location" + option_anywhere = 0 + option_6_coins = 1 + option_10_coins = 2 + option_10_fairies = 3 + default = 0 + + +@dataclass +class TunicOptions(PerGameCommonOptions): + sword_progression: SwordProgression + start_with_sword: StartWithSword + keys_behind_bosses: KeysBehindBosses + ability_shuffling: AbilityShuffling + logic_rules: LogicRules + entrance_rando: EntranceRando + fixed_shop: FixedShop + fool_traps: FoolTraps + hexagon_quest: HexagonQuest + hexagon_goal: HexagonGoal + extra_hexagon_percentage: ExtraHexagonPercentage + lanternless: Lanternless + maskless: Maskless + laurels_location: LaurelsLocation + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py new file mode 100644 index 000000000000..5d5248f210d6 --- /dev/null +++ b/worlds/tunic/regions.py @@ -0,0 +1,25 @@ +from typing import Dict, Set + +tunic_regions: Dict[str, Set[str]] = { + "Menu": {"Overworld"}, + "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", + "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", + "Spirit Arena"}, + "Overworld Holy Cross": set(), + "East Forest": {"Eastern Vault Fortress"}, + "Dark Tomb": {"West Garden"}, + "Beneath the Well": {"Dark Tomb"}, + "West Garden": {"Overworld", "Dark Tomb"}, + "Ruined Atoll": {"Frog's Domain", "Library"}, + "Frog's Domain": set(), + "Library": set(), + "Eastern Vault Fortress": {"Beneath the Vault"}, + "Beneath the Vault": {"Eastern Vault Fortress"}, + "Quarry Back": {"Quarry"}, + "Quarry": {"Lower Quarry", "Rooted Ziggurat"}, + "Lower Quarry": {"Rooted Ziggurat"}, + "Rooted Ziggurat": set(), + "Swamp": {"Cathedral"}, + "Cathedral": set(), + "Spirit Arena": set() +} diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py new file mode 100644 index 000000000000..6e5639b4ebaf --- /dev/null +++ b/worlds/tunic/rules.py @@ -0,0 +1,344 @@ +from random import Random +from typing import Dict, TYPE_CHECKING + +from worlds.generic.Rules import set_rule, forbid_item +from BaseClasses import CollectionState +from .options import TunicOptions +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +icebolt = "Pages 52-53 (Icebolt)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: + ability_requirement = [1, 1, 1] + if options.hexagon_quest.value: + hexagon_goal = options.hexagon_goal.value + # Set ability unlocks to 25, 50, and 75% of goal amount + ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4] + abilities = [prayer, holy_cross, icebolt] + random.shuffle(abilities) + return dict(zip(abilities, ability_requirement)) + + +def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, + ability_unlocks: Dict[str, int]) -> bool: + 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) + + +# 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_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 + + if not long_range: + return state.has_all({ice_dagger, grapple}, player) + else: + return state.has_all({ice_dagger, fire_wand, grapple}, player) and \ + has_ability(state, player, icebolt, options, ability_unlocks) + + +def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.logic_rules == "unrestricted" and has_stick(state, player): + return True + else: + return False + + +def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.maskless: + return True + else: + return state.has(mask, player) + + +def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.lanternless: + return True + else: + return state.has(lantern, player) + + +def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + 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 = \ + lambda state: state.has(laurels, player) \ + or can_ladder_storage(state, player, options) + multiworld.get_entrance("Beneath the Well -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("West Garden -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(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) + multiworld.get_entrance("East Forest -> 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) + # 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 = \ + 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 = \ + 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("Quarry -> Rooted Ziggurat", player).access_rule = \ + lambda state: has_ice_grapple_logic(False, state, player, 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 \ + has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) + + +def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), 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)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + 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), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Old House - Normal Chest", player), + 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), + 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), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and has_sword(state, player))) + 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), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + 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), + 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("Secret Gathering Place - 10 Fairy Reward", player), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + 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), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + 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)) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + 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), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + lambda state: has_sword(state, player) or state.has(laurels, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or can_ladder_storage(state, player, options)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player) or options.logic_rules) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, 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))) + + # 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)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(laurels, player) and state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + 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)) diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py new file mode 100644 index 000000000000..d7ae47f7d74c --- /dev/null +++ b/worlds/tunic/test/__init__.py @@ -0,0 +1,6 @@ +from test.bases import WorldTestBase + + +class TunicTestBase(WorldTestBase): + game = "TUNIC" + player: int = 1 \ No newline at end of file diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py new file mode 100644 index 000000000000..d74858bd27ef --- /dev/null +++ b/worlds/tunic/test/test_access.py @@ -0,0 +1,70 @@ +from . import TunicTestBase +from .. import options + + +class TestAccess(TunicTestBase): + # test whether you can get into the temple without laurels + def test_temple_access(self): + self.collect_all_but(["Hero's Laurels", "Lantern"]) + self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup")) + self.collect_by_name(["Lantern"]) + self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup")) + + # test that the wells function properly. Since fairies is written the same way, that should succeed too + def test_wells(self): + self.collect_all_but(["Golden Coin"]) + self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins")) + self.collect_by_name(["Golden Coin"]) + self.assertTrue(self.can_reach_location("Coins in the Well - 3 Coins")) + + +class TestStandardShuffle(TunicTestBase): + options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need to get holy cross to open the hc door in overworld + def test_hc_door(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Pages 42-43 (Holy Cross)") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need the gold questagons to open the hc door in overworld + def test_hc_door_hex_shuffle(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Gold Questagon") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestNoShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false} + + # test that you can get the item behind the overworld hc door with nothing and no ability shuffle + def test_hc_door_no_shuffle(self): + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestNormalGoal(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + # test that you need the three colored hexes to reach the Heir in standard + def test_normal_goal(self): + location = ["The Heir"] + items = [["Red Questagon", "Blue Questagon", "Green Questagon"]] + self.assertAccessDependency(location, items) + + +class TestER(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + def test_overworld_hc_chest(self): + # 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")) diff --git a/worlds/v6/Options.py b/worlds/v6/Options.py index 107fbab465e1..1950d1bcbd02 100644 --- a/worlds/v6/Options.py +++ b/worlds/v6/Options.py @@ -1,8 +1,10 @@ import typing -from Options import Option, DeathLink, Range, Toggle +from dataclasses import dataclass +from Options import Option, DeathLink, Range, Toggle, PerGameCommonOptions class DoorCost(Range): """Amount of Trinkets required to enter Areas. Set to 0 to disable artificial locks.""" + display_name = "Door Cost" range_start = 0 range_end = 3 default = 3 @@ -13,6 +15,7 @@ class AreaCostRandomizer(Toggle): class DeathLinkAmnesty(Range): """Amount of Deaths to take before sending a DeathLink signal, for balancing difficulty""" + display_name = "Death Link Amnesty" range_start = 0 range_end = 30 default = 15 @@ -25,11 +28,11 @@ class MusicRandomizer(Toggle): """Randomize Music""" display_name = "Music Randomizer" -v6_options: typing.Dict[str,type(Option)] = { - "MusicRandomizer": MusicRandomizer, - "AreaRandomizer": AreaRandomizer, - "DoorCost": DoorCost, - "AreaCostRandomizer": AreaCostRandomizer, - "death_link": DeathLink, - "DeathLinkAmnesty": DeathLinkAmnesty -} \ No newline at end of file +@dataclass +class V6Options(PerGameCommonOptions): + music_rando: MusicRandomizer + area_rando: AreaRandomizer + door_cost: DoorCost + area_cost: AreaCostRandomizer + death_link: DeathLink + death_link_amnesty: DeathLinkAmnesty diff --git a/worlds/v6/Regions.py b/worlds/v6/Regions.py index 5a8f0315f44a..f6e9ee753890 100644 --- a/worlds/v6/Regions.py +++ b/worlds/v6/Regions.py @@ -31,14 +31,3 @@ def create_regions(world: MultiWorld, player: int): locWrp_names = ["Edge Games"] regWrp.locations += [V6Location(player, loc_name, location_table[loc_name], regWrp) for loc_name in locWrp_names] world.regions.append(regWrp) - - -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule): - sourceRegion = world.get_region(source, player) - targetRegion = world.get_region(target, player) - - connection = Entrance(player,'', sourceRegion) - connection.access_rule = rule - - sourceRegion.exits.append(connection) - connection.connect(targetRegion) \ No newline at end of file diff --git a/worlds/v6/Rules.py b/worlds/v6/Rules.py index ecb34f2f32ff..bf0d60499eb5 100644 --- a/worlds/v6/Rules.py +++ b/worlds/v6/Rules.py @@ -1,6 +1,6 @@ import typing from ..generic.Rules import add_rule -from .Regions import connect_regions, v6areas +from .Regions import v6areas def _has_trinket_range(state, player, start, end) -> bool: @@ -10,34 +10,36 @@ def _has_trinket_range(state, player, start, end) -> bool: return True -def set_rules(world, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]): +def set_rules(multiworld, options, player, area_connections: typing.Dict[int, int], area_cost_map: typing.Dict[int, int]): areashuffle = list(range(len(v6areas))) - if world.AreaRandomizer[player].value: - world.random.shuffle(areashuffle) + if options.area_rando: + multiworld.random.shuffle(areashuffle) area_connections.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)}) area_connections.update({0: 0}) - if world.AreaCostRandomizer[player].value: - world.random.shuffle(areashuffle) + if options.area_cost: + multiworld.random.shuffle(areashuffle) area_cost_map.update({(index + 1): (value + 1) for index, value in enumerate(areashuffle)}) area_cost_map.update({0: 0}) + menu_region = multiworld.get_region("Menu", player) for i in range(1, 5): - connect_regions(world, player, "Menu", v6areas[area_connections[i] - 1], - lambda state, i=i: _has_trinket_range(state, player, - world.DoorCost[player].value * (area_cost_map[i] - 1), - world.DoorCost[player].value * area_cost_map[i])) + target_region = multiworld.get_region(v6areas[area_connections[i] - 1], player) + menu_region.connect(connecting_region=target_region, + rule=lambda state, i=i: _has_trinket_range(state, player, + options.door_cost * (area_cost_map[i] - 1), + options.door_cost * area_cost_map[i])) # Special Rule for V - add_rule(world.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and + add_rule(multiworld.get_location("V", player), lambda state: state.can_reach("Laboratory", 'Region', player) and state.can_reach("The Tower", 'Region', player) and state.can_reach("Space Station 2", 'Region', player) and state.can_reach("Warp Zone", 'Region', player)) # Special Rule for NPC Trinket - add_rule(world.get_location("NPC Trinket", player), + add_rule(multiworld.get_location("NPC Trinket", player), lambda state: state.can_reach("Laboratory", 'Region', player) or (state.can_reach("The Tower", 'Region', player) and state.can_reach("Space Station 2", 'Region', player) and state.can_reach("Warp Zone", 'Region', player))) - world.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player) + multiworld.completion_condition[player] = lambda state: state.can_reach("V", 'Location', player) diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 6ff7fba60c2d..30a76f82cce6 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -2,7 +2,7 @@ import os, json from .Items import item_table, V6Item from .Locations import location_table, V6Location -from .Options import v6_options +from .Options import V6Options from .Rules import set_rules from .Regions import create_regions from BaseClasses import Item, ItemClassification, Tutorial @@ -41,7 +41,7 @@ class V6World(World): music_map: typing.Dict[int,int] - option_definitions = v6_options + options_dataclass = V6Options def create_regions(self): create_regions(self.multiworld, self.player) @@ -49,7 +49,7 @@ def create_regions(self): def set_rules(self): self.area_connections = {} self.area_cost_map = {} - set_rules(self.multiworld, self.player, self.area_connections, 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) @@ -61,7 +61,7 @@ def create_items(self): def generate_basic(self): musiclist_o = [1,2,3,4,9,12] musiclist_s = musiclist_o.copy() - if self.multiworld.MusicRandomizer[self.player].value: + if self.options.music_rando: self.multiworld.random.shuffle(musiclist_s) self.music_map = dict(zip(musiclist_o, musiclist_s)) @@ -69,10 +69,10 @@ def fill_slot_data(self): return { "MusicRando": self.music_map, "AreaRando": self.area_connections, - "DoorCost": self.multiworld.DoorCost[self.player].value, + "DoorCost": self.options.door_cost.value, "AreaCostRando": self.area_cost_map, - "DeathLink": self.multiworld.death_link[self.player].value, - "DeathLink_Amnesty": self.multiworld.DeathLinkAmnesty[self.player].value + "DeathLink": self.options.death_link.value, + "DeathLink_Amnesty": self.options.death_link_amnesty.value } def generate_output(self, output_directory: str): diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 750d6bd4ebec..e17464a0923a 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -16,6 +16,7 @@ Symbols: 72 - Colored Squares 80 - Arrows 200 - Progressive Dots - Dots,Full Dots +210 - Progressive Symmetry - Symmetry,Colored Dots 260 - Progressive Stars - Stars,Stars + Same Colored Symbol Useful: @@ -29,8 +30,9 @@ Filler: #503 - Energy Fill (Max) - 1 Traps: -600 - Slowness - 8 +600 - Slowness - 6 610 - Power Surge - 2 +615 - Bonk - 1 Jokes: 650 - Functioning Brain @@ -41,10 +43,13 @@ Doors: 1102 - Tutorial Outpost Exit (Panel) - 0x04CA4 1105 - Symmetry Island Lower (Panel) - 0x000B0 1107 - Symmetry Island Upper (Panel) - 0x1C349 +1108 - Desert Surface 3 Control (Panel) - 0x09FA0 +1109 - Desert Surface 8 Control (Panel) - 0x09F86 1110 - Desert Light Room Entry (Panel) - 0x0C339 1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B 1112 - Desert Light Control (Panel) - 0x09FAA 1113 - Desert Flood Room Entry (Panel) - 0x0A249 +1114 - Desert Elevator Room Hexagonal Control (Panel) - 0x0A015 1115 - Quarry Elevator Control (Panel) - 0x17CC4 1117 - Quarry Entry 1 (Panel) - 0x09E57 1118 - Quarry Entry 2 (Panel) - 0x17C09 @@ -69,6 +74,7 @@ Doors: 1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A 1169 - Town Windmill Entry (Panel) - 0x17F5F 1172 - Town Cargo Box Entry (Panel) - 0x0A0C8 +1173 - Town Desert Laser Redirect Control (Panel) - 0x09F98 1182 - Windmill Turn Control (Panel) - 0x17D02 1184 - Theater Entry (Panel) - 0x17F89 1185 - Theater Video Input (Panel) - 0x00815 @@ -231,10 +237,10 @@ Doors: 1984 - Caves Shortcuts - 0x2D859,0x2D73F 1987 - Tunnels Doors - 0x27739,0x27263,0x09E87,0x0348A -2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0A015,0x09FA0,0x09F86 2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675 2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA -2015 - Town Control Panels - 0x2896A,0x334D8 +2015 - Town Control Panels - 0x2896A,0x334D8,0x09F98 2020 - Windmill & Theater Control Panels - 0x17D02,0x00815 2025 - Bunker Control Panels - 0x34BC5,0x34BC6,0x0A079 2030 - Swamp Control Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07 @@ -242,7 +248,7 @@ Doors: 2100 - Symmetry Island Panels - 0x1C349,0x000B0 2101 - Tutorial Outpost Panels - 0x0A171,0x04CA4 -2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249 +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 2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA @@ -250,6 +256,7 @@ Doors: 2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 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 diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index acfbe8c14eb0..ec0922bec697 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +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 @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +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 diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index b1d9b8e30e40..056ae145c47e 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +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 @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +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 diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 779ead6bde4b..71af12f76dbb 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -209,12 +209,12 @@ Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: -158111 - 0x17C31 (Final Transparent) - True - True -158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True -158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent 1) - True - True -158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True -158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True +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 @@ -474,7 +474,7 @@ Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x2 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 -158707 - 0x09F98 (Desert Laser Redirect) - True - True +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True 158220 - 0x18590 (Transparent) - True - Symmetry 158221 - 0x28AE3 (Vines) - 0x18590 - True 158222 - 0x28938 (Apple Tree) - 0x28AE3 - True @@ -895,9 +895,9 @@ Mountainside Vault (Mountainside): Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True -158406 - 0x09F7F (Box Short) - 7 Lasers - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True 158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Black/White Squares -158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True +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 diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 6360c33aefb9..d99aab5cffbd 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -6,6 +6,7 @@ from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState from Options import PerGameCommonOptions, Toggle +from .presets import witness_option_presets from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ get_priority_hint_items, make_hints, generate_joke_hints from worlds.AutoWorld import World, WebWorld @@ -15,7 +16,7 @@ from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData from .regions import WitnessRegions from .rules import set_rules -from .Options import TheWitnessOptions +from .options import TheWitnessOptions from .utils import get_audio_logs from logging import warning, error @@ -31,6 +32,8 @@ class WitnessWebWorld(WebWorld): ["NewSoupVi", "Jarno"] )] + options_presets = witness_option_presets + class WitnessWorld(World): """ @@ -40,7 +43,6 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 14 StaticWitnessLogic() StaticWitnessLocations() @@ -88,10 +90,10 @@ def _get_slot_data(self): } def generate_early(self): - disabled_locations = self.multiworld.exclude_locations[self.player].value + disabled_locations = self.options.exclude_locations.value self.player_logic = WitnessPlayerLogic( - self, disabled_locations, self.multiworld.start_inventory[self.player].value + self, disabled_locations, self.options.start_inventory.value ) self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) @@ -102,14 +104,29 @@ def generate_early(self): self.log_ids_to_hints = dict() - if not (self.options.shuffle_symbols or self.options.shuffle_doors or self.options.shuffle_lasers): - if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" - f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't" - f" seem right.") - else: - raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any" - f" progression items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle.") + interacts_with_multiworld = ( + self.options.shuffle_symbols or + self.options.shuffle_doors or + self.options.shuffle_lasers == "anywhere" + ) + + has_progression = ( + interacts_with_multiworld + or self.options.shuffle_lasers == "local" + or self.options.shuffle_boat + or self.options.early_caves == "add_to_pool" + ) + + if not has_progression and self.multiworld.players == 1: + warning(f"{self.multiworld.get_player_name(self.player)}'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_with_multiworld and self.multiworld.players > 1: + raise Exception(f"{self.multiworld.get_player_name(self.player)}'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 non-local Laser Shuffle.") + + if self.options.shuffle_lasers == "local": + self.options.local_items.value |= self.item_name_groups["Lasers"] def create_regions(self): self.regio.create_regions(self, self.player_logic) @@ -144,7 +161,7 @@ def create_regions(self): early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] if early_items: random_early_item = self.random.choice(early_items) - if self.options.puzzle_randomization == 1: + 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: @@ -167,7 +184,7 @@ def create_regions(self): # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items needed_size = 3 - needed_size += self.options.puzzle_randomization == 1 + needed_size += self.options.puzzle_randomization == "sigma_expert" needed_size += self.options.shuffle_symbols needed_size += self.options.shuffle_doors > 0 @@ -176,7 +193,8 @@ def create_regions(self): extra_checks = [ ("First Hallway Room", "First Hallway Bend"), ("First Hallway", "First Hallway Straight"), - ("Desert Outside", "Desert Surface 3"), + ("Desert Outside", "Desert Surface 1"), + ("Desert Outside", "Desert Surface 2"), ] for i in range(num_early_locs, needed_size): @@ -253,7 +271,7 @@ def create_items(self): self.own_itempool += new_items self.multiworld.itempool += new_items if self.items.item_data[item_name].local_only: - self.multiworld.local_items[self.player].value.add(item_name) + self.options.local_items.value.add(item_name) def fill_slot_data(self) -> dict: hint_amount = self.options.hint_amount.value @@ -266,7 +284,7 @@ def fill_slot_data(self) -> dict: audio_logs = get_audio_logs().copy() - if hint_amount != 0: + if hint_amount: generated_hints = make_hints(self, hint_amount, self.own_itempool) self.random.shuffle(audio_logs) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index d238aa4adfb6..0354660b5ee0 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -75,6 +75,9 @@ "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.", "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.", "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.", + "Have you tried Final Fantasy Mystic Quest?\nApparently, it was made in an attempt to simplify Final Fantasy for the western market.\nThey were right, I suck at RPGs.", + "Have you tried Shivers?\nWitness 2 should totally feature a haunted Museum.", + "Have you tried Heretic?\nWait, there is a Doom Engine game where you can look UP AND DOWN???", "One day I was fascinated by the subject of generation of waves by wind.", "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", @@ -148,7 +151,7 @@ "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!", "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.", "How many of you have personally witnessed a total solar eclipse?", - "In the Treehouse area, you will find \n[Error: Data not found] progression items.", + "In the Treehouse area, you will find 69 progression items.\nNice.\n(Source: Just trust me)", "Lingo\nLingoing\nLingone", "The name of the captain was Albert Einstein.", "Panel impossible Sigma plz fix", @@ -173,22 +176,22 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]: wincon = world.options.victory_condition if discards: - if difficulty == 1: + if difficulty == "sigma_expert": always.append("Arrows") else: always.append("Triangles") - if wincon == 0: + if wincon == "elevator": always += ["Mountain Bottom Floor Final Room Entry (Door)", "Mountain Bottom Floor Doors"] - if wincon == 1: + if wincon == "challenge": always += ["Challenge Entry (Panel)", "Caves Panels"] return always -def get_always_hint_locations(_: "WitnessWorld") -> List[str]: - return [ +def get_always_hint_locations(world: "WitnessWorld") -> List[str]: + always = [ "Challenge Vault Box", "Mountain Bottom Floor Discard", "Theater Eclipse EP", @@ -196,6 +199,16 @@ def get_always_hint_locations(_: "WitnessWorld") -> List[str]: "Mountainside Cloud Cycle EP", ] + # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side + if world.options.EP_difficulty == "eclipse": + always.append("Town Obelisk Side 6") # Eclipse EP + + if world.options.EP_difficulty != "normal": + always.append("Treehouse Obelisk Side 4") # Couch EP + always.append("River Obelisk Side 1") # Cloud Cycle EP. Needs to be changed to "Mountainside Obelisk" soon + + return always + def get_priority_hint_items(world: "WitnessWorld") -> List[str]: priority = { @@ -217,9 +230,8 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: "Eraser", "Black/White Squares", "Colored Squares", - "Colored Dots", "Sound Dots", - "Symmetry" + "Progressive Symmetry" ] priority.update(world.random.sample(symbols, 5)) @@ -249,8 +261,8 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]: return sorted(priority) -def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: - return [ +def get_priority_hint_locations(world: "WitnessWorld") -> List[str]: + priority = [ "Swamp Purple Underwater", "Shipwreck Vault Box", "Town RGB Room Left", @@ -265,6 +277,13 @@ def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", ] + + # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side + if world.options.EP_difficulty != "normal": + priority.append("Town Obelisk Side 6") # Theater Flowers EP + priority.append("Treehouse Obelisk Side 4") # Shipwreck Green EP + + return priority def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): diff --git a/worlds/witness/items.py b/worlds/witness/items.py index a8c889de937a..41bc3c1bb8da 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -112,30 +112,12 @@ def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: Witn or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME } - # Adjust item classifications based on game settings. - eps_shuffled = self._world.options.shuffle_EPs - come_to_you = self._world.options.elevators_come_to_you - difficulty = self._world.options.puzzle_randomization + # Downgrade door items for item_name, item_data in self.item_data.items(): - if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", - "Monastery Shortcuts", - "Quarry Boathouse Hook Control (Panel)", - "Windmill Turn Control (Panel)"}: - # Downgrade doors that only gate progress in EP shuffle. - item_data.classification = ItemClassification.useful - elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)", - "Swamp Long Bridge (Panel)"}: - # These Bridges/Elevators are not logical access because they may leave you stuck. - item_data.classification = ItemClassification.useful - elif item_name in {"River Monastery Garden Shortcut (Door)", - "Monastery Laser Shortcut (Door)", - "Orchard Second Gate (Door)", - "Jungle Bamboo Laser Shortcut (Door)", - "Caves Elevator Controls (Panel)"}: - # Downgrade doors that don't gate progress. - item_data.classification = ItemClassification.useful - elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled): - # PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary + if not isinstance(item_data.definition, DoorItemDefinition): + continue + + if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes): item_data.classification = ItemClassification.useful # Build the mandatory item list. @@ -228,7 +210,7 @@ def get_early_items(self) -> List[str]: output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"} if self._world.options.shuffle_discarded_panels: - if self._world.options.puzzle_randomization == 1: + if self._world.options.puzzle_randomization == "sigma_expert": output.add("Arrows") else: output.add("Triangles") diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index d20be2794056..781cc4e25d94 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -55,8 +55,8 @@ class StaticWitnessLocations: "Desert Light Room 3", "Desert Pond Room 5", "Desert Flood Room 6", - "Desert Final Hexagonal", - "Desert Final Bent 3", + "Desert Elevator Room Hexagonal", + "Desert Elevator Room Bent 3", "Desert Laser Panel", "Quarry Entry 1 Panel", @@ -509,9 +509,9 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): if world.options.shuffle_vault_boxes: self.PANEL_TYPES_TO_SHUFFLE.add("Vault") - if world.options.shuffle_EPs == 1: + if world.options.shuffle_EPs == "individual": self.PANEL_TYPES_TO_SHUFFLE.add("EP") - elif world.options.shuffle_EPs == 2: + elif world.options.shuffle_EPs == "obelisk_sides": self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side") for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES: @@ -543,7 +543,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): ) event_locations = { - p for p in player_logic.EVENT_PANELS + p for p in player_logic.USED_EVENT_NAMES_BY_HEX } self.EVENT_LOCATION_TABLE = { diff --git a/worlds/witness/Options.py b/worlds/witness/options.py similarity index 82% rename from worlds/witness/Options.py rename to worlds/witness/options.py index 4c4b4f76267f..ac1f2bc82830 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/options.py @@ -28,11 +28,14 @@ class ShuffleSymbols(DefaultOnToggle): display_name = "Shuffle Symbols" -class ShuffleLasers(Toggle): +class ShuffleLasers(Choice): """If on, the 11 lasers are turned into items and will activate on their own upon receiving them. Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still be redirected as normal, for both applications of redirection.""" display_name = "Shuffle Lasers" + option_off = 0 + option_local = 1 + option_anywhere = 2 class ShuffleDoors(Choice): @@ -114,9 +117,13 @@ class ShufflePostgame(Toggle): class VictoryCondition(Choice): - """Change the victory condition from the original game's ending (elevator) to beating the Challenge - or solving the mountaintop box, either using the short solution - (7 lasers or whatever you've changed it to) or the long solution (11 lasers or whatever you've changed it to).""" + """Set the victory condition for this world. + Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers). + 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). + 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.""" display_name = "Victory Condition" option_elevator = 0 option_challenge = 1 @@ -133,10 +140,13 @@ class PuzzleRandomization(Choice): class MountainLasers(Range): - """Sets the amount of beams required to enter the final area.""" + """Sets the amount of lasers required to enter the Mountain. + If set to a higher amount than 7, the mountaintop box will be slightly rotated to make it possible to solve without + the hatch being opened. + This change will also be applied logically to the long solution ("Challenge Lasers" setting).""" display_name = "Required Lasers for Mountain Entry" range_start = 1 - range_end = 7 + range_end = 11 default = 7 @@ -182,10 +192,19 @@ class HintAmount(Range): class DeathLink(Toggle): """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies. - The effect of a "death" in The Witness is a Power Surge.""" + The effect of a "death" in The Witness is a Bonk Trap.""" display_name = "Death Link" +class DeathLinkAmnesty(Range): + """Number of panel fails to allow before sending a death through Death Link. + 0 means every panel fail will send a death, 1 means every other panel fail will send a death, etc.""" + display_name = "Death Link Amnesty" + range_start = 0 + range_end = 5 + default = 1 + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -209,3 +228,4 @@ class TheWitnessOptions(PerGameCommonOptions): puzzle_skip_amount: PuzzleSkipAmount hint_amount: HintAmount death_link: DeathLink + death_link_amnesty: DeathLinkAmnesty diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index e1ef1ae4319e..229da0a2879a 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -101,9 +101,13 @@ def reduce_req_within_region(self, panel_hex: str) -> FrozenSet[FrozenSet[str]]: for option_entity in option: dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) - if option_entity in self.EVENT_NAMES_BY_HEX: + if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: new_items = frozenset({frozenset([option_entity])}) - elif option_entity in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: + elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS: + new_items = frozenset({frozenset([option_entity])}) + self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)] + elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", + "PP2 Weirdness", "Theater to Tunnels"}: new_items = frozenset({frozenset([option_entity])}) else: new_items = self.reduce_req_within_region(option_entity) @@ -169,14 +173,11 @@ def make_single_adjustment(self, adj_type: str, line: str): if adj_type == "Event Items": line_split = line.split(" - ") new_event_name = line_split[0] - hex_set = line_split[1].split(",") - - for entity, event_name in self.EVENT_NAMES_BY_HEX.items(): - if event_name == new_event_name: - self.DONT_MAKE_EVENTS.add(entity) + entity_hex = line_split[1] + dependent_hex_set = line_split[2].split(",") - for hex_code in hex_set: - self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name + for dependent_hex in dependent_hex_set: + self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name return @@ -252,51 +253,101 @@ def make_single_adjustment(self, adj_type: str, line: str): line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"] self.ADDED_CHECKS.add(line) + @staticmethod + def handle_postgame(world: "WitnessWorld"): + # In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled. + # This has a lot of complicated considerations, which I'll try my best to explain. + postgame_adjustments = [] + + # Make some quick references to some options + doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications. + early_caves = world.options.early_caves + victory = world.options.victory_condition + mnt_lasers = world.options.mountain_lasers + chal_lasers = world.options.challenge_lasers + + # Goal is "short box" but short box requires more lasers than long box + reverse_shortbox_goal = victory == "mountain_box_short" and mnt_lasers > chal_lasers + + # Goal is "short box", and long box requires at least as many lasers as short box (as god intended) + proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers + + # Goal is "long box", but short box requires at least as many lasers than long box. + reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers + + # If goal is shortbox or "reverse longbox", you will never enter the mountain from the top before winning. + mountain_enterable_from_top = not (victory == "mountain_box_short" or reverse_longbox_goal) + + # Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game" + # This is technically imprecise, but it matches player expectations better. + if not (early_caves or doors): + postgame_adjustments.append(get_caves_exclusion_list()) + postgame_adjustments.append(get_beyond_challenge_exclusion_list()) + + # If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself + if not victory == "challenge": + postgame_adjustments.append(get_path_to_challenge_exclusion_list()) + postgame_adjustments.append(get_challenge_vault_box_exclusion_list()) + + # Challenge can only have something if the goal is not challenge or longbox itself. + # In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers. + # In that case, it'd also have to be a doors mode, but that's already covered by the previous block. + if not (victory == "elevator" or reverse_shortbox_goal): + postgame_adjustments.append(get_beyond_challenge_exclusion_list()) + if not victory == "challenge": + postgame_adjustments.append(get_challenge_vault_box_exclusion_list()) + + # Mountain can't be reached if the goal is shortbox (or "reverse long box") + if not mountain_enterable_from_top: + postgame_adjustments.append(get_mountain_upper_exclusion_list()) + + # Same goes for lower mountain, but that one *can* be reached in remote doors modes. + if not doors: + postgame_adjustments.append(get_mountain_lower_exclusion_list()) + + # The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard) + # In Elevator Goal, it is definitionally in the post-game, unless remote doors is played. + # In Challenge Goal, it is before the Challenge, so it is not post-game. + # In Short Box Goal, you can win before turning it on, UNLESS Short Box requires MORE lasers than long box. + # In Long Box Goal, it is always in the post-game because solving long box is what turns it on. + if not ((victory == "elevator" and doors) or victory == "challenge" or (reverse_shortbox_goal and doors)): + # We now know Bottom Floor Discard is in the post-game. + # This has different consequences depending on whether remote doors is being played. + # If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well. + if doors: + postgame_adjustments.append(get_bottom_floor_discard_exclusion_list()) + else: + postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list()) + + # In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard, + # including the Caves Shortcuts themselves if playing "early_caves: start_inventory". + # This is another thing that was deemed "unfun" more than fitting the actual definition of post-game. + if victory == "challenge" and early_caves and not doors: + postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list()) + + # If we have a proper short box goal, long box will never be activated first. + if proper_shortbox_goal: + postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + + return postgame_adjustments + def make_options_adjustments(self, world: "WitnessWorld"): """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] - # Postgame + # Make condensed references to some options - doors = world.options.shuffle_doors >= 2 + doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications. lasers = world.options.shuffle_lasers - early_caves = world.options.early_caves > 0 victory = world.options.victory_condition mnt_lasers = world.options.mountain_lasers chal_lasers = world.options.challenge_lasers - mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mnt_lasers) - + # Exclude panels from the post-game if shuffle_postgame is false. if not world.options.shuffle_postgame: - if not (early_caves or doors): - adjustment_linesets_in_order.append(get_caves_exclusion_list()) - if not victory == 1: - adjustment_linesets_in_order.append(get_path_to_challenge_exclusion_list()) - adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) - adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) - - if not ((doors or early_caves) and (victory == 0 or (victory == 2 and mnt_lasers > chal_lasers))): - adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) - if not victory == 1: - adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) - - if not (doors or mountain_enterable_from_top): - adjustment_linesets_in_order.append(get_mountain_lower_exclusion_list()) - - if not mountain_enterable_from_top: - adjustment_linesets_in_order.append(get_mountain_upper_exclusion_list()) - - if not ((victory == 0 and doors) or victory == 1 or (victory == 2 and mnt_lasers > chal_lasers and doors)): - if doors: - adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) - else: - adjustment_linesets_in_order.append(get_bottom_floor_discard_nondoors_exclusion_list()) - - if victory == 2 and chal_lasers >= mnt_lasers: - adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + adjustment_linesets_in_order += self.handle_postgame(world) # Exclude Discards / Vaults - if not world.options.shuffle_discarded_panels: # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both # (remote) doors and lasers are shuffled. @@ -308,21 +359,24 @@ def make_options_adjustments(self, world: "WitnessWorld"): if not world.options.shuffle_vault_boxes: adjustment_linesets_in_order.append(get_vault_exclusion_list()) - if not victory == 1: + if not victory == "challenge": adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) # Victory Condition - if victory == 0: + if victory == "elevator": self.VICTORY_LOCATION = "0x3D9A9" - elif victory == 1: + elif victory == "challenge": self.VICTORY_LOCATION = "0x0356B" - elif victory == 2: + elif victory == "mountain_box_short": self.VICTORY_LOCATION = "0x09F7F" - elif victory == 3: + elif victory == "mountain_box_long": self.VICTORY_LOCATION = "0xFFF00" - if chal_lasers <= 7: + # Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less + # (challenge_lasers <= 7), you can now solve it without opening Mountain Entry first. + # Furthermore, if the user sets mountain_lasers > 7, the box is rotated to not require Mountain Entry either. + if chal_lasers <= 7 or mnt_lasers > 7: adjustment_linesets_in_order.append([ "Requirement Changes:", "0xFFF00 - 11 Lasers - True", @@ -334,36 +388,36 @@ def make_options_adjustments(self, world: "WitnessWorld"): if world.options.shuffle_symbols: adjustment_linesets_in_order.append(get_symbol_shuffle_list()) - if world.options.EP_difficulty == 0: + if world.options.EP_difficulty == "normal": adjustment_linesets_in_order.append(get_ep_easy()) - elif world.options.EP_difficulty == 1: + elif world.options.EP_difficulty == "tedious": adjustment_linesets_in_order.append(get_ep_no_eclipse()) - if world.options.door_groupings == 1: - if world.options.shuffle_doors == 1: + if world.options.door_groupings == "regional": + if world.options.shuffle_doors == "panels": adjustment_linesets_in_order.append(get_simple_panels()) - elif world.options.shuffle_doors == 2: + elif world.options.shuffle_doors == "doors": adjustment_linesets_in_order.append(get_simple_doors()) - elif world.options.shuffle_doors == 3: + elif world.options.shuffle_doors == "mixed": adjustment_linesets_in_order.append(get_simple_doors()) adjustment_linesets_in_order.append(get_simple_additional_panels()) else: - if world.options.shuffle_doors == 1: + if world.options.shuffle_doors == "panels": adjustment_linesets_in_order.append(get_complex_door_panels()) adjustment_linesets_in_order.append(get_complex_additional_panels()) - elif world.options.shuffle_doors == 2: + elif world.options.shuffle_doors == "doors": adjustment_linesets_in_order.append(get_complex_doors()) - elif world.options.shuffle_doors == 3: + elif world.options.shuffle_doors == "mixed": adjustment_linesets_in_order.append(get_complex_doors()) adjustment_linesets_in_order.append(get_complex_additional_panels()) if world.options.shuffle_boat: adjustment_linesets_in_order.append(get_boat()) - if world.options.early_caves == 2: + if world.options.early_caves == "starting_inventory": adjustment_linesets_in_order.append(get_early_caves_start_list()) - if world.options.early_caves == 1 and not doors: + if world.options.early_caves == "add_to_pool" and not doors: adjustment_linesets_in_order.append(get_early_caves_list()) if world.options.elevators_come_to_you: @@ -383,29 +437,25 @@ def make_options_adjustments(self, world: "WitnessWorld"): obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]] obelisk_name = obelisk["checkName"] ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] - self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" + self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" else: adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) - if world.options.shuffle_EPs == 0: + if not world.options.shuffle_EPs: adjustment_linesets_in_order.append(["Irrelevant Locations:"] + get_ep_all_individual()[1:]) - yaml_disabled_eps = [] - for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS: if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: continue loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location] - if loc_obj["entityType"] == "EP" and world.options.shuffle_EPs != 0: - yaml_disabled_eps.append(loc_obj["entity_hex"]) + if loc_obj["entityType"] == "EP": + self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"]) - if loc_obj["entityType"] in {"EP", "General", "Vault", "Discard"}: + elif loc_obj["entityType"] in {"General", "Vault", "Discard"}: self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"]) - adjustment_linesets_in_order.append(["Disabled Locations:"] + yaml_disabled_eps) - for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -455,7 +505,8 @@ def make_dependency_reduced_checklist(self): for option in connection[1]: individual_entity_requirements = [] for entity in option: - if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: + if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX + or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX): individual_entity_requirements.append(frozenset({frozenset({entity})})) else: entity_req = self.reduce_req_within_region(entity) @@ -472,6 +523,72 @@ def make_dependency_reduced_checklist(self): self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + def solvability_guaranteed(self, entity_hex: str): + return not ( + entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY + or entity_hex in self.COMPLETELY_DISABLED_ENTITIES + or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES + ) + + def determine_unrequired_entities(self, world: "WitnessWorld"): + """Figure out which major items are actually useless in this world's settings""" + + # Gather quick references to relevant options + eps_shuffled = world.options.shuffle_EPs + come_to_you = world.options.elevators_come_to_you + difficulty = world.options.puzzle_randomization + discards_shuffled = world.options.shuffle_discarded_panels + boat_shuffled = world.options.shuffle_boat + symbols_shuffled = world.options.shuffle_symbols + disable_non_randomized = world.options.disable_non_randomized_puzzles + postgame_included = world.options.shuffle_postgame + goal = world.options.victory_condition + doors = world.options.shuffle_doors + shortbox_req = world.options.mountain_lasers + longbox_req = world.options.challenge_lasers + + # Make some helper booleans so it is easier to follow what's going on + mountain_upper_is_in_postgame = ( + goal == "mountain_box_short" + or goal == "mountain_box_long" and longbox_req <= shortbox_req + ) + mountain_upper_included = postgame_included or not mountain_upper_is_in_postgame + remote_doors = doors >= 2 + door_panels = doors == "panels" or doors == "mixed" + + # It is easier to think about when these items *are* required, so we make that dict first + # If the entity is disabled anyway, we don't need to consider that case + is_item_required_dict = { + "0x03750": eps_shuffled, # Monastery Garden Entry Door + "0x275FA": eps_shuffled, # Boathouse Hook Control + "0x17D02": eps_shuffled, # Windmill Turn Control + "0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door + "0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier + "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 + "0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel + "0x3873B": False, # Jungle Bamboo Laser Shortcut Door + "0x335AB": False, # Caves Elevator Controls + "0x335AC": False, # Caves Elevator Controls + "0x3369D": False, # Caves Elevator Controls + "0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2 + "0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door + "0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel + "0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door + "0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID, + # Jungle Popup Wall Panel + } + + # 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, panel: str): """ Makes a pair of an event panel and its event item @@ -479,21 +596,23 @@ def make_event_item_pair(self, panel: str): action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action - if panel not in self.EVENT_NAMES_BY_HEX: + if panel not in self.USED_EVENT_NAMES_BY_HEX: warning("Panel \"" + name + "\" does not have an associated event name.") - self.EVENT_NAMES_BY_HEX[panel] = name + " Event" - pair = (name, self.EVENT_NAMES_BY_HEX[panel]) + self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event" + pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel]) return pair def make_event_panel_lists(self): - self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" + self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): - if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: - continue - self.EVENT_PANELS.add(event_hex) + self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) - for panel in self.EVENT_PANELS: + self.USED_EVENT_NAMES_BY_HEX = { + event_hex: event_name for event_hex, event_name 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] @@ -506,6 +625,8 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() + self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS_NO_MULTI = set() self.MULTI_AMOUNTS = defaultdict(lambda: 1) @@ -515,13 +636,13 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} self.STARTING_INVENTORY = set() - self.DIFFICULTY = world.options.puzzle_randomization.value + self.DIFFICULTY = world.options.puzzle_randomization - if self.DIFFICULTY == 0: + if self.DIFFICULTY == "sigma_normal": self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal - elif self.DIFFICULTY == 1: + elif self.DIFFICULTY == "sigma_expert": self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert - elif self.DIFFICULTY == 2: + elif self.DIFFICULTY == "none": self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) @@ -530,16 +651,14 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in # Determining which panels need to be events is a difficult process. # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. - self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() - self.DONT_MAKE_EVENTS = set() self.COMPLETELY_DISABLED_ENTITIES = set() self.PRECOMPLETED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set() self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" - self.EVENT_NAMES_BY_HEX = { + self.ALWAYS_EVENT_NAMES_BY_HEX = { "0x00509": "+1 Laser (Symmetry Laser)", "0x012FB": "+1 Laser (Desert Laser)", "0x09F98": "Desert Laser Redirection", @@ -552,10 +671,14 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in "0x0C2B2": "+1 Laser (Bunker Laser)", "0x00BF6": "+1 Laser (Swamp Laser)", "0x028A4": "+1 Laser (Treehouse Laser)", - "0x09F7F": "Mountain Entry", + "0x17C34": "Mountain Entry", "0xFFF00": "Bottom Floor Discard Turns On", } + self.USED_EVENT_NAMES_BY_HEX = {} + self.CONDITIONAL_EVENTS = {} + self.make_options_adjustments(world) + self.determine_unrequired_entities(world) self.make_dependency_reduced_checklist() self.make_event_panel_lists() diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py new file mode 100644 index 000000000000..1fee1a7968b2 --- /dev/null +++ b/worlds/witness/presets.py @@ -0,0 +1,101 @@ +from typing import Any, Dict + +from .options import * + +witness_option_presets: Dict[str, Dict[str, Any]] = { + # Great for short syncs & scratching that "speedrun with light routing elements" itch. + "Short & Dense": { + "progression_balancing": 30, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": False, + "shuffle_doors": ShuffleDoors.option_panels, + "door_groupings": DoorGroupings.option_off, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_local, + + "disable_non_randomized_puzzles": True, + "shuffle_discarded_panels": False, + "shuffle_vault_boxes": False, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_off, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_mountain_box_short, + "mountain_lasers": 7, + "challenge_lasers": 11, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": False, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": PuzzleSkipAmount.default, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, + + # For relative beginners who want to move to the next step. + "Advanced, But Chill": { + "progression_balancing": 30, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": True, + "shuffle_doors": ShuffleDoors.option_doors, + "door_groupings": DoorGroupings.option_regional, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_off, + + "disable_non_randomized_puzzles": False, + "shuffle_discarded_panels": True, + "shuffle_vault_boxes": True, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_obelisk_sides, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_mountain_box_long, + "mountain_lasers": 6, + "challenge_lasers": 9, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": False, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": 15, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, + + # Allsanity but without the BS (no expert, no tedious EPs). + "Nice Allsanity": { + "progression_balancing": 50, + + "puzzle_randomization": PuzzleRandomization.option_sigma_normal, + + "shuffle_symbols": True, + "shuffle_doors": ShuffleDoors.option_mixed, + "door_groupings": DoorGroupings.option_off, + "shuffle_boat": True, + "shuffle_lasers": ShuffleLasers.option_anywhere, + + "disable_non_randomized_puzzles": False, + "shuffle_discarded_panels": True, + "shuffle_vault_boxes": True, + "shuffle_EPs": ShuffleEnvironmentalPuzzles.option_individual, + "EP_difficulty": EnvironmentalPuzzlesDifficulty.option_normal, + "shuffle_postgame": False, + + "victory_condition": VictoryCondition.option_challenge, + "mountain_lasers": 6, + "challenge_lasers": 9, + + "early_caves": EarlyCaves.option_off, + "elevators_come_to_you": True, + + "trap_percentage": TrapPercentage.default, + "puzzle_skip_amount": 15, + "hint_amount": HintAmount.default, + "death_link": DeathLink.default, + }, +} diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index e09702480515..3a1a1781b77e 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -134,13 +134,13 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic world.multiworld.regions += final_regions_list def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"): - difficulty = world.options.puzzle_randomization.value + difficulty = world.options.puzzle_randomization - if difficulty == 0: + if difficulty == "sigma_normal": self.reference_logic = StaticWitnessLogic.sigma_normal - elif difficulty == 1: + elif difficulty == "sigma_expert": self.reference_logic = StaticWitnessLogic.sigma_expert - elif difficulty == 2: + elif difficulty == "none": self.reference_logic = StaticWitnessLogic.vanilla self.locat = locat diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 75c662ac0f26..8636829a4ef1 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -29,8 +29,9 @@ ] -def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[CollectionState], bool]: - if laser_hex == "0x012FB": +def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, + redirect_required: bool) -> Callable[[CollectionState], bool]: + if laser_hex == "0x012FB" and redirect_required: return lambda state: ( _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state) and state.has("Desert Laser Redirection", player) @@ -39,11 +40,11 @@ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[ return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat) -def _has_lasers(amount: int, world: "WitnessWorld") -> Callable[[CollectionState], bool]: +def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> Callable[[CollectionState], bool]: laser_lambdas = [] for laser_hex in laser_hexes: - has_laser_lambda = _has_laser(laser_hex, world, world.player) + has_laser_lambda = _has_laser(laser_hex, world, world.player, redirect_required) laser_lambdas.append(has_laser_lambda) @@ -155,15 +156,21 @@ def _has_item(item: str, world: "WitnessWorld", player: int, return lambda state: state.can_reach(item, "Region", player) if item == "7 Lasers": laser_req = world.options.mountain_lasers.value - return _has_lasers(laser_req, world) + return _has_lasers(laser_req, world, False) + if item == "7 Lasers + Redirect": + laser_req = world.options.mountain_lasers.value + return _has_lasers(laser_req, world, True) if item == "11 Lasers": laser_req = world.options.challenge_lasers.value - return _has_lasers(laser_req, world) + return _has_lasers(laser_req, world, False) + if item == "11 Lasers + Redirect": + laser_req = world.options.challenge_lasers.value + return _has_lasers(laser_req, world, True) elif item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) elif item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - if item in player_logic.EVENT_PANELS: + if item in player_logic.USED_EVENT_NAMES_BY_HEX: return _can_solve_panel(item, world, player, player_logic, locat) prog_item = StaticWitnessLogic.get_parent_progressive_item(item) diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt index 79bda7ea2281..b84370908524 100644 --- a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt +++ b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt @@ -1,4 +1,7 @@ Items: +Desert Surface 3 Control (Panel) +Desert Surface 8 Control (Panel) +Desert Elevator Room Hexagonal Control (Panel) Desert Flood Controls (Panel) Desert Light Control (Panel) Quarry Elevator Control (Panel) @@ -10,6 +13,7 @@ Quarry Boathouse Hook Control (Panel) Monastery Shutters Control (Panel) Town Maze Rooftop Bridge (Panel) Town RGB Control (Panel) +Town Desert Laser Redirect Control (Panel) Windmill Turn Control (Panel) Theater Video Input (Panel) Bunker Drop-Down Door Controls (Panel) diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt index 79da154491b7..42258bca1a47 100644 --- a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt +++ b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt @@ -10,7 +10,7 @@ Monastery Panels Town Church & RGB House Panels Town Maze Panels Windmill & Theater Panels -Town Cargo Box Entry (Panel) +Town Dockside House Panels Treehouse Panels Bunker Panels Swamp Panels diff --git a/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt index 2419bde06c14..09c366cfaabd 100644 --- a/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt @@ -1,9 +1,9 @@ Event Items: -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 -Bunker Laser Activation - 0x00061,0x17D01,0x17C42 -Shadows Laser Activation - 0x00021,0x17D28,0x17C71 -Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7 -Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB +Monastery Laser Activation - 0x17C65 - 0x00A5B,0x17CE7,0x17FA9 +Bunker Laser Activation - 0x0C2B2 - 0x00061,0x17D01,0x17C42 +Shadows Laser Activation - 0x181B3 - 0x00021,0x17D28,0x17C71 +Town Tower 4th Door Opens - 0x2779A - 0x17CFB,0x3C12B,0x17CF7 +Jungle Popup Wall Lifts - 0x1475B - 0x17FA0,0x17D27,0x17F9B,0x17CAB Requirement Changes: 0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 diff --git a/worlds/witness/settings/Symbol_Shuffle.txt b/worlds/witness/settings/Symbol_Shuffle.txt index 3d0342f5e2a9..253fe98bad42 100644 --- a/worlds/witness/settings/Symbol_Shuffle.txt +++ b/worlds/witness/settings/Symbol_Shuffle.txt @@ -1,9 +1,8 @@ Items: Arrows Progressive Dots -Colored Dots Sound Dots -Symmetry +Progressive Symmetry Triangles Eraser Shapers diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 29c171d45c33..0e8d649af6ff 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -109,7 +109,6 @@ def read_logic_file(self, lines): "Laser", "Laser Hedges", "Laser Pressure Plates", - "Desert Laser Redirect" } is_vault_or_video = "Vault" in entity_name or "Video" in entity_name diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 3f441d12ab34..d30bef144464 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import settings import threading import typing -from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast +from typing import Any, Dict, List, Set, Tuple, Optional, cast import os import logging @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionOptions, ZillionStartChar, validate +from .options import ZillionOptions, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -225,7 +225,7 @@ def access_rule_wrapped(zz_loc_local: ZzLocation, loc.access_rule = access_rule if not (limited_skill >= zz_loc.req): loc.progress_type = LocationProgressType.EXCLUDED - self.multiworld.exclude_locations[p].value.add(loc.name) + self.options.exclude_locations.value.add(loc.name) here.locations.append(loc) self.my_locations.append(loc) @@ -288,15 +288,15 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: if group["game"] == "Zillion": assert "item_pool" in group item_pool = group["item_pool"] - to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ" + to_stay: Chars = "JJ" if "JJ" in item_pool: assert "players" in group group_players = group["players"] - start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) - players_start_chars = [ - (player, start_chars[player].current_option_name) - for player in group_players - ] + players_start_chars: List[Tuple[int, Chars]] = [] + for player in group_players: + z_world = multiworld.worlds[player] + assert isinstance(z_world, ZillionWorld) + players_start_chars.append((player, z_world.options.start_char.get_char())) start_char_counts = Counter(sc for _, sc in players_start_chars) # majority rules if start_char_counts["Apple"] > start_char_counts["Champ"]: @@ -304,7 +304,7 @@ 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[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ") + choices: Tuple[Chars, ...] = ("Apple", "Champ") to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index ac73f6db50c8..b10507aaf885 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -16,7 +16,7 @@ from zilliandomizer.options import Chars from zilliandomizer.patch import RescueInfo -from .id_maps import make_id_to_others +from .id_maps import loc_name_to_id, make_id_to_others from .config import base_id, zillion_map @@ -323,6 +323,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": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} ])) self.finished_game = True diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 305546c78b62..dcbc6131f1a9 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,9 +1,11 @@ -from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter +from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter + from BaseClasses import CollectionState + +from zilliandomizer.logic_components.items import Item, items from zilliandomizer.logic_components.locations import Location from zilliandomizer.randomizer import Randomizer -from zilliandomizer.logic_components.items import Item, items -from .region import ZillionLocation + from .item import ZillionItem from .id_maps import item_name_to_id @@ -18,11 +20,12 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: returns a hash of the player and of the set locations with their items """ + from . import ZillionWorld z_world = cs.multiworld.worlds[p] - my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations")) + assert isinstance(z_world, ZillionWorld) _hash = p - for z_loc in my_locations: + for z_loc in z_world.my_locations: zz_name = z_loc.zz_loc.name zz_item = z_loc.item.zz_item \ if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \ diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index cb861e962128..97f8b817f77c 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,14 @@ from collections import Counter from dataclasses import dataclass -from typing import Dict, Tuple +from typing import ClassVar, Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice -from zilliandomizer.options import \ - Options as ZzOptions, char_to_gun, char_to_jump, ID, \ - VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts +from zilliandomizer.options import ( + Options as ZzOptions, char_to_gun, char_to_jump, ID, + VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts +) from zilliandomizer.options.parsing import validate as zz_validate @@ -107,6 +108,15 @@ class ZillionStartChar(Choice): display_name = "start character" default = "random" + _name_capitalization: ClassVar[Dict[int, Chars]] = { + option_jj: "JJ", + option_apple: "Apple", + option_champ: "Champ", + } + + def get_char(self) -> Chars: + return ZillionStartChar._name_capitalization[self.value] + class ZillionIDCardCount(Range): """ @@ -348,16 +358,6 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met - name_capitalization: Dict[str, Chars] = { - "jj": "JJ", - "apple": "Apple", - "champ": "Champ", - } - - start_char = options.start_char - start_char_name = name_capitalization[start_char.current_key] - assert start_char_name in chars - starting_cards = options.starting_cards room_gen = options.room_gen @@ -371,7 +371,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": max_level.value, False, # tutorial skill, - start_char_name, + options.start_char.get_char(), floppy_req.value, options.continues.value, bool(options.randomize_alarms.value), diff --git a/worlds_disabled/README.md b/worlds_disabled/README.md index b891bc71d4ba..a7bffe222b14 100644 --- a/worlds_disabled/README.md +++ b/worlds_disabled/README.md @@ -3,3 +3,11 @@ This folder is for already merged worlds that are unmaintained and currently broken. If you are interested in fixing and stepping up as maintainer for any of these worlds, please review the [world maintainer](/docs/world%20maintainer.md) documentation. + +## Information for Disabled Worlds + +For each disabled world, a README file can be found detailing when the world was disabled and the reasons that it +was disabled. In order to be considered for reactivation, these concerns should be handled at a bare minimum. However, +each world may have additional issues that also need to be handled, such as deprecated API calls or missing components. + + diff --git a/worlds_disabled/oribf/README.md b/worlds_disabled/oribf/README.md new file mode 100644 index 000000000000..0c78c23bea0d --- /dev/null +++ b/worlds_disabled/oribf/README.md @@ -0,0 +1,7 @@ +### Ori and the Blind Forest + +This world was disabled for the following reasons: + +* Missing client +* Unmaintained +* Outdated, fails tests as of Jun 29, 2023