From cf0ae5e31b3a0f8b61849bacea97b3bef508e701 Mon Sep 17 00:00:00 2001 From: Jonathan Tan Date: Sat, 22 Mar 2025 19:42:17 -0400 Subject: [PATCH] The Wind Waker: Implement New Game (#4458) Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there. --- .gitignore | 1 + README.md | 1 + docs/CODEOWNERS | 3 + worlds/tww/Items.py | 373 ++++++ worlds/tww/Locations.py | 1272 +++++++++++++++++++ worlds/tww/Macros.py | 1114 +++++++++++++++++ worlds/tww/Options.py | 854 +++++++++++++ worlds/tww/Presets.py | 138 +++ worlds/tww/Rules.py | 1414 ++++++++++++++++++++++ worlds/tww/TWWClient.py | 739 +++++++++++ worlds/tww/__init__.py | 598 +++++++++ worlds/tww/assets/icon.ico | Bin 0 -> 220863 bytes worlds/tww/assets/icon.png | Bin 0 -> 4717 bytes worlds/tww/docs/en_The Wind Waker.md | 120 ++ worlds/tww/docs/setup_en.md | 67 + worlds/tww/randomizers/Charts.py | 125 ++ worlds/tww/randomizers/Dungeons.py | 284 +++++ worlds/tww/randomizers/Entrances.py | 878 ++++++++++++++ worlds/tww/randomizers/ItemPool.py | 205 ++++ worlds/tww/randomizers/RequiredBosses.py | 121 ++ worlds/tww/requirements.txt | 1 + 21 files changed, 8308 insertions(+) create mode 100644 worlds/tww/Items.py create mode 100644 worlds/tww/Locations.py create mode 100644 worlds/tww/Macros.py create mode 100644 worlds/tww/Options.py create mode 100644 worlds/tww/Presets.py create mode 100644 worlds/tww/Rules.py create mode 100644 worlds/tww/TWWClient.py create mode 100644 worlds/tww/__init__.py create mode 100644 worlds/tww/assets/icon.ico create mode 100644 worlds/tww/assets/icon.png create mode 100644 worlds/tww/docs/en_The Wind Waker.md create mode 100644 worlds/tww/docs/setup_en.md create mode 100644 worlds/tww/randomizers/Charts.py create mode 100644 worlds/tww/randomizers/Dungeons.py create mode 100644 worlds/tww/randomizers/Entrances.py create mode 100644 worlds/tww/randomizers/ItemPool.py create mode 100644 worlds/tww/randomizers/RequiredBosses.py create mode 100644 worlds/tww/requirements.txt diff --git a/.gitignore b/.gitignore index 5da42dc1..f50fc17e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.apmc *.apz5 *.aptloz +*.aptww *.apemerald *.pyc *.pyd diff --git a/README.md b/README.md index d119d560..5e14ef5d 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Currently, the following games are supported: * Castlevania: Circle of the Moon * Inscryption * Civilization VI +* The Legend of Zelda: The Wind Waker 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/docs/CODEOWNERS b/docs/CODEOWNERS index ac889213..29de6bbf 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -214,6 +214,9 @@ # Wargroove /worlds/wargroove/ @FlySniper +# The Wind Waker +/worlds/tww/ @tanjo3 + # The Witness /worlds/witness/ @NewSoupVi @blastron diff --git a/worlds/tww/Items.py b/worlds/tww/Items.py new file mode 100644 index 00000000..1e310bf2 --- /dev/null +++ b/worlds/tww/Items.py @@ -0,0 +1,373 @@ +from collections.abc import Iterable +from typing import TYPE_CHECKING, NamedTuple, Optional + +from BaseClasses import Item +from BaseClasses import ItemClassification as IC +from worlds.AutoWorld import World + +if TYPE_CHECKING: + from .randomizers.Dungeons import Dungeon + + +def item_factory(items: str | Iterable[str], world: World) -> Item | list[Item]: + """ + Create items based on their names. + Depending on the input, this function can return a single item or a list of items. + + :param items: The name or names of the items to create. + :param world: The game world. + :raises KeyError: If an unknown item name is provided. + :return: A single item or a list of items. + """ + ret: list[Item] = [] + singleton = False + if isinstance(items, str): + items = [items] + singleton = True + for item in items: + if item in ITEM_TABLE: + ret.append(world.create_item(item)) + else: + raise KeyError(f"Unknown item {item}") + + return ret[0] if singleton else ret + + +class TWWItemData(NamedTuple): + """ + This class represents the data for an item in The Wind Waker. + + :param type: The type of the item (e.g., "Item", "Dungeon Item"). + :param classification: The item's classification (progression, useful, filler). + :param code: The unique code identifier for the item. + :param quantity: The number of this item available. + :param item_id: The ID used to represent the item in-game. + """ + + type: str + classification: IC + code: Optional[int] + quantity: int + item_id: Optional[int] + + +class TWWItem(Item): + """ + This class represents an item in The Wind Waker. + + :param name: The item's name. + :param player: The ID of the player who owns the item. + :param data: The data associated with this item. + :param classification: Optional classification to override the default. + """ + + game: str = "The Wind Waker" + type: Optional[str] + dungeon: Optional["Dungeon"] = None + + def __init__(self, name: str, player: int, data: TWWItemData, classification: Optional[IC] = None) -> None: + super().__init__( + name, + data.classification if classification is None else classification, + None if data.code is None else TWWItem.get_apid(data.code), + player, + ) + + self.type = data.type + self.item_id = data.item_id + + @staticmethod + def get_apid(code: int) -> int: + """ + Compute the Archipelago ID for the given item code. + + :param code: The unique code for the item. + :return: The computed Archipelago ID. + """ + base_id: int = 2322432 + return base_id + code + + @property + def dungeon_item(self) -> Optional[str]: + """ + Determine if the item is a dungeon item and, if so, returns its type. + + :return: The type of dungeon item, or `None` if it is not a dungeon item. + """ + if self.type in ("Small Key", "Big Key", "Map", "Compass"): + return self.type + return None + + +ITEM_TABLE: dict[str, TWWItemData] = { + "Telescope": TWWItemData("Item", IC.useful, 0, 1, 0x20), + # "Boat's Sail": TWWItemData("Item", IC.progression, 1, 1, 0x78), # noqa: E131 + "Wind Waker": TWWItemData("Item", IC.progression, 2, 1, 0x22), + "Grappling Hook": TWWItemData("Item", IC.progression, 3, 1, 0x25), + "Spoils Bag": TWWItemData("Item", IC.progression, 4, 1, 0x24), + "Boomerang": TWWItemData("Item", IC.progression, 5, 1, 0x2D), + "Deku Leaf": TWWItemData("Item", IC.progression, 6, 1, 0x34), + "Tingle Tuner": TWWItemData("Item", IC.progression, 7, 1, 0x21), + "Iron Boots": TWWItemData("Item", IC.progression, 8, 1, 0x29), + "Magic Armor": TWWItemData("Item", IC.progression, 9, 1, 0x2A), + "Bait Bag": TWWItemData("Item", IC.progression, 10, 1, 0x2C), + "Bombs": TWWItemData("Item", IC.progression, 11, 1, 0x31), + "Delivery Bag": TWWItemData("Item", IC.progression, 12, 1, 0x30), + "Hookshot": TWWItemData("Item", IC.progression, 13, 1, 0x2F), + "Skull Hammer": TWWItemData("Item", IC.progression, 14, 1, 0x33), + "Power Bracelets": TWWItemData("Item", IC.progression, 15, 1, 0x28), + + "Hero's Charm": TWWItemData("Item", IC.useful, 16, 1, 0x43), + "Hurricane Spin": TWWItemData("Item", IC.useful, 17, 1, 0xAA), + "Dragon Tingle Statue": TWWItemData("Item", IC.progression, 18, 1, 0xA3), + "Forbidden Tingle Statue": TWWItemData("Item", IC.progression, 19, 1, 0xA4), + "Goddess Tingle Statue": TWWItemData("Item", IC.progression, 20, 1, 0xA5), + "Earth Tingle Statue": TWWItemData("Item", IC.progression, 21, 1, 0xA6), + "Wind Tingle Statue": TWWItemData("Item", IC.progression, 22, 1, 0xA7), + + "Wind's Requiem": TWWItemData("Item", IC.progression, 23, 1, 0x6D), + "Ballad of Gales": TWWItemData("Item", IC.progression, 24, 1, 0x6E), + "Command Melody": TWWItemData("Item", IC.progression, 25, 1, 0x6F), + "Earth God's Lyric": TWWItemData("Item", IC.progression, 26, 1, 0x70), + "Wind God's Aria": TWWItemData("Item", IC.progression, 27, 1, 0x71), + "Song of Passing": TWWItemData("Item", IC.progression, 28, 1, 0x72), + + "Triforce Shard 1": TWWItemData("Item", IC.progression, 29, 1, 0x61), + "Triforce Shard 2": TWWItemData("Item", IC.progression, 30, 1, 0x62), + "Triforce Shard 3": TWWItemData("Item", IC.progression, 31, 1, 0x63), + "Triforce Shard 4": TWWItemData("Item", IC.progression, 32, 1, 0x64), + "Triforce Shard 5": TWWItemData("Item", IC.progression, 33, 1, 0x65), + "Triforce Shard 6": TWWItemData("Item", IC.progression, 34, 1, 0x66), + "Triforce Shard 7": TWWItemData("Item", IC.progression, 35, 1, 0x67), + "Triforce Shard 8": TWWItemData("Item", IC.progression, 36, 1, 0x68), + + "Skull Necklace": TWWItemData("Item", IC.filler, 37, 9, 0x45), + "Boko Baba Seed": TWWItemData("Item", IC.filler, 38, 1, 0x46), + "Golden Feather": TWWItemData("Item", IC.filler, 39, 9, 0x47), + "Knight's Crest": TWWItemData("Item", IC.filler, 40, 3, 0x48), + "Red Chu Jelly": TWWItemData("Item", IC.filler, 41, 1, 0x49), + "Green Chu Jelly": TWWItemData("Item", IC.filler, 42, 1, 0x4A), + "Joy Pendant": TWWItemData("Item", IC.filler, 43, 20, 0x1F), + "All-Purpose Bait": TWWItemData("Item", IC.filler, 44, 1, 0x82), + "Hyoi Pear": TWWItemData("Item", IC.filler, 45, 4, 0x83), + + "Note to Mom": TWWItemData("Item", IC.progression, 46, 1, 0x99), + "Maggie's Letter": TWWItemData("Item", IC.progression, 47, 1, 0x9A), + "Moblin's Letter": TWWItemData("Item", IC.progression, 48, 1, 0x9B), + "Cabana Deed": TWWItemData("Item", IC.progression, 49, 1, 0x9C), + "Fill-Up Coupon": TWWItemData("Item", IC.useful, 50, 1, 0x9E), + + "Nayru's Pearl": TWWItemData("Item", IC.progression, 51, 1, 0x69), + "Din's Pearl": TWWItemData("Item", IC.progression, 52, 1, 0x6A), + "Farore's Pearl": TWWItemData("Item", IC.progression, 53, 1, 0x6B), + + "Progressive Sword": TWWItemData("Item", IC.progression, 54, 4, 0x38), + "Progressive Shield": TWWItemData("Item", IC.progression, 55, 2, 0x3B), + "Progressive Picto Box": TWWItemData("Item", IC.progression, 56, 2, 0x23), + "Progressive Bow": TWWItemData("Item", IC.progression, 57, 3, 0x27), + "Progressive Magic Meter": TWWItemData("Item", IC.progression, 58, 2, 0xB1), + "Quiver Capacity Upgrade": TWWItemData("Item", IC.progression, 59, 2, 0xAF), + "Bomb Bag Capacity Upgrade": TWWItemData("Item", IC.useful, 60, 2, 0xAD), + "Wallet Capacity Upgrade": TWWItemData("Item", IC.progression, 61, 2, 0xAB), + "Empty Bottle": TWWItemData("Item", IC.progression, 62, 4, 0x50), + + "Triforce Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 63, 1, 0xFE), + "Triforce Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 64, 1, 0xFD), + "Triforce Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 65, 1, 0xFC), + "Triforce Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 66, 1, 0xFB), + "Triforce Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 67, 1, 0xFA), + "Triforce Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 68, 1, 0xF9), + "Triforce Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 69, 1, 0xF8), + "Triforce Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 70, 1, 0xF7), + "Treasure Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 71, 1, 0xE7), + "Treasure Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 72, 1, 0xEE), + "Treasure Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 73, 1, 0xE0), + "Treasure Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 74, 1, 0xE1), + "Treasure Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 75, 1, 0xF2), + "Treasure Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 76, 1, 0xEA), + "Treasure Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 77, 1, 0xCC), + "Treasure Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 78, 1, 0xD4), + "Treasure Chart 9": TWWItemData("Item", IC.progression_skip_balancing, 79, 1, 0xDA), + "Treasure Chart 10": TWWItemData("Item", IC.progression_skip_balancing, 80, 1, 0xDE), + "Treasure Chart 11": TWWItemData("Item", IC.progression_skip_balancing, 81, 1, 0xF6), + "Treasure Chart 12": TWWItemData("Item", IC.progression_skip_balancing, 82, 1, 0xE9), + "Treasure Chart 13": TWWItemData("Item", IC.progression_skip_balancing, 83, 1, 0xCF), + "Treasure Chart 14": TWWItemData("Item", IC.progression_skip_balancing, 84, 1, 0xDD), + "Treasure Chart 15": TWWItemData("Item", IC.progression_skip_balancing, 85, 1, 0xF5), + "Treasure Chart 16": TWWItemData("Item", IC.progression_skip_balancing, 86, 1, 0xE3), + "Treasure Chart 17": TWWItemData("Item", IC.progression_skip_balancing, 87, 1, 0xD7), + "Treasure Chart 18": TWWItemData("Item", IC.progression_skip_balancing, 88, 1, 0xE4), + "Treasure Chart 19": TWWItemData("Item", IC.progression_skip_balancing, 89, 1, 0xD1), + "Treasure Chart 20": TWWItemData("Item", IC.progression_skip_balancing, 90, 1, 0xF3), + "Treasure Chart 21": TWWItemData("Item", IC.progression_skip_balancing, 91, 1, 0xCE), + "Treasure Chart 22": TWWItemData("Item", IC.progression_skip_balancing, 92, 1, 0xD9), + "Treasure Chart 23": TWWItemData("Item", IC.progression_skip_balancing, 93, 1, 0xF1), + "Treasure Chart 24": TWWItemData("Item", IC.progression_skip_balancing, 94, 1, 0xEB), + "Treasure Chart 25": TWWItemData("Item", IC.progression_skip_balancing, 95, 1, 0xD6), + "Treasure Chart 26": TWWItemData("Item", IC.progression_skip_balancing, 96, 1, 0xD3), + "Treasure Chart 27": TWWItemData("Item", IC.progression_skip_balancing, 97, 1, 0xCD), + "Treasure Chart 28": TWWItemData("Item", IC.progression_skip_balancing, 98, 1, 0xE2), + "Treasure Chart 29": TWWItemData("Item", IC.progression_skip_balancing, 99, 1, 0xE6), + "Treasure Chart 30": TWWItemData("Item", IC.progression_skip_balancing, 100, 1, 0xF4), + "Treasure Chart 31": TWWItemData("Item", IC.progression_skip_balancing, 101, 1, 0xF0), + "Treasure Chart 32": TWWItemData("Item", IC.progression_skip_balancing, 102, 1, 0xD0), + "Treasure Chart 33": TWWItemData("Item", IC.progression_skip_balancing, 103, 1, 0xEF), + "Treasure Chart 34": TWWItemData("Item", IC.progression_skip_balancing, 104, 1, 0xE5), + "Treasure Chart 35": TWWItemData("Item", IC.progression_skip_balancing, 105, 1, 0xE8), + "Treasure Chart 36": TWWItemData("Item", IC.progression_skip_balancing, 106, 1, 0xD8), + "Treasure Chart 37": TWWItemData("Item", IC.progression_skip_balancing, 107, 1, 0xD5), + "Treasure Chart 38": TWWItemData("Item", IC.progression_skip_balancing, 108, 1, 0xED), + "Treasure Chart 39": TWWItemData("Item", IC.progression_skip_balancing, 109, 1, 0xEC), + "Treasure Chart 40": TWWItemData("Item", IC.progression_skip_balancing, 110, 1, 0xDF), + "Treasure Chart 41": TWWItemData("Item", IC.progression_skip_balancing, 111, 1, 0xD2), + + "Tingle's Chart": TWWItemData("Item", IC.filler, 112, 1, 0xDC), + "Ghost Ship Chart": TWWItemData("Item", IC.progression, 113, 1, 0xDB), + "Octo Chart": TWWItemData("Item", IC.filler, 114, 1, 0xCA), + "Great Fairy Chart": TWWItemData("Item", IC.filler, 115, 1, 0xC9), + "Secret Cave Chart": TWWItemData("Item", IC.filler, 116, 1, 0xC6), + "Light Ring Chart": TWWItemData("Item", IC.filler, 117, 1, 0xC5), + "Platform Chart": TWWItemData("Item", IC.filler, 118, 1, 0xC4), + "Beedle's Chart": TWWItemData("Item", IC.filler, 119, 1, 0xC3), + "Submarine Chart": TWWItemData("Item", IC.filler, 120, 1, 0xC2), + + "Green Rupee": TWWItemData("Item", IC.filler, 121, 1, 0x01), + "Blue Rupee": TWWItemData("Item", IC.filler, 122, 2, 0x02), + "Yellow Rupee": TWWItemData("Item", IC.filler, 123, 3, 0x03), + "Red Rupee": TWWItemData("Item", IC.filler, 124, 8, 0x04), + "Purple Rupee": TWWItemData("Item", IC.filler, 125, 10, 0x05), + "Orange Rupee": TWWItemData("Item", IC.useful, 126, 15, 0x06), + "Silver Rupee": TWWItemData("Item", IC.useful, 127, 20, 0x0F), + "Rainbow Rupee": TWWItemData("Item", IC.useful, 128, 1, 0xB8), + + "Piece of Heart": TWWItemData("Item", IC.useful, 129, 44, 0x07), + "Heart Container": TWWItemData("Item", IC.useful, 130, 6, 0x08), + + "DRC Big Key": TWWItemData("Big Key", IC.progression, 131, 1, 0x14), + "DRC Small Key": TWWItemData("Small Key", IC.progression, 132, 4, 0x13), + "FW Big Key": TWWItemData("Big Key", IC.progression, 133, 1, 0x40), + "FW Small Key": TWWItemData("Small Key", IC.progression, 134, 1, 0x1D), + "TotG Big Key": TWWItemData("Big Key", IC.progression, 135, 1, 0x5C), + "TotG Small Key": TWWItemData("Small Key", IC.progression, 136, 2, 0x5B), + "ET Big Key": TWWItemData("Big Key", IC.progression, 138, 1, 0x74), + "ET Small Key": TWWItemData("Small Key", IC.progression, 139, 3, 0x73), + "WT Big Key": TWWItemData("Big Key", IC.progression, 140, 1, 0x81), + "WT Small Key": TWWItemData("Small Key", IC.progression, 141, 2, 0x77), + "DRC Dungeon Map": TWWItemData("Map", IC.filler, 142, 1, 0x1B), + "DRC Compass": TWWItemData("Compass", IC.filler, 143, 1, 0x1C), + "FW Dungeon Map": TWWItemData("Map", IC.filler, 144, 1, 0x41), + "FW Compass": TWWItemData("Compass", IC.filler, 145, 1, 0x5A), + "TotG Dungeon Map": TWWItemData("Map", IC.filler, 146, 1, 0x5D), + "TotG Compass": TWWItemData("Compass", IC.filler, 147, 1, 0x5E), + "FF Dungeon Map": TWWItemData("Map", IC.filler, 148, 1, 0x5F), + "FF Compass": TWWItemData("Compass", IC.filler, 149, 1, 0x60), + "ET Dungeon Map": TWWItemData("Map", IC.filler, 150, 1, 0x75), + "ET Compass": TWWItemData("Compass", IC.filler, 151, 1, 0x76), + "WT Dungeon Map": TWWItemData("Map", IC.filler, 152, 1, 0x84), + "WT Compass": TWWItemData("Compass", IC.filler, 153, 1, 0x85), + + "Victory": TWWItemData("Event", IC.progression, None, 1, None), +} + +ISLAND_NUMBER_TO_CHART_NAME = { + 1: "Treasure Chart 25", + 2: "Treasure Chart 7", + 3: "Treasure Chart 24", + 4: "Triforce Chart 2", + 5: "Treasure Chart 11", + 6: "Triforce Chart 7", + 7: "Treasure Chart 13", + 8: "Treasure Chart 41", + 9: "Treasure Chart 29", + 10: "Treasure Chart 22", + 11: "Treasure Chart 18", + 12: "Treasure Chart 30", + 13: "Treasure Chart 39", + 14: "Treasure Chart 19", + 15: "Treasure Chart 8", + 16: "Treasure Chart 2", + 17: "Treasure Chart 10", + 18: "Treasure Chart 26", + 19: "Treasure Chart 3", + 20: "Treasure Chart 37", + 21: "Treasure Chart 27", + 22: "Treasure Chart 38", + 23: "Triforce Chart 1", + 24: "Treasure Chart 21", + 25: "Treasure Chart 6", + 26: "Treasure Chart 14", + 27: "Treasure Chart 34", + 28: "Treasure Chart 5", + 29: "Treasure Chart 28", + 30: "Treasure Chart 35", + 31: "Triforce Chart 3", + 32: "Triforce Chart 6", + 33: "Treasure Chart 1", + 34: "Treasure Chart 20", + 35: "Treasure Chart 36", + 36: "Treasure Chart 23", + 37: "Treasure Chart 12", + 38: "Treasure Chart 16", + 39: "Treasure Chart 4", + 40: "Treasure Chart 17", + 41: "Treasure Chart 31", + 42: "Triforce Chart 5", + 43: "Treasure Chart 9", + 44: "Triforce Chart 4", + 45: "Treasure Chart 40", + 46: "Triforce Chart 8", + 47: "Treasure Chart 15", + 48: "Treasure Chart 32", + 49: "Treasure Chart 33", +} + + +LOOKUP_ID_TO_NAME: dict[int, str] = { + TWWItem.get_apid(data.code): item for item, data in ITEM_TABLE.items() if data.code is not None +} + +item_name_groups = { + "Songs": { + "Wind's Requiem", + "Ballad of Gales", + "Command Melody", + "Earth God's Lyric", + "Wind God's Aria", + "Song of Passing", + }, + "Mail": { + "Note to Mom", + "Maggie's Letter", + "Moblin's Letter", + }, + "Special Charts": { + "Tingle's Chart", + "Ghost Ship Chart", + "Octo Chart", + "Great Fairy Chart", + "Secret Cave Chart", + "Light Ring Chart", + "Platform Chart", + "Beedle's Chart", + "Submarine Chart", + }, +} +# generic groups, (Name, substring) +_simple_groups = { + ("Tingle Statues", "Tingle Statue"), + ("Shards", "Shard"), + ("Pearls", "Pearl"), + ("Triforce Charts", "Triforce Chart"), + ("Treasure Charts", "Treasure Chart"), + ("Small Keys", "Small Key"), + ("Big Keys", "Big Key"), + ("Rupees", "Rupee"), + ("Dungeon Items", "Compass"), + ("Dungeon Items", "Map"), +} +for basename, substring in _simple_groups: + if basename not in item_name_groups: + item_name_groups[basename] = set() + for itemname in ITEM_TABLE: + if substring in itemname: + item_name_groups[basename].add(itemname) diff --git a/worlds/tww/Locations.py b/worlds/tww/Locations.py new file mode 100644 index 00000000..477b91ee --- /dev/null +++ b/worlds/tww/Locations.py @@ -0,0 +1,1272 @@ +from enum import Enum, Flag, auto +from typing import TYPE_CHECKING, NamedTuple, Optional + +from BaseClasses import Location, Region + +if TYPE_CHECKING: + from .randomizers.Dungeons import Dungeon + + +class TWWFlag(Flag): + """ + This class represents flags used for categorizing game locations. + Flags are used to group locations by their specific gameplay or logic attributes. + """ + + ALWAYS = auto() + DUNGEON = auto() + TNGL_CT = auto() + DG_SCRT = auto() + PZL_CVE = auto() + CBT_CVE = auto() + SAVAGE = auto() + GRT_FRY = auto() + SHRT_SQ = auto() + LONG_SQ = auto() + SPOILS = auto() + MINIGME = auto() + SPLOOSH = auto() + FREE_GF = auto() + MAILBOX = auto() + PLTFRMS = auto() + SUBMRIN = auto() + EYE_RFS = auto() + BG_OCTO = auto() + TRI_CHT = auto() + TRE_CHT = auto() + XPENSVE = auto() + ISLND_P = auto() + MISCELL = auto() + BOSS = auto() + OTHER = auto() + + +class TWWLocationType(Enum): + """ + This class defines constants for various types of locations in The Wind Waker. + """ + + CHART = auto() + BOCTO = auto() + CHEST = auto() + SWTCH = auto() + PCKUP = auto() + EVENT = auto() + SPECL = auto() + + +class TWWLocationData(NamedTuple): + """ + This class represents the data for a location in The Wind Waker. + + :param code: The unique code identifier for the location. + :param flags: The flags that categorize the location. + :param region: The name of the region where the location resides. + :param stage_id: The ID of the stage where the location resides. + :param type: The type of the location. + :param bit: The bit in memory that is associated with the location. This is combined with other location data to + determine where in memory to determine whether the location has been checked. If the location is a special type, + this bit is ignored. + :param address: For certain location types, this variable contains the address of the byte with the check bit for + that location. Defaults to `None`. + """ + + code: Optional[int] + flags: TWWFlag + region: str + stage_id: int + type: TWWLocationType + bit: int + address: Optional[int] = None + + +class TWWLocation(Location): + """ + This class represents a location in The Wind Waker. + + :param player: The ID of the player whose world the location is in. + :param name: The name of the location. + :param parent: The location's parent region. + :param data: The data associated with this location. + """ + + game: str = "The Wind Waker" + dungeon: Optional["Dungeon"] = None + + def __init__(self, player: int, name: str, parent: Region, data: TWWLocationData): + address = None if data.code is None else TWWLocation.get_apid(data.code) + super().__init__(player, name, address=address, parent=parent) + + self.code = data.code + self.flags = data.flags + self.region = data.region + self.stage_id = data.stage_id + self.type = data.type + self.bit = data.bit + self.address = self.address + + @staticmethod + def get_apid(code: int) -> int: + """ + Compute the Archipelago ID for the given location code. + + :param code: The unique code for the location. + :return: The computed Archipelago ID. + """ + base_id: int = 2326528 + return base_id + code + + +DUNGEON_NAMES = [ + "Dragon Roost Cavern", + "Forbidden Woods", + "Tower of the Gods", + "Forsaken Fortress", + "Earth Temple", + "Wind Temple", +] + +LOCATION_TABLE: dict[str, TWWLocationData] = { + # Outset Island + "Outset Island - Underneath Link's House": TWWLocationData( + 0, TWWFlag.MISCELL, "The Great Sea", 0xB, TWWLocationType.CHEST, 5 + ), + "Outset Island - Mesa the Grasscutter's House": TWWLocationData( + 1, TWWFlag.MISCELL, "The Great Sea", 0xB, TWWLocationType.CHEST, 4 + ), + "Outset Island - Orca - Give 10 Knight's Crests": TWWLocationData( + 2, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 5, 0x803C5237 + ), + # "Outset Island - Orca - Hit 500 Times": TWWLocationData( + # 3, TWWFlag.OTHER, "The Great Sea" + # ), + "Outset Island - Great Fairy": TWWLocationData( + 4, TWWFlag.GRT_FRY, "The Great Sea", 0xC, TWWLocationType.EVENT, 4, 0x803C525C + ), + "Outset Island - Jabun's Cave": TWWLocationData( + 5, TWWFlag.ISLND_P, "The Great Sea", 0xB, TWWLocationType.CHEST, 6 + ), + "Outset Island - Dig up Black Soil": TWWLocationData( + 6, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.PCKUP, 2 + ), + "Outset Island - Savage Labyrinth - Floor 30": TWWLocationData( + 7, TWWFlag.SAVAGE, "Savage Labyrinth", 0xD, TWWLocationType.CHEST, 11 + ), + "Outset Island - Savage Labyrinth - Floor 50": TWWLocationData( + 8, TWWFlag.SAVAGE, "Savage Labyrinth", 0xD, TWWLocationType.CHEST, 12 + ), + + # Windfall Island + "Windfall Island - Jail - Tingle - First Gift": TWWLocationData( + 9, TWWFlag.FREE_GF, "The Great Sea", 0xB, TWWLocationType.SWTCH, 53 + ), + "Windfall Island - Jail - Tingle - Second Gift": TWWLocationData( + 10, TWWFlag.FREE_GF, "The Great Sea", 0xB, TWWLocationType.SWTCH, 54 + ), + "Windfall Island - Jail - Maze Chest": TWWLocationData( + 11, TWWFlag.ISLND_P, "The Great Sea", 0xB, TWWLocationType.CHEST, 0 + ), + "Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly": TWWLocationData( + 12, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 2, 0x803C5239 + ), + "Windfall Island - Chu Jelly Juice Shop - Give 15 Blue Chu Jelly": TWWLocationData( + 13, TWWFlag.SPOILS | TWWFlag.LONG_SQ, "The Great Sea", 0xB, TWWLocationType.EVENT, 1, 0x803C5239 + ), + "Windfall Island - Ivan - Catch Killer Bees": TWWLocationData( + 14, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 6, 0x803C523F + ), + "Windfall Island - Mrs. Marie - Catch Killer Bees": TWWLocationData( + 15, TWWFlag.SHRT_SQ, "The Great Sea", 0xB, TWWLocationType.EVENT, 7, 0x803C524B + ), + "Windfall Island - Mrs. Marie - Give 1 Joy Pendant": TWWLocationData( + 16, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C52EC + ), + "Windfall Island - Mrs. Marie - Give 21 Joy Pendants": TWWLocationData( + 17, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 3, 0x803C5248 + ), + "Windfall Island - Mrs. Marie - Give 40 Joy Pendants": TWWLocationData( + 18, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 2, 0x803C5248 + ), + "Windfall Island - Lenzo's House - Left Chest": TWWLocationData( + 19, TWWFlag.SHRT_SQ, "The Great Sea", 0xB, TWWLocationType.CHEST, 1 + ), + "Windfall Island - Lenzo's House - Right Chest": TWWLocationData( + 20, TWWFlag.SHRT_SQ, "The Great Sea", 0xB, TWWLocationType.CHEST, 2 + ), + "Windfall Island - Lenzo's House - Become Lenzo's Assistant": TWWLocationData( + 21, TWWFlag.LONG_SQ, "The Great Sea", 0xB, TWWLocationType.SPECL, 0, 0x803C52F0 + ), + "Windfall Island - Lenzo's House - Bring Forest Firefly": TWWLocationData( + 22, TWWFlag.LONG_SQ, "The Great Sea", 0xB, TWWLocationType.EVENT, 5, 0x803C5295 + ), + "Windfall Island - House of Wealth Chest": TWWLocationData( + 23, TWWFlag.MISCELL, "The Great Sea", 0xB, TWWLocationType.CHEST, 3 + ), + "Windfall Island - Maggie's Father - Give 20 Skull Necklaces": TWWLocationData( + 24, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 4, 0x803C52F1 + ), + "Windfall Island - Maggie - Free Item": TWWLocationData( + 25, TWWFlag.FREE_GF, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C5296 + ), + "Windfall Island - Maggie - Delivery Reward": TWWLocationData( + # TODO: Where is the flag for this location. Using a temporary workaround for now. + 26, TWWFlag.SHRT_SQ, "The Great Sea", 0xB, TWWLocationType.SPECL, 0 + ), + "Windfall Island - Cafe Bar - Postman": TWWLocationData( + 27, TWWFlag.SHRT_SQ, "The Great Sea", 0xB, TWWLocationType.EVENT, 1, 0x803C5296 + ), + "Windfall Island - Kreeb - Light Up Lighthouse": TWWLocationData( + 28, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 5, 0x803C5247 + ), + "Windfall Island - Transparent Chest": TWWLocationData( + 29, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.CHEST, 10 + ), + "Windfall Island - Tott - Teach Rhythm": TWWLocationData( + 30, TWWFlag.FREE_GF, "The Great Sea", 0x0, TWWLocationType.EVENT, 6, 0x803C5238 + ), + "Windfall Island - Pirate Ship": TWWLocationData( + 31, TWWFlag.MINIGME, "The Great Sea", 0xD, TWWLocationType.CHEST, 5 + ), + "Windfall Island - 5 Rupee Auction": TWWLocationData( + 32, TWWFlag.XPENSVE | TWWFlag.MINIGME, "The Great Sea", 0xB, TWWLocationType.EVENT, 7, 0x803C523C + ), + "Windfall Island - 40 Rupee Auction": TWWLocationData( + 33, TWWFlag.XPENSVE | TWWFlag.MINIGME, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C523B + ), + "Windfall Island - 60 Rupee Auction": TWWLocationData( + 34, TWWFlag.XPENSVE | TWWFlag.MINIGME, "The Great Sea", 0xB, TWWLocationType.EVENT, 6, 0x803C523C + ), + "Windfall Island - 80 Rupee Auction": TWWLocationData( + 35, TWWFlag.XPENSVE | TWWFlag.MINIGME, "The Great Sea", 0xB, TWWLocationType.EVENT, 5, 0x803C523C + ), + "Windfall Island - Zunari - Stock Exotic Flower in Zunari's Shop": TWWLocationData( + 36, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 6, 0x803C5295 + ), + "Windfall Island - Sam - Decorate the Town": TWWLocationData( + 37, TWWFlag.LONG_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 4, 0x803C5247 + ), + # "Windfall Island - Kane - Place Shop Guru Statue on Gate": TWWLocationData( + # 38, TWWFlag.OTHER, "The Great Sea", 0x0, TWWLocationType.EVENT, 4, 0x803C5250 + # ), + # "Windfall Island - Kane - Place Postman Statue on Gate": TWWLocationData( + # 39, TWWFlag.OTHER, "The Great Sea", 0x0, TWWLocationType.EVENT, 3, 0x803C5250 + # ), + # "Windfall Island - Kane - Place Six Flags on Gate": TWWLocationData( + # 40, TWWFlag.OTHER, "The Great Sea", 0x0, TWWLocationType.EVENT, 2, 0x803C5250 + # ), + # "Windfall Island - Kane - Place Six Idols on Gate": TWWLocationData( + # 41, TWWFlag.OTHER, "The Great Sea", 0x0, TWWLocationType.EVENT, 1, 0x803C5250 + # ), + "Windfall Island - Mila - Follow the Thief": TWWLocationData( + 42, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 3, 0x803C523A + ), + "Windfall Island - Battlesquid - First Prize": TWWLocationData( + 43, TWWFlag.SPLOOSH, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C532A + ), + "Windfall Island - Battlesquid - Second Prize": TWWLocationData( + 44, TWWFlag.SPLOOSH, "The Great Sea", 0xB, TWWLocationType.EVENT, 1, 0x803C532A + ), + "Windfall Island - Battlesquid - Under 20 Shots Prize": TWWLocationData( + 45, TWWFlag.SPLOOSH, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C532B + ), + "Windfall Island - Pompie and Vera - Secret Meeting Photo": TWWLocationData( + 46, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 2, 0x803C5295 + ), + "Windfall Island - Kamo - Full Moon Photo": TWWLocationData( + 47, TWWFlag.LONG_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 4, 0x803C5295 + ), + "Windfall Island - Minenco - Miss Windfall Photo": TWWLocationData( + 48, TWWFlag.SHRT_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 3, 0x803C5295 + ), + "Windfall Island - Linda and Anton": TWWLocationData( + 49, TWWFlag.LONG_SQ, "The Great Sea", 0xB, TWWLocationType.EVENT, 7, 0x803C524E + ), + + # Dragon Roost Island + "Dragon Roost Island - Wind Shrine": TWWLocationData( + 50, TWWFlag.MISCELL, "The Great Sea", 0x0, TWWLocationType.EVENT, 3, 0x803C5253 + ), + "Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers": TWWLocationData( + 51, TWWFlag.SPOILS, "The Great Sea", 0xB, TWWLocationType.EVENT, 7, 0x803C524D + ), + "Dragon Roost Island - Chest on Top of Boulder": TWWLocationData( + 52, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 8 + ), + "Dragon Roost Island - Fly Across Platforms Around Island": TWWLocationData( + 53, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 9 + ), + "Dragon Roost Island - Rito Aerie - Mail Sorting": TWWLocationData( + 54, TWWFlag.MINIGME, "The Great Sea", 0xB, TWWLocationType.EVENT, 0, 0x803C5253 + ), + "Dragon Roost Island - Secret Cave": TWWLocationData( + 55, TWWFlag.CBT_CVE, "Dragon Roost Island Secret Cave", 0xD, TWWLocationType.CHEST, 0 + ), + + # Dragon Roost Cavern + "Dragon Roost Cavern - First Room": TWWLocationData( + 56, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 0 + ), + "Dragon Roost Cavern - Alcove With Water Jugs": TWWLocationData( + 57, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 2 + ), + "Dragon Roost Cavern - Water Jug on Upper Shelf": TWWLocationData( + 58, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Dragon Roost Cavern", 0x3, TWWLocationType.PCKUP, 1 + ), + "Dragon Roost Cavern - Boarded Up Chest": TWWLocationData( + 59, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 1 + ), + "Dragon Roost Cavern - Chest Across Lava Pit": TWWLocationData( + 60, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 13 + ), + "Dragon Roost Cavern - Rat Room": TWWLocationData( + 61, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 14 + ), + "Dragon Roost Cavern - Rat Room Boarded Up Chest": TWWLocationData( + 62, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 3 + ), + "Dragon Roost Cavern - Bird's Nest": TWWLocationData( + 63, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.PCKUP, 3 + ), + "Dragon Roost Cavern - Dark Room": TWWLocationData( + 64, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 4 + ), + "Dragon Roost Cavern - Tingle Chest in Hub Room": TWWLocationData( + 65, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 16 + ), + "Dragon Roost Cavern - Pot on Upper Shelf in Pot Room": TWWLocationData( + 66, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Dragon Roost Cavern", 0x3, TWWLocationType.PCKUP, 0 + ), + "Dragon Roost Cavern - Pot Room Chest": TWWLocationData( + 67, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 6 + ), + "Dragon Roost Cavern - Miniboss": TWWLocationData( + 68, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 17 + ), + "Dragon Roost Cavern - Under Rope Bridge": TWWLocationData( + 69, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 7 + ), + "Dragon Roost Cavern - Tingle Statue Chest": TWWLocationData( + 70, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 15 + ), + "Dragon Roost Cavern - Big Key Chest": TWWLocationData( + 71, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 12 + ), + "Dragon Roost Cavern - Boss Stairs Right Chest": TWWLocationData( + 72, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 11 + ), + "Dragon Roost Cavern - Boss Stairs Left Chest": TWWLocationData( + 73, TWWFlag.DUNGEON, "Dragon Roost Cavern", 0x3, TWWLocationType.CHEST, 10 + ), + "Dragon Roost Cavern - Boss Stairs Right Pot": TWWLocationData( + 74, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Dragon Roost Cavern", 0x3, TWWLocationType.PCKUP, 6 + ), + "Dragon Roost Cavern - Gohma Heart Container": TWWLocationData( + 75, TWWFlag.DUNGEON | TWWFlag.BOSS, "Gohma Boss Arena", 0x3, TWWLocationType.PCKUP, 21 + ), + + # Forest Haven + "Forest Haven - On Tree Branch": TWWLocationData( + 76, TWWFlag.ISLND_P, "The Great Sea", 0xB, TWWLocationType.PCKUP, 2 + ), + "Forest Haven - Small Island Chest": TWWLocationData( + 77, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 7 + ), + + # Forbidden Woods + "Forbidden Woods - First Room": TWWLocationData( + 78, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 0 + ), + "Forbidden Woods - Inside Hollow Tree's Mouth": TWWLocationData( + 79, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 1 + ), + "Forbidden Woods - Climb to Top Using Boko Baba Bulbs": TWWLocationData( + 80, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 2 + ), + "Forbidden Woods - Pot High Above Hollow Tree": TWWLocationData( + 81, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Forbidden Woods", 0x4, TWWLocationType.PCKUP, 1 + ), + "Forbidden Woods - Hole in Tree": TWWLocationData( + 82, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 6 + ), + "Forbidden Woods - Morth Pit": TWWLocationData( + 83, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 8 + ), + "Forbidden Woods - Vine Maze Left Chest": TWWLocationData( + 84, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 7 + ), + "Forbidden Woods - Vine Maze Right Chest": TWWLocationData( + 85, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 5 + ), + "Forbidden Woods - Highest Pot in Vine Maze": TWWLocationData( + 86, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Forbidden Woods", 0x4, TWWLocationType.PCKUP, 22 + ), + "Forbidden Woods - Tall Room Before Miniboss": TWWLocationData( + 87, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 12 + ), + "Forbidden Woods - Mothula Miniboss Room": TWWLocationData( + 88, TWWFlag.DUNGEON, "Forbidden Woods Miniboss Arena", 0x4, TWWLocationType.CHEST, 10 + ), + "Forbidden Woods - Past Seeds Hanging by Vines": TWWLocationData( + 89, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 3 + ), + "Forbidden Woods - Chest Across Red Hanging Flower": TWWLocationData( + 90, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 11 + ), + "Forbidden Woods - Tingle Statue Chest": TWWLocationData( + 91, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 15 + ), + "Forbidden Woods - Chest in Locked Tree Trunk": TWWLocationData( + 92, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 9 + ), + "Forbidden Woods - Big Key Chest": TWWLocationData( + 93, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 4 + ), + "Forbidden Woods - Double Mothula Room": TWWLocationData( + 94, TWWFlag.DUNGEON, "Forbidden Woods", 0x4, TWWLocationType.CHEST, 14 + ), + "Forbidden Woods - Kalle Demos Heart Container": TWWLocationData( + 95, TWWFlag.DUNGEON | TWWFlag.BOSS, "Kalle Demos Boss Arena", 0x4, TWWLocationType.PCKUP, 21 + ), + + # Greatfish Isle + "Greatfish Isle - Hidden Chest": TWWLocationData( + 96, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 6 + ), + + # Tower of the Gods + "Tower of the Gods - Chest Behind Bombable Walls": TWWLocationData( + 97, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 2 + ), + "Tower of the Gods - Pot Behind Bombable Walls": TWWLocationData( + 98, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Tower of the Gods", 0x5, TWWLocationType.PCKUP, 0 + ), + "Tower of the Gods - Hop Across Floating Boxes": TWWLocationData( + 99, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 1 + ), + "Tower of the Gods - Light Two Torches": TWWLocationData( + 100, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 10 + ), + "Tower of the Gods - Skulls Room Chest": TWWLocationData( + 101, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 3 + ), + "Tower of the Gods - Shoot Eye Above Skulls Room Chest": TWWLocationData( + 102, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 9 + ), + "Tower of the Gods - Tingle Statue Chest": TWWLocationData( + 103, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 15 + ), + "Tower of the Gods - First Chest Guarded by Armos Knights": TWWLocationData( + 104, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 6 + ), + "Tower of the Gods - Stone Tablet": TWWLocationData( + 105, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.EVENT, 4, 0x803C5251 + ), + "Tower of the Gods - Darknut Miniboss Room": TWWLocationData( + 106, TWWFlag.DUNGEON, "Tower of the Gods Miniboss Arena", 0x5, TWWLocationType.CHEST, 5 + ), + "Tower of the Gods - Second Chest Guarded by Armos Knights": TWWLocationData( + 107, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 8 + ), + "Tower of the Gods - Floating Platforms Room": TWWLocationData( + 108, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 4 + ), + "Tower of the Gods - Top of Floating Platforms Room": TWWLocationData( + 109, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 11 + ), + "Tower of the Gods - Eastern Pot in Big Key Chest Room": TWWLocationData( + 110, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Tower of the Gods", 0x5, TWWLocationType.PCKUP, 1 + ), + "Tower of the Gods - Big Key Chest": TWWLocationData( + 111, TWWFlag.DUNGEON, "Tower of the Gods", 0x5, TWWLocationType.CHEST, 0 + ), + "Tower of the Gods - Gohdan Heart Container": TWWLocationData( + 112, TWWFlag.DUNGEON | TWWFlag.BOSS, "Gohdan Boss Arena", 0x5, TWWLocationType.PCKUP, 21 + ), + + # Hyrule + "Hyrule - Master Sword Chamber": TWWLocationData( + 113, TWWFlag.DUNGEON, "Master Sword Chamber", 0x9, TWWLocationType.CHEST, 0 + ), + + # Forsaken Fortress + "Forsaken Fortress - Phantom Ganon": TWWLocationData( + 114, TWWFlag.DUNGEON, "The Great Sea", 0x0, TWWLocationType.CHEST, 16 + ), + "Forsaken Fortress - Chest Outside Upper Jail Cell": TWWLocationData( + 115, TWWFlag.DUNGEON, "The Great Sea", 0x2, TWWLocationType.CHEST, 0 + ), + "Forsaken Fortress - Chest Inside Lower Jail Cell": TWWLocationData( + 116, TWWFlag.DUNGEON, "The Great Sea", 0x2, TWWLocationType.CHEST, 3 + ), + "Forsaken Fortress - Chest Guarded By Bokoblin": TWWLocationData( + 117, TWWFlag.DUNGEON, "The Great Sea", 0x2, TWWLocationType.CHEST, 2 + ), + "Forsaken Fortress - Chest on Bed": TWWLocationData( + 118, TWWFlag.DUNGEON, "The Great Sea", 0x2, TWWLocationType.CHEST, 1 + ), + "Forsaken Fortress - Helmaroc King Heart Container": TWWLocationData( + 119, TWWFlag.DUNGEON | TWWFlag.BOSS, "Helmaroc King Boss Arena", 0x2, TWWLocationType.PCKUP, 21 + ), + + # Mother and Child Isles + "Mother and Child Isles - Inside Mother Isle": TWWLocationData( + 120, TWWFlag.MISCELL, "The Great Sea", 0x0, TWWLocationType.CHEST, 28 + ), + + # Fire Mountain + "Fire Mountain - Cave - Chest": TWWLocationData( + 121, TWWFlag.PZL_CVE | TWWFlag.CBT_CVE, "Fire Mountain Secret Cave", 0xC, TWWLocationType.CHEST, 0 + ), + "Fire Mountain - Lookout Platform Chest": TWWLocationData( + 122, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 1 + ), + "Fire Mountain - Lookout Platform - Destroy the Cannons": TWWLocationData( + 123, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 0 + ), + "Fire Mountain - Big Octo": TWWLocationData( + 124, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C51F0 + ), + + # Ice Ring Isle + "Ice Ring Isle - Frozen Chest": TWWLocationData( + 125, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 18 + ), + "Ice Ring Isle - Cave - Chest": TWWLocationData( + 126, TWWFlag.PZL_CVE, "Ice Ring Isle Secret Cave", 0xC, TWWLocationType.CHEST, 1 + ), + "Ice Ring Isle - Inner Cave - Chest": TWWLocationData( + 127, TWWFlag.PZL_CVE | TWWFlag.CBT_CVE, "Ice Ring Isle Inner Cave", 0xC, TWWLocationType.CHEST, 21 + ), + + # Headstone Island + "Headstone Island - Top of the Island": TWWLocationData( + 128, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.PCKUP, 8 + ), + "Headstone Island - Submarine": TWWLocationData( + 129, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 4 + ), + + # Earth Temple + "Earth Temple - Transparent Chest In Warp Pot Room": TWWLocationData( + 130, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 0 + ), + "Earth Temple - Behind Curtain In Warp Pot Room": TWWLocationData( + 131, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Earth Temple", 0x6, TWWLocationType.PCKUP, 0 + ), + "Earth Temple - Transparent Chest in First Crypt": TWWLocationData( + 132, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 1 + ), + "Earth Temple - Chest Behind Destructible Walls": TWWLocationData( + 133, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 12 + ), + "Earth Temple - Chest In Three Blocks Room": TWWLocationData( + 134, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 2 + ), + "Earth Temple - Chest Behind Statues": TWWLocationData( + 135, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 3 + ), + "Earth Temple - Casket in Second Crypt": TWWLocationData( + 136, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.PCKUP, 14 + ), + "Earth Temple - Stalfos Miniboss Room": TWWLocationData( + 137, TWWFlag.DUNGEON, "Earth Temple Miniboss Arena", 0x6, TWWLocationType.CHEST, 7 + ), + "Earth Temple - Tingle Statue Chest": TWWLocationData( + 138, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 15 + ), + "Earth Temple - End of Foggy Room With Floormasters": TWWLocationData( + 139, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 4 + ), + "Earth Temple - Kill All Floormasters in Foggy Room": TWWLocationData( + 140, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 11 + ), + "Earth Temple - Behind Curtain Next to Hammer Button": TWWLocationData( + 141, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Earth Temple", 0x6, TWWLocationType.PCKUP, 1 + ), + "Earth Temple - Chest in Third Crypt": TWWLocationData( + 142, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 5 + ), + "Earth Temple - Many Mirrors Room Right Chest": TWWLocationData( + 143, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 9 + ), + "Earth Temple - Many Mirrors Room Left Chest": TWWLocationData( + 144, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 10 + ), + "Earth Temple - Stalfos Crypt Room": TWWLocationData( + 145, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 14 + ), + "Earth Temple - Big Key Chest": TWWLocationData( + 146, TWWFlag.DUNGEON, "Earth Temple", 0x6, TWWLocationType.CHEST, 6 + ), + "Earth Temple - Jalhalla Heart Container": TWWLocationData( + 147, TWWFlag.DUNGEON | TWWFlag.BOSS, "Jalhalla Boss Arena", 0x6, TWWLocationType.PCKUP, 21 + ), + + # Wind Temple + "Wind Temple - Chest Between Two Dirt Patches": TWWLocationData( + 148, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 0 + ), + "Wind Temple - Behind Stone Head in Hidden Upper Room": TWWLocationData( + 149, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Wind Temple", 0x7, TWWLocationType.PCKUP, 0 + ), + "Wind Temple - Tingle Statue Chest": TWWLocationData( + 150, TWWFlag.TNGL_CT | TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 15 + ), + "Wind Temple - Chest Behind Stone Head": TWWLocationData( + 151, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 3 + ), + "Wind Temple - Chest in Left Alcove": TWWLocationData( + 152, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 7 + ), + "Wind Temple - Big Key Chest": TWWLocationData( + 153, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 8 + ), + "Wind Temple - Chest In Many Cyclones Room": TWWLocationData( + 154, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 11 + ), + "Wind Temple - Behind Stone Head in Many Cyclones Room": TWWLocationData( + 155, TWWFlag.DUNGEON | TWWFlag.DG_SCRT, "Wind Temple", 0x7, TWWLocationType.PCKUP, 1 + ), + "Wind Temple - Chest In Middle Of Hub Room": TWWLocationData( + 156, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 13 + ), + "Wind Temple - Spike Wall Room - First Chest": TWWLocationData( + 157, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 9 + ), + "Wind Temple - Spike Wall Room - Destroy All Cracked Floors": TWWLocationData( + 158, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 10 + ), + "Wind Temple - Wizzrobe Miniboss Room": TWWLocationData( + 159, TWWFlag.DUNGEON, "Wind Temple Miniboss Arena", 0x7, TWWLocationType.CHEST, 5 + ), + "Wind Temple - Chest at Top of Hub Room": TWWLocationData( + 160, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 2 + ), + "Wind Temple - Chest Behind Seven Armos": TWWLocationData( + 161, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 4 + ), + "Wind Temple - Kill All Enemies in Tall Basement Room": TWWLocationData( + 162, TWWFlag.DUNGEON, "Wind Temple", 0x7, TWWLocationType.CHEST, 12 + ), + "Wind Temple - Molgera Heart Container": TWWLocationData( + 163, TWWFlag.DUNGEON | TWWFlag.BOSS, "Molgera Boss Arena", 0x7, TWWLocationType.PCKUP, 21 + ), + + # Ganon's Tower + "Ganon's Tower - Maze Chest": TWWLocationData( + 164, TWWFlag.DUNGEON, "The Great Sea", 0x8, TWWLocationType.CHEST, 0 + ), + + # Mailbox + "Mailbox - Letter from Hoskit's Girlfriend": TWWLocationData( + 165, TWWFlag.MAILBOX | TWWFlag.SPOILS, "The Great Sea", 0x0, TWWLocationType.SPECL, 0, 0x803C52DA + ), + "Mailbox - Letter from Baito's Mother": TWWLocationData( + 166, TWWFlag.MAILBOX, "The Great Sea", 0x0, TWWLocationType.SPECL, 0, 0x803C52D8 + ), + "Mailbox - Letter from Baito": TWWLocationData( + 167, TWWFlag.MAILBOX | TWWFlag.DUNGEON, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52A8 + ), + "Mailbox - Letter from Komali's Father": TWWLocationData( + 168, TWWFlag.MAILBOX, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52E1 + ), + "Mailbox - Letter Advertising Bombs in Beedle's Shop": TWWLocationData( + 169, TWWFlag.MAILBOX, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52A9 + ), + "Mailbox - Letter Advertising Rock Spire Shop Ship": TWWLocationData( + 170, TWWFlag.MAILBOX, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52A6 + ), + # "Mailbox - Beedle's Silver Membership Reward": TWWLocationData( + # 171, TWWFlag.OTHER, "The Great Sea" + # ), + # "Mailbox - Beedle's Gold Membership Reward": TWWLocationData( + # 172, TWWFlag.OTHER, "The Great Sea" + # ), + "Mailbox - Letter from Orca": TWWLocationData( + 173, TWWFlag.MAILBOX | TWWFlag.DUNGEON, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52A7 + ), + "Mailbox - Letter from Grandma": TWWLocationData( + 174, TWWFlag.MAILBOX, "The Great Sea", 0x0, TWWLocationType.SPECL, 0, 0x803C52C9 + ), + "Mailbox - Letter from Aryll": TWWLocationData( + 175, TWWFlag.MAILBOX | TWWFlag.DUNGEON, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52B7 + ), + "Mailbox - Letter from Tingle": TWWLocationData( + 176, + TWWFlag.MAILBOX | TWWFlag.DUNGEON | TWWFlag.XPENSVE, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52DE + ), + + # The Great Sea + "The Great Sea - Beedle's Shop Ship - 20 Rupee Item": TWWLocationData( + 177, TWWFlag.MISCELL, "The Great Sea", 0xA, TWWLocationType.EVENT, 1, 0x803C5295 + ), + "The Great Sea - Salvage Corp Gift": TWWLocationData( + 178, TWWFlag.FREE_GF, "The Great Sea", 0x0, TWWLocationType.EVENT, 7, 0x803C5295 + ), + "The Great Sea - Cyclos": TWWLocationData( + 179, TWWFlag.MISCELL, "The Great Sea", 0x0, TWWLocationType.EVENT, 4, 0x803C5253 + ), + "The Great Sea - Goron Trading Reward": TWWLocationData( + 180, TWWFlag.LONG_SQ | TWWFlag.XPENSVE, "The Great Sea", 0x0, TWWLocationType.EVENT, 2, 0x803C526A + ), + "The Great Sea - Withered Trees": TWWLocationData( + 181, TWWFlag.LONG_SQ, "The Great Sea", 0x0, TWWLocationType.EVENT, 5, 0x803C525A + ), + "The Great Sea - Ghost Ship": TWWLocationData( + 182, TWWFlag.MISCELL, "The Great Sea", 0xA, TWWLocationType.CHEST, 23 + ), + + # Private Oasis + "Private Oasis - Chest at Top of Waterfall": TWWLocationData( + 183, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 19 + ), + "Private Oasis - Cabana Labyrinth - Lower Floor Chest": TWWLocationData( + 184, TWWFlag.PZL_CVE, "Cabana Labyrinth", 0xC, TWWLocationType.CHEST, 22 + ), + "Private Oasis - Cabana Labyrinth - Upper Floor Chest": TWWLocationData( + 185, TWWFlag.PZL_CVE, "Cabana Labyrinth", 0xC, TWWLocationType.CHEST, 17 + ), + "Private Oasis - Big Octo": TWWLocationData( + 186, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C520A + ), + + # Spectacle Island + "Spectacle Island - Barrel Shooting - First Prize": TWWLocationData( + 187, TWWFlag.MINIGME, "The Great Sea", 0x0, TWWLocationType.EVENT, 0, 0x803C52E3 + ), + "Spectacle Island - Barrel Shooting - Second Prize": TWWLocationData( + 188, TWWFlag.MINIGME, "The Great Sea", 0x0, TWWLocationType.EVENT, 1, 0x803C52E3 + ), + + # Needle Rock Isle + "Needle Rock Isle - Chest": TWWLocationData( + 189, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 3 + ), + "Needle Rock Isle - Cave": TWWLocationData( + 190, TWWFlag.PZL_CVE, "Needle Rock Isle Secret Cave", 0xD, TWWLocationType.CHEST, 9 + ), + "Needle Rock Isle - Golden Gunboat": TWWLocationData( + 191, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 2, 0x803C5202 + ), + + # Angular Isles + "Angular Isles - Peak": TWWLocationData( + 192, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 0 + ), + "Angular Isles - Cave": TWWLocationData( + 193, TWWFlag.PZL_CVE, "Angular Isles Secret Cave", 0xD, TWWLocationType.CHEST, 6 + ), + + # Boating Course + "Boating Course - Raft": TWWLocationData( + 194, TWWFlag.PLTFRMS, "The Great Sea", 0x0, TWWLocationType.CHEST, 21 + ), + "Boating Course - Cave": TWWLocationData( + 195, TWWFlag.PZL_CVE | TWWFlag.CBT_CVE, "Boating Course Secret Cave", 0xD, TWWLocationType.CHEST, 15 + ), + + # Stone Watcher Island + "Stone Watcher Island - Cave": TWWLocationData( + 196, TWWFlag.CBT_CVE, "Stone Watcher Island Secret Cave", 0xC, TWWLocationType.CHEST, 10 + ), + "Stone Watcher Island - Lookout Platform Chest": TWWLocationData( + 197, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 18 + ), + "Stone Watcher Island - Lookout Platform - Destroy the Cannons": TWWLocationData( + 198, TWWFlag.PLTFRMS, "The Great Sea", 0x0, TWWLocationType.CHEST, 20 + ), + + # Islet of Steel + "Islet of Steel - Interior": TWWLocationData( + 199, TWWFlag.MISCELL, "The Great Sea", 0xC, TWWLocationType.CHEST, 4 + ), + "Islet of Steel - Lookout Platform - Defeat the Enemies": TWWLocationData( + 200, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 16 + ), + + # Overlook Island + "Overlook Island - Cave": TWWLocationData( + 201, TWWFlag.CBT_CVE, "Overlook Island Secret Cave", 0xC, TWWLocationType.CHEST, 11 + ), + + # Bird's Peak Rock + "Bird's Peak Rock - Cave": TWWLocationData( + 202, TWWFlag.PZL_CVE, "Bird's Peak Rock Secret Cave", 0xC, TWWLocationType.CHEST, 16 + ), + + # Pawprint Isle + "Pawprint Isle - Chuchu Cave - Chest": TWWLocationData( + 203, TWWFlag.PZL_CVE, "Pawprint Isle Chuchu Cave", 0xC, TWWLocationType.CHEST, 26 + ), + "Pawprint Isle - Chuchu Cave - Behind Left Boulder": TWWLocationData( + 204, TWWFlag.PZL_CVE, "Pawprint Isle Chuchu Cave", 0xC, TWWLocationType.CHEST, 24 + ), + "Pawprint Isle - Chuchu Cave - Behind Right Boulder": TWWLocationData( + 205, TWWFlag.PZL_CVE, "Pawprint Isle Chuchu Cave", 0xC, TWWLocationType.CHEST, 25 + ), + "Pawprint Isle - Chuchu Cave - Scale the Wall": TWWLocationData( + 206, TWWFlag.PZL_CVE, "Pawprint Isle Chuchu Cave", 0xC, TWWLocationType.CHEST, 2 + ), + "Pawprint Isle - Wizzrobe Cave": TWWLocationData( + 207, TWWFlag.CBT_CVE, "Pawprint Isle Wizzrobe Cave", 0xD, TWWLocationType.CHEST, 2 + ), + "Pawprint Isle - Lookout Platform - Defeat the Enemies": TWWLocationData( + 208, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 5 + ), + + # Thorned Fairy Island + "Thorned Fairy Island - Great Fairy": TWWLocationData( + 209, TWWFlag.GRT_FRY, "Thorned Fairy Fountain", 0xC, TWWLocationType.EVENT, 0, 0x803C525C + ), + "Thorned Fairy Island - Northeastern Lookout Platform - Destroy the Cannons": TWWLocationData( + 210, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 14 + ), + "Thorned Fairy Island - Southwestern Lookout Platform - Defeat the Enemies": TWWLocationData( + 211, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 15 + ), + + # Eastern Fairy Island + "Eastern Fairy Island - Great Fairy": TWWLocationData( + 212, TWWFlag.GRT_FRY, "Eastern Fairy Fountain", 0xC, TWWLocationType.EVENT, 3, 0x803C525C + ), + "Eastern Fairy Island - Lookout Platform - Defeat the Cannons and Enemies": TWWLocationData( + 213, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 10 + ), + + # Western Fairy Island + "Western Fairy Island - Great Fairy": TWWLocationData( + 214, TWWFlag.GRT_FRY, "Western Fairy Fountain", 0xC, TWWLocationType.EVENT, 1, 0x803C525C + ), + "Western Fairy Island - Lookout Platform": TWWLocationData( + 215, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 6 + ), + + # Southern Fairy Island + "Southern Fairy Island - Great Fairy": TWWLocationData( + 216, TWWFlag.GRT_FRY, "Southern Fairy Fountain", 0xC, TWWLocationType.EVENT, 2, 0x803C525C + ), + "Southern Fairy Island - Lookout Platform - Destroy the Northwest Cannons": TWWLocationData( + 217, TWWFlag.PLTFRMS, "The Great Sea", 0x0, TWWLocationType.CHEST, 23 + ), + "Southern Fairy Island - Lookout Platform - Destroy the Southeast Cannons": TWWLocationData( + 218, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 17 + ), + + # Northern Fairy Island + "Northern Fairy Island - Great Fairy": TWWLocationData( + 219, TWWFlag.GRT_FRY, "Northern Fairy Fountain", 0xC, TWWLocationType.EVENT, 5, 0x803C525C + ), + "Northern Fairy Island - Submarine": TWWLocationData( + 220, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 6 + ), + + # Tingle Island + "Tingle Island - Ankle - Reward for All Tingle Statues": TWWLocationData( + 221, TWWFlag.MISCELL, "The Great Sea", 0x0, TWWLocationType.SPECL, 0 + ), + "Tingle Island - Big Octo": TWWLocationData( + 222, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C51EA + ), + + # Diamond Steppe Island + "Diamond Steppe Island - Warp Maze Cave - First Chest": TWWLocationData( + 223, TWWFlag.PZL_CVE, "Diamond Steppe Island Warp Maze Cave", 0xC, TWWLocationType.CHEST, 23 + ), + "Diamond Steppe Island - Warp Maze Cave - Second Chest": TWWLocationData( + 224, TWWFlag.PZL_CVE, "Diamond Steppe Island Warp Maze Cave", 0xC, TWWLocationType.CHEST, 3 + ), + "Diamond Steppe Island - Big Octo": TWWLocationData( + 225, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C5210 + ), + + # Bomb Island + "Bomb Island - Cave": TWWLocationData( + 226, TWWFlag.PZL_CVE, "Bomb Island Secret Cave", 0xC, TWWLocationType.CHEST, 5 + ), + "Bomb Island - Lookout Platform - Defeat the Enemies": TWWLocationData( + 227, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 3 + ), + "Bomb Island - Submarine": TWWLocationData( + 228, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 2 + ), + + # Rock Spire Isle + "Rock Spire Isle - Cave": TWWLocationData( + 229, TWWFlag.CBT_CVE, "Rock Spire Isle Secret Cave", 0xC, TWWLocationType.CHEST, 8 + ), + "Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item": TWWLocationData( + 230, TWWFlag.XPENSVE, "The Great Sea", 0xA, TWWLocationType.EVENT, 5, 0x803C524C + ), + "Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item": TWWLocationData( + 231, TWWFlag.XPENSVE, "The Great Sea", 0xA, TWWLocationType.EVENT, 4, 0x803C524C + ), + "Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item": TWWLocationData( + 232, TWWFlag.XPENSVE, "The Great Sea", 0xA, TWWLocationType.EVENT, 3, 0x803C524C + ), + "Rock Spire Isle - Western Lookout Platform - Destroy the Cannons": TWWLocationData( + 233, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 23 + ), + "Rock Spire Isle - Eastern Lookout Platform - Destroy the Cannons": TWWLocationData( + 234, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 24 + ), + "Rock Spire Isle - Center Lookout Platform": TWWLocationData( + 235, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 25 + ), + "Rock Spire Isle - Southeast Gunboat": TWWLocationData( + 236, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C51E8 + ), + + # Shark Island + "Shark Island - Cave": TWWLocationData( + 237, TWWFlag.CBT_CVE, "Shark Island Secret Cave", 0xD, TWWLocationType.CHEST, 22 + ), + + # Cliff Plateau Isles + "Cliff Plateau Isles - Cave": TWWLocationData( + 238, TWWFlag.PZL_CVE, "Cliff Plateau Isles Secret Cave", 0xC, TWWLocationType.CHEST, 7 + ), + "Cliff Plateau Isles - Highest Isle": TWWLocationData( + 239, TWWFlag.PZL_CVE, "Cliff Plateau Isles Inner Cave", 0x0, TWWLocationType.CHEST, 1 + ), + "Cliff Plateau Isles - Lookout Platform": TWWLocationData( + 240, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 19 + ), + + # Crescent Moon Island + "Crescent Moon Island - Chest": TWWLocationData( + 241, TWWFlag.MISCELL, "The Great Sea", 0x0, TWWLocationType.CHEST, 4 + ), + "Crescent Moon Island - Submarine": TWWLocationData( + 242, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 7 + ), + + # Horseshoe Island + "Horseshoe Island - Play Golf": TWWLocationData( + 243, TWWFlag.ISLND_P, "The Great Sea", 0x0, TWWLocationType.CHEST, 5 + ), + "Horseshoe Island - Cave": TWWLocationData( + 244, TWWFlag.CBT_CVE, "Horseshoe Island Secret Cave", 0xD, TWWLocationType.CHEST, 1 + ), + "Horseshoe Island - Northwestern Lookout Platform": TWWLocationData( + 245, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 26 + ), + "Horseshoe Island - Southeastern Lookout Platform": TWWLocationData( + 246, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 27 + ), + + # Flight Control Platform + "Flight Control Platform - Bird-Man Contest - First Prize": TWWLocationData( + 247, TWWFlag.MINIGME, "The Great Sea", 0x0, TWWLocationType.EVENT, 6, 0x803C5257 + ), + "Flight Control Platform - Submarine": TWWLocationData( + 248, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 3 + ), + + # Star Island + "Star Island - Cave": TWWLocationData( + 249, TWWFlag.CBT_CVE, "Star Island Secret Cave", 0xC, TWWLocationType.CHEST, 6 + ), + "Star Island - Lookout Platform": TWWLocationData( + 250, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 4 + ), + + # Star Belt Archipelago + "Star Belt Archipelago - Lookout Platform": TWWLocationData( + 251, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 11 + ), + + # Five-Star Isles + "Five-Star Isles - Lookout Platform - Destroy the Cannons": TWWLocationData( + 252, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 2 + ), + "Five-Star Isles - Raft": TWWLocationData( + 253, TWWFlag.PLTFRMS, "The Great Sea", 0x0, TWWLocationType.CHEST, 2 + ), + "Five-Star Isles - Submarine": TWWLocationData( + 254, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 1 + ), + + # Seven-Star Isles + "Seven-Star Isles - Center Lookout Platform": TWWLocationData( + 255, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 8 + ), + "Seven-Star Isles - Northern Lookout Platform": TWWLocationData( + 256, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 7 + ), + "Seven-Star Isles - Southern Lookout Platform": TWWLocationData( + 257, TWWFlag.PLTFRMS, "The Great Sea", 0x0, TWWLocationType.CHEST, 22 + ), + "Seven-Star Isles - Big Octo": TWWLocationData( + 258, TWWFlag.BG_OCTO, "The Great Sea", 0x0, TWWLocationType.BOCTO, 0, 0x803C51D4 + ), + + # Cyclops Reef + "Cyclops Reef - Destroy the Cannons and Gunboats": TWWLocationData( + 259, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 11 + ), + "Cyclops Reef - Lookout Platform - Defeat the Enemies": TWWLocationData( + 260, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 12 + ), + + # Two-Eye Reef + "Two-Eye Reef - Destroy the Cannons and Gunboats": TWWLocationData( + 261, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 13 + ), + "Two-Eye Reef - Lookout Platform": TWWLocationData( + 262, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 21 + ), + "Two-Eye Reef - Big Octo Great Fairy": TWWLocationData( + 263, TWWFlag.BG_OCTO | TWWFlag.GRT_FRY, "The Great Sea", 0x0, TWWLocationType.SWTCH, 52 + ), + + # Three-Eye Reef + "Three-Eye Reef - Destroy the Cannons and Gunboats": TWWLocationData( + 264, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 12 + ), + + # Four-Eye Reef + "Four-Eye Reef - Destroy the Cannons and Gunboats": TWWLocationData( + 265, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 14 + ), + + # Five-Eye Reef + "Five-Eye Reef - Destroy the Cannons": TWWLocationData( + 266, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 15 + ), + "Five-Eye Reef - Lookout Platform": TWWLocationData( + 267, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 20 + ), + + # Six-Eye Reef + "Six-Eye Reef - Destroy the Cannons and Gunboats": TWWLocationData( + 268, TWWFlag.EYE_RFS, "The Great Sea", 0x0, TWWLocationType.CHEST, 17 + ), + "Six-Eye Reef - Lookout Platform - Destroy the Cannons": TWWLocationData( + 269, TWWFlag.PLTFRMS, "The Great Sea", 0x1, TWWLocationType.CHEST, 13 + ), + "Six-Eye Reef - Submarine": TWWLocationData( + 270, TWWFlag.SUBMRIN, "The Great Sea", 0xA, TWWLocationType.CHEST, 0 + ), + + # Sunken Treasure + "Forsaken Fortress Sector - Sunken Treasure": TWWLocationData( + 271, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 8 + ), + "Star Island - Sunken Treasure": TWWLocationData( + 272, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 18 + ), + "Northern Fairy Island - Sunken Treasure": TWWLocationData( + 273, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 51 + ), + "Gale Isle - Sunken Treasure": TWWLocationData( + 274, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 33 + ), + "Crescent Moon Island - Sunken Treasure": TWWLocationData( + 275, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 40 + ), + "Seven-Star Isles - Sunken Treasure": TWWLocationData( + 276, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 38 + ), + "Overlook Island - Sunken Treasure": TWWLocationData( + 277, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 15 + ), + "Four-Eye Reef - Sunken Treasure": TWWLocationData( + 278, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 12 + ), + "Mother and Child Isles - Sunken Treasure": TWWLocationData( + 279, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 56 + ), + "Spectacle Island - Sunken Treasure": TWWLocationData( + 280, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 5 + ), + "Windfall Island - Sunken Treasure": TWWLocationData( + 281, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 58 + ), + "Pawprint Isle - Sunken Treasure": TWWLocationData( + 282, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 42 + ), + "Dragon Roost Island - Sunken Treasure": TWWLocationData( + 283, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 50 + ), + "Flight Control Platform - Sunken Treasure": TWWLocationData( + 284, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 13 + ), + "Western Fairy Island - Sunken Treasure": TWWLocationData( + 285, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 10 + ), + "Rock Spire Isle - Sunken Treasure": TWWLocationData( + 286, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 48 + ), + "Tingle Island - Sunken Treasure": TWWLocationData( + 287, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 0 + ), + "Northern Triangle Island - Sunken Treasure": TWWLocationData( + 288, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 11 + ), + "Eastern Fairy Island - Sunken Treasure": TWWLocationData( + 289, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 62 + ), + "Fire Mountain - Sunken Treasure": TWWLocationData( + 290, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 9 + ), + "Star Belt Archipelago - Sunken Treasure": TWWLocationData( + 291, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 17 + ), + "Three-Eye Reef - Sunken Treasure": TWWLocationData( + 292, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 49 + ), + "Greatfish Isle - Sunken Treasure": TWWLocationData( + 293, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 32 + ), + "Cyclops Reef - Sunken Treasure": TWWLocationData( + 294, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 16 + ), + "Six-Eye Reef - Sunken Treasure": TWWLocationData( + 295, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 52 + ), + "Tower of the Gods Sector - Sunken Treasure": TWWLocationData( + 296, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 1 + ), + "Eastern Triangle Island - Sunken Treasure": TWWLocationData( + 297, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 57 + ), + "Thorned Fairy Island - Sunken Treasure": TWWLocationData( + 298, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 44 + ), + "Needle Rock Isle - Sunken Treasure": TWWLocationData( + 299, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 60 + ), + "Islet of Steel - Sunken Treasure": TWWLocationData( + 300, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 54 + ), + "Stone Watcher Island - Sunken Treasure": TWWLocationData( + 301, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 34 + ), + "Southern Triangle Island - Sunken Treasure": TWWLocationData( + 302, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 37 + ), + "Private Oasis - Sunken Treasure": TWWLocationData( + 303, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 55 + ), + "Bomb Island - Sunken Treasure": TWWLocationData( + 304, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 43 + ), + "Bird's Peak Rock - Sunken Treasure": TWWLocationData( + 305, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 6 + ), + "Diamond Steppe Island - Sunken Treasure": TWWLocationData( + 306, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 45 + ), + "Five-Eye Reef - Sunken Treasure": TWWLocationData( + 307, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 53 + ), + "Shark Island - Sunken Treasure": TWWLocationData( + 308, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 59 + ), + "Southern Fairy Island - Sunken Treasure": TWWLocationData( + 309, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 61 + ), + "Ice Ring Isle - Sunken Treasure": TWWLocationData( + 310, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 7 + ), + "Forest Haven - Sunken Treasure": TWWLocationData( + 311, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 46 + ), + "Cliff Plateau Isles - Sunken Treasure": TWWLocationData( + 312, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 36 + ), + "Horseshoe Island - Sunken Treasure": TWWLocationData( + 313, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 4 + ), + "Outset Island - Sunken Treasure": TWWLocationData( + 314, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 35 + ), + "Headstone Island - Sunken Treasure": TWWLocationData( + 315, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 63 + ), + "Two-Eye Reef - Sunken Treasure": TWWLocationData( + 316, TWWFlag.TRI_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 39 + ), + "Angular Isles - Sunken Treasure": TWWLocationData( + 317, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 41 + ), + "Boating Course - Sunken Treasure": TWWLocationData( + 318, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 14 + ), + "Five-Star Isles - Sunken Treasure": TWWLocationData( + 319, TWWFlag.TRE_CHT, "The Great Sea", 0x0, TWWLocationType.CHART, 47 + ), + + # Defeat Ganondorf + "Defeat Ganondorf": TWWLocationData( + None, TWWFlag.ALWAYS, "The Great Sea", 0x8, TWWLocationType.SWTCH, 64 + ), +} + + +ISLAND_NAME_TO_SALVAGE_BIT: dict[str, int] = { + "Forsaken Fortress Sector": 8, + "Star Island": 18, + "Northern Fairy Island": 51, + "Gale Isle": 33, + "Crescent Moon Island": 40, + "Seven-Star Isles": 38, + "Overlook Island": 15, + "Four-Eye Reef": 12, + "Mother and Child Isles": 56, + "Spectacle Island": 5, + "Windfall Island": 58, + "Pawprint Isle": 42, + "Dragon Roost Island": 50, + "Flight Control Platform": 13, + "Western Fairy Island": 10, + "Rock Spire Isle": 48, + "Tingle Island": 0, + "Northern Triangle Island": 11, + "Eastern Fairy Island": 62, + "Fire Mountain": 9, + "Star Belt Archipelago": 17, + "Three-Eye Reef": 49, + "Greatfish Isle": 32, + "Cyclops Reef": 16, + "Six-Eye Reef": 52, + "Tower of the Gods Sector": 1, + "Eastern Triangle Island": 57, + "Thorned Fairy Island": 44, + "Needle Rock Isle": 60, + "Islet of Steel": 54, + "Stone Watcher Island": 34, + "Southern Triangle Island": 37, + "Private Oasis": 55, + "Bomb Island": 43, + "Bird's Peak Rock": 6, + "Diamond Steppe Island": 45, + "Five-Eye Reef": 53, + "Shark Island": 59, + "Southern Fairy Island": 61, + "Ice Ring Isle": 7, + "Forest Haven": 46, + "Cliff Plateau Isles": 36, + "Horseshoe Island": 4, + "Outset Island": 35, + "Headstone Island": 63, + "Two-Eye Reef": 39, + "Angular Isles": 41, + "Boating Course": 14, + "Five-Star Isles": 47, +} + + +def split_location_name_by_zone(location_name: str) -> tuple[str, str]: + """ + Split a location name into its zone name and specific name. + + :param location_name: The full name of the location. + :return: A tuple containing the zone and specific name. + """ + if " - " in location_name: + zone_name, specific_location_name = location_name.split(" - ", 1) + else: + zone_name = specific_location_name = location_name + + return zone_name, specific_location_name diff --git a/worlds/tww/Macros.py b/worlds/tww/Macros.py new file mode 100644 index 00000000..5ed405ed --- /dev/null +++ b/worlds/tww/Macros.py @@ -0,0 +1,1114 @@ +from BaseClasses import CollectionState + + +def can_play_winds_requiem(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Wind's Requiem"], player) + + +def can_play_ballad_of_gales(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Ballad of Gales"], player) + + +def can_play_command_melody(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Command Melody"], player) + + +def can_play_earth_gods_lyric(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Earth God's Lyric"], player) + + +def can_play_wind_gods_aria(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Wind God's Aria"], player) + + +def can_play_song_of_passing(state: CollectionState, player: int) -> bool: + return state.has_all(["Wind Waker", "Song of Passing"], player) + + +def can_fan_with_deku_leaf(state: CollectionState, player: int) -> bool: + return state.has("Deku Leaf", player) + + +def can_fly_with_deku_leaf_indoors(state: CollectionState, player: int) -> bool: + return state.has("Deku Leaf", player) and has_magic_meter(state, player) + + +def can_fly_with_deku_leaf_outdoors(state: CollectionState, player: int) -> bool: + return state.has("Deku Leaf", player) and has_magic_meter(state, player) and can_play_winds_requiem(state, player) + + +def can_use_magic_armor(state: CollectionState, player: int) -> bool: + return state.has("Magic Armor", player) and has_magic_meter(state, player) + + +def can_aim_mirror_shield(state: CollectionState, player: int) -> bool: + return has_mirror_shield(state, player) and ( + state.has_any(["Wind Waker", "Grappling Hook", "Boomerang", "Deku Leaf", "Hookshot"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_move_boulders(state: CollectionState, player: int) -> bool: + return state.has_any(["Bombs", "Power Bracelets"], player) + + +def can_defeat_door_flowers(state: CollectionState, player: int) -> bool: + return state.has_any(["Boomerang", "Bombs", "Hookshot"], player) or has_heros_bow(state, player) + + +def can_hit_diamond_switches_at_range(state: CollectionState, player: int) -> bool: + return state.has_any(["Boomerang", "Hookshot"], player) or has_heros_bow(state, player) + + +def can_destroy_seeds_hanging_by_vines(state: CollectionState, player: int) -> bool: + return state.has_any(["Boomerang", "Bombs", "Hookshot"], player) or has_heros_bow(state, player) + + +def has_heros_sword(state: CollectionState, player: int) -> bool: + return state.has("Progressive Sword", player, 1) + + +def has_any_master_sword(state: CollectionState, player: int) -> bool: + return state.has("Progressive Sword", player, 2) + + +def has_full_power_master_sword(state: CollectionState, player: int) -> bool: + return state.has("Progressive Sword", player, 4) + + +def has_heros_shield(state: CollectionState, player: int) -> bool: + return state.has("Progressive Shield", player, 1) + + +def has_mirror_shield(state: CollectionState, player: int) -> bool: + return state.has("Progressive Shield", player, 2) + + +def has_heros_bow(state: CollectionState, player: int) -> bool: + return state.has("Progressive Bow", player, 1) + + +def has_fire_arrows(state: CollectionState, player: int) -> bool: + return state.has("Progressive Bow", player, 2) and has_magic_meter(state, player) + + +def has_ice_arrows(state: CollectionState, player: int) -> bool: + return state.has("Progressive Bow", player, 2) and has_magic_meter(state, player) + + +def has_light_arrows(state: CollectionState, player: int) -> bool: + return state.has("Progressive Bow", player, 3) and has_magic_meter(state, player) + + +def has_any_wallet_upgrade(state: CollectionState, player: int) -> bool: + return state.has("Wallet Capacity Upgrade", player, 1) + + +def has_picto_box(state: CollectionState, player: int) -> bool: + return state.has("Progressive Picto Box", player, 1) + + +def has_deluxe_picto_box(state: CollectionState, player: int) -> bool: + return state.has("Progressive Picto Box", player, 2) + + +def has_any_quiver_upgrade(state: CollectionState, player: int) -> bool: + return state.has("Quiver Capacity Upgrade", player, 1) + + +def has_magic_meter(state: CollectionState, player: int) -> bool: + return state.has("Progressive Magic Meter", player, 1) + + +def has_magic_meter_upgrade(state: CollectionState, player: int) -> bool: + return state.has("Progressive Magic Meter", player, 2) + + +def has_all_8_triforce_shards(state: CollectionState, player: int) -> bool: + return state.has_group_unique("Shards", player, 8) + + +def has_tingle_bombs(state: CollectionState, player: int) -> bool: + return state.has("Bombs", player) or (state._tww_tuner_logic_enabled(player) and state.has("Tingle Tuner", player)) + + +def can_reach_outset_island_upper_level(state: CollectionState, player: int) -> bool: + return can_cut_down_outset_trees(state, player) or ( + can_fly_with_deku_leaf_outdoors(state, player) and state._tww_obscure_1(player) + ) + + +def can_access_forest_of_fairies(state: CollectionState, player: int) -> bool: + return can_reach_outset_island_upper_level(state, player) and can_fly_with_deku_leaf_outdoors(state, player) + + +def can_reach_dragon_roost_cavern_gaping_maw(state: CollectionState, player: int) -> bool: + return state.has("DRC Small Key", player, 1) and ( + (state.has("DRC Small Key", player, 4) and can_cut_down_hanging_drc_platform(state, player)) + or (can_fly_with_deku_leaf_indoors(state, player) and state._tww_obscure_2(player)) + or (has_ice_arrows(state, player) and state._tww_obscure_2(player) and state._tww_precise_1(player)) + ) + + +def can_reach_dragon_roost_cavern_boss_stairs(state: CollectionState, player: int) -> bool: + return state.has("DRC Small Key", player, 4) and ( + state.has_any(["Grappling Hook", "Hookshot"], player) + or can_fly_with_deku_leaf_indoors(state, player) + or has_ice_arrows(state, player) + ) + + +def can_reach_tower_of_the_gods_second_floor(state: CollectionState, player: int) -> bool: + return state.has_all(["Bombs", "TotG Small Key"], player) and can_defeat_yellow_chuchus(state, player) + + +def can_reach_tower_of_the_gods_third_floor(state: CollectionState, player: int) -> bool: + return ( + can_reach_tower_of_the_gods_second_floor(state, player) + and can_bring_west_servant_of_the_tower(state, player) + and can_bring_north_servant_of_the_tower(state, player) + and state.has("Wind Waker", player) + ) + + +def can_bring_west_servant_of_the_tower(state: CollectionState, player: int) -> bool: + return ( + (state.has("Grappling Hook", player) or can_fly_with_deku_leaf_indoors(state, player)) + and can_play_command_melody(state, player) + and has_heros_bow(state, player) + ) + + +def can_bring_north_servant_of_the_tower(state: CollectionState, player: int) -> bool: + return ( + state.has("TotG Small Key", player, 2) + and (can_fly_with_deku_leaf_indoors(state, player) or state._tww_obscure_1(player)) + and can_play_command_melody(state, player) + ) + + +def can_reach_earth_temple_sun_statue_room(state: CollectionState, player: int) -> bool: + return ( + can_play_command_melody(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_green_chuchus(state, player) + ) + + +def can_reach_earth_temple_right_path(state: CollectionState, player: int) -> bool: + return ( + can_reach_earth_temple_sun_statue_room(state, player) + and can_play_command_melody(state, player) + and state.has("Skull Hammer", player) + ) + + +def can_reach_earth_temple_left_path(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_sun_statue_room(state, player) and state.has("ET Small Key", player, 2) + + +def can_reach_earth_temple_moblins_and_poes_room(state: CollectionState, player: int) -> bool: + return ( + can_reach_earth_temple_left_path(state, player) + and has_fire_arrows(state, player) + and state.has("Power Bracelets", player) + and can_defeat_floormasters(state, player) + and (can_play_command_melody(state, player) or has_mirror_shield(state, player)) + ) + + +def can_reach_earth_temple_basement(state: CollectionState, player: int) -> bool: + return ( + can_reach_earth_temple_sun_statue_room(state, player) + and can_play_command_melody(state, player) + and can_aim_mirror_shield(state, player) + ) + + +def can_reach_earth_temple_redead_hub_room(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_basement(state, player) and can_play_earth_gods_lyric(state, player) + + +def can_reach_earth_temple_third_crypt(state: CollectionState, player: int) -> bool: + return ( + can_reach_earth_temple_redead_hub_room(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)) + and state.has_all_counts({"Power Bracelets": 1, "Skull Hammer": 1, "ET Small Key": 3}, player) + and (can_defeat_red_bubbles(state, player) or state._tww_precise_2(player)) + and can_play_command_melody(state, player) + and can_aim_mirror_shield(state, player) + ) + + +def can_reach_earth_temple_tall_vine_room(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_third_crypt(state, player) and can_play_earth_gods_lyric(state, player) + + +def can_reach_earth_temple_many_mirrors_room(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_tall_vine_room(state, player) + + +def can_reach_wind_temple_kidnapping_room(state: CollectionState, player: int) -> bool: + return ( + can_play_command_melody(state, player) + and state.has("Iron Boots", player) + and can_fly_with_deku_leaf_indoors(state, player) + ) + + +def can_reach_end_of_wind_temple_many_cyclones_room(state: CollectionState, player: int) -> bool: + return can_reach_wind_temple_kidnapping_room(state, player) and ( + ( + state.has("Iron Boots", player) + and can_fan_with_deku_leaf(state, player) + and can_fly_with_deku_leaf_indoors(state, player) + and can_cut_grass(state, player) + ) + or ( + state.has("Hookshot", player) + and can_defeat_blue_bubbles(state, player) + and can_fly_with_deku_leaf_indoors(state, player) + ) + or ( + state.has("Hookshot", player) + and can_fly_with_deku_leaf_indoors(state, player) + and state._tww_obscure_1(player) + and state._tww_precise_2(player) + ) + ) + + +def can_open_wind_temple_upper_giant_grate(state: CollectionState, player: int) -> bool: + return can_reach_end_of_wind_temple_many_cyclones_room(state, player) and state.has("Iron Boots", player) + + +def can_activate_wind_temple_giant_fan(state: CollectionState, player: int) -> bool: + return can_open_wind_temple_upper_giant_grate(state, player) and can_play_command_melody(state, player) + + +def can_open_wind_temple_lower_giant_grate(state: CollectionState, player: int) -> bool: + return ( + can_reach_wind_temple_kidnapping_room(state, player) + and state.has("Hookshot", player) + and can_defeat_blue_bubbles(state, player) + ) + + +def can_reach_wind_temple_tall_basement_room(state: CollectionState, player: int) -> bool: + return ( + can_open_wind_temple_upper_giant_grate(state, player) + and can_open_wind_temple_lower_giant_grate(state, player) + and state.has("WT Small Key", player, 2) + ) + + +def can_access_dungeon_entrance_on_dragon_roost_island(state: CollectionState, player: int) -> bool: + return True + + +def can_access_forest_haven(state: CollectionState, player: int) -> bool: + return state.has("Grappling Hook", player) or can_fly_with_deku_leaf_outdoors(state, player) + + +def can_access_dungeon_entrance_in_forest_haven_sector(state: CollectionState, player: int) -> bool: + return ( + can_access_forest_haven(state, player) + and ( + state.has("Grappling Hook", player) + or ( + can_fly_with_deku_leaf_indoors(state, player) + and can_fly_with_deku_leaf_outdoors(state, player) + and state._tww_obscure_1(player) + and state._tww_precise_1(player) + ) + ) + and can_fly_with_deku_leaf_outdoors(state, player) + and (can_cut_grass(state, player) or has_magic_meter_upgrade(state, player)) + ) + + +def can_access_dungeon_entrance_in_tower_of_the_gods_sector(state: CollectionState, player: int) -> bool: + return state.has_group_unique("Pearls", player, 3) + + +def can_access_dungeon_entrance_in_forsaken_fortress_sector(state: CollectionState, player: int) -> bool: + return False + + +def can_access_dungeon_entrance_on_headstone_island(state: CollectionState, player: int) -> bool: + return state.has("Power Bracelets", player) + + +def can_access_dungeon_entrance_on_gale_isle(state: CollectionState, player: int) -> bool: + return state.has_all(["Iron Boots", "Skull Hammer"], player) + + +def can_access_miniboss_entrance_in_forbidden_woods(state: CollectionState, player: int) -> bool: + return ( + can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "FW Small Key"], player) + ) + + +def can_access_miniboss_entrance_in_tower_of_the_gods(state: CollectionState, player: int) -> bool: + return ( + can_reach_tower_of_the_gods_second_floor(state, player) + and (state.has("Grappling Hook", player) or can_fly_with_deku_leaf_indoors(state, player)) + and (can_play_command_melody(state, player) or has_heros_bow(state, player)) + ) + + +def can_access_miniboss_entrance_in_earth_temple(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_moblins_and_poes_room(state, player) and state.has("ET Small Key", player, 3) + + +def can_access_miniboss_entrance_in_wind_temple(state: CollectionState, player: int) -> bool: + return can_open_wind_temple_upper_giant_grate(state, player) and state.has("WT Small Key", player, 2) + + +def can_access_miniboss_entrance_in_hyrule_castle(state: CollectionState, player: int) -> bool: + return can_access_hyrule(state, player) + + +def can_access_boss_entrance_in_dragon_roost_cavern(state: CollectionState, player: int) -> bool: + return can_reach_dragon_roost_cavern_boss_stairs(state, player) and state.has("DRC Big Key", player) + + +def can_access_boss_entrance_in_forbidden_woods(state: CollectionState, player: int) -> bool: + return ( + can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and (can_defeat_door_flowers(state, player) or state.has("Grappling Hook", player)) + and can_defeat_mothulas(state, player) + and state.has("FW Big Key", player) + ) + + +def can_access_boss_entrance_in_tower_of_the_gods(state: CollectionState, player: int) -> bool: + return ( + can_reach_tower_of_the_gods_third_floor(state, player) + and can_defeat_armos(state, player) + and state.has("TotG Big Key", player) + ) + + +def can_access_boss_entrance_in_forsaken_fortress(state: CollectionState, player: int) -> bool: + return ( + can_get_inside_forsaken_fortress(state, player) + and state.has("Skull Hammer", player) + and ( + can_fly_with_deku_leaf_indoors(state, player) + or state.has("Hookshot", player) + or (state._tww_obscure_2(player) and state._tww_precise_2(player)) + ) + and ( + can_defeat_bokoblins(state, player) + or can_fly_with_deku_leaf_outdoors(state, player) + or state.has("Grappling Hook", player) + ) + ) + + +def can_access_boss_entrance_in_earth_temple(state: CollectionState, player: int) -> bool: + return can_reach_earth_temple_tall_vine_room(state, player) and state.has("ET Big Key", player) + + +def can_access_boss_entrance_in_wind_temple(state: CollectionState, player: int) -> bool: + return ( + can_reach_wind_temple_tall_basement_room(state, player) + and state.has_all(["Hookshot", "Iron Boots", "WT Big Key"], player) + and can_play_command_melody(state, player) + and can_play_wind_gods_aria(state, player) + ) + + +def can_access_secret_cave_entrance_on_outset_island(state: CollectionState, player: int) -> bool: + return ( + (can_reach_outset_island_upper_level(state, player) and can_fly_with_deku_leaf_outdoors(state, player)) + or state.has("Hookshot", player) + ) and state.has("Power Bracelets", player) + + +def can_access_secret_cave_entrance_on_dragon_roost_island(state: CollectionState, player: int) -> bool: + return can_move_boulders(state, player) + + +def can_access_secret_cave_entrance_on_fire_mountain(state: CollectionState, player: int) -> bool: + return has_ice_arrows(state, player) + + +def can_access_secret_cave_entrance_on_ice_ring_isle(state: CollectionState, player: int) -> bool: + return has_fire_arrows(state, player) + + +def can_access_secret_cave_entrance_on_private_oasis(state: CollectionState, player: int) -> bool: + return state.has_all(["Delivery Bag", "Cabana Deed", "Grappling Hook"], player) + + +def can_access_secret_cave_entrance_on_needle_rock_isle(state: CollectionState, player: int) -> bool: + return has_fire_arrows(state, player) + + +def can_access_secret_cave_entrance_on_angular_isles(state: CollectionState, player: int) -> bool: + return can_fly_with_deku_leaf_outdoors(state, player) or state.has("Hookshot", player) + + +def can_access_secret_cave_entrance_on_boating_course(state: CollectionState, player: int) -> bool: + return can_fly_with_deku_leaf_outdoors(state, player) or state.has("Hookshot", player) + + +def can_access_secret_cave_entrance_on_stone_watcher_island(state: CollectionState, player: int) -> bool: + return state.has("Power Bracelets", player) + + +def can_access_secret_cave_entrance_on_overlook_island(state: CollectionState, player: int) -> bool: + return state.has("Hookshot", player) + + +def can_access_secret_cave_entrance_on_birds_peak_rock(state: CollectionState, player: int) -> bool: + return state.has("Bait Bag", player) + + +def can_access_secret_cave_entrance_on_pawprint_isle(state: CollectionState, player: int) -> bool: + return True + + +def can_access_secret_cave_entrance_on_pawprint_isle_side_isle(state: CollectionState, player: int) -> bool: + return state.has("Hookshot", player) + + +def can_access_secret_cave_entrance_on_diamond_steppe_island(state: CollectionState, player: int) -> bool: + return state.has("Hookshot", player) + + +def can_access_secret_cave_entrance_on_bomb_island(state: CollectionState, player: int) -> bool: + return can_move_boulders(state, player) + + +def can_access_secret_cave_entrance_on_rock_spire_isle(state: CollectionState, player: int) -> bool: + return state.has("Bombs", player) + + +def can_access_secret_cave_entrance_on_shark_island(state: CollectionState, player: int) -> bool: + return state.has_all(["Iron Boots", "Skull Hammer"], player) + + +def can_access_secret_cave_entrance_on_cliff_plateau_isles(state: CollectionState, player: int) -> bool: + return True + + +def can_access_secret_cave_entrance_on_horseshoe_island(state: CollectionState, player: int) -> bool: + return can_fan_with_deku_leaf(state, player) + + +def can_access_secret_cave_entrance_on_star_island(state: CollectionState, player: int) -> bool: + return can_move_boulders(state, player) + + +def can_access_inner_entrance_in_ice_ring_isle_secret_cave(state: CollectionState, player: int) -> bool: + return state.has("Iron Boots", player) + + +def can_access_inner_entrance_in_cliff_plateau_isles_secret_cave(state: CollectionState, player: int) -> bool: + return can_defeat_boko_babas(state, player) and can_fly_with_deku_leaf_indoors(state, player) + + +def can_access_fairy_fountain_entrance_on_outset_island(state: CollectionState, player: int) -> bool: + return can_access_forest_of_fairies(state, player) and can_move_boulders(state, player) + + +def can_access_fairy_fountain_entrance_on_thorned_fairy_island(state: CollectionState, player: int) -> bool: + return state.has("Skull Hammer", player) + + +def can_access_fairy_fountain_entrance_on_eastern_fairy_island(state: CollectionState, player: int) -> bool: + return can_move_boulders(state, player) + + +def can_access_fairy_fountain_entrance_on_western_fairy_island(state: CollectionState, player: int) -> bool: + return state.has("Skull Hammer", player) + + +def can_access_fairy_fountain_entrance_on_southern_fairy_island(state: CollectionState, player: int) -> bool: + return state.has("Bombs", player) or has_fire_arrows(state, player) + + +def can_access_fairy_fountain_entrance_on_northern_fairy_island(state: CollectionState, player: int) -> bool: + return True + + +def can_get_past_forsaken_fortress_gate(state: CollectionState, player: int) -> bool: + return ( + state.has("Bombs", player) + or (state._tww_obscure_1(player) and state._tww_precise_1(player)) + or (can_open_ganons_tower_dark_portal(state, player) and state._tww_obscure_1(player)) + ) + + +def can_get_inside_forsaken_fortress(state: CollectionState, player: int) -> bool: + return can_get_past_forsaken_fortress_gate(state, player) and state.has("Skull Hammer", player) + + +def can_reach_and_defeat_phantom_ganon(state: CollectionState, player: int) -> bool: + return can_get_past_forsaken_fortress_gate(state, player) and can_defeat_phantom_ganon(state, player) + + +def can_defeat_phantom_ganon(state: CollectionState, player: int) -> bool: + return (state._tww_outside_swordless_mode(player) and has_any_master_sword(state, player)) or ( + state._tww_in_swordless_mode(player) and state.has("Skull Hammer", player) + ) + + +def can_access_hyrule(state: CollectionState, player: int) -> bool: + return has_all_8_triforce_shards(state, player) + + +def can_get_past_hyrule_barrier(state: CollectionState, player: int) -> bool: + return can_access_hyrule(state, player) and ( + has_full_power_master_sword(state, player) or state._tww_in_swordless_mode(player) + ) + + +def can_access_ganons_tower(state: CollectionState, player: int) -> bool: + return can_get_past_hyrule_barrier(state, player) and ( + state.has("Hookshot", player) or can_fly_with_deku_leaf_indoors(state, player) + ) + + +def can_complete_memory_dragon_roost_cavern_and_gohma(state: CollectionState, player: int) -> bool: + return ( + state.has("Grappling Hook", player) + and can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_gohma(state, player) + ) + + +def can_complete_memory_forbidden_woods_and_kalle_demos(state: CollectionState, player: int) -> bool: + return ( + can_fan_with_deku_leaf(state, player) + and can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_kalle_demos(state, player) + ) + + +def can_complete_memory_earth_temple_and_jalhalla(state: CollectionState, player: int) -> bool: + return can_defeat_jalhalla(state, player) + + +def can_complete_memory_wind_temple_and_molgera(state: CollectionState, player: int) -> bool: + return can_fly_with_deku_leaf_indoors(state, player) and can_defeat_molgera(state, player) + + +def can_complete_all_memory_dungeons_and_bosses(state: CollectionState, player: int) -> bool: + return ( + can_complete_memory_dragon_roost_cavern_and_gohma(state, player) + and can_complete_memory_forbidden_woods_and_kalle_demos(state, player) + and can_complete_memory_earth_temple_and_jalhalla(state, player) + and can_complete_memory_wind_temple_and_molgera(state, player) + ) + + +def can_unlock_ganons_tower_four_boss_door(state: CollectionState, player: int) -> bool: + return can_complete_all_memory_dungeons_and_bosses(state, player) or state._tww_rematch_bosses_skipped(player) + + +def can_reach_ganons_tower_phantom_ganon_room(state: CollectionState, player: int) -> bool: + return can_access_ganons_tower(state, player) and can_unlock_ganons_tower_four_boss_door(state, player) + + +def can_open_ganons_tower_dark_portal(state: CollectionState, player: int) -> bool: + return can_reach_ganons_tower_phantom_ganon_room(state, player) and state.has("Boomerang", player) + + +def can_reach_and_defeat_puppet_ganon(state: CollectionState, player: int) -> bool: + return ( + can_reach_ganons_tower_phantom_ganon_room(state, player) + and has_light_arrows(state, player) + and can_unlock_puppet_ganon_door(state, player) + and can_defeat_puppet_ganon(state, player) + ) + + +def can_unlock_puppet_ganon_door(state: CollectionState, player: int) -> bool: + return ( + can_defeat_moblins(state, player) + and can_defeat_mighty_darknuts(state, player) + and ( + state._tww_outside_required_bosses_mode(player) + or (state._tww_in_required_bosses_mode(player) and state._tww_can_defeat_all_required_bosses(player)) + ) + ) + + +def can_defeat_puppet_ganon(state: CollectionState, player: int) -> bool: + return has_light_arrows(state, player) and (state.has("Boomerang", player) or state._tww_precise_2(player)) + + +def can_reach_and_defeat_ganondorf(state: CollectionState, player: int) -> bool: + return ( + can_reach_and_defeat_puppet_ganon(state, player) + and state.has_all(["Grappling Hook", "Hookshot"], player) + and can_defeat_ganondorf(state, player) + ) + + +def can_defeat_ganondorf(state: CollectionState, player: int) -> bool: + return (has_heros_sword(state, player) or state._tww_in_swordless_mode(player)) and ( + has_heros_shield(state, player) or (state.has("Skull Hammer", player) and state._tww_obscure_2(player)) + ) + + +def can_farm_knights_crests(state: CollectionState, player: int) -> bool: + return state.has_all(["Grappling Hook", "Spoils Bag"], player) and ( + # (Can Access Item Location "Ice Ring Isle - Inner Cave - Chest") + (state.can_reach_region("Ice Ring Isle Inner Cave", player) and has_fire_arrows(state, player)) + # | (Can Access Item Location "Outset Island - Savage Labyrinth - Floor 30") + or ( + state.can_reach_region("Savage Labyrinth", player) + and can_defeat_keese(state, player) + and can_defeat_miniblins(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_magtails(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_peahats(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_boko_babas(state, player) + and can_defeat_mothulas(state, player) + and can_defeat_winged_mothulas(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_armos(state, player) + and can_defeat_yellow_chuchus(state, player) + and can_defeat_red_bubbles(state, player) + and can_defeat_darknuts(state, player) + and can_play_winds_requiem(state, player) + and (state.has_any(["Grappling Hook", "Skull Hammer"], player) or has_heros_sword(state, player)) + ) + # | (Can Access Item Location "Earth Temple - Big Key Chest" & Can Defeat Darknuts Easily) + or ( + state.can_reach_region("Earth Temple", player) + and can_reach_earth_temple_many_mirrors_room(state, player) + and state.has("Power Bracelets", player) + and can_play_command_melody(state, player) + and can_aim_mirror_shield(state, player) + and ( + can_defeat_blue_bubbles(state, player) + or (has_heros_bow(state, player) and state._tww_obscure_1(player)) + or ( + ( + has_heros_sword(state, player) + or has_any_master_sword(state, player) + or state.has("Skull Hammer", player) + ) + and state._tww_obscure_1(player) + and state._tww_precise_1(player) + ) + ) + and can_defeat_darknuts_easily(state, player) + ) + # | (Can Access Item Location "Wind Temple - Big Key Chest" & Can Defeat Darknuts Easily) + or ( + state.can_reach_region("Wind Temple", player) + and can_reach_wind_temple_kidnapping_room(state, player) + and state.has("Iron Boots", player) + and can_fan_with_deku_leaf(state, player) + and can_play_wind_gods_aria(state, player) + and can_defeat_darknuts_easily(state, player) + ) + # | (Can Access Item Location "Shark Island - Cave") + or (state.can_reach_region("Shark Island Secret Cave", player) and can_defeat_miniblins(state, player)) + # | (Can Access Item Location "Stone Watcher Island - Cave" & Can Defeat Darknuts Easily) + or ( + state.can_reach_region("Stone Watcher Island Secret Cave", player) + and can_defeat_armos(state, player) + and can_defeat_wizzrobes(state, player) + and can_play_winds_requiem(state, player) + and can_defeat_darknuts_easily(state, player) + ) + # | (Can Access Item Location "Overlook Island - Cave" & Can Defeat Darknuts Easily) + or ( + state.can_reach_region("Overlook Island Secret Cave", player) + and can_defeat_stalfos(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_keese(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_morths(state, player) + and can_defeat_kargarocs(state, player) + and can_play_winds_requiem(state, player) + and can_defeat_darknuts_easily(state, player) + ) + # | (Can Access Hyrule) + or can_access_hyrule(state, player) + ) + + +def can_farm_joy_pendants(state: CollectionState, player: int) -> bool: + return state.has_all(["Grappling Hook", "Spoils Bag"], player) + + +def can_farm_skull_necklaces(state: CollectionState, player: int) -> bool: + return state.has_all(["Grappling Hook", "Spoils Bag"], player) + + +def can_farm_golden_feathers(state: CollectionState, player: int) -> bool: + return state.has_all(["Grappling Hook", "Spoils Bag"], player) + + +def can_farm_green_chu_jelly(state: CollectionState, player: int) -> bool: + return state.has_all(["Grappling Hook", "Spoils Bag"], player) + + +def can_obtain_15_blue_chu_jelly(state: CollectionState, player: int) -> bool: + return ( + can_get_blue_chu_jelly_from_blue_chuchus(state, player) + and ( + state.has_any(["Hookshot", "Bombs", "Grappling Hook"], player) + or can_move_boulders(state, player) + or ( + can_access_secret_cave_entrance_on_shark_island(state, player) + and can_fly_with_deku_leaf_outdoors(state, player) + ) + or state.can_reach_region("Cliff Plateau Isles Inner Cave", player) + or can_fan_with_deku_leaf(state, player) + or can_access_secret_cave_entrance_on_boating_course(state, player) + ) + and state.has("Spoils Bag", player) + ) + + +def can_defeat_bokoblins(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_moblins(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_darknuts(state: CollectionState, player: int) -> bool: + return has_heros_sword(state, player) or has_light_arrows(state, player) or state.has("Skull Hammer", player) + + +def can_defeat_darknuts_easily(state: CollectionState, player: int) -> bool: + return has_heros_sword(state, player) or has_light_arrows(state, player) + + +def can_defeat_mighty_darknuts(state: CollectionState, player: int) -> bool: + return can_defeat_darknuts_easily(state, player) or ( + state.has("Skull Hammer", player) and state._tww_precise_3(player) + ) + + +def can_defeat_miniblins(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Grappling Hook", "Boomerang", "Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_miniblins_easily(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_red_chuchus(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Skull Hammer", "Bombs"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_green_chuchus(state: CollectionState, player: int) -> bool: + return can_defeat_red_chuchus(state, player) + + +def can_defeat_yellow_chuchus(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or (state.has("Boomerang", player) and has_heros_sword(state, player)) + or has_heros_bow(state, player) + or (can_fan_with_deku_leaf(state, player) and has_heros_sword(state, player)) + or ( + state.has("Grappling Hook", player) + and has_heros_sword(state, player) + and state._tww_obscure_1(player) + and state._tww_precise_2(player) + ) + ) + + +def can_defeat_blue_chuchus(state: CollectionState, player: int) -> bool: + return can_defeat_yellow_chuchus(state, player) + + +def can_get_blue_chu_jelly_from_blue_chuchus(state: CollectionState, player: int) -> bool: + return can_defeat_blue_chuchus(state, player) or state.has("Grappling Hook", player) + + +def can_defeat_keese(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Hookshot", "Grappling Hook", "Boomerang", "Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_fire_keese(state: CollectionState, player: int) -> bool: + return can_defeat_keese(state, player) + + +def can_defeat_magtails(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Skull Hammer", "Boomerang", "Hookshot", "Bombs", "Grappling Hook"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_stun_magtails(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Skull Hammer", "Boomerang", "Hookshot", "Bombs", "Grappling Hook"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_kargarocs(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Skull Hammer", "Bombs"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_peahats(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Skull Hammer", "Bombs"], player) + or (state.has("Hookshot", player) and has_heros_sword(state, player)) + or (can_fan_with_deku_leaf(state, player) and has_heros_sword(state, player)) + or has_heros_bow(state, player) + ) + + +def can_remove_peahat_armor(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Hookshot", "Skull Hammer", "Bombs"], player) + or can_fan_with_deku_leaf(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_seahats(state: CollectionState, player: int) -> bool: + return state.has_any(["Boomerang", "Hookshot", "Bombs"], player) or has_heros_bow(state, player) + + +def can_defeat_boko_babas(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Skull Hammer", "Hookshot", "Bombs"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + or (can_fan_with_deku_leaf(state, player) and state.has("Grappling Hook", player)) + ) + + +def can_defeat_mothulas(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_winged_mothulas(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_wizzrobes(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Hookshot", "Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_wizzrobes_at_range(state: CollectionState, player: int) -> bool: + return has_heros_bow(state, player) or (state.has("Hookshot", player) and state._tww_precise_1(player)) + + +def can_defeat_armos(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer", "Hookshot"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_big_octos(state: CollectionState, player: int) -> bool: + return state.has_any(["Bombs", "Boomerang"], player) or has_heros_bow(state, player) + + +def can_defeat_12_eye_big_octos(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Boomerang"], player) + or (has_heros_bow(state, player) and has_any_quiver_upgrade(state, player)) + or has_light_arrows(state, player) + ) + + +def can_defeat_red_bubbles(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Skull Hammer", "Bombs"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + or ( + (can_fan_with_deku_leaf(state, player) or state.has("Hookshot", player)) + and state.has("Grappling Hook", player) + ) + ) + + +def can_defeat_blue_bubbles(state: CollectionState, player: int) -> bool: + return ( + has_ice_arrows(state, player) + or state.has("Bombs", player) + or ( + (can_fan_with_deku_leaf(state, player) or state.has("Hookshot", player)) + and ( + state.has_any(["Grappling Hook", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + ) + ) + + +def can_defeat_redeads(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Skull Hammer", "Bombs"], player) + or has_heros_sword(state, player) + or has_light_arrows(state, player) + ) + + +def can_defeat_jalhalla_poes(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_stalfos(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_light_arrows(state, player) + ) + + +def can_defeat_floormasters(state: CollectionState, player: int) -> bool: + return ( + has_heros_sword(state, player) + or has_heros_bow(state, player) + or (state.has("Skull Hammer", player) and state._tww_precise_1(player)) + ) + + +def can_defeat_morths(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Hookshot"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_bombchus(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Skull Hammer", "Grappling Hook"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_defeat_gohma(state: CollectionState, player: int) -> bool: + return state.has("Grappling Hook", player) + + +def can_defeat_kalle_demos(state: CollectionState, player: int) -> bool: + return state.has("Boomerang", player) + + +def can_defeat_gohdan(state: CollectionState, player: int) -> bool: + return ( + has_heros_bow(state, player) + or (state.has("Hookshot", player) and state._tww_obscure_1(player) and state._tww_precise_2(player)) + ) and state.has("Bombs", player) + + +def can_defeat_helmaroc_king(state: CollectionState, player: int) -> bool: + return state.has("Skull Hammer", player) + + +def can_defeat_jalhalla(state: CollectionState, player: int) -> bool: + return ( + (can_aim_mirror_shield(state, player) or has_light_arrows(state, player)) + and state.has("Power Bracelets", player) + and can_defeat_jalhalla_poes(state, player) + ) + + +def can_defeat_molgera(state: CollectionState, player: int) -> bool: + return state.has("Hookshot", player) and ( + state.has_any(["Boomerang", "Grappling Hook", "Skull Hammer", "Bombs"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + ) + + +def can_destroy_cannons(state: CollectionState, player: int) -> bool: + return state.has_any(["Bombs", "Boomerang"], player) + + +def can_cut_down_outset_trees(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Boomerang", "Skull Hammer"], player) + or has_heros_sword(state, player) + or (state.has("Power Bracelets", player) and state._tww_obscure_3(player)) + ) + + +def can_cut_down_hanging_drc_platform(state: CollectionState, player: int) -> bool: + return ( + state.has_any(["Bombs", "Skull Hammer"], player) + or has_heros_sword(state, player) + or has_heros_bow(state, player) + or (state.has("Hookshot", player) and state._tww_precise_1(player)) + or (state.has("Grappling Hook", player) and state._tww_precise_1(player)) + ) + + +def can_cut_grass(state: CollectionState, player: int) -> bool: + return state.has_any(["Skull Hammer", "Boomerang", "Bombs"], player) or has_heros_sword(state, player) + + +def can_sword_fight_with_orca(state: CollectionState, player: int) -> bool: + return has_heros_sword(state, player) or state._tww_in_swordless_mode(player) diff --git a/worlds/tww/Options.py b/worlds/tww/Options.py new file mode 100644 index 00000000..6e7724e2 --- /dev/null +++ b/worlds/tww/Options.py @@ -0,0 +1,854 @@ +from dataclasses import dataclass + +from Options import ( + Choice, + DeathLink, + DefaultOnToggle, + OptionGroup, + OptionSet, + PerGameCommonOptions, + Range, + StartInventoryPool, + Toggle, +) + +from .Locations import DUNGEON_NAMES + + +class Dungeons(DefaultOnToggle): + """ + This controls whether dungeon locations are randomized. + """ + + display_name = "Dungeons" + + +class TingleChests(Toggle): + """ + Tingle Chests are hidden in dungeons and must be bombed to make them appear. (2 in DRC, 1 each in FW, TotG, ET, and + WT). This controls whether they are randomized. + """ + + display_name = "Tingle Chests" + + +class DungeonSecrets(Toggle): + """ + DRC, FW, TotG, ET, and WT each contain 2-3 secret items (11 in total). This controls whether these are randomized. + + The items are relatively well-hidden (they aren't in chests), so don't select this option unless you're prepared to + search each dungeon high and low! + """ + + display_name = "Dungeon Secrets" + + +class PuzzleSecretCaves(DefaultOnToggle): + """ + This controls whether the rewards from puzzle-focused secret caves are randomized locations. + """ + + display_name = "Puzzle Secret Caves" + + +class CombatSecretCaves(Toggle): + """ + This controls whether the rewards from combat-focused secret caves (besides Savage Labyrinth) are randomized + locations. + """ + + display_name = "Combat Secret Caves" + + +class SavageLabyrinth(Toggle): + """ + This controls whether the two locations in Savage Labyrinth are randomized. + """ + + display_name = "Savage Labyrinth" + + +class GreatFairies(DefaultOnToggle): + """ + This controls whether the items given by Great Fairies are randomized. + """ + + display_name = "Great Fairies" + + +class ShortSidequests(Toggle): + """ + This controls whether sidequests that can be completed quickly are randomized. + """ + + display_name = "Short Sidequests" + + +class LongSidequests(Toggle): + """ + This controls whether long sidequests (e.g., Lenzo's assistant, withered trees, goron trading) are randomized. + """ + + display_name = "Long Sidequests" + + +class SpoilsTrading(Toggle): + """ + This controls whether the items you get by trading in spoils to NPCs are randomized. + """ + + display_name = "Spoils Trading" + + +class Minigames(Toggle): + """ + This controls whether most minigames are randomized (auctions, mail sorting, barrel shooting, bird-man contest). + """ + + display_name = "Minigames" + + +class Battlesquid(Toggle): + """ + This controls whether the Windfall battleship minigame is randomized. + """ + + display_name = "Battlesquid Minigame" + + +class FreeGifts(DefaultOnToggle): + """ + This controls whether gifts freely given by NPCs are randomized (Tott, Salvage Corp, imprisoned Tingle). + """ + + display_name = "Free Gifts" + + +class Mail(Toggle): + """ + This controls whether items received from the mail are randomized. + """ + + display_name = "Mail" + + +class PlatformsRafts(Toggle): + """ + This controls whether lookout platforms and rafts are randomized. + """ + + display_name = "Lookout Platforms and Rafts" + + +class Submarines(Toggle): + """ + This controls whether submarines are randomized. + """ + + display_name = "Submarines" + + +class EyeReefChests(Toggle): + """ + This controls whether the chests that appear after clearing out the eye reefs are randomized. + """ + + display_name = "Eye Reef Chests" + + +class BigOctosGunboats(Toggle): + """ + This controls whether the items dropped by Big Octos and Gunboats are randomized. + """ + + display_name = "Big Octos and Gunboats" + + +class TriforceCharts(Toggle): + """ + This controls whether the sunken treasure chests marked on Triforce Charts are randomized. + """ + + display_name = "Sunken Treasure (From Triforce Charts)" + + +class TreasureCharts(Toggle): + """ + This controls whether the sunken treasure chests marked on Treasure Charts are randomized. + """ + + display_name = "Sunken Treasure (From Treasure Charts)" + + +class ExpensivePurchases(DefaultOnToggle): + """ + This controls whether items that cost many Rupees are randomized (Rock Spire shop, auctions, Tingle's letter, + trading quest). + """ + + display_name = "Expensive Purchases" + + +class IslandPuzzles(Toggle): + """ + This controls whether various island puzzles are randomized (e.g., chests hidden in unusual places). + """ + + display_name = "Island Puzzles" + + +class Misc(Toggle): + """ + Miscellaneous locations that don't fit into any of the above categories (outdoors chests, wind shrine, Cyclos, etc). + This controls whether these are randomized. + """ + + display_name = "Miscellaneous" + + +class DungeonItem(Choice): + """ + This is the base class for the shuffle options for dungeon items. + """ + + value: int + option_startwith = 0 + option_vanilla = 1 + option_dungeon = 2 + option_any_dungeon = 3 + option_local = 4 + option_keylunacy = 5 + default = 2 + + @property + def in_dungeon(self) -> bool: + """ + Return whether the item should be shuffled into a dungeon. + + :return: Whether the item is shuffled into a dungeon. + """ + return self.value in (2, 3) + + +class RandomizeMapCompass(DungeonItem): + """ + Controls how dungeon maps and compasses are randomized. + + - **Start With Maps & Compasses:** You will start the game with the dungeon maps and compasses for all dungeons. + - **Vanilla Maps & Compasses:** Dungeon maps and compasses will be kept in their vanilla location (non-randomized). + - **Own Dungeon Maps & Compasses:** Dungeon maps and compasses will be randomized locally within their own dungeon. + - **Any Dungeon Maps & Compasses:** Dungeon maps and compasses will be randomized locally within any dungeon. + - **Local Maps & Compasses:** Dungeon maps and compasses will be randomized locally anywhere. + - **Key-Lunacy:** Dungeon maps and compasses can be found anywhere, without restriction. + """ + + item_name_group = "Dungeon Items" + display_name = "Randomize Maps & Compasses" + default = 2 + + +class RandomizeSmallKeys(DungeonItem): + """ + Controls how small keys are randomized. + + - **Start With Small Keys:** You will start the game with the small keys for all dungeons. + - **Vanilla Small Keys:** Small keys will be kept in their vanilla location (non-randomized). + - **Own Dungeon Small Keys:** Small keys will be randomized locally within their own dungeon. + - **Any Dungeon Small Keys:** Small keys will be randomized locally within any dungeon. + - **Local Small Keys:** Small keys will be randomized locally anywhere. + - **Key-Lunacy:** Small keys can be found in any progression location, if dungeons are randomized. + """ + + item_name_group = "Small Keys" + display_name = "Randomize Small Keys" + default = 2 + + +class RandomizeBigKeys(DungeonItem): + """ + Controls how big keys are randomized. + + - **Start With Big Keys:** You will start the game with the big keys for all dungeons. + - **Vanilla Big Keys:** Big keys will be kept in their vanilla location (non-randomized). + - **Own Dungeon Big Keys:** Big keys will be randomized locally within their own dungeon. + - **Any Dungeon Big Keys:** Big keys will be randomized locally within any dungeon. + - **Local Big Keys:** Big keys will be randomized locally anywhere. + - **Key-Lunacy:** Big keys can be found in any progression location, if dungeons are randomized. + """ + + item_name_group = "Big Keys" + display_name = "Randomize Big Keys" + default = 2 + + +class SwordMode(Choice): + """ + Controls whether you start with the Hero's Sword, the Hero's Sword is randomized, or if there are no swords in the + entire game. + + - **Start with Hero's Sword:** You will start the game with the basic Hero's Sword already in your inventory. + - **No Starting Sword:** You will start the game with no sword, and have to find it somewhere in the world like + other randomized items. + - **Swords Optional:** You will start the game with no sword, but they'll still be randomized. However, they are not + necessary to beat the game. The Hyrule Barrier will be gone, Phantom Ganon in FF is vulnerable to Skull Hammer, + and the logic does not expect you to have a sword. + - **Swordless:** You will start the game with no sword, and won't be able to find it anywhere. You have to beat the + entire game using other items as weapons instead of the sword. (Note that Phantom Ganon in FF becomes vulnerable + to Skull Hammer in this mode.) + """ + + display_name = "Sword Mode" + option_start_with_sword = 0 + option_no_starting_sword = 1 + option_swords_optional = 2 + option_swordless = 3 + default = 0 + + +class RequiredBosses(Toggle): + """ + In this mode, you will not be allowed to beat the game until certain randomly-chosen bosses are defeated. Nothing in + dungeons for other bosses will ever be required. + + You can see which islands have the required bosses on them by opening the sea chart and checking which islands have + blue quest markers. + """ + + display_name = "Required Bosses Mode" + + +class NumRequiredBosses(Range): + """ + Select the number of randomly-chosen bosses that are required in Required Bosses Mode. + + The door to Puppet Ganon will not unlock until you've defeated all of these bosses. Nothing in dungeons for other + bosses will ever be required. + """ + + display_name = "Number of Required Bosses" + range_start = 1 + range_end = 6 + default = 4 + + +class IncludedDungeons(OptionSet): + """ + A list of dungeons that should always be included when required bosses mode is on. + """ + + display_name = "Included Dungeons" + valid_keys = frozenset(DUNGEON_NAMES) + + +class ExcludedDungeons(OptionSet): + """ + A list of dungeons that should always be excluded when required bosses mode is on. + """ + + display_name = "Excluded Dungeons" + valid_keys = frozenset(DUNGEON_NAMES) + + +class ChestTypeMatchesContents(Toggle): + """ + Changes the chest type to reflect its contents. A metal chest has a progress item, a wooden chest has a non-progress + item or a consumable, and a green chest has a potentially required dungeon key. + """ + + display_name = "Chest Type Matches Contents" + + +class TrapChests(Toggle): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + Allows the randomizer to place several trapped chests across the game that do not give you items. Perfect for + spicing up any run! + """ + + display_name = "Enable Trap Chests" + + +class HeroMode(Toggle): + """ + In Hero Mode, you take four times more damage than normal and heart refills will not drop. + """ + + display_name = "Hero Mode" + + +class LogicObscurity(Choice): + """ + Obscure tricks are ways of obtaining items that are not obvious and may involve thinking outside the box. + + This option controls the maximum difficulty of obscure tricks the randomizer will require you to do to beat the + game. + """ + + display_name = "Obscure Tricks Required" + option_none = 0 + option_normal = 1 + option_hard = 2 + option_very_hard = 3 + default = 0 + + +class LogicPrecision(Choice): + """ + Precise tricks are ways of obtaining items that involve difficult inputs such as accurate aiming or perfect timing. + + This option controls the maximum difficulty of precise tricks the randomizer will require you to do to beat the + game. + """ + + display_name = "Precise Tricks Required" + option_none = 0 + option_normal = 1 + option_hard = 2 + option_very_hard = 3 + default = 0 + + +class EnableTunerLogic(Toggle): + """ + If enabled, the randomizer can logically expect the Tingle Tuner for Tingle Chests. + + The randomizer behavior of logically expecting Bombs/bomb flowers to spawn in Tingle Chests remains unchanged. + """ + + display_name = "Enable Tuner Logic" + + +class RandomizeDungeonEntrances(Toggle): + """ + Shuffles around which dungeon entrances take you into which dungeons. + + (No effect on Forsaken Fortress or Ganon's Tower.) + """ + + display_name = "Randomize Dungeons" + + +class RandomizeSecretCavesEntrances(Toggle): + """ + Shuffles around which secret cave entrances take you into which secret caves. + """ + + display_name = "Randomize Secret Caves" + + +class RandomizeMinibossEntrances(Toggle): + """ + Allows dungeon miniboss doors to act as entrances to be randomized. + + If on with random dungeon entrances, dungeons may nest within each other, forming chains of connected dungeons. + """ + + display_name = "Randomize Nested Minibosses" + + +class RandomizeBossEntrances(Toggle): + """ + Allows dungeon boss doors to act as entrances to be randomized. + + If on with random dungeon entrances, dungeons may nest within each other, forming chains of connected dungeons. + """ + + display_name = "Randomize Nested Bosses" + + +class RandomizeSecretCaveInnerEntrances(Toggle): + """ + Allows the pit in Ice Ring Isle's secret cave and the rear exit out of Cliff Plateau Isles' secret cave to act as + entrances to be randomized.""" + + display_name = "Randomize Inner Secret Caves" + + +class RandomizeFairyFountainEntrances(Toggle): + """ + Allows the pits that lead down into Fairy Fountains to act as entrances to be randomized. + """ + + display_name = "Randomize Fairy Fountains" + + +class MixEntrances(Choice): + """ + Controls how the different types (pools) of randomized entrances should be shuffled. + + - **Separate Pools:** Each pool of randomized entrances will shuffle into itself (e.g., dungeons into dungeons). + - **Mix Pools:** All pools of randomized entrances will be combined into one pool to be shuffled. + """ + + display_name = "Mix Entrances" + option_separate_pools = 0 + option_mix_pools = 1 + default = 0 + + +class RandomizeEnemies(Toggle): + """ + Randomizes the placement of non-boss enemies. + + This option is an *incomplete* option from the base randomizer and **may result in unbeatable seeds! Use at your own + risk!** + """ + + display_name = "Randomize Enemies" + + +# class RandomizeMusic(Toggle): +# """ +# Shuffles around all the music in the game. This affects background music, combat music, fanfares, etc. +# """ + +# display_name = "Randomize Music" + + +class RandomizeStartingIsland(Toggle): + """ + Randomizes which island you start the game on. + """ + + display_name = "Randomize Starting Island" + + +class RandomizeCharts(Toggle): + """ + Randomizes which sector is drawn on each Triforce/Treasure Chart. + """ + + display_name = "Randomize Charts" + + +class HoHoHints(DefaultOnToggle): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + Places hints on Old Man Ho Ho. Old Man Ho Ho appears at 10 different islands in the game. Talk to Old Man Ho Ho to + get hints. + """ + + display_name = "Place Hints on Old Man Ho Ho" + + +class FishmenHints(DefaultOnToggle): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + Places hints on the fishmen. There is one fishman at each of the 49 islands of the Great Sea. Each fishman must be + fed an All-Purpose Bait before he will give a hint. + """ + + display_name = "Place Hints on Fishmen" + + +class KoRLHints(Toggle): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + Places hints on the King of Red Lions. Talk to the King of Red Lions to get hints. + """ + + display_name = "Place Hints on King of Red Lions" + + +class NumItemHints(Range): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + The number of item hints that will be placed. Item hints tell you which area contains a particular progress item in + this seed. + + If multiple hint placement options are selected, the hint count will be split evenly among the placement options. + """ + + display_name = "Item Hints" + range_start = 0 + range_end = 15 + default = 15 + + +class NumLocationHints(Range): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + The number of location hints that will be placed. Location hints tell you what item is at a specific location in + this seed. + + If multiple hint placement options are selected, the hint count will be split evenly among the placement options. + """ + + display_name = "Location Hints" + range_start = 0 + range_end = 15 + default = 5 + + +class NumBarrenHints(Range): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + The number of barren hints that will be placed. Barren hints tell you that an area does not contain any required + items in this seed. + + If multiple hint placement options are selected, the hint count will be split evenly among the placement options. + """ + + display_name = "Barren Hints" + range_start = 0 + range_end = 15 + default = 0 + + +class NumPathHints(Range): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + The number of path hints that will be placed. Path hints tell you that an area contains an item that is required to + reach a particular goal in this seed. + + If multiple hint placement options are selected, the hint count will be split evenly among the placement options. + """ + + display_name = "Path Hints" + range_start = 0 + range_end = 15 + default = 0 + + +class PrioritizeRemoteHints(Toggle): + """ + **DEV NOTE:** This option is currently unimplemented and will be ignored. + + When this option is selected, certain locations that are out of the way and time-consuming to complete will take + precedence over normal location hints.""" + + display_name = "Prioritize Remote Location Hints" + + +class SwiftSail(DefaultOnToggle): + """ + Sailing speed is doubled and the direction of the wind is always at your back as long as the sail is out. + """ + + display_name = "Swift Sail" + + +class InstantTextBoxes(DefaultOnToggle): + """ + Text appears instantly. Also, the B button is changed to instantly skip through text as long as you hold it down. + """ + + display_name = "Instant Text Boxes" + + +class RevealFullSeaChart(DefaultOnToggle): + """ + Start the game with the sea chart fully drawn out. + """ + + display_name = "Reveal Full Sea Chart" + + +class AddShortcutWarpsBetweenDungeons(Toggle): + """ + Adds new warp pots that act as shortcuts connecting dungeons to each other directly. (DRC, FW, TotG, and separately + FF, ET, WT.) + + Each pot must be unlocked before it can be used, so you cannot use them to access dungeons + you wouldn't already have access to. + """ + + display_name = "Add Shortcut Warps Between Dungeons" + + +class SkipRematchBosses(DefaultOnToggle): + """ + Removes the door in Ganon's Tower that only unlocks when you defeat the rematch versions of Gohma, Kalle Demos, + Jalhalla, and Molgera. + """ + + display_name = "Skip Boss Rematches" + + +class RemoveMusic(Toggle): + """ + Mutes all ingame music. + """ + + display_name = "Remove Music" + + +@dataclass +class TWWOptions(PerGameCommonOptions): + """ + A data class that encapsulates all configuration options for The Wind Waker. + """ + + start_inventory_from_pool: StartInventoryPool + progression_dungeons: Dungeons + progression_tingle_chests: TingleChests + progression_dungeon_secrets: DungeonSecrets + progression_puzzle_secret_caves: PuzzleSecretCaves + progression_combat_secret_caves: CombatSecretCaves + progression_savage_labyrinth: SavageLabyrinth + progression_great_fairies: GreatFairies + progression_short_sidequests: ShortSidequests + progression_long_sidequests: LongSidequests + progression_spoils_trading: SpoilsTrading + progression_minigames: Minigames + progression_battlesquid: Battlesquid + progression_free_gifts: FreeGifts + progression_mail: Mail + progression_platforms_rafts: PlatformsRafts + progression_submarines: Submarines + progression_eye_reef_chests: EyeReefChests + progression_big_octos_gunboats: BigOctosGunboats + progression_triforce_charts: TriforceCharts + progression_treasure_charts: TreasureCharts + progression_expensive_purchases: ExpensivePurchases + progression_island_puzzles: IslandPuzzles + progression_misc: Misc + randomize_mapcompass: RandomizeMapCompass + randomize_smallkeys: RandomizeSmallKeys + randomize_bigkeys: RandomizeBigKeys + sword_mode: SwordMode + required_bosses: RequiredBosses + num_required_bosses: NumRequiredBosses + included_dungeons: IncludedDungeons + excluded_dungeons: ExcludedDungeons + chest_type_matches_contents: ChestTypeMatchesContents + # trap_chests: TrapChests + hero_mode: HeroMode + logic_obscurity: LogicObscurity + logic_precision: LogicPrecision + enable_tuner_logic: EnableTunerLogic + randomize_dungeon_entrances: RandomizeDungeonEntrances + randomize_secret_cave_entrances: RandomizeSecretCavesEntrances + randomize_miniboss_entrances: RandomizeMinibossEntrances + randomize_boss_entrances: RandomizeBossEntrances + randomize_secret_cave_inner_entrances: RandomizeSecretCaveInnerEntrances + randomize_fairy_fountain_entrances: RandomizeFairyFountainEntrances + mix_entrances: MixEntrances + randomize_enemies: RandomizeEnemies + # randomize_music: RandomizeMusic + randomize_starting_island: RandomizeStartingIsland + randomize_charts: RandomizeCharts + # hoho_hints: HoHoHints + # fishmen_hints: FishmenHints + # korl_hints: KoRLHints + # num_item_hints: NumItemHints + # num_location_hints: NumLocationHints + # num_barren_hints: NumBarrenHints + # num_path_hints: NumPathHints + # prioritize_remote_hints: PrioritizeRemoteHints + swift_sail: SwiftSail + instant_text_boxes: InstantTextBoxes + reveal_full_sea_chart: RevealFullSeaChart + add_shortcut_warps_between_dungeons: AddShortcutWarpsBetweenDungeons + skip_rematch_bosses: SkipRematchBosses + remove_music: RemoveMusic + death_link: DeathLink + + +tww_option_groups: list[OptionGroup] = [ + OptionGroup( + "Progression Locations", + [ + Dungeons, + DungeonSecrets, + TingleChests, + PuzzleSecretCaves, + CombatSecretCaves, + SavageLabyrinth, + IslandPuzzles, + GreatFairies, + Submarines, + PlatformsRafts, + ShortSidequests, + LongSidequests, + SpoilsTrading, + EyeReefChests, + BigOctosGunboats, + Misc, + Minigames, + Battlesquid, + FreeGifts, + Mail, + ExpensivePurchases, + TriforceCharts, + TreasureCharts, + ], + ), + OptionGroup( + "Item Randomizer Modes", + [ + SwordMode, + RandomizeMapCompass, + RandomizeSmallKeys, + RandomizeBigKeys, + ChestTypeMatchesContents, + # TrapChests, + ], + ), + OptionGroup( + "Entrance Randomizer Options", + [ + RandomizeDungeonEntrances, + RandomizeBossEntrances, + RandomizeMinibossEntrances, + RandomizeSecretCavesEntrances, + RandomizeSecretCaveInnerEntrances, + RandomizeFairyFountainEntrances, + MixEntrances, + ], + ), + OptionGroup( + "Other Randomizers", + [ + RandomizeStartingIsland, + RandomizeCharts, + # RandomizeMusic, + ], + ), + OptionGroup( + "Convenience Tweaks", + [ + SwiftSail, + InstantTextBoxes, + RevealFullSeaChart, + SkipRematchBosses, + AddShortcutWarpsBetweenDungeons, + RemoveMusic, + ], + ), + OptionGroup( + "Required Bosses", + [ + RequiredBosses, + NumRequiredBosses, + IncludedDungeons, + ExcludedDungeons, + ], + start_collapsed=True, + ), + OptionGroup( + "Difficulty Options", + [ + HeroMode, + LogicObscurity, + LogicPrecision, + EnableTunerLogic, + ], + start_collapsed=True, + ), + OptionGroup( + "Work-in-Progress Options", + [ + RandomizeEnemies, + ], + start_collapsed=True, + ), +] diff --git a/worlds/tww/Presets.py b/worlds/tww/Presets.py new file mode 100644 index 00000000..28649426 --- /dev/null +++ b/worlds/tww/Presets.py @@ -0,0 +1,138 @@ +from typing import Any + +tww_options_presets: dict[str, dict[str, Any]] = { + "Tournament S7": { + "progression_dungeon_secrets": True, + "progression_combat_secret_caves": True, + "progression_short_sidequests": True, + "progression_spoils_trading": True, + "progression_big_octos_gunboats": True, + "progression_mail": True, + "progression_island_puzzles": True, + "progression_misc": True, + "randomize_mapcompass": "startwith", + "required_bosses": True, + "num_required_bosses": 3, + "chest_type_matches_contents": True, + "logic_obscurity": "hard", + "randomize_starting_island": True, + "add_shortcut_warps_between_dungeons": True, + "start_inventory_from_pool": { + "Telescope": 1, + "Wind Waker": 1, + "Goddess Tingle Statue": 1, + "Earth Tingle Statue": 1, + "Wind Tingle Statue": 1, + "Wind's Requiem": 1, + "Ballad of Gales": 1, + "Earth God's Lyric": 1, + "Wind God's Aria": 1, + "Song of Passing": 1, + "Progressive Magic Meter": 2, + }, + "start_location_hints": ["Ganon's Tower - Maze Chest"], + "exclude_locations": [ + "Outset Island - Orca - Give 10 Knight's Crests", + "Outset Island - Great Fairy", + "Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly", + "Windfall Island - Mrs. Marie - Give 21 Joy Pendants", + "Windfall Island - Mrs. Marie - Give 40 Joy Pendants", + "Windfall Island - Maggie's Father - Give 20 Skull Necklaces", + "Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers", + "Fire Mountain - Big Octo", + "Mailbox - Letter from Hoskit's Girlfriend", + "Private Oasis - Big Octo", + "Stone Watcher Island - Cave", + "Overlook Island - Cave", + "Thorned Fairy Island - Great Fairy", + "Eastern Fairy Island - Great Fairy", + "Western Fairy Island - Great Fairy", + "Southern Fairy Island - Great Fairy", + "Northern Fairy Island - Great Fairy", + "Tingle Island - Big Octo", + "Diamond Steppe Island - Big Octo", + "Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item", + "Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item", + "Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item", + "Shark Island - Cave", + "Seven-Star Isles - Big Octo", + ], + }, + "Miniblins 2025": { + "progression_great_fairies": False, + "progression_short_sidequests": True, + "progression_mail": True, + "progression_expensive_purchases": False, + "progression_island_puzzles": True, + "progression_misc": True, + "randomize_mapcompass": "startwith", + "required_bosses": True, + "num_required_bosses": 2, + "chest_type_matches_contents": True, + "randomize_starting_island": True, + "add_shortcut_warps_between_dungeons": True, + "start_inventory_from_pool": { + "Telescope": 1, + "Wind Waker": 1, + "Wind's Requiem": 1, + "Ballad of Gales": 1, + "Command Melody": 1, + "Earth God's Lyric": 1, + "Wind God's Aria": 1, + "Song of Passing": 1, + "Nayru's Pearl": 1, + "Din's Pearl": 1, + "Progressive Shield": 1, + "Progressive Magic Meter": 2, + "Quiver Capacity Upgrade": 1, + "Bomb Bag Capacity Upgrade": 1, + "Piece of Heart": 12, + }, + "start_location_hints": ["Ganon's Tower - Maze Chest"], + "exclude_locations": [ + "Outset Island - Jabun's Cave", + "Windfall Island - Jail - Tingle - First Gift", + "Windfall Island - Jail - Tingle - Second Gift", + "Windfall Island - Jail - Maze Chest", + "Windfall Island - Maggie - Delivery Reward", + "Windfall Island - Cafe Bar - Postman", + "Windfall Island - Zunari - Stock Exotic Flower in Zunari's Shop", + "Tingle Island - Ankle - Reward for All Tingle Statues", + "Horseshoe Island - Play Golf", + ], + }, + "Mixed Pools": { + "progression_tingle_chests": True, + "progression_dungeon_secrets": True, + "progression_combat_secret_caves": True, + "progression_short_sidequests": True, + "progression_mail": True, + "progression_submarines": True, + "progression_expensive_purchases": False, + "progression_island_puzzles": True, + "progression_misc": True, + "randomize_mapcompass": "startwith", + "required_bosses": True, + "num_required_bosses": 6, + "chest_type_matches_contents": True, + "randomize_dungeon_entrances": True, + "randomize_secret_cave_entrances": True, + "randomize_miniboss_entrances": True, + "randomize_boss_entrances": True, + "randomize_secret_cave_inner_entrances": True, + "randomize_fairy_fountain_entrances": True, + "mix_entrances": "mix_pools", + "randomize_starting_island": True, + "add_shortcut_warps_between_dungeons": True, + "start_inventory_from_pool": { + "Telescope": 1, + "Wind Waker": 1, + "Wind's Requiem": 1, + "Ballad of Gales": 1, + "Earth God's Lyric": 1, + "Wind God's Aria": 1, + "Song of Passing": 1, + }, + "start_location_hints": ["Ganon's Tower - Maze Chest", "Shark Island - Cave"], + }, +} diff --git a/worlds/tww/Rules.py b/worlds/tww/Rules.py new file mode 100644 index 00000000..d4c74378 --- /dev/null +++ b/worlds/tww/Rules.py @@ -0,0 +1,1414 @@ +# flake8: noqa + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from BaseClasses import MultiWorld +from worlds.AutoWorld import LogicMixin +from worlds.generic.Rules import set_rule + +from .Macros import * + +if TYPE_CHECKING: + from . import TWWWorld + + +class TWWLogic(LogicMixin): + """ + This class implements some of the game logic for The Wind Waker. + + This class's methods reference the world's options. All methods defined in this class should be prefixed with + "_tww." + """ + + multiworld: MultiWorld + + def _tww_has_chart_for_island(self, player: int, island_number: int) -> bool: + chart_item_name = self.multiworld.worlds[player].charts.island_number_to_chart_name[island_number] + + if "Triforce Chart" in chart_item_name: + return self.has(chart_item_name, player) and has_any_wallet_upgrade(self, player) + else: + return self.has(chart_item_name, player) + + def _tww_can_defeat_all_required_bosses(self, player: int) -> bool: + required_boss_item_locations = self.multiworld.worlds[player].boss_reqs.required_boss_item_locations + for loc in required_boss_item_locations: + if not self.can_reach_location(loc, player): + return False + return True + + def _tww_rematch_bosses_skipped(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_rematch_bosses_skipped + + def _tww_in_swordless_mode(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_in_swordless_mode + + def _tww_outside_swordless_mode(self, player: int) -> bool: + return not self.multiworld.worlds[player].logic_in_swordless_mode + + def _tww_in_required_bosses_mode(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_in_required_bosses_mode + + def _tww_outside_required_bosses_mode(self, player: int) -> bool: + return not self.multiworld.worlds[player].logic_in_required_bosses_mode + + def _tww_obscure_1(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_obscure_1 + + def _tww_obscure_2(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_obscure_2 + + def _tww_obscure_3(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_obscure_3 + + def _tww_precise_1(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_precise_1 + + def _tww_precise_2(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_precise_2 + + def _tww_precise_3(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_precise_3 + + def _tww_tuner_logic_enabled(self, player: int) -> bool: + return self.multiworld.worlds[player].logic_tuner_logic_enabled + + +def set_rules(world: "TWWWorld") -> None: # noqa: F405 + """ + Define the logic rules for locations in The Wind Waker. + Rules are only set for locations if they are present in the world. + + :param world: The Wind Waker game world. + """ + + def set_rule_if_exists(location_name: str, rule: Callable[[CollectionState], bool]) -> None: + if location_name in world.progress_locations: + set_rule(world.get_location(location_name), rule) + + player = world.player + + # Outset Island + set_rule_if_exists("Outset Island - Underneath Link's House", lambda state: True) + set_rule_if_exists("Outset Island - Mesa the Grasscutter's House", lambda state: True) + set_rule_if_exists( + "Outset Island - Orca - Give 10 Knight's Crests", + lambda state: state.has("Spoils Bag", player) + and can_sword_fight_with_orca(state, player) + and has_magic_meter(state, player) + and can_farm_knights_crests(state, player), + ) + # set_rule_if_exists("Outset Island - Orca - Hit 500 Times", lambda state: can_sword_fight_with_orca(state, player)) + set_rule_if_exists( + "Outset Island - Great Fairy", lambda state: state.can_reach_region("Outset Fairy Fountain", player) + ) + set_rule_if_exists("Outset Island - Jabun's Cave", lambda state: state.has("Bombs", player)) + set_rule_if_exists( + "Outset Island - Dig up Black Soil", lambda state: state.has_all(["Bait Bag", "Power Bracelets"], player) + ) + set_rule_if_exists( + "Outset Island - Savage Labyrinth - Floor 30", + lambda state: can_defeat_keese(state, player) + and can_defeat_miniblins(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_magtails(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_peahats(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_boko_babas(state, player) + and can_defeat_mothulas(state, player) + and can_defeat_winged_mothulas(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_armos(state, player) + and can_defeat_yellow_chuchus(state, player) + and can_defeat_red_bubbles(state, player) + and can_defeat_darknuts(state, player) + and can_play_winds_requiem(state, player) + and (state.has_any(["Grappling Hook", "Skull Hammer"], player) or has_heros_sword(state, player)), + ) + set_rule_if_exists( + "Outset Island - Savage Labyrinth - Floor 50", + lambda state: can_defeat_keese(state, player) + and can_defeat_miniblins(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_magtails(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_peahats(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_boko_babas(state, player) + and can_defeat_mothulas(state, player) + and can_defeat_winged_mothulas(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_armos(state, player) + and can_defeat_yellow_chuchus(state, player) + and can_defeat_red_bubbles(state, player) + and can_defeat_darknuts(state, player) + and can_play_winds_requiem(state, player) + and (state.has_any(["Grappling Hook", "Skull Hammer"], player) or has_heros_sword(state, player)) + and can_aim_mirror_shield(state, player) + and can_defeat_redeads(state, player) + and can_defeat_blue_bubbles(state, player) + and can_defeat_stalfos(state, player) + and state.has("Skull Hammer", player), + ) + + # Windfall Island + set_rule_if_exists("Windfall Island - Jail - Tingle - First Gift", lambda state: True) + set_rule_if_exists("Windfall Island - Jail - Tingle - Second Gift", lambda state: True) + set_rule_if_exists("Windfall Island - Jail - Maze Chest", lambda state: True) + set_rule_if_exists( + "Windfall Island - Chu Jelly Juice Shop - Give 15 Green Chu Jelly", + lambda state: can_farm_green_chu_jelly(state, player), + ) + set_rule_if_exists( + "Windfall Island - Chu Jelly Juice Shop - Give 15 Blue Chu Jelly", + lambda state: can_obtain_15_blue_chu_jelly(state, player), + ) + set_rule_if_exists("Windfall Island - Ivan - Catch Killer Bees", lambda state: True) + set_rule_if_exists("Windfall Island - Mrs. Marie - Catch Killer Bees", lambda state: True) + set_rule_if_exists( + "Windfall Island - Mrs. Marie - Give 1 Joy Pendant", + # In Archipelago, the non-randomized Joy Pendant on Windfall is not obtainable, so require the player to have + # a way to collect Joy Pendants. + lambda state: state.has("Spoils Bag", player) and can_farm_joy_pendants(state, player), + ) + set_rule_if_exists( + "Windfall Island - Mrs. Marie - Give 21 Joy Pendants", + lambda state: state.has("Spoils Bag", player) and can_farm_joy_pendants(state, player), + ) + set_rule_if_exists( + "Windfall Island - Mrs. Marie - Give 40 Joy Pendants", + lambda state: state.has("Spoils Bag", player) and can_farm_joy_pendants(state, player), + ) + set_rule_if_exists( + "Windfall Island - Lenzo's House - Left Chest", + lambda state: can_play_winds_requiem(state, player) and has_picto_box(state, player), + ) + set_rule_if_exists( + "Windfall Island - Lenzo's House - Right Chest", + lambda state: can_play_winds_requiem(state, player) and has_picto_box(state, player), + ) + set_rule_if_exists( + "Windfall Island - Lenzo's House - Become Lenzo's Assistant", lambda state: has_picto_box(state, player) + ) + set_rule_if_exists( + "Windfall Island - Lenzo's House - Bring Forest Firefly", + lambda state: has_picto_box(state, player) + and state.has("Empty Bottle", player) + and can_access_forest_haven(state, player), + ) + set_rule_if_exists("Windfall Island - House of Wealth Chest", lambda state: True) + set_rule_if_exists( + "Windfall Island - Maggie's Father - Give 20 Skull Necklaces", + lambda state: state.has("Spoils Bag", player) and can_farm_skull_necklaces(state, player), + ) + set_rule_if_exists("Windfall Island - Maggie - Free Item", lambda state: True) + set_rule_if_exists( + "Windfall Island - Maggie - Delivery Reward", + lambda state: state.has_all(["Delivery Bag", "Moblin's Letter"], player), + ) + set_rule_if_exists( + "Windfall Island - Cafe Bar - Postman", lambda state: state.has_all(["Delivery Bag", "Maggie's Letter"], player) + ) + set_rule_if_exists( + "Windfall Island - Kreeb - Light Up Lighthouse", + lambda state: can_play_winds_requiem(state, player) and has_fire_arrows(state, player), + ) + set_rule_if_exists( + "Windfall Island - Transparent Chest", + lambda state: can_play_winds_requiem(state, player) + and has_fire_arrows(state, player) + and (can_fly_with_deku_leaf_outdoors(state, player) or state.has("Hookshot", player)), + ) + set_rule_if_exists("Windfall Island - Tott - Teach Rhythm", lambda state: state.has("Wind Waker", player)) + set_rule_if_exists("Windfall Island - Pirate Ship", lambda state: True) + set_rule_if_exists("Windfall Island - 5 Rupee Auction", lambda state: True) + set_rule_if_exists("Windfall Island - 40 Rupee Auction", lambda state: True) + set_rule_if_exists("Windfall Island - 60 Rupee Auction", lambda state: True) + set_rule_if_exists("Windfall Island - 80 Rupee Auction", lambda state: True) + set_rule_if_exists( + "Windfall Island - Zunari - Stock Exotic Flower in Zunari's Shop", + lambda state: state.has("Delivery Bag", player), + ) + set_rule_if_exists("Windfall Island - Sam - Decorate the Town", lambda state: state.has("Delivery Bag", player)) + # set_rule_if_exists( + # "Windfall Island - Kane - Place Shop Guru Statue on Gate", lambda state: state.has("Delivery Bag", player) + # ) + # set_rule_if_exists( + # "Windfall Island - Kane - Place Postman Statue on Gate", lambda state: state.has("Delivery Bag", player) + # ) + # set_rule_if_exists( + # "Windfall Island - Kane - Place Six Flags on Gate", lambda state: state.has("Delivery Bag", player) + # ) + # set_rule_if_exists( + # "Windfall Island - Kane - Place Six Idols on Gate", lambda state: state.has("Delivery Bag", player) + # ) + set_rule_if_exists("Windfall Island - Mila - Follow the Thief", lambda state: True) + set_rule_if_exists("Windfall Island - Battlesquid - First Prize", lambda state: True) + set_rule_if_exists("Windfall Island - Battlesquid - Second Prize", lambda state: True) + set_rule_if_exists("Windfall Island - Battlesquid - Under 20 Shots Prize", lambda state: True) + set_rule_if_exists( + "Windfall Island - Pompie and Vera - Secret Meeting Photo", + lambda state: can_play_winds_requiem(state, player) and has_picto_box(state, player), + ) + set_rule_if_exists( + "Windfall Island - Kamo - Full Moon Photo", + lambda state: has_deluxe_picto_box(state, player) and can_play_song_of_passing(state, player), + ) + set_rule_if_exists( + "Windfall Island - Minenco - Miss Windfall Photo", lambda state: has_deluxe_picto_box(state, player) + ) + set_rule_if_exists( + "Windfall Island - Linda and Anton", + lambda state: has_deluxe_picto_box(state, player) and can_play_song_of_passing(state, player), + ) + + # Dragon Roost Island + set_rule_if_exists("Dragon Roost Island - Wind Shrine", lambda state: state.has("Wind Waker", player)) + set_rule_if_exists( + "Dragon Roost Island - Rito Aerie - Give Hoskit 20 Golden Feathers", + lambda state: state.has("Spoils Bag", player) and can_farm_golden_feathers(state, player), + ) + set_rule_if_exists( + "Dragon Roost Island - Chest on Top of Boulder", + lambda state: state.has_any(["Boomerang", "Bombs", "Bait Bag"], player) or has_heros_bow(state, player), + ) + set_rule_if_exists( + "Dragon Roost Island - Fly Across Platforms Around Island", + lambda state: can_fly_with_deku_leaf_outdoors(state, player) + and (can_cut_grass(state, player) or has_magic_meter_upgrade(state, player)), + ) + set_rule_if_exists("Dragon Roost Island - Rito Aerie - Mail Sorting", lambda state: True) + set_rule_if_exists( + "Dragon Roost Island - Secret Cave", + lambda state: can_defeat_keese(state, player) and can_defeat_red_chuchus(state, player), + ) + + # Dragon Roost Cavern + set_rule_if_exists("Dragon Roost Cavern - First Room", lambda state: True) + set_rule_if_exists( + "Dragon Roost Cavern - Alcove With Water Jugs", lambda state: state.has("DRC Small Key", player, 1) + ) + set_rule_if_exists( + "Dragon Roost Cavern - Water Jug on Upper Shelf", lambda state: state.has("DRC Small Key", player, 1) + ) + set_rule_if_exists("Dragon Roost Cavern - Boarded Up Chest", lambda state: state.has("DRC Small Key", player, 1)) + set_rule_if_exists( + "Dragon Roost Cavern - Chest Across Lava Pit", + lambda state: state.has("DRC Small Key", player, 2) + and ( + state.has("Grappling Hook", player) + or can_fly_with_deku_leaf_indoors(state, player) + or (state.has("Hookshot", player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists("Dragon Roost Cavern - Rat Room", lambda state: state.has("DRC Small Key", player, 2)) + set_rule_if_exists( + "Dragon Roost Cavern - Rat Room Boarded Up Chest", lambda state: state.has("DRC Small Key", player, 2) + ) + set_rule_if_exists("Dragon Roost Cavern - Bird's Nest", lambda state: state.has("DRC Small Key", player, 3)) + set_rule_if_exists("Dragon Roost Cavern - Dark Room", lambda state: state.has("DRC Small Key", player, 4)) + set_rule_if_exists( + "Dragon Roost Cavern - Tingle Chest in Hub Room", + lambda state: state.has("DRC Small Key", player, 4) and has_tingle_bombs(state, player), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Pot on Upper Shelf in Pot Room", lambda state: state.has("DRC Small Key", player, 4) + ) + set_rule_if_exists("Dragon Roost Cavern - Pot Room Chest", lambda state: state.has("DRC Small Key", player, 4)) + set_rule_if_exists("Dragon Roost Cavern - Miniboss", lambda state: state.has("DRC Small Key", player, 4)) + set_rule_if_exists( + "Dragon Roost Cavern - Under Rope Bridge", + lambda state: state.has("DRC Small Key", player, 4) + and (state.has("Grappling Hook", player) or can_fly_with_deku_leaf_outdoors(state, player)), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Tingle Statue Chest", + lambda state: can_reach_dragon_roost_cavern_gaping_maw(state, player) + and state.has("Grappling Hook", player) + and has_tingle_bombs(state, player), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Big Key Chest", + lambda state: can_reach_dragon_roost_cavern_gaping_maw(state, player) + and state.has("Grappling Hook", player) + and can_stun_magtails(state, player), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Boss Stairs Right Chest", + lambda state: can_reach_dragon_roost_cavern_boss_stairs(state, player), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Boss Stairs Left Chest", + lambda state: can_reach_dragon_roost_cavern_boss_stairs(state, player), + ) + set_rule_if_exists( + "Dragon Roost Cavern - Boss Stairs Right Pot", + lambda state: can_reach_dragon_roost_cavern_boss_stairs(state, player), + ) + set_rule_if_exists("Dragon Roost Cavern - Gohma Heart Container", lambda state: can_defeat_gohma(state, player)) + + # Forest Haven + set_rule_if_exists( + "Forest Haven - On Tree Branch", + lambda state: can_access_forest_haven(state, player) + and ( + state.has("Grappling Hook", player) + or ( + can_fly_with_deku_leaf_indoors(state, player) + and can_fly_with_deku_leaf_outdoors(state, player) + and state._tww_obscure_1(player) + and ( + (can_cut_grass(state, player) and state._tww_precise_1(player)) + or (has_magic_meter_upgrade(state, player) and state._tww_precise_2(player)) + ) + ) + ), + ) + set_rule_if_exists( + "Forest Haven - Small Island Chest", + lambda state: can_access_forest_haven(state, player) + and ( + state.has("Grappling Hook", player) + or ( + can_fly_with_deku_leaf_indoors(state, player) + and can_fly_with_deku_leaf_outdoors(state, player) + and state._tww_obscure_1(player) + and ( + (can_cut_grass(state, player) and state._tww_precise_1(player)) + or (has_magic_meter_upgrade(state, player) and state._tww_precise_2(player)) + ) + ) + ) + and can_fly_with_deku_leaf_outdoors(state, player) + and (can_cut_grass(state, player) or has_magic_meter_upgrade(state, player)), + ) + + # Forbidden Woods + set_rule_if_exists("Forbidden Woods - First Room", lambda state: True) + set_rule_if_exists( + "Forbidden Woods - Inside Hollow Tree's Mouth", + lambda state: (can_defeat_door_flowers(state, player) or can_defeat_boko_babas(state, player)), + ) + set_rule_if_exists( + "Forbidden Woods - Climb to Top Using Boko Baba Bulbs", + lambda state: can_fly_with_deku_leaf_indoors(state, player) and can_defeat_door_flowers(state, player), + ) + set_rule_if_exists( + "Forbidden Woods - Pot High Above Hollow Tree", lambda state: can_fly_with_deku_leaf_indoors(state, player) + ) + set_rule_if_exists( + "Forbidden Woods - Hole in Tree", + lambda state: can_fly_with_deku_leaf_indoors(state, player) and can_defeat_boko_babas(state, player), + ) + set_rule_if_exists( + "Forbidden Woods - Morth Pit", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has("Grappling Hook", player), + ) + set_rule_if_exists( + "Forbidden Woods - Vine Maze Left Chest", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has("Grappling Hook", player), + ) + set_rule_if_exists( + "Forbidden Woods - Vine Maze Right Chest", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has("Grappling Hook", player), + ) + set_rule_if_exists( + "Forbidden Woods - Highest Pot in Vine Maze", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has("Grappling Hook", player), + ) + set_rule_if_exists( + "Forbidden Woods - Tall Room Before Miniboss", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "FW Small Key"], player) + and (can_defeat_peahats(state, player) or state._tww_precise_2(player)), + ) + set_rule_if_exists( + "Forbidden Woods - Mothula Miniboss Room", lambda state: can_defeat_winged_mothulas(state, player) + ) + set_rule_if_exists( + "Forbidden Woods - Past Seeds Hanging by Vines", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "FW Small Key"], player) + and can_defeat_door_flowers(state, player) + and (can_destroy_seeds_hanging_by_vines(state, player) or state._tww_precise_1(player)), + ) + set_rule_if_exists( + "Forbidden Woods - Chest Across Red Hanging Flower", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "Boomerang"], player), + ) + set_rule_if_exists( + "Forbidden Woods - Tingle Statue Chest", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and state.has_all(["Grappling Hook", "Boomerang"], player), + ) + set_rule_if_exists( + "Forbidden Woods - Chest in Locked Tree Trunk", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "Boomerang"], player), + ) + set_rule_if_exists( + "Forbidden Woods - Big Key Chest", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and state.has_all(["Grappling Hook", "Boomerang"], player), + ) + set_rule_if_exists( + "Forbidden Woods - Double Mothula Room", + lambda state: can_fly_with_deku_leaf_indoors(state, player) + and can_defeat_boko_babas(state, player) + and (can_defeat_door_flowers(state, player) or state.has("Grappling Hook", player)) + and can_defeat_mothulas(state, player), + ) + set_rule_if_exists( + "Forbidden Woods - Kalle Demos Heart Container", lambda state: can_defeat_kalle_demos(state, player) + ) + + # Greatfish Isle + set_rule_if_exists("Greatfish Isle - Hidden Chest", lambda state: can_fly_with_deku_leaf_outdoors(state, player)) + + # Tower of the Gods + set_rule_if_exists("Tower of the Gods - Chest Behind Bombable Walls", lambda state: state.has("Bombs", player)) + set_rule_if_exists("Tower of the Gods - Pot Behind Bombable Walls", lambda state: state.has("Bombs", player)) + set_rule_if_exists("Tower of the Gods - Hop Across Floating Boxes", lambda state: True) + set_rule_if_exists("Tower of the Gods - Light Two Torches", lambda state: state.has("Bombs", player)) + set_rule_if_exists("Tower of the Gods - Skulls Room Chest", lambda state: state.has("Bombs", player)) + set_rule_if_exists( + "Tower of the Gods - Shoot Eye Above Skulls Room Chest", + lambda state: state.has("Bombs", player) and has_heros_bow(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - Tingle Statue Chest", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) and has_tingle_bombs(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - First Chest Guarded by Armos Knights", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) and has_heros_bow(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - Stone Tablet", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) and state.has("Wind Waker", player), + ) + set_rule_if_exists("Tower of the Gods - Darknut Miniboss Room", lambda state: can_defeat_darknuts(state, player)) + set_rule_if_exists( + "Tower of the Gods - Second Chest Guarded by Armos Knights", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) + and state.has("Bombs", player) + and can_play_winds_requiem(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - Floating Platforms Room", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) + and ( + has_heros_bow(state, player) + or (can_fly_with_deku_leaf_indoors(state, player) and state._tww_precise_1(player)) + or (state.has("Hookshot", player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Tower of the Gods - Top of Floating Platforms Room", + lambda state: can_reach_tower_of_the_gods_second_floor(state, player) and has_heros_bow(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - Eastern Pot in Big Key Chest Room", + lambda state: can_reach_tower_of_the_gods_third_floor(state, player), + ) + set_rule_if_exists( + "Tower of the Gods - Big Key Chest", lambda state: can_reach_tower_of_the_gods_third_floor(state, player) + ) + set_rule_if_exists("Tower of the Gods - Gohdan Heart Container", lambda state: can_defeat_gohdan(state, player)) + + # Hyrule + set_rule_if_exists("Hyrule - Master Sword Chamber", lambda state: can_defeat_mighty_darknuts(state, player)) + + # Forsaken Fortress + set_rule_if_exists( + "Forsaken Fortress - Phantom Ganon", lambda state: can_reach_and_defeat_phantom_ganon(state, player) + ) + set_rule_if_exists( + "Forsaken Fortress - Chest Outside Upper Jail Cell", + lambda state: can_get_inside_forsaken_fortress(state, player) + and ( + can_fly_with_deku_leaf_indoors(state, player) + or state.has("Hookshot", player) + or state._tww_obscure_1(player) + ), + ) + set_rule_if_exists( + "Forsaken Fortress - Chest Inside Lower Jail Cell", + lambda state: can_get_inside_forsaken_fortress(state, player), + ) + set_rule_if_exists( + "Forsaken Fortress - Chest Guarded By Bokoblin", lambda state: can_get_inside_forsaken_fortress(state, player) + ) + set_rule_if_exists( + "Forsaken Fortress - Chest on Bed", lambda state: can_get_inside_forsaken_fortress(state, player) + ) + set_rule_if_exists( + "Forsaken Fortress - Helmaroc King Heart Container", lambda state: can_defeat_helmaroc_king(state, player) + ) + + # Mother and Child Isles + set_rule_if_exists( + "Mother and Child Isles - Inside Mother Isle", lambda state: can_play_ballad_of_gales(state, player) + ) + + # Fire Mountain + set_rule_if_exists("Fire Mountain - Cave - Chest", lambda state: can_defeat_magtails(state, player)) + set_rule_if_exists("Fire Mountain - Lookout Platform Chest", lambda state: True) + set_rule_if_exists( + "Fire Mountain - Lookout Platform - Destroy the Cannons", lambda state: can_destroy_cannons(state, player) + ) + set_rule_if_exists( + "Fire Mountain - Big Octo", + lambda state: can_defeat_big_octos(state, player) and state.has("Grappling Hook", player), + ) + + # Ice Ring Isle + set_rule_if_exists("Ice Ring Isle - Frozen Chest", lambda state: has_fire_arrows(state, player)) + set_rule_if_exists("Ice Ring Isle - Cave - Chest", lambda state: True) + set_rule_if_exists("Ice Ring Isle - Inner Cave - Chest", lambda state: has_fire_arrows(state, player)) + + # Headstone Island + set_rule_if_exists("Headstone Island - Top of the Island", lambda state: state.has("Bait Bag", player)) + set_rule_if_exists("Headstone Island - Submarine", lambda state: can_defeat_bombchus(state, player)) + + # Earth Temple + set_rule_if_exists( + "Earth Temple - Transparent Chest In Warp Pot Room", lambda state: can_play_command_melody(state, player) + ) + set_rule_if_exists( + "Earth Temple - Behind Curtain In Warp Pot Room", + lambda state: can_play_command_melody(state, player) + and has_fire_arrows(state, player) + and state.has_any(["Boomerang", "Hookshot"], player), + ) + set_rule_if_exists( + "Earth Temple - Transparent Chest in First Crypt", + lambda state: can_reach_earth_temple_right_path(state, player) + and state.has("Power Bracelets", player) + and (can_play_command_melody(state, player) or has_mirror_shield(state, player)), + ) + set_rule_if_exists( + "Earth Temple - Chest Behind Destructible Walls", + lambda state: can_reach_earth_temple_right_path(state, player) and has_mirror_shield(state, player), + ) + set_rule_if_exists( + "Earth Temple - Chest In Three Blocks Room", + lambda state: can_reach_earth_temple_left_path(state, player) + and has_fire_arrows(state, player) + and state.has("Power Bracelets", player) + and can_defeat_floormasters(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)), + ) + set_rule_if_exists( + "Earth Temple - Chest Behind Statues", + lambda state: can_reach_earth_temple_moblins_and_poes_room(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)), + ) + set_rule_if_exists( + "Earth Temple - Casket in Second Crypt", + lambda state: can_reach_earth_temple_moblins_and_poes_room(state, player), + ) + set_rule_if_exists( + "Earth Temple - Stalfos Miniboss Room", + lambda state: can_defeat_stalfos(state, player) or state.has("Hookshot", player), + ) + set_rule_if_exists( + "Earth Temple - Tingle Statue Chest", + lambda state: can_reach_earth_temple_basement(state, player) and has_tingle_bombs(state, player), + ) + set_rule_if_exists( + "Earth Temple - End of Foggy Room With Floormasters", + lambda state: can_reach_earth_temple_redead_hub_room(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)), + ) + set_rule_if_exists( + "Earth Temple - Kill All Floormasters in Foggy Room", + lambda state: can_reach_earth_temple_redead_hub_room(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)) + and can_defeat_floormasters(state, player), + ) + set_rule_if_exists( + "Earth Temple - Behind Curtain Next to Hammer Button", + lambda state: can_reach_earth_temple_redead_hub_room(state, player) + and (can_play_command_melody(state, player) or can_aim_mirror_shield(state, player)) + and has_fire_arrows(state, player) + and state.has_any(["Boomerang", "Hookshot"], player), + ) + set_rule_if_exists( + "Earth Temple - Chest in Third Crypt", lambda state: can_reach_earth_temple_third_crypt(state, player) + ) + set_rule_if_exists( + "Earth Temple - Many Mirrors Room Right Chest", + lambda state: can_reach_earth_temple_many_mirrors_room(state, player) + and can_play_command_melody(state, player), + ) + set_rule_if_exists( + "Earth Temple - Many Mirrors Room Left Chest", + lambda state: can_reach_earth_temple_many_mirrors_room(state, player) + and state.has("Power Bracelets", player) + and can_play_command_melody(state, player) + and can_aim_mirror_shield(state, player), + ) + set_rule_if_exists( + "Earth Temple - Stalfos Crypt Room", + lambda state: can_reach_earth_temple_many_mirrors_room(state, player) and can_defeat_stalfos(state, player), + ) + set_rule_if_exists( + "Earth Temple - Big Key Chest", + lambda state: can_reach_earth_temple_many_mirrors_room(state, player) + and state.has("Power Bracelets", player) + and can_play_command_melody(state, player) + and can_aim_mirror_shield(state, player) + and ( + can_defeat_blue_bubbles(state, player) + or (has_heros_bow(state, player) and state._tww_obscure_1(player)) + or ( + ( + has_heros_sword(state, player) + or has_any_master_sword(state, player) + or state.has("Skull Hammer", player) + ) + and state._tww_obscure_1(player) + and state._tww_precise_1(player) + ) + ) + and can_defeat_darknuts(state, player), + ) + set_rule_if_exists("Earth Temple - Jalhalla Heart Container", lambda state: can_defeat_jalhalla(state, player)) + + # Wind Temple + set_rule_if_exists( + "Wind Temple - Chest Between Two Dirt Patches", lambda state: can_play_command_melody(state, player) + ) + set_rule_if_exists( + "Wind Temple - Behind Stone Head in Hidden Upper Room", + lambda state: can_play_command_melody(state, player) + and state.has_all(["Iron Boots", "Hookshot"], player) + and can_fly_with_deku_leaf_indoors(state, player), + ) + set_rule_if_exists( + "Wind Temple - Tingle Statue Chest", + lambda state: can_reach_wind_temple_kidnapping_room(state, player) and has_tingle_bombs(state, player), + ) + set_rule_if_exists( + "Wind Temple - Chest Behind Stone Head", + lambda state: can_reach_wind_temple_kidnapping_room(state, player) + and state.has_all(["Iron Boots", "Hookshot"], player), + ) + set_rule_if_exists( + "Wind Temple - Chest in Left Alcove", + lambda state: can_reach_wind_temple_kidnapping_room(state, player) + and state.has("Iron Boots", player) + and can_fan_with_deku_leaf(state, player), + ) + set_rule_if_exists( + "Wind Temple - Big Key Chest", + lambda state: can_reach_wind_temple_kidnapping_room(state, player) + and state.has("Iron Boots", player) + and can_fan_with_deku_leaf(state, player) + and can_play_wind_gods_aria(state, player) + and can_defeat_darknuts(state, player), + ) + set_rule_if_exists( + "Wind Temple - Chest In Many Cyclones Room", + lambda state: can_reach_wind_temple_kidnapping_room(state, player) + and ( + ( + state.has("Iron Boots", player) + and can_fan_with_deku_leaf(state, player) + and can_fly_with_deku_leaf_indoors(state, player) + and (can_cut_grass(state, player) or has_magic_meter_upgrade(state, player)) + ) + or ( + state.has("Hookshot", player) + and can_defeat_blue_bubbles(state, player) + and can_fly_with_deku_leaf_indoors(state, player) + ) + or ( + state.has("Hookshot", player) + and can_fly_with_deku_leaf_indoors(state, player) + and state._tww_obscure_1(player) + and state._tww_precise_2(player) + ) + ), + ) + set_rule_if_exists( + "Wind Temple - Behind Stone Head in Many Cyclones Room", + lambda state: can_reach_end_of_wind_temple_many_cyclones_room(state, player) and state.has("Hookshot", player), + ) + set_rule_if_exists( + "Wind Temple - Chest In Middle Of Hub Room", lambda state: can_open_wind_temple_upper_giant_grate(state, player) + ) + set_rule_if_exists( + "Wind Temple - Spike Wall Room - First Chest", + lambda state: can_open_wind_temple_upper_giant_grate(state, player) and state.has("Iron Boots", player), + ) + set_rule_if_exists( + "Wind Temple - Spike Wall Room - Destroy All Cracked Floors", + lambda state: can_open_wind_temple_upper_giant_grate(state, player) and state.has("Iron Boots", player), + ) + set_rule_if_exists( + "Wind Temple - Wizzrobe Miniboss Room", + lambda state: can_defeat_darknuts(state, player) and can_remove_peahat_armor(state, player), + ) + set_rule_if_exists( + "Wind Temple - Chest at Top of Hub Room", lambda state: can_activate_wind_temple_giant_fan(state, player) + ) + set_rule_if_exists( + "Wind Temple - Chest Behind Seven Armos", + lambda state: can_activate_wind_temple_giant_fan(state, player) and can_defeat_armos(state, player), + ) + set_rule_if_exists( + "Wind Temple - Kill All Enemies in Tall Basement Room", + lambda state: can_reach_wind_temple_tall_basement_room(state, player) + and can_defeat_stalfos(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_morths(state, player), + ) + set_rule_if_exists("Wind Temple - Molgera Heart Container", lambda state: can_defeat_molgera(state, player)) + + # Ganon's Tower + set_rule_if_exists( + "Ganon's Tower - Maze Chest", + lambda state: can_reach_ganons_tower_phantom_ganon_room(state, player) + and can_defeat_phantom_ganon(state, player), + ) + + # Mailbox + set_rule_if_exists( + "Mailbox - Letter from Hoskit's Girlfriend", + lambda state: state.has("Spoils Bag", player) + and can_farm_golden_feathers(state, player) + and can_play_song_of_passing(state, player), + ) + set_rule_if_exists( + "Mailbox - Letter from Baito's Mother", + lambda state: state.has_all(["Delivery Bag", "Note to Mom"], player) + and can_play_song_of_passing(state, player), + ) + set_rule_if_exists( + "Mailbox - Letter from Baito", + lambda state: state.has_all(["Delivery Bag", "Note to Mom"], player) + and state.can_reach_region("Jalhalla Boss Arena", player) + and can_defeat_jalhalla(state, player), + ) + set_rule_if_exists("Mailbox - Letter from Komali's Father", lambda state: state.has("Farore's Pearl", player)) + set_rule_if_exists("Mailbox - Letter Advertising Bombs in Beedle's Shop", lambda state: state.has("Bombs", player)) + set_rule_if_exists( + "Mailbox - Letter Advertising Rock Spire Shop Ship", lambda state: has_any_wallet_upgrade(state, player) + ) + # set_rule_if_exists( + # "Mailbox - Beedle's Silver Membership Reward", + # lambda state: (state.has_any(["Bait Bag", "Bombs", "Empty Bottle"], player) or has_heros_bow(state, player)) + # and can_play_song_of_passing(state, player), + # ) + # set_rule_if_exists( + # "Mailbox - Beedle's Gold Membership Reward", + # lambda state: (state.has_any(["Bait Bag", "Bombs", "Empty Bottle"], player) or has_heros_bow(state, player)) + # and can_play_song_of_passing(state, player), + # ) + set_rule_if_exists( + "Mailbox - Letter from Orca", + lambda state: state.can_reach_region("Kalle Demos Boss Arena", player) + and can_defeat_kalle_demos(state, player), + ) + set_rule_if_exists( + "Mailbox - Letter from Grandma", + lambda state: state.has("Empty Bottle", player) and can_play_song_of_passing(state, player), + ) + set_rule_if_exists( + "Mailbox - Letter from Aryll", + lambda state: state.can_reach_region("Helmaroc King Boss Arena", player) + and can_defeat_helmaroc_king(state, player) + and can_play_song_of_passing(state, player), + ) + set_rule_if_exists( + "Mailbox - Letter from Tingle", + lambda state: has_any_wallet_upgrade(state, player) + and state.can_reach_region("Helmaroc King Boss Arena", player) + and can_defeat_helmaroc_king(state, player) + and can_play_song_of_passing(state, player), + ) + + # The Great Sea + set_rule_if_exists("The Great Sea - Beedle's Shop Ship - 20 Rupee Item", lambda state: True) + set_rule_if_exists("The Great Sea - Salvage Corp Gift", lambda state: True) + set_rule_if_exists("The Great Sea - Cyclos", lambda state: has_heros_bow(state, player)) + set_rule_if_exists("The Great Sea - Goron Trading Reward", lambda state: state.has("Delivery Bag", player)) + set_rule_if_exists( + "The Great Sea - Withered Trees", + lambda state: can_access_forest_haven(state, player) + and state.has("Empty Bottle", player) + and can_play_ballad_of_gales(state, player) + and state.can_reach_region("Cliff Plateau Isles Inner Cave", player), + ) + set_rule_if_exists( + "The Great Sea - Ghost Ship", + lambda state: state.has("Ghost Ship Chart", player) + and can_play_ballad_of_gales(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_redeads(state, player) + and can_defeat_stalfos(state, player), + ) + + # Private Oasis + set_rule_if_exists( + "Private Oasis - Chest at Top of Waterfall", + lambda state: state.has("Hookshot", player) or can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists( + "Private Oasis - Cabana Labyrinth - Lower Floor Chest", lambda state: state.has("Skull Hammer", player) + ) + set_rule_if_exists( + "Private Oasis - Cabana Labyrinth - Upper Floor Chest", + lambda state: state.has("Skull Hammer", player) and can_play_winds_requiem(state, player), + ) + set_rule_if_exists( + "Private Oasis - Big Octo", + lambda state: can_defeat_big_octos(state, player) and state.has("Grappling Hook", player), + ) + + # Spectacle Island + set_rule_if_exists("Spectacle Island - Barrel Shooting - First Prize", lambda state: True) + set_rule_if_exists("Spectacle Island - Barrel Shooting - Second Prize", lambda state: True) + + # Needle Rock Isle + set_rule_if_exists("Needle Rock Isle - Chest", lambda state: state.has("Bait Bag", player)) + set_rule_if_exists("Needle Rock Isle - Cave", lambda state: has_fire_arrows(state, player)) + set_rule_if_exists( + "Needle Rock Isle - Golden Gunboat", lambda state: state.has_all(["Bombs", "Grappling Hook"], player) + ) + + # Angular Isles + set_rule_if_exists("Angular Isles - Peak", lambda state: True) + set_rule_if_exists( + "Angular Isles - Cave", + lambda state: can_aim_mirror_shield(state, player) + and (can_fly_with_deku_leaf_indoors(state, player) or state.has("Hookshot", player)), + ) + + # Boating Course + set_rule_if_exists("Boating Course - Raft", lambda state: True) + set_rule_if_exists( + "Boating Course - Cave", + lambda state: can_hit_diamond_switches_at_range(state, player) + and (can_defeat_miniblins_easily(state, player) or state._tww_precise_2(player)), + ) + + # Stone Watcher Island + set_rule_if_exists( + "Stone Watcher Island - Cave", + lambda state: can_defeat_armos(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_darknuts(state, player) + and can_play_winds_requiem(state, player), + ) + set_rule_if_exists("Stone Watcher Island - Lookout Platform Chest", lambda state: True) + set_rule_if_exists( + "Stone Watcher Island - Lookout Platform - Destroy the Cannons", + lambda state: can_destroy_cannons(state, player), + ) + + # Islet of Steel + set_rule_if_exists( + "Islet of Steel - Interior", lambda state: state.has("Bombs", player) and can_play_winds_requiem(state, player) + ) + set_rule_if_exists( + "Islet of Steel - Lookout Platform - Defeat the Enemies", + lambda state: can_defeat_wizzrobes_at_range(state, player), + ) + + # Overlook Island + set_rule_if_exists( + "Overlook Island - Cave", + lambda state: can_defeat_stalfos(state, player) + and can_defeat_wizzrobes(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_keese(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_morths(state, player) + and can_defeat_kargarocs(state, player) + and can_defeat_darknuts(state, player) + and can_play_winds_requiem(state, player), + ) + + # Bird's Peak Rock + set_rule_if_exists("Bird's Peak Rock - Cave", lambda state: can_play_winds_requiem(state, player)) + + # Pawprint Isle + set_rule_if_exists("Pawprint Isle - Chuchu Cave - Chest", lambda state: True) + set_rule_if_exists( + "Pawprint Isle - Chuchu Cave - Behind Left Boulder", lambda state: can_move_boulders(state, player) + ) + set_rule_if_exists( + "Pawprint Isle - Chuchu Cave - Behind Right Boulder", lambda state: can_move_boulders(state, player) + ) + set_rule_if_exists( + "Pawprint Isle - Chuchu Cave - Scale the Wall", lambda state: state.has("Grappling Hook", player) + ) + set_rule_if_exists( + "Pawprint Isle - Wizzrobe Cave", + lambda state: can_defeat_wizzrobes_at_range(state, player) + and can_defeat_fire_keese(state, player) + and can_defeat_magtails(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_yellow_chuchus(state, player) + and can_defeat_red_bubbles(state, player) + and can_remove_peahat_armor(state, player), + ) + set_rule_if_exists("Pawprint Isle - Lookout Platform - Defeat the Enemies", lambda state: True) + + # Thorned Fairy Island + set_rule_if_exists("Thorned Fairy Island - Great Fairy", lambda state: True) + set_rule_if_exists( + "Thorned Fairy Island - Northeastern Lookout Platform - Destroy the Cannons", + lambda state: can_destroy_cannons(state, player), + ) + set_rule_if_exists( + "Thorned Fairy Island - Southwestern Lookout Platform - Defeat the Enemies", + lambda state: can_fly_with_deku_leaf_outdoors(state, player), + ) + + # Eastern Fairy Island + set_rule_if_exists("Eastern Fairy Island - Great Fairy", lambda state: True) + set_rule_if_exists( + "Eastern Fairy Island - Lookout Platform - Defeat the Cannons and Enemies", + lambda state: can_destroy_cannons(state, player), + ) + + # Western Fairy Island + set_rule_if_exists("Western Fairy Island - Great Fairy", lambda state: True) + set_rule_if_exists("Western Fairy Island - Lookout Platform", lambda state: True) + + # Southern Fairy Island + set_rule_if_exists("Southern Fairy Island - Great Fairy", lambda state: True) + set_rule_if_exists( + "Southern Fairy Island - Lookout Platform - Destroy the Northwest Cannons", + lambda state: can_destroy_cannons(state, player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists( + "Southern Fairy Island - Lookout Platform - Destroy the Southeast Cannons", + lambda state: can_destroy_cannons(state, player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + + # Northern Fairy Island + set_rule_if_exists("Northern Fairy Island - Great Fairy", lambda state: True) + set_rule_if_exists("Northern Fairy Island - Submarine", lambda state: True) + + # Tingle Island + set_rule_if_exists( + "Tingle Island - Ankle - Reward for All Tingle Statues", + lambda state: state.has_group_unique("Tingle Statues", player, 5), + ) + set_rule_if_exists( + "Tingle Island - Big Octo", + lambda state: can_defeat_12_eye_big_octos(state, player) and state.has("Grappling Hook", player), + ) + + # Diamond Steppe Island + set_rule_if_exists("Diamond Steppe Island - Warp Maze Cave - First Chest", lambda state: True) + set_rule_if_exists("Diamond Steppe Island - Warp Maze Cave - Second Chest", lambda state: True) + set_rule_if_exists( + "Diamond Steppe Island - Big Octo", + lambda state: can_defeat_big_octos(state, player) and state.has("Grappling Hook", player), + ) + + # Bomb Island + set_rule_if_exists("Bomb Island - Cave", lambda state: can_stun_magtails(state, player)) + set_rule_if_exists("Bomb Island - Lookout Platform - Defeat the Enemies", lambda state: True) + set_rule_if_exists("Bomb Island - Submarine", lambda state: True) + + # Rock Spire Isle + set_rule_if_exists("Rock Spire Isle - Cave", lambda state: True) + set_rule_if_exists( + "Rock Spire Isle - Beedle's Special Shop Ship - 500 Rupee Item", + lambda state: has_any_wallet_upgrade(state, player), + ) + set_rule_if_exists( + "Rock Spire Isle - Beedle's Special Shop Ship - 950 Rupee Item", + lambda state: has_any_wallet_upgrade(state, player), + ) + set_rule_if_exists( + "Rock Spire Isle - Beedle's Special Shop Ship - 900 Rupee Item", + lambda state: has_any_wallet_upgrade(state, player), + ) + set_rule_if_exists( + "Rock Spire Isle - Western Lookout Platform - Destroy the Cannons", + lambda state: can_destroy_cannons(state, player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists( + "Rock Spire Isle - Eastern Lookout Platform - Destroy the Cannons", + lambda state: can_destroy_cannons(state, player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists("Rock Spire Isle - Center Lookout Platform", lambda state: True) + set_rule_if_exists( + "Rock Spire Isle - Southeast Gunboat", lambda state: state.has_all(["Bombs", "Grappling Hook"], player) + ) + + # Shark Island + set_rule_if_exists("Shark Island - Cave", lambda state: can_defeat_miniblins(state, player)) + + # Cliff Plateau Isles + set_rule_if_exists( + "Cliff Plateau Isles - Cave", + lambda state: can_defeat_boko_babas(state, player) + or (state.has("Grappling Hook", player) and state._tww_obscure_1(player) and state._tww_precise_1(player)), + ) + set_rule_if_exists("Cliff Plateau Isles - Highest Isle", lambda state: True) + set_rule_if_exists("Cliff Plateau Isles - Lookout Platform", lambda state: True) + + # Crescent Moon Island + set_rule_if_exists("Crescent Moon Island - Chest", lambda state: True) + set_rule_if_exists("Crescent Moon Island - Submarine", lambda state: can_defeat_miniblins(state, player)) + + # Horseshoe Island + set_rule_if_exists( + "Horseshoe Island - Play Golf", + lambda state: can_fan_with_deku_leaf(state, player) + and (can_fly_with_deku_leaf_outdoors(state, player) or state.has("Hookshot", player)), + ) + set_rule_if_exists( + "Horseshoe Island - Cave", + lambda state: can_defeat_mothulas(state, player) and can_defeat_winged_mothulas(state, player), + ) + set_rule_if_exists("Horseshoe Island - Northwestern Lookout Platform", lambda state: True) + set_rule_if_exists("Horseshoe Island - Southeastern Lookout Platform", lambda state: True) + + # Flight Control Platform + set_rule_if_exists( + "Flight Control Platform - Bird-Man Contest - First Prize", + lambda state: can_fly_with_deku_leaf_outdoors(state, player) and has_magic_meter_upgrade(state, player), + ) + set_rule_if_exists( + "Flight Control Platform - Submarine", + lambda state: can_defeat_wizzrobes(state, player) + and can_defeat_red_chuchus(state, player) + and can_defeat_green_chuchus(state, player) + and can_defeat_miniblins(state, player) + and can_defeat_wizzrobes_at_range(state, player), + ) + + # Star Island + set_rule_if_exists("Star Island - Cave", lambda state: can_defeat_magtails(state, player)) + set_rule_if_exists("Star Island - Lookout Platform", lambda state: True) + + # Star Belt Archipelago + set_rule_if_exists("Star Belt Archipelago - Lookout Platform", lambda state: True) + + # Five-Star Isles + set_rule_if_exists( + "Five-Star Isles - Lookout Platform - Destroy the Cannons", lambda state: can_destroy_cannons(state, player) + ) + set_rule_if_exists("Five-Star Isles - Raft", lambda state: True) + set_rule_if_exists("Five-Star Isles - Submarine", lambda state: True) + + # Seven-Star Isles + set_rule_if_exists("Seven-Star Isles - Center Lookout Platform", lambda state: True) + set_rule_if_exists("Seven-Star Isles - Northern Lookout Platform", lambda state: True) + set_rule_if_exists( + "Seven-Star Isles - Southern Lookout Platform", lambda state: can_defeat_wizzrobes_at_range(state, player) + ) + set_rule_if_exists( + "Seven-Star Isles - Big Octo", + lambda state: can_defeat_12_eye_big_octos(state, player) and state.has("Grappling Hook", player), + ) + + # Cyclops Reef + set_rule_if_exists( + "Cyclops Reef - Destroy the Cannons and Gunboats", + lambda state: state.has("Bombs", player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists("Cyclops Reef - Lookout Platform - Defeat the Enemies", lambda state: True) + + # Two-Eye Reef + set_rule_if_exists( + "Two-Eye Reef - Destroy the Cannons and Gunboats", + lambda state: state.has("Bombs", player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists("Two-Eye Reef - Lookout Platform", lambda state: True) + set_rule_if_exists("Two-Eye Reef - Big Octo Great Fairy", lambda state: can_defeat_big_octos(state, player)) + + # Three-Eye Reef + set_rule_if_exists( + "Three-Eye Reef - Destroy the Cannons and Gunboats", + lambda state: state.has("Bombs", player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + + # Four-Eye Reef + set_rule_if_exists( + "Four-Eye Reef - Destroy the Cannons and Gunboats", + lambda state: state.has("Bombs", player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + + # Five-Eye Reef + set_rule_if_exists( + "Five-Eye Reef - Destroy the Cannons", + lambda state: can_destroy_cannons(state, player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists("Five-Eye Reef - Lookout Platform", lambda state: True) + + # Six-Eye Reef + set_rule_if_exists( + "Six-Eye Reef - Destroy the Cannons and Gunboats", + lambda state: state.has("Bombs", player) and can_fly_with_deku_leaf_outdoors(state, player), + ) + set_rule_if_exists( + "Six-Eye Reef - Lookout Platform - Destroy the Cannons", lambda state: can_destroy_cannons(state, player) + ) + set_rule_if_exists("Six-Eye Reef - Submarine", lambda state: True) + + # Sunken Treasure + set_rule_if_exists( + "Forsaken Fortress Sector - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 1), + ) + set_rule_if_exists( + "Star Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 2), + ) + set_rule_if_exists( + "Northern Fairy Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 3), + ) + set_rule_if_exists( + "Gale Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 4), + ) + set_rule_if_exists( + "Crescent Moon Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 5), + ) + set_rule_if_exists( + "Seven-Star Isles - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 6) + and (state.has("Bombs", player) or state._tww_precise_1(player)), + ) + set_rule_if_exists( + "Overlook Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 7), + ) + set_rule_if_exists( + "Four-Eye Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 8) + and ( + state.has("Bombs", player) + or state._tww_precise_1(player) + or (can_use_magic_armor(state, player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Mother and Child Isles - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 9), + ) + set_rule_if_exists( + "Spectacle Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 10), + ) + set_rule_if_exists( + "Windfall Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 11), + ) + set_rule_if_exists( + "Pawprint Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 12), + ) + set_rule_if_exists( + "Dragon Roost Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 13), + ) + set_rule_if_exists( + "Flight Control Platform - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 14), + ) + set_rule_if_exists( + "Western Fairy Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 15), + ) + set_rule_if_exists( + "Rock Spire Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 16), + ) + set_rule_if_exists( + "Tingle Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 17), + ) + set_rule_if_exists( + "Northern Triangle Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 18), + ) + set_rule_if_exists( + "Eastern Fairy Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 19), + ) + set_rule_if_exists( + "Fire Mountain - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 20), + ) + set_rule_if_exists( + "Star Belt Archipelago - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 21), + ) + set_rule_if_exists( + "Three-Eye Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 22) + and ( + state.has("Bombs", player) + or state._tww_precise_1(player) + or (can_use_magic_armor(state, player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Greatfish Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 23), + ) + set_rule_if_exists( + "Cyclops Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 24) + and ( + state.has("Bombs", player) + or state._tww_precise_1(player) + or (can_use_magic_armor(state, player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Six-Eye Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 25) + and ( + state.has("Bombs", player) + or state._tww_precise_1(player) + or (can_use_magic_armor(state, player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Tower of the Gods Sector - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 26), + ) + set_rule_if_exists( + "Eastern Triangle Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 27), + ) + set_rule_if_exists( + "Thorned Fairy Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 28), + ) + set_rule_if_exists( + "Needle Rock Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 29), + ) + set_rule_if_exists( + "Islet of Steel - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 30), + ) + set_rule_if_exists( + "Stone Watcher Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 31), + ) + set_rule_if_exists( + "Southern Triangle Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 32) + and (can_defeat_seahats(state, player) or state._tww_precise_1(player)), + ) + set_rule_if_exists( + "Private Oasis - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 33), + ) + set_rule_if_exists( + "Bomb Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 34), + ) + set_rule_if_exists( + "Bird's Peak Rock - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 35), + ) + set_rule_if_exists( + "Diamond Steppe Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 36), + ) + set_rule_if_exists( + "Five-Eye Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 37) + and can_destroy_cannons(state, player), + ) + set_rule_if_exists( + "Shark Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 38), + ) + set_rule_if_exists( + "Southern Fairy Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 39), + ) + set_rule_if_exists( + "Ice Ring Isle - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 40), + ) + set_rule_if_exists( + "Forest Haven - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 41), + ) + set_rule_if_exists( + "Cliff Plateau Isles - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 42), + ) + set_rule_if_exists( + "Horseshoe Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 43), + ) + set_rule_if_exists( + "Outset Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 44), + ) + set_rule_if_exists( + "Headstone Island - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 45), + ) + set_rule_if_exists( + "Two-Eye Reef - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) + and state._tww_has_chart_for_island(player, 46) + and ( + state.has("Bombs", player) + or state._tww_precise_1(player) + or (can_use_magic_armor(state, player) and state._tww_obscure_1(player)) + ), + ) + set_rule_if_exists( + "Angular Isles - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 47), + ) + set_rule_if_exists( + "Boating Course - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 48), + ) + set_rule_if_exists( + "Five-Star Isles - Sunken Treasure", + lambda state: state.has("Grappling Hook", player) and state._tww_has_chart_for_island(player, 49), + ) + + set_rule_if_exists("Defeat Ganondorf", lambda state: can_reach_and_defeat_ganondorf(state, player)) + + world.multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/tww/TWWClient.py b/worlds/tww/TWWClient.py new file mode 100644 index 00000000..84173659 --- /dev/null +++ b/worlds/tww/TWWClient.py @@ -0,0 +1,739 @@ +import asyncio +import time +import traceback +from typing import TYPE_CHECKING, Any, Optional + +import dolphin_memory_engine + +import Utils +from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, gui_enabled, logger, server_loop +from NetUtils import ClientStatus + +from .Items import ITEM_TABLE, LOOKUP_ID_TO_NAME +from .Locations import ISLAND_NAME_TO_SALVAGE_BIT, LOCATION_TABLE, TWWLocation, TWWLocationData, TWWLocationType +from .randomizers.Charts import ISLAND_NUMBER_TO_NAME + +if TYPE_CHECKING: + import kvui + +CONNECTION_REFUSED_GAME_STATUS = ( + "Dolphin failed to connect. Please load a randomized ROM for The Wind Waker. Trying again in 5 seconds..." +) +CONNECTION_REFUSED_SAVE_STATUS = ( + "Dolphin failed to connect. Please load into the save file. Trying again in 5 seconds..." +) +CONNECTION_LOST_STATUS = ( + "Dolphin connection was lost. Please restart your emulator and make sure The Wind Waker is running." +) +CONNECTION_CONNECTED_STATUS = "Dolphin connected successfully." +CONNECTION_INITIAL_STATUS = "Dolphin connection has not been initiated." + + +# This address is used to check/set the player's health for DeathLink. +CURR_HEALTH_ADDR = 0x803C4C0A + +# These addresses are used for the Moblin's Letter check. +LETTER_BASE_ADDR = 0x803C4C8E +LETTER_OWND_ADDR = 0x803C4C98 + +# These addresses are used to check flags for locations. +CHARTS_BITFLD_ADDR = 0x803C4CFC +BASE_CHESTS_BITFLD_ADDR = 0x803C4F88 +BASE_SWITCHES_BITFLD_ADDR = 0x803C4F8C +BASE_PICKUPS_BITFLD_ADDR = 0x803C4F9C +CURR_STAGE_CHESTS_BITFLD_ADDR = 0x803C5380 +CURR_STAGE_SWITCHES_BITFLD_ADDR = 0x803C5384 +CURR_STAGE_PICKUPS_BITFLD_ADDR = 0x803C5394 + +# The expected index for the following item that should be received. Uses event bits 0x60 and 0x61. +EXPECTED_INDEX_ADDR = 0x803C528C + +# These bytes contain whether the player has been rewarded for finding a particular Tingle statue. +TINGLE_STATUE_1_ADDR = 0x803C523E # 0x40 is the bit for the Dragon Tingle statue. +TINGLE_STATUE_2_ADDR = 0x803C5249 # 0x0F are the bits for the remaining Tingle statues. + +# This address contains the current stage ID. +CURR_STAGE_ID_ADDR = 0x803C53A4 + +# This address is used to check the stage name to verify that the player is in-game before sending items. +CURR_STAGE_NAME_ADDR = 0x803C9D3C + +# This is an array of length 0x10 where each element is a byte and contains item IDs for items to give the player. +# 0xFF represents no item. The array is read and cleared every frame. +GIVE_ITEM_ARRAY_ADDR = 0x803FE87C + +# This is the address that holds the player's slot name. +# This way, the player does not have to manually authenticate their slot name. +SLOT_NAME_ADDR = 0x803FE8A0 + +# This address is the start of an array that we use to inform us of which charts lead where. +# The array is of length 49, and each element is two bytes. The index represents the chart's original destination, and +# the value represents the new destination. +# The chart name is inferrable from the chart's original destination. +CHARTS_MAPPING_ADDR = 0x803FE8E0 + +# This address contains the most recent spawn ID from which the player spawned. +MOST_RECENT_SPAWN_ID_ADDR = 0x803C9D44 + +# This address contains the most recent room number the player spawned in. +MOST_RECENT_ROOM_NUMBER_ADDR = 0x803C9D46 + +# Values used to detect exiting onto the highest isle in Cliff Plateau Isles. +# 42. Starting at 1 and going left to right, top to bottom, Cliff Plateau Isles is the 42nd square in the sea stage. +CLIFF_PLATEAU_ISLES_ROOM_NUMBER = 0x2A +CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_SPAWN_ID = 1 # As a note, the lower isle's spawn ID is 2. +# The dummy stage name used to identify the highest isle in Cliff Plateau Isles. +CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_DUMMY_STAGE_NAME = "CliPlaH" + +# Data storage key +AP_VISITED_STAGE_NAMES_KEY_FORMAT = "tww_visited_stages_%i" + + +class TWWCommandProcessor(ClientCommandProcessor): + """ + Command Processor for The Wind Waker client commands. + + This class handles commands specific to The Wind Waker. + """ + + def __init__(self, ctx: CommonContext): + """ + Initialize the command processor with the provided context. + + :param ctx: Context for the client. + """ + super().__init__(ctx) + + def _cmd_dolphin(self) -> None: + """ + Display the current Dolphin emulator connection status. + """ + if isinstance(self.ctx, TWWContext): + logger.info(f"Dolphin Status: {self.ctx.dolphin_status}") + + +class TWWContext(CommonContext): + """ + The context for The Wind Waker client. + + This class manages all interactions with the Dolphin emulator and the Archipelago server for The Wind Waker. + """ + + command_processor = TWWCommandProcessor + game: str = "The Wind Waker" + items_handling: int = 0b111 + + def __init__(self, server_address: Optional[str], password: Optional[str]) -> None: + """ + Initialize the TWW context. + + :param server_address: Address of the Archipelago server. + :param password: Password for server authentication. + """ + + super().__init__(server_address, password) + self.dolphin_sync_task: Optional[asyncio.Task[None]] = None + self.dolphin_status: str = CONNECTION_INITIAL_STATUS + self.awaiting_rom: bool = False + self.has_send_death: bool = False + + # Bitfields used for checking locations. + self.charts_bitfield: int + self.chests_bitfields: dict[int, int] + self.switches_bitfields: dict[int, int] + self.pickups_bitfields: dict[int, int] + self.curr_stage_chests_bitfield: int + self.curr_stage_switches_bitfield: int + self.curr_stage_pickups_bitfield: int + + # Keep track of whether the player has yet received their first progressive magic meter. + self.received_magic: bool = False + + # A dictionary that maps salvage locations to their sunken treasure bit. + self.salvage_locations_map: dict[str, int] = {} + + # Name of the current stage as read from the game's memory. Sent to trackers whenever its value changes to + # facilitate automatically switching to the map of the current stage. + self.current_stage_name: str = "" + + # Set of visited stages. A dictionary (used as a set) of all visited stages is set in the server's data storage + # and updated when the player visits a new stage for the first time. To track which stages are new and need to + # cause the server's data storage to update, the TWW AP Client keeps track of the visited stages in a set. + # Trackers can request the dictionary from data storage to see which stages the player has visited. + # It starts as `None` until it has been read from the server. + self.visited_stage_names: Optional[set[str]] = None + + # Length of the item get array in memory. + self.len_give_item_array: int = 0x10 + + async def disconnect(self, allow_autoreconnect: bool = False) -> None: + """ + Disconnect the client from the server and reset game state variables. + + :param allow_autoreconnect: Allow the client to auto-reconnect to the server. Defaults to `False`. + + """ + self.auth = None + self.salvage_locations_map = {} + self.current_stage_name = "" + self.visited_stage_names = None + await super().disconnect(allow_autoreconnect) + + async def server_auth(self, password_requested: bool = False) -> None: + """ + Authenticate with the Archipelago server. + + :param password_requested: Whether the server requires a password. Defaults to `False`. + """ + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + if self.awaiting_rom: + return + self.awaiting_rom = True + logger.info("Awaiting connection to Dolphin to get player information.") + return + await self.send_connect() + + def on_package(self, cmd: str, args: dict[str, Any]) -> None: + """ + Handle incoming packages from the server. + + :param cmd: The command received from the server. + :param args: The command arguments. + """ + if cmd == "Connected": + self.update_salvage_locations_map() + if "death_link" in args["slot_data"]: + Utils.async_start(self.update_death_link(bool(args["slot_data"]["death_link"]))) + # Request the connected slot's dictionary (used as a set) of visited stages. + visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot + Utils.async_start(self.send_msgs([{"cmd": "Get", "keys": [visited_stages_key]}])) + elif cmd == "Retrieved": + requested_keys_dict = args["keys"] + # Read the connected slot's dictionary (used as a set) of visited stages. + if self.slot is not None: + visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot + if visited_stages_key in requested_keys_dict: + visited_stages = requested_keys_dict[visited_stages_key] + # If it has not been set before, the value in the response will be `None`. + visited_stage_names = set() if visited_stages is None else set(visited_stages.keys()) + # If the current stage name is not in the set, send a message to update the dictionary on the + # server. + current_stage_name = self.current_stage_name + if current_stage_name and current_stage_name not in visited_stage_names: + visited_stage_names.add(current_stage_name) + Utils.async_start(self.update_visited_stages(current_stage_name)) + self.visited_stage_names = visited_stage_names + + def on_deathlink(self, data: dict[str, Any]) -> None: + """ + Handle a DeathLink event. + + :param data: The data associated with the DeathLink event. + """ + super().on_deathlink(data) + _give_death(self) + + def make_gui(self) -> type["kvui.GameManager"]: + """ + Initialize the GUI for The Wind Waker client. + + :return: The client's GUI. + """ + ui = super().make_gui() + ui.base_title = "Archipelago The Wind Waker Client" + return ui + + async def update_visited_stages(self, newly_visited_stage_name: str) -> None: + """ + Update the server's data storage of the visited stage names to include the newly visited stage name. + + :param newly_visited_stage_name: The name of the stage recently visited. + """ + if self.slot is not None: + visited_stages_key = AP_VISITED_STAGE_NAMES_KEY_FORMAT % self.slot + await self.send_msgs( + [ + { + "cmd": "Set", + "key": visited_stages_key, + "default": {}, + "want_reply": False, + "operations": [{"operation": "update", "value": {newly_visited_stage_name: True}}], + } + ] + ) + + def update_salvage_locations_map(self) -> None: + """ + Update the client's mapping of salvage locations to their bitfield bit. + + This is necessary for the client to handle randomized charts correctly. + """ + self.salvage_locations_map = {} + for offset in range(49): + island_name = ISLAND_NUMBER_TO_NAME[offset + 1] + salvage_bit = ISLAND_NAME_TO_SALVAGE_BIT[island_name] + + shuffled_island_number = read_short(CHARTS_MAPPING_ADDR + offset * 2) + shuffled_island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number] + salvage_location_name = f"{shuffled_island_name} - Sunken Treasure" + + self.salvage_locations_map[salvage_location_name] = salvage_bit + + +def read_short(console_address: int) -> int: + """ + Read a 2-byte short from Dolphin memory. + + :param console_address: Address to read from. + :return: The value read from memory. + """ + return int.from_bytes(dolphin_memory_engine.read_bytes(console_address, 2), byteorder="big") + + +def write_short(console_address: int, value: int) -> None: + """ + Write a 2-byte short to Dolphin memory. + + :param console_address: Address to write to. + :param value: Value to write. + """ + dolphin_memory_engine.write_bytes(console_address, value.to_bytes(2, byteorder="big")) + + +def read_string(console_address: int, strlen: int) -> str: + """ + Read a string from Dolphin memory. + + :param console_address: Address to start reading from. + :param strlen: Length of the string to read. + :return: The string. + """ + return dolphin_memory_engine.read_bytes(console_address, strlen).split(b"\0", 1)[0].decode() + + +def _give_death(ctx: TWWContext) -> None: + """ + Trigger the player's death in-game by setting their current health to zero. + + :param ctx: The Wind Waker client context. + """ + if ( + ctx.slot is not None + and dolphin_memory_engine.is_hooked() + and ctx.dolphin_status == CONNECTION_CONNECTED_STATUS + and check_ingame() + ): + ctx.has_send_death = True + write_short(CURR_HEALTH_ADDR, 0) + + +def _give_item(ctx: TWWContext, item_name: str) -> bool: + """ + Give an item to the player in-game. + + :param ctx: The Wind Waker client context. + :param item_name: Name of the item to give. + :return: Whether the item was successfully given. + """ + if not check_ingame() or dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR) == 0xFF: + return False + + item_id = ITEM_TABLE[item_name].item_id + + # Loop through the item array, placing the item in an empty slot. + for idx in range(ctx.len_give_item_array): + slot = dolphin_memory_engine.read_byte(GIVE_ITEM_ARRAY_ADDR + idx) + if slot == 0xFF: + # Special case: Use a different item ID for the second progressive magic meter. + if item_name == "Progressive Magic Meter": + if ctx.received_magic: + item_id = 0xB2 + else: + ctx.received_magic = True + dolphin_memory_engine.write_byte(GIVE_ITEM_ARRAY_ADDR + idx, item_id) + return True + + # If unable to place the item in the array, return `False`. + return False + + +async def give_items(ctx: TWWContext) -> None: + """ + Give the player all outstanding items they have yet to receive. + + :param ctx: The Wind Waker client context. + """ + if check_ingame() and dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR) != 0xFF: + # Read the expected index of the player, which is the index of the next item they're expecting to receive. + # The expected index starts at 0 for a fresh save file. + expected_idx = read_short(EXPECTED_INDEX_ADDR) + + # Check if there are new items. + received_items = ctx.items_received + if len(received_items) <= expected_idx: + # There are no new items. + return + + # Loop through items to give. + # Give the player all items at an index greater than or equal to the expected index. + for idx, item in enumerate(received_items[expected_idx:], start=expected_idx): + # Attempt to give the item and increment the expected index. + while not _give_item(ctx, LOOKUP_ID_TO_NAME[item.item]): + await asyncio.sleep(0.01) + + # Increment the expected index. + write_short(EXPECTED_INDEX_ADDR, idx + 1) + + +def check_special_location(location_name: str, data: TWWLocationData) -> bool: + """ + Check that the player has checked a given location. + This function handles locations that require special logic. + + :param location_name: The name of the location. + :param data: The data associated with the location. + :raises NotImplementedError: If an unknown location name is provided. + """ + checked = False + + # For "Windfall Island - Lenzo's House - Become Lenzo's Assistant" + # 0x6 is delivered the final picture for Lenzo, 0x7 is a day has passed since becoming his assistant + # Either is fine for sending the check, so check both conditions. + if location_name == "Windfall Island - Lenzo's House - Become Lenzo's Assistant": + checked = ( + dolphin_memory_engine.read_byte(data.address) & 0x6 == 0x6 + or dolphin_memory_engine.read_byte(data.address) & 0x7 == 0x7 + ) + + # The "Windfall Island - Maggie - Delivery Reward" flag remains unknown. + # However, as a temporary workaround, we can check if the player had Moblin's letter at some point, but it's no + # longer in their Delivery Bag. + elif location_name == "Windfall Island - Maggie - Delivery Reward": + was_moblins_owned = (dolphin_memory_engine.read_word(LETTER_OWND_ADDR) >> 15) & 1 + dbag_contents = [dolphin_memory_engine.read_byte(LETTER_BASE_ADDR + offset) for offset in range(8)] + checked = was_moblins_owned and 0x9B not in dbag_contents + + # For Letter from Hoskit's Girlfriend, we need to check two bytes. + # 0x1 = Golden Feathers delivered, 0x2 = Mail sent by Hoskit's Girlfriend, 0x3 = Mail read by Link + elif location_name == "Mailbox - Letter from Hoskit's Girlfriend": + checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3 + + # For Letter from Baito's Mother, we need to check two bytes. + # 0x1 = Note to Mom sent, 0x2 = Mail sent by Baito's Mother, 0x3 = Mail read by Link + elif location_name == "Mailbox - Letter from Baito's Mother": + checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3 + + # For Letter from Grandma, we need to check two bytes. + # 0x1 = Grandma saved, 0x2 = Mail sent by Grandma, 0x3 = Mail read by Link + elif location_name == "Mailbox - Letter from Grandma": + checked = dolphin_memory_engine.read_byte(data.address) & 0x3 == 0x3 + + # We check if the bits for turning all five statues are set for the Ankle's reward. + # For some reason, the bit for the Dragon Tingle Statue is separate from the rest. + elif location_name == "Tingle Island - Ankle - Reward for All Tingle Statues": + dragon_tingle_statue_rewarded = dolphin_memory_engine.read_byte(TINGLE_STATUE_1_ADDR) & 0x40 == 0x40 + other_tingle_statues_rewarded = dolphin_memory_engine.read_byte(TINGLE_STATUE_2_ADDR) & 0x0F == 0x0F + checked = dragon_tingle_statue_rewarded and other_tingle_statues_rewarded + + else: + raise NotImplementedError(f"Unknown special location: {location_name}") + + return checked + + +def check_regular_location(ctx: TWWContext, curr_stage_id: int, data: TWWLocationData) -> bool: + """ + Check that the player has checked a given location. + This function handles locations that only require checking that a particular bit is set. + + The check looks at the saved data for the stage at which the location is located and the data for the current stage. + In the latter case, this data includes data that has not yet been written to the saved data. + + :param ctx: The Wind Waker client context. + :param curr_stage_id: The current stage at which the player is. + :param data: The data associated with the location. + :raises NotImplementedError: If a location with an unknown type is provided. + """ + checked = False + + # Check the saved bitfields for the stage. + if data.type == TWWLocationType.CHEST: + checked = bool((ctx.chests_bitfields[data.stage_id] >> data.bit) & 1) + elif data.type == TWWLocationType.SWTCH: + checked = bool((ctx.switches_bitfields[data.stage_id] >> data.bit) & 1) + elif data.type == TWWLocationType.PCKUP: + checked = bool((ctx.pickups_bitfields[data.stage_id] >> data.bit) & 1) + else: + raise NotImplementedError(f"Unknown location type: {data.type}") + + # If the location is in the current stage, check the bitfields for the current stage as well. + if not checked and curr_stage_id == data.stage_id: + if data.type == TWWLocationType.CHEST: + checked = bool((ctx.curr_stage_chests_bitfield >> data.bit) & 1) + elif data.type == TWWLocationType.SWTCH: + checked = bool((ctx.curr_stage_switches_bitfield >> data.bit) & 1) + elif data.type == TWWLocationType.PCKUP: + checked = bool((ctx.curr_stage_pickups_bitfield >> data.bit) & 1) + else: + raise NotImplementedError(f"Unknown location type: {data.type}") + + return checked + + +async def check_locations(ctx: TWWContext) -> None: + """ + Iterate through all locations and check whether the player has checked each location. + + Update the server with all newly checked locations since the last update. If the player has completed the goal, + notify the server. + + :param ctx: The Wind Waker client context. + """ + # Read the bitfield for sunken treasure locations. + ctx.charts_bitfield = int.from_bytes(dolphin_memory_engine.read_bytes(CHARTS_BITFLD_ADDR, 8), byteorder="big") + + # Read the bitfields once before the loop to speed things up a bit. + ctx.chests_bitfields = {} + ctx.switches_bitfields = {} + ctx.pickups_bitfields = {} + for stage_id in range(0xE): + chest_bitfield_addr = BASE_CHESTS_BITFLD_ADDR + (0x24 * stage_id) + switches_bitfield_addr = BASE_SWITCHES_BITFLD_ADDR + (0x24 * stage_id) + pickups_bitfield_addr = BASE_PICKUPS_BITFLD_ADDR + (0x24 * stage_id) + + ctx.chests_bitfields[stage_id] = int(dolphin_memory_engine.read_word(chest_bitfield_addr)) + ctx.switches_bitfields[stage_id] = int.from_bytes( + dolphin_memory_engine.read_bytes(switches_bitfield_addr, 10), byteorder="big" + ) + ctx.pickups_bitfields[stage_id] = int(dolphin_memory_engine.read_word(pickups_bitfield_addr)) + + ctx.curr_stage_chests_bitfield = int(dolphin_memory_engine.read_word(CURR_STAGE_CHESTS_BITFLD_ADDR)) + ctx.curr_stage_switches_bitfield = int.from_bytes( + dolphin_memory_engine.read_bytes(CURR_STAGE_SWITCHES_BITFLD_ADDR, 10), byteorder="big" + ) + ctx.curr_stage_pickups_bitfield = int(dolphin_memory_engine.read_word(CURR_STAGE_PICKUPS_BITFLD_ADDR)) + + # We check which locations are currently checked on the current stage. + curr_stage_id = dolphin_memory_engine.read_byte(CURR_STAGE_ID_ADDR) + + # Loop through all locations to see if each has been checked. + for location, data in LOCATION_TABLE.items(): + checked = False + if data.type == TWWLocationType.CHART: + assert location in ctx.salvage_locations_map, f'Location "{location}" salvage bit not set!' + salvage_bit = ctx.salvage_locations_map[location] + checked = bool((ctx.charts_bitfield >> salvage_bit) & 1) + elif data.type == TWWLocationType.BOCTO: + assert data.address is not None + checked = bool((read_short(data.address) >> data.bit) & 1) + elif data.type == TWWLocationType.EVENT: + checked = bool((dolphin_memory_engine.read_byte(data.address) >> data.bit) & 1) + elif data.type == TWWLocationType.SPECL: + checked = check_special_location(location, data) + else: + checked = check_regular_location(ctx, curr_stage_id, data) + + if checked: + if data.code is None: + if not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + else: + ctx.locations_checked.add(TWWLocation.get_apid(data.code)) + + # Send the list of newly-checked locations to the server. + locations_checked = ctx.locations_checked.difference(ctx.checked_locations) + if locations_checked: + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations_checked}]) + + +async def check_current_stage_changed(ctx: TWWContext) -> None: + """ + Check if the player has moved to a new stage. + If so, update all trackers with the new stage name. + If the stage has never been visited, additionally update the server. + + :param ctx: The Wind Waker client context. + """ + new_stage_name = read_string(CURR_STAGE_NAME_ADDR, 8) + + # Special handling is required for the Cliff Plateau Isles Inner Cave exit, which exits out onto the sea stage + # rather than a unique stage. + if ( + new_stage_name == "sea" + and dolphin_memory_engine.read_byte(MOST_RECENT_ROOM_NUMBER_ADDR) == CLIFF_PLATEAU_ISLES_ROOM_NUMBER + and read_short(MOST_RECENT_SPAWN_ID_ADDR) == CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_SPAWN_ID + ): + new_stage_name = CLIFF_PLATEAU_ISLES_HIGHEST_ISLE_DUMMY_STAGE_NAME + + current_stage_name = ctx.current_stage_name + if new_stage_name != current_stage_name: + ctx.current_stage_name = new_stage_name + # Send a Bounced message containing the new stage name to all trackers connected to the current slot. + data_to_send = {"tww_stage_name": new_stage_name} + message = { + "cmd": "Bounce", + "slots": [ctx.slot], + "data": data_to_send, + } + await ctx.send_msgs([message]) + + # If the stage has never been visited before, update the server's data storage to indicate that it has been + # visited. + visited_stage_names = ctx.visited_stage_names + if visited_stage_names is not None and new_stage_name not in visited_stage_names: + visited_stage_names.add(new_stage_name) + await ctx.update_visited_stages(new_stage_name) + + +async def check_alive() -> bool: + """ + Check if the player is currently alive in-game. + + :return: `True` if the player is alive, otherwise `False`. + """ + cur_health = read_short(CURR_HEALTH_ADDR) + return cur_health > 0 + + +async def check_death(ctx: TWWContext) -> None: + """ + Check if the player is currently dead in-game. + If DeathLink is on, notify the server of the player's death. + + :return: `True` if the player is dead, otherwise `False`. + """ + if ctx.slot is not None and check_ingame(): + cur_health = read_short(CURR_HEALTH_ADDR) + if cur_health <= 0: + if not ctx.has_send_death and time.time() >= ctx.last_death_link + 3: + ctx.has_send_death = True + await ctx.send_death(ctx.player_names[ctx.slot] + " ran out of hearts.") + else: + ctx.has_send_death = False + + +def check_ingame() -> bool: + """ + Check if the player is currently in-game. + + :return: `True` if the player is in-game, otherwise `False`. + """ + return read_string(CURR_STAGE_NAME_ADDR, 8) not in ["", "sea_T", "Name"] + + +async def dolphin_sync_task(ctx: TWWContext) -> None: + """ + The task loop for managing the connection to Dolphin. + + While connected, read the emulator's memory to look for any relevant changes made by the player in the game. + + :param ctx: The Wind Waker client context. + """ + logger.info("Starting Dolphin connector. Use /dolphin for status information.") + sleep_time = 0.0 + while not ctx.exit_event.is_set(): + if sleep_time > 0.0: + try: + # ctx.watcher_event gets set when receiving ReceivedItems or LocationInfo, or when shutting down. + await asyncio.wait_for(ctx.watcher_event.wait(), sleep_time) + except asyncio.TimeoutError: + pass + sleep_time = 0.0 + ctx.watcher_event.clear() + + try: + if dolphin_memory_engine.is_hooked() and ctx.dolphin_status == CONNECTION_CONNECTED_STATUS: + if not check_ingame(): + # Reset the give item array while not in the game. + dolphin_memory_engine.write_bytes(GIVE_ITEM_ARRAY_ADDR, bytes([0xFF] * ctx.len_give_item_array)) + sleep_time = 0.1 + continue + if ctx.slot is not None: + if "DeathLink" in ctx.tags: + await check_death(ctx) + await give_items(ctx) + await check_locations(ctx) + await check_current_stage_changed(ctx) + else: + if not ctx.auth: + ctx.auth = read_string(SLOT_NAME_ADDR, 0x40) + if ctx.awaiting_rom: + await ctx.server_auth() + sleep_time = 0.1 + else: + if ctx.dolphin_status == CONNECTION_CONNECTED_STATUS: + logger.info("Connection to Dolphin lost, reconnecting...") + ctx.dolphin_status = CONNECTION_LOST_STATUS + logger.info("Attempting to connect to Dolphin...") + dolphin_memory_engine.hook() + if dolphin_memory_engine.is_hooked(): + if dolphin_memory_engine.read_bytes(0x80000000, 6) != b"GZLE99": + logger.info(CONNECTION_REFUSED_GAME_STATUS) + ctx.dolphin_status = CONNECTION_REFUSED_GAME_STATUS + dolphin_memory_engine.un_hook() + sleep_time = 5 + else: + logger.info(CONNECTION_CONNECTED_STATUS) + ctx.dolphin_status = CONNECTION_CONNECTED_STATUS + ctx.locations_checked = set() + else: + logger.info("Connection to Dolphin failed, attempting again in 5 seconds...") + ctx.dolphin_status = CONNECTION_LOST_STATUS + await ctx.disconnect() + sleep_time = 5 + continue + except Exception: + dolphin_memory_engine.un_hook() + logger.info("Connection to Dolphin failed, attempting again in 5 seconds...") + logger.error(traceback.format_exc()) + ctx.dolphin_status = CONNECTION_LOST_STATUS + await ctx.disconnect() + sleep_time = 5 + continue + + +def main(connect: Optional[str] = None, password: Optional[str] = None) -> None: + """ + Run the main async loop for the Wind Waker client. + + :param connect: Address of the Archipelago server. + :param password: Password for server authentication. + """ + Utils.init_logging("The Wind Waker Client") + + async def _main(connect: Optional[str], password: Optional[str]) -> None: + ctx = TWWContext(connect, password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + await asyncio.sleep(1) + + ctx.dolphin_sync_task = asyncio.create_task(dolphin_sync_task(ctx), name="DolphinSync") + + await ctx.exit_event.wait() + # Wake the sync task, if it is currently sleeping, so it can start shutting down when it sees that the + # exit_event is set. + ctx.watcher_event.set() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.dolphin_sync_task: + await ctx.dolphin_sync_task + + import colorama + + colorama.init() + asyncio.run(_main(connect, password)) + colorama.deinit() + + +if __name__ == "__main__": + parser = get_base_parser() + args = parser.parse_args() + main(args.connect, args.password) diff --git a/worlds/tww/__init__.py b/worlds/tww/__init__.py new file mode 100644 index 00000000..36ed77f9 --- /dev/null +++ b/worlds/tww/__init__.py @@ -0,0 +1,598 @@ +import os +import zipfile +from base64 import b64encode +from collections.abc import Mapping +from typing import Any, ClassVar + +import yaml + +from BaseClasses import Item +from BaseClasses import ItemClassification as IC +from BaseClasses import MultiWorld, Region, Tutorial +from Options import Toggle +from worlds.AutoWorld import WebWorld, World +from worlds.Files import APContainer, AutoPatchRegister +from worlds.generic.Rules import add_item_rule +from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess + +from .Items import ISLAND_NUMBER_TO_CHART_NAME, ITEM_TABLE, TWWItem, item_name_groups +from .Locations import LOCATION_TABLE, TWWFlag, TWWLocation +from .Options import TWWOptions, tww_option_groups +from .Presets import tww_options_presets +from .randomizers.Charts import ISLAND_NUMBER_TO_NAME, ChartRandomizer +from .randomizers.Dungeons import Dungeon, create_dungeons +from .randomizers.Entrances import ALL_EXITS, BOSS_EXIT_TO_DUNGEON, MINIBOSS_EXIT_TO_DUNGEON, EntranceRandomizer +from .randomizers.ItemPool import generate_itempool +from .randomizers.RequiredBosses import RequiredBossesRandomizer +from .Rules import set_rules + +VERSION: tuple[int, int, int] = (3, 0, 0) + + +def run_client() -> None: + """ + Launch the The Wind Waker client. + """ + print("Running The Wind Waker Client") + from .TWWClient import main + + launch_subprocess(main, name="TheWindWakerClient") + + +components.append( + Component( + "The Wind Waker Client", + func=run_client, + component_type=Type.CLIENT, + file_identifier=SuffixIdentifier(".aptww"), + icon="The Wind Waker", + ) +) +icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png" + + +class TWWContainer(APContainer, metaclass=AutoPatchRegister): + """ + This class defines the container file for The Wind Waker. + """ + + game: str = "The Wind Waker" + patch_file_ending: str = ".aptww" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + if "data" in kwargs: + self.data = kwargs["data"] + del kwargs["data"] + + super().__init__(*args, **kwargs) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + """ + Write the contents of the container file. + """ + super().write_contents(opened_zipfile) + + # Record the data for the game under the key `plando`. + opened_zipfile.writestr("plando", b64encode(bytes(yaml.safe_dump(self.data, sort_keys=False), "utf-8"))) + + +class TWWWeb(WebWorld): + """ + This class handles the web interface for The Wind Waker. + + The web interface includes the setup guide and the options page for generating YAMLs. + """ + + tutorials = [ + Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Archipelago The Wind Waker software on your computer.", + "English", + "setup_en.md", + "setup/en", + ["tanjo3", "Lunix"], + ) + ] + theme = "ocean" + options_presets = tww_options_presets + option_groups = tww_option_groups + rich_text_options_doc = True + + +class TWWWorld(World): + """ + Legend has it that whenever evil has appeared, a hero named Link has arisen to defeat it. The legend continues on + the surface of a vast and mysterious sea as Link sets sail in his most epic, awe-inspiring adventure yet. Aided by a + magical conductor's baton called the Wind Waker, he will face unimaginable monsters, explore puzzling dungeons, and + meet a cast of unforgettable characters as he searches for his kidnapped sister. + """ + + options_dataclass = TWWOptions + options: TWWOptions + + game: ClassVar[str] = "The Wind Waker" + topology_present: bool = True + + item_name_to_id: ClassVar[dict[str, int]] = { + name: TWWItem.get_apid(data.code) for name, data in ITEM_TABLE.items() if data.code is not None + } + location_name_to_id: ClassVar[dict[str, int]] = { + name: TWWLocation.get_apid(data.code) for name, data in LOCATION_TABLE.items() if data.code is not None + } + + item_name_groups: ClassVar[dict[str, set[str]]] = item_name_groups + + required_client_version: tuple[int, int, int] = (0, 5, 1) + + web: ClassVar[TWWWeb] = TWWWeb() + + origin_region_name: str = "The Great Sea" + + create_items = generate_itempool + + logic_rematch_bosses_skipped: bool + logic_in_swordless_mode: bool + logic_in_required_bosses_mode: bool + logic_obscure_1: bool + logic_obscure_2: bool + logic_obscure_3: bool + logic_precise_1: bool + logic_precise_2: bool + logic_precise_3: bool + logic_tuner_logic_enabled: bool + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.progress_locations: set[str] = set() + self.nonprogress_locations: set[str] = set() + + self.dungeon_local_item_names: set[str] = set() + self.dungeon_specific_item_names: set[str] = set() + self.dungeons: dict[str, Dungeon] = {} + + self.item_classification_overrides: dict[str, IC] = {} + + self.useful_pool: list[str] = [] + self.filler_pool: list[str] = [] + + self.charts = ChartRandomizer(self) + self.entrances = EntranceRandomizer(self) + self.boss_reqs = RequiredBossesRandomizer(self) + + def _determine_item_classification_overrides(self) -> None: + """ + Determine item classification overrides. The classification of an item may be affected by which options are + enabled or disabled. + """ + options = self.options + item_classification_overrides = self.item_classification_overrides + + # Override certain items to be filler depending on user options. + # TODO: Calculate filler items dynamically + override_as_filler = [] + if not options.progression_dungeons: + override_as_filler.extend(item_name_groups["Small Keys"] | item_name_groups["Big Keys"]) + override_as_filler.extend(("Command Melody", "Earth God's Lyric", "Wind God's Aria")) + if not options.progression_short_sidequests: + override_as_filler.extend(("Maggie's Letter", "Moblin's Letter")) + if not (options.progression_short_sidequests or options.progression_long_sidequests): + override_as_filler.append("Progressive Picto Box") + if not options.progression_spoils_trading: + override_as_filler.append("Spoils Bag") + if not options.progression_triforce_charts: + override_as_filler.extend(item_name_groups["Triforce Charts"]) + if not options.progression_treasure_charts: + override_as_filler.extend(item_name_groups["Treasure Charts"]) + if not options.progression_misc: + override_as_filler.extend(item_name_groups["Tingle Statues"]) + + for item_name in override_as_filler: + item_classification_overrides[item_name] = IC.filler + + # Override certain items to be useful depending on user options. + # TODO: Calculate useful items dynamically + override_as_useful = [] + if not options.progression_big_octos_gunboats: + override_as_useful.append("Quiver Capacity Upgrade") + if options.sword_mode in ("swords_optional", "swordless"): + override_as_useful.append("Progressive Sword") + if not options.enable_tuner_logic: + override_as_useful.append("Tingle Tuner") + + for item_name in override_as_useful: + item_classification_overrides[item_name] = IC.useful + + def _determine_progress_and_nonprogress_locations(self) -> tuple[set[str], set[str]]: + """ + Determine which locations are progress and nonprogress in the world based on the player's options. + + :return: A tuple of two sets, the first containing the names of the progress locations and the second containing + the names of the nonprogress locations. + """ + + def add_flag(option: Toggle, flag: TWWFlag) -> TWWFlag: + return flag if option else TWWFlag.ALWAYS + + options = self.options + + enabled_flags = TWWFlag.ALWAYS + enabled_flags |= add_flag(options.progression_dungeons, TWWFlag.DUNGEON | TWWFlag.BOSS) + enabled_flags |= add_flag(options.progression_tingle_chests, TWWFlag.TNGL_CT) + enabled_flags |= add_flag(options.progression_dungeon_secrets, TWWFlag.DG_SCRT) + enabled_flags |= add_flag(options.progression_puzzle_secret_caves, TWWFlag.PZL_CVE) + enabled_flags |= add_flag(options.progression_combat_secret_caves, TWWFlag.CBT_CVE) + enabled_flags |= add_flag(options.progression_savage_labyrinth, TWWFlag.SAVAGE) + enabled_flags |= add_flag(options.progression_great_fairies, TWWFlag.GRT_FRY) + enabled_flags |= add_flag(options.progression_short_sidequests, TWWFlag.SHRT_SQ) + enabled_flags |= add_flag(options.progression_long_sidequests, TWWFlag.LONG_SQ) + enabled_flags |= add_flag(options.progression_spoils_trading, TWWFlag.SPOILS) + enabled_flags |= add_flag(options.progression_minigames, TWWFlag.MINIGME) + enabled_flags |= add_flag(options.progression_battlesquid, TWWFlag.SPLOOSH) + enabled_flags |= add_flag(options.progression_free_gifts, TWWFlag.FREE_GF) + enabled_flags |= add_flag(options.progression_mail, TWWFlag.MAILBOX) + enabled_flags |= add_flag(options.progression_platforms_rafts, TWWFlag.PLTFRMS) + enabled_flags |= add_flag(options.progression_submarines, TWWFlag.SUBMRIN) + enabled_flags |= add_flag(options.progression_eye_reef_chests, TWWFlag.EYE_RFS) + enabled_flags |= add_flag(options.progression_big_octos_gunboats, TWWFlag.BG_OCTO) + enabled_flags |= add_flag(options.progression_expensive_purchases, TWWFlag.XPENSVE) + enabled_flags |= add_flag(options.progression_island_puzzles, TWWFlag.ISLND_P) + enabled_flags |= add_flag(options.progression_misc, TWWFlag.MISCELL) + + progress_locations: set[str] = set() + nonprogress_locations: set[str] = set() + for location, data in LOCATION_TABLE.items(): + if data.flags & enabled_flags == data.flags: + progress_locations.add(location) + else: + nonprogress_locations.add(location) + assert progress_locations.isdisjoint(nonprogress_locations) + + return progress_locations, nonprogress_locations + + @staticmethod + def _get_classification_name(classification: IC) -> str: + """ + Return a string representation of the item's highest-order classification. + + :param classification: The item's classification. + :return: A string representation of the item's highest classification. The order of classification is + progression > trap > useful > filler. + """ + + if IC.progression in classification: + return "progression" + elif IC.trap in classification: + return "trap" + elif IC.useful in classification: + return "useful" + else: + return "filler" + + def generate_early(self) -> None: + """ + Run before any general steps of the MultiWorld other than options. + """ + options = self.options + + # Only randomize secret cave inner entrances if both puzzle secret caves and combat secret caves are enabled. + if not (options.progression_puzzle_secret_caves and options.progression_combat_secret_caves): + options.randomize_secret_cave_inner_entrances.value = False + + # Determine which locations are progression and which are not from options. + self.progress_locations, self.nonprogress_locations = self._determine_progress_and_nonprogress_locations() + + for dungeon_item in ["randomize_smallkeys", "randomize_bigkeys", "randomize_mapcompass"]: + option = getattr(options, dungeon_item) + if option == "local": + options.local_items.value |= self.item_name_groups[option.item_name_group] + elif option.in_dungeon: + self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] + if option == "dungeon": + self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group] + else: + options.local_items.value |= self.dungeon_local_item_names + + # Resolve logic options and set them onto the world instance for faster lookup in logic rules. + self.logic_rematch_bosses_skipped = bool(options.skip_rematch_bosses.value) + self.logic_in_swordless_mode = options.sword_mode in ("swords_optional", "swordless") + self.logic_in_required_bosses_mode = bool(options.required_bosses.value) + self.logic_obscure_3 = options.logic_obscurity == "very_hard" + self.logic_obscure_2 = self.logic_obscure_3 or options.logic_obscurity == "hard" + self.logic_obscure_1 = self.logic_obscure_2 or options.logic_obscurity == "normal" + self.logic_precise_3 = options.logic_precision == "very_hard" + self.logic_precise_2 = self.logic_precise_3 or options.logic_precision == "hard" + self.logic_precise_1 = self.logic_precise_2 or options.logic_precision == "normal" + self.logic_tuner_logic_enabled = bool(options.enable_tuner_logic.value) + + # Determine any item classification overrides based on user options. + self._determine_item_classification_overrides() + + def create_regions(self) -> None: + """ + Create and connect regions for the The Wind Waker world. + + This method first randomizes the charts and picks the required bosses if these options are enabled. + It then loops through all the world's progress locations and creates the locations, assigning dungeon locations + to their respective dungeons. + Finally, the flags for sunken treasure locations are updated as appropriate, and the entrances are randomized + if that option is enabled. + """ + multiworld = self.multiworld + player = self.player + options = self.options + + # "The Great Sea" region contains all locations that are not in a randomizable region. + great_sea_region = Region("The Great Sea", player, multiworld) + multiworld.regions.append(great_sea_region) + + # Add all randomizable regions. + for _exit in ALL_EXITS: + multiworld.regions.append(Region(_exit.unique_name, player, multiworld)) + + # Set up sunken treasure locations, randomizing the charts if necessary. + self.charts.setup_progress_sunken_treasure_locations() + + # Select required bosses. + if options.required_bosses: + self.boss_reqs.randomize_required_bosses() + self.progress_locations -= self.boss_reqs.banned_locations + self.nonprogress_locations |= self.boss_reqs.banned_locations + + # Create the dungeon classes. + create_dungeons(self) + + # Assign each location to their region. + # Progress locations are sorted for deterministic results. + for location_name in sorted(self.progress_locations): + data = LOCATION_TABLE[location_name] + + region = self.get_region(data.region) + location = TWWLocation(player, location_name, region, data) + + # Additionally, assign dungeon locations to the appropriate dungeon. + if region.name in self.dungeons: + location.dungeon = self.dungeons[region.name] + elif region.name in MINIBOSS_EXIT_TO_DUNGEON and not options.randomize_miniboss_entrances: + location.dungeon = self.dungeons[MINIBOSS_EXIT_TO_DUNGEON[region.name]] + elif region.name in BOSS_EXIT_TO_DUNGEON and not options.randomize_boss_entrances: + location.dungeon = self.dungeons[BOSS_EXIT_TO_DUNGEON[region.name]] + elif location.name in [ + "Forsaken Fortress - Phantom Ganon", + "Forsaken Fortress - Chest Outside Upper Jail Cell", + "Forsaken Fortress - Chest Inside Lower Jail Cell", + "Forsaken Fortress - Chest Guarded By Bokoblin", + "Forsaken Fortress - Chest on Bed", + ]: + location.dungeon = self.dungeons["Forsaken Fortress"] + region.locations.append(location) + + # Correct the flags of the sunken treasure locations if the charts are randomized. + self.charts.update_chart_location_flags() + + # Connect the regions in the multiworld. Randomize entrances to exits if the option is set. + self.entrances.randomize_entrances() + + def set_rules(self) -> None: + """ + Set access and item rules on locations. + """ + # Set the access rules for all progression locations. + set_rules(self) + + # Ban the Bait Bag slot from having bait. + # Beedle's shop does not work correctly if the same item is in multiple slots in the same shop. + if "The Great Sea - Beedle's Shop Ship - 20 Rupee Item" in self.progress_locations: + beedle_20 = self.get_location("The Great Sea - Beedle's Shop Ship - 20 Rupee Item") + add_item_rule(beedle_20, lambda item: item.name not in ["All-Purpose Bait", "Hyoi Pear"]) + + # For the same reason, the same item should not appear more than once on the Rock Spire Isle shop ship. + # All non-TWW items use the same item (Father's Letter), so at most one non-TWW item can appear in the shop. + # The rest must be (unique, but not necessarily local) TWW items. + locations = [f"Rock Spire Isle - Beedle's Special Shop Ship - {v} Rupee Item" for v in [500, 950, 900]] + if all(loc in self.progress_locations for loc in locations): + rock_spire_shop_ship_locations = [self.get_location(location_name) for location_name in locations] + + for i in range(len(rock_spire_shop_ship_locations)): + curr_loc = rock_spire_shop_ship_locations[i] + other_locs = rock_spire_shop_ship_locations[:i] + rock_spire_shop_ship_locations[i + 1:] + + add_item_rule( + curr_loc, + lambda item, locations=other_locs: ( + item.game == "The Wind Waker" + and all(location.item is None or item.name != location.item.name for location in locations) + ) + or ( + item.game != "The Wind Waker" + and all( + location.item is None or location.item.game == "The Wind Waker" for location in locations + ) + ), + ) + + @classmethod + def stage_set_rules(cls, multiworld: MultiWorld) -> None: + """ + Class method used to modify the rules for The Wind Waker dungeon locations. + + :param multiworld: The MultiWorld. + """ + from .randomizers.Dungeons import modify_dungeon_location_rules + + # Set additional rules on dungeon locations as necessary. + modify_dungeon_location_rules(multiworld) + + @classmethod + def stage_pre_fill(cls, multiworld: MultiWorld) -> None: + """ + Class method used to correctly place dungeon items for The Wind Waker worlds. + + :param multiworld: The MultiWorld. + """ + from .randomizers.Dungeons import fill_dungeons_restrictive + + fill_dungeons_restrictive(multiworld) + + def generate_output(self, output_directory: str) -> None: + """ + Create the output APTWW file that is used to randomize the ISO. + + :param output_directory: The output directory for the APTWW file. + """ + multiworld = self.multiworld + player = self.player + + # Determine the current arrangement for charts. + # Create a list where the original island number is the index, and the value is the new island number. + # Without randomized charts, this array would be just an ordered list of the numbers 1 to 49. + # With randomized charts, the new island number is where the chart for the original island now leads. + chart_name_to_island_number = { + chart_name: island_number for island_number, chart_name in self.charts.island_number_to_chart_name.items() + } + charts_mapping: list[int] = [] + for i in range(1, 49 + 1): + original_chart_name = ISLAND_NUMBER_TO_CHART_NAME[i] + new_island_number = chart_name_to_island_number[original_chart_name] + charts_mapping.append(new_island_number) + + # Output seed name and slot number to seed RNG in randomizer client. + output_data = { + "Version": list(VERSION), + "Seed": multiworld.seed_name, + "Slot": player, + "Name": self.player_name, + "Options": self.options.as_dict(*self.options_dataclass.type_hints), + "Required Bosses": self.boss_reqs.required_boss_item_locations, + "Locations": {}, + "Entrances": {}, + "Charts": charts_mapping, + } + + # Output which item has been placed at each location. + output_locations = output_data["Locations"] + locations = multiworld.get_locations(player) + for location in locations: + if location.name != "Defeat Ganondorf": + if location.item: + item_info = { + "player": location.item.player, + "name": location.item.name, + "game": location.item.game, + "classification": self._get_classification_name(location.item.classification), + } + else: + item_info = {"name": "Nothing", "game": "The Wind Waker", "classification": "filler"} + output_locations[location.name] = item_info + + # Output the mapping of entrances to exits. + output_entrances = output_data["Entrances"] + for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items(): + output_entrances[zone_entrance.entrance_name] = zone_exit.unique_name + + # Output the plando details to file. + aptww = TWWContainer( + path=os.path.join( + output_directory, f"{multiworld.get_out_file_name_base(player)}{TWWContainer.patch_file_ending}" + ), + player=player, + player_name=self.player_name, + data=output_data, + ) + aptww.write() + + def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None: + """ + Fill in additional information text into locations, displayed when hinted. + + :param hint_data: A dictionary of mapping a player ID to a dictionary mapping location IDs to the extra hint + information text. This dictionary should be modified as a side-effect of this method. + """ + # Create a mapping of island names to numbers for sunken treasure hints. + island_name_to_number = {v: k for k, v in ISLAND_NUMBER_TO_NAME.items()} + + hint_data[self.player] = {} + for location in self.multiworld.get_locations(self.player): + if location.address is not None and location.item is not None: + # Regardless of ER settings, always hint at the outermost entrance for every "interior" location. + zone_exit = self.entrances.get_zone_exit_for_item_location(location.name) + if zone_exit is not None: + outermost_entrance = self.entrances.get_outermost_entrance_for_exit(zone_exit) + assert outermost_entrance is not None and outermost_entrance.island_name is not None + hint_data[self.player][location.address] = outermost_entrance.island_name + + # Hint at which chart leads to the sunken treasure for these locations. + if location.name.endswith(" - Sunken Treasure"): + island_name = location.name.removesuffix(" - Sunken Treasure") + island_number = island_name_to_number[island_name] + chart_name = self.charts.island_number_to_chart_name[island_number] + hint_data[self.player][location.address] = chart_name + + def create_item(self, name: str) -> TWWItem: + """ + Create an item for this world type and player. + + :param name: The name of the item to create. + :raises KeyError: If an invalid item name is provided. + """ + if name in ITEM_TABLE: + return TWWItem(name, self.player, ITEM_TABLE[name], self.item_classification_overrides.get(name)) + raise KeyError(f"Invalid item name: {name}") + + def get_filler_item_name(self, strict: bool = True) -> str: + """ + This method is called when the item pool needs to be filled with additional items to match the location count. + + :param strict: Whether the item should be one strictly classified as filler. Defaults to `True`. + :return: The name of a filler item from this world. + """ + # If there are still useful items to place, place those first. + if not strict and len(self.useful_pool) > 0: + return self.useful_pool.pop() + + # If there are still vanilla filler items to place, place those first. + if len(self.filler_pool) > 0: + return self.filler_pool.pop() + + # Use the same weights for filler items used in the base randomizer. + filler_consumables = ["Yellow Rupee", "Red Rupee", "Purple Rupee", "Joy Pendant"] + filler_weights = [3, 7, 10, 3] + if not strict: + filler_consumables.append("Orange Rupee") + filler_weights.append(15) + return self.multiworld.random.choices(filler_consumables, weights=filler_weights, k=1)[0] + + def get_pre_fill_items(self) -> list[Item]: + """ + Return items that need to be collected when creating a fresh `all_state` but don't exist in the multiworld's + item pool. + + :return: A list of pre-fill items. + """ + res = [] + if self.dungeon_local_item_names: + for dungeon in self.dungeons.values(): + for item in dungeon.all_items: + if item.name in self.dungeon_local_item_names: + res.append(item) + return res + + def fill_slot_data(self) -> Mapping[str, Any]: + """ + Return the `slot_data` field that will be in the `Connected` network package. + + This is a way the generator can give custom data to the client. + The client will receive this as JSON in the `Connected` response. + + :return: A dictionary to be sent to the client when it connects to the server. + """ + slot_data = self.options.as_dict(*self.options_dataclass.type_hints) + + # Add entrances to `slot_data`. This is the same data that is written to the .aptww file. + entrances = { + zone_entrance.entrance_name: zone_exit.unique_name + for zone_entrance, zone_exit in self.entrances.done_entrances_to_exits.items() + } + slot_data["entrances"] = entrances + + return slot_data diff --git a/worlds/tww/assets/icon.ico b/worlds/tww/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1e9ab32e4e2ff1791951ed6bdbbdf23c37a211c GIT binary patch literal 220863 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 literal 0 HcmV?d00001 diff --git a/worlds/tww/docs/en_The Wind Waker.md b/worlds/tww/docs/en_The Wind Waker.md new file mode 100644 index 00000000..4d6e999c --- /dev/null +++ b/worlds/tww/docs/en_The Wind Waker.md @@ -0,0 +1,120 @@ +# The Wind Waker + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items get shuffled between the different locations in the game, so each playthrough is unique. Randomized locations +include chests, items received from NPC, and treasure salvaged from the ocean floor. The randomizer also includes +quality-of-life features such as a fully opened world, removing many cutscenes, increased sailing speed, and more. + +## Which locations get shuffled? + +Only locations put into logic by the world's settings will be randomized. The remaining locations in the game will have +a yellow Rupee, which includes a message that the location is not randomized. + +## What is the goal of The Wind Waker? + +Reach and defeat Ganondorf atop Ganon's Tower. This will require all eight shards of the Triforce of Courage, the +fully-powered Master Sword (unless it's swordless mode), Light Arrows, and any other items necessary to reach Ganondorf. + +## What does another world's item look like in TWW? + +Items belonging to other non-TWW worlds are represented by Father's Letter (the letter Medli gives you to give to +Komali), an unused item in the randomizer. + +## When the player receives an item, what happens? + +When the player receives an item, it will automatically be added to Link's inventory. Unlike many other Zelda +randomizers, Link **will not** hold the item above his head. + +## I need help! What do I do? + +Refer to the [FAQ](https://lagolunatic.github.io/wwrando/faq/) first. Then, try the troubleshooting steps in the +[setup guide](/tutorial/The%20Wind%20Waker/setup/en). If you are still stuck, please ask in the Wind Waker channel in +the Archipelago server. + +## Known issues + +- Randomized freestanding rupees, spoils, and bait will also be given to the player picking up the item. The item will + be sent properly, but the collecting player will receive an extra copy. +- Demo items (items which are held over Link's head) which are **not** randomized, such as rupees from salvages from + random light rings or rewards from minigames, will not work. +- Item get messages for progressive items received on locations that send earlier than intended will be incorrect. This + does not affect gameplay. +- The Heart Piece count in item get messages will be off by one. This does not affect gameplay. +- It has been reported that item links can be buggy. Nothing game-breaking, but do be aware of it. + +Feel free to report any other issues or suggest improvements in the Wind Waker channel in the Archipelago server! + +## Tips and Tricks + +### Where are dungeon secrets found in the dungeons? + +[This document](https://docs.google.com/document/d/1LrjGr6W9970XEA-pzl8OhwnqMqTbQaxCX--M-kdsLos/edit?usp=sharing) has +images of each of the dungeon secrets. + +### What exactly do the obscure and precise difficulty options do? + +The `logic_obscurity` and `logic_precision` options modify the randomizer's logic to put various tricks and techniques +into logic. +[This document](https://docs.google.com/spreadsheets/d/14ToE1SvNr9yRRqU4GK2qxIsuDUs9Edegik3wUbLtzH8/edit?usp=sharing) +neatly lists the changes that are made. The options are progressive, so, for instance, hard obscure difficulty includes +both normal and hard obscure tricks. Some changes require a combination of both options. For example, to put having the +Forsaken Fortress cannons blow the door up for you into logic requires both obscure and precise difficulty to be set to +at least normal. + +### What are the different options presets? + +A few presets are available on the [player options page](../player-options) for your convenience. + +- **Tournament S7**: These are (as close to as possible) the settings used in the WWR Racing Server's + [Season 7 Tournament](https://docs.google.com/document/d/1mJj7an-DvpYilwNt-DdlFOy1fz5_NMZaPZvHeIekplc). + The preset features 3 required bosses and hard obscurity difficulty, and while the list of enabled progression options + may seem intimidating, the preset also excludes several locations. +- **Miniblins 2025**: These are (as close to as possible) the settings used in the WWR Racing Server's + [2025 Season of Minblins](https://docs.google.com/document/d/19vT68eU6PepD2BD2ZjR9ikElfqs8pXfqQucZ-TcscV8). This + preset is great if you're new to Wind Waker! There aren't too many locations in the world, and you only need to + complete two dungeons. You also start with many convenience items, such as double magic, a capacity upgrade for your + bow and bombs, and six hearts. +- **Mixed Pools**: These are the settings used in the WWR Racing Server's + [Mixed Pools Co-op Tournament](https://docs.google.com/document/d/1YGPTtEgP978TIi0PUAD792OtZbE2jBQpI8XCAy63qpg). This + preset features full entrance rando and includes many locations behind a randomized entrance. There are also a bunch + of overworld locations, as these settings were intended to be played in a two-person co-op team. The preset also has 6 + required bosses, but since entrance pools are randomized, the bosses could be found anywhere! Check your Sea Chart to + find out which island the bosses are on. + +## Planned Features + +- Dynamic CTMC based on enabled options +- Hint implementation from base randomizer (hint placement options and hint types) +- Integration with Archipelago's hint system (e.g., auction hints) +- EnergyLink support +- Swift Sail logic as an option +- Continued bugfixes + +## Credits + +This randomizer would not be possible without the help from: + +- BigSharkZ: (icon artwork) +- Celeste (Maëlle): (logic and typo fixes, additional programming) +- Chavu: (logic difficulty document) +- CrainWWR: (multiworld and Dolphin memory assistance, additional programming) +- Cyb3R: (reference for `TWWClient`) +- DeamonHunter: (additional programming) +- Dev5ter: (initial TWW AP implmentation) +- Gamma / SageOfMirrors: (additional programming) +- LagoLunatic: (base randomizer, additional assistance) +- Lunix: (Linux support, additional programming) +- Mysteryem: (tracker support, additional programming) +- Necrofitz: (additional documentation) +- Ouro: (tracker support) +- tal (matzahTalSoup): (dungeon secrets guide) +- Tubamann: (additional programming) + +The Archipelago logo © 2022 by Krista Corkos and Christopher Wilson, licensed under +[CC BY-NC 4.0](http://creativecommons.org/licenses/by-nc/4.0/). diff --git a/worlds/tww/docs/setup_en.md b/worlds/tww/docs/setup_en.md new file mode 100644 index 00000000..fa22b48b --- /dev/null +++ b/worlds/tww/docs/setup_en.md @@ -0,0 +1,67 @@ +# Setup Guide for The Wind Waker Archipelago + +Welcome to The Wind Waker Archipelago! This guide will help you set up the randomizer and play your first multiworld. +If you're playing The Wind Waker, you must follow a few simple steps to get started. + +## Requirements + +You'll need the following components to be able to play with The Wind Waker: +* Install [Dolphin Emulator](https://dolphin-emu.org/download/). **We recommend using the latest release.** + * For Linux users, you can use the flatpak package + [available on Flathub](https://flathub.org/apps/org.DolphinEmu.dolphin-emu). +* The 2.5.0 version of the [TWW AP Randomizer Build](https://github.com/tanjo3/wwrando/releases/tag/ap_2.5.0). +* A The Wind Waker ISO (North American version), probably named "Legend of Zelda, The - The Wind Waker (USA).iso". + +Optionally, you can also download: +* [Wind Waker Tracker](https://github.com/Mysteryem/ww-poptracker/releases/latest) + * Requires [PopTracker](https://github.com/black-sliver/PopTracker/releases) +* [Custom Wind Waker Player Models](https://github.com/Sage-of-Mirrors/Custom-Wind-Waker-Player-Models) + +## Setting Up a YAML + +All players playing The Wind Waker must provide the room host with a YAML file containing the settings for their world. +Visit the [The Wind Waker options page](/games/The%20Wind%20Waker/player-options) to generate a YAML with your desired +options. Only locations categorized under the options enabled under "Progression Locations" will be randomized in your +world. Once you're happy with your settings, provide the room host with your YAML file and proceed to the next step. + +## Connecting to a Room + +The multiworld host will provide you a link to download your `aptww` file or a zip file containing everyone's files. The +`aptww` file should be named `P#__XXXXX.aptww`, where `#` is your player ID, `` is your player name, and +`XXXXX` is the room ID. The host should also provide you with the room's server name and port number. + +Once you do, follow these steps to connect to the room: +1. Run the TWW AP Randomizer Build. If this is the first time you've opened the randomizer, you'll need to specify the +path to your The Wind Waker ISO and the output folder for the randomized ISO. These will be saved for the next time you +open the program. +2. Modify any cosmetic convenience tweaks and player customization options as desired. +3. For the APTWW file, browse and locate the path to your `aptww` file. +4. Click `Randomize` at the bottom-right. This randomizes the ISO and puts it in the output folder you specified. The +file will be named `TWW AP_YYYYY_P# ().iso`, where `YYYYY` is the seed name, `#` is your player ID, and `` +is your player (slot) name. Verify that the values are correct for the multiworld. +5. Open Dolphin and use it to open the randomized ISO. +6. Start `ArchipelagoLauncher.exe` (without `.exe` on Linux) and choose `The Wind Waker Client`, which will open the +text client. If Dolphin is not already open, or you have yet to start a new file, you will be prompted to do so. + * Once you've opened the ISO in Dolphin, the client should say "Dolphin connected successfully.". +7. Connect to the room by entering the server name and port number at the top and pressing `Connect`. For rooms hosted +on the website, this will be `archipelago.gg:`, where `` is the port number. If a game is hosted from the +`ArchipelagoServer.exe` (without `.exe` on Linux), the port number will default to `38281` but may be changed in the +`host.yaml`. +8. If you've opened a ROM corresponding to the multiworld to which you are connected, it should authenticate your slot +name automatically when you start a new save file. + +## Troubleshooting + +* Ensure you are running the same version of Archipelago on which the multiworld was generated. +* Ensure `tww.apworld` is not in your Archipelago installation's `custom_worlds` folder. +* Ensure you are using the correct randomizer build for the version of Archipelago you are using. The build should +provide an error message directing you to the correct version. You can also look at the release notes of TWW AP builds +[here](https://github.com/tanjo3/wwrando/releases) to see which versions of Archipelago each build is compatible with. +* If you encounter issues with authenticating, ensure that the randomized ROM is open in Dolphin and corresponds to the +multiworld to which you are connecting. +* Ensure that you do not have any Dolphin cheats or codes enabled. Some cheats or codes can unexpectedly interfere with +emulation and make troubleshooting errors difficult. +* If you get an error message, ensure that `Enable Emulated Memory Size Override` in Dolphin (under `Options` > +`Configuration` > `Advanced`) is **disabled**. +* If you run with a custom GC boot menu, you'll need to skip it by going to `Options` > `Configuration` > `GameCube` +and checking `Skip Main Menu`. diff --git a/worlds/tww/randomizers/Charts.py b/worlds/tww/randomizers/Charts.py new file mode 100644 index 00000000..695f2dc0 --- /dev/null +++ b/worlds/tww/randomizers/Charts.py @@ -0,0 +1,125 @@ +from typing import TYPE_CHECKING + +from ..Items import ISLAND_NUMBER_TO_CHART_NAME +from ..Locations import TWWFlag, TWWLocation + +if TYPE_CHECKING: + from .. import TWWWorld + +ISLAND_NUMBER_TO_NAME: dict[int, str] = { + 1: "Forsaken Fortress Sector", + 2: "Star Island", + 3: "Northern Fairy Island", + 4: "Gale Isle", + 5: "Crescent Moon Island", + 6: "Seven-Star Isles", + 7: "Overlook Island", + 8: "Four-Eye Reef", + 9: "Mother and Child Isles", + 10: "Spectacle Island", + 11: "Windfall Island", + 12: "Pawprint Isle", + 13: "Dragon Roost Island", + 14: "Flight Control Platform", + 15: "Western Fairy Island", + 16: "Rock Spire Isle", + 17: "Tingle Island", + 18: "Northern Triangle Island", + 19: "Eastern Fairy Island", + 20: "Fire Mountain", + 21: "Star Belt Archipelago", + 22: "Three-Eye Reef", + 23: "Greatfish Isle", + 24: "Cyclops Reef", + 25: "Six-Eye Reef", + 26: "Tower of the Gods Sector", + 27: "Eastern Triangle Island", + 28: "Thorned Fairy Island", + 29: "Needle Rock Isle", + 30: "Islet of Steel", + 31: "Stone Watcher Island", + 32: "Southern Triangle Island", + 33: "Private Oasis", + 34: "Bomb Island", + 35: "Bird's Peak Rock", + 36: "Diamond Steppe Island", + 37: "Five-Eye Reef", + 38: "Shark Island", + 39: "Southern Fairy Island", + 40: "Ice Ring Isle", + 41: "Forest Haven", + 42: "Cliff Plateau Isles", + 43: "Horseshoe Island", + 44: "Outset Island", + 45: "Headstone Island", + 46: "Two-Eye Reef", + 47: "Angular Isles", + 48: "Boating Course", + 49: "Five-Star Isles", +} + + +class ChartRandomizer: + """ + This class handles the randomization of charts. + Each chart points to a specific island on the map, and this randomizer shuffles these mappings. + + :param world: The Wind Waker game world. + """ + + def __init__(self, world: "TWWWorld") -> None: + self.world = world + self.multiworld = world.multiworld + + self.island_number_to_chart_name = ISLAND_NUMBER_TO_CHART_NAME.copy() + + def setup_progress_sunken_treasure_locations(self) -> None: + """ + Create the locations for sunken treasure locations and update them as progression and non-progression + appropriately. If the option is enabled, randomize which charts point to which sector. + """ + options = self.world.options + + original_item_names = list(self.island_number_to_chart_name.values()) + + # Shuffles the list of island numbers if charts are randomized. + # The shuffled island numbers determine which sector each chart points to. + shuffled_island_numbers = list(self.island_number_to_chart_name.keys()) + if options.randomize_charts: + self.world.random.shuffle(shuffled_island_numbers) + + for original_item_name in reversed(original_item_names): + # Assign each chart to its new island. + shuffled_island_number = shuffled_island_numbers.pop() + self.island_number_to_chart_name[shuffled_island_number] = original_item_name + + # Additionally, determine if that location is a progress location or not. + island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number] + island_location = f"{island_name} - Sunken Treasure" + if options.progression_triforce_charts or options.progression_treasure_charts: + if original_item_name.startswith("Triforce Chart "): + if options.progression_triforce_charts: + self.world.progress_locations.add(island_location) + self.world.nonprogress_locations.remove(island_location) + else: + if options.progression_treasure_charts: + self.world.progress_locations.add(island_location) + self.world.nonprogress_locations.remove(island_location) + else: + self.world.nonprogress_locations.add(island_location) + + def update_chart_location_flags(self) -> None: + """ + Update the flags for sunken treasure locations based on the current chart mappings. + """ + for shuffled_island_number, item_name in self.island_number_to_chart_name.items(): + island_name = ISLAND_NUMBER_TO_NAME[shuffled_island_number] + island_location_str = f"{island_name} - Sunken Treasure" + + if island_location_str in self.world.progress_locations: + island_location = self.world.get_location(island_location_str) + assert isinstance(island_location, TWWLocation) + if item_name.startswith("Triforce Chart "): + island_location.flags = TWWFlag.TRI_CHT + else: + island_location.flags = TWWFlag.TRE_CHT diff --git a/worlds/tww/randomizers/Dungeons.py b/worlds/tww/randomizers/Dungeons.py new file mode 100644 index 00000000..3a009f78 --- /dev/null +++ b/worlds/tww/randomizers/Dungeons.py @@ -0,0 +1,284 @@ +from typing import TYPE_CHECKING, Any, Optional + +from BaseClasses import CollectionState, Item, Location, MultiWorld +from Fill import fill_restrictive +from worlds.generic.Rules import add_item_rule + +from ..Items import item_factory + +if TYPE_CHECKING: + from .. import TWWWorld + + +class Dungeon: + """ + This class represents a dungeon in The Wind Waker, including its dungeon items. + + :param name: The name of the dungeon. + :param big_key: The big key item for the dungeon. + :param small_keys: A list of small key items for the dungeon. + :param dungeon_items: A list of other items specific to the dungeon. + :param player: The ID of the player associated with the dungeon. + """ + + def __init__( + self, + name: str, + big_key: Optional[Item], + small_keys: list[Item], + dungeon_items: list[Item], + player: int, + ): + self.name = name + self.big_key = big_key + self.small_keys = small_keys + self.dungeon_items = dungeon_items + self.player = player + + @property + def keys(self) -> list[Item]: + """ + Retrieve all the keys for the dungeon. + + :return: A list of Small Keys and the Big Key (if it exists). + """ + return self.small_keys + ([self.big_key] if self.big_key else []) + + @property + def all_items(self) -> list[Item]: + """ + Retrieve all items associated with the dungeon. + + :return: A list of all items associated with the dungeon. + """ + return self.dungeon_items + self.keys + + def __eq__(self, other: Any) -> bool: + """ + Check equality between this dungeon and another object. + + :param other: The object to compare. + :return: `True` if the other object is a Dungeon with the same name and player, `False` otherwise. + """ + if isinstance(other, Dungeon): + return self.name == other.name and self.player == other.player + return False + + def __repr__(self) -> str: + """ + Provide a string representation of the dungeon. + + :return: A string representing the dungeon. + """ + return self.__str__() + + def __str__(self) -> str: + """ + Convert the dungeon to a human-readable string. + + :return: A string in the format " (Player )". + """ + return f"{self.name} (Player {self.player})" + + +def create_dungeons(world: "TWWWorld") -> None: + """ + Create and assign dungeons to the given world based on game options. + + :param world: The Wind Waker game world. + """ + player = world.player + options = world.options + + def make_dungeon(name: str, big_key: Optional[Item], small_keys: list[Item], dungeon_items: list[Item]) -> Dungeon: + dungeon = Dungeon(name, big_key, small_keys, dungeon_items, player) + for item in dungeon.all_items: + item.dungeon = dungeon + return dungeon + + if options.progression_dungeons: + if not options.required_bosses or "Dragon Roost Cavern" in world.boss_reqs.required_dungeons: + world.dungeons["Dragon Roost Cavern"] = make_dungeon( + "Dragon Roost Cavern", + item_factory("DRC Big Key", world), + item_factory(["DRC Small Key"] * 4, world), + item_factory(["DRC Dungeon Map", "DRC Compass"], world), + ) + + if not options.required_bosses or "Forbidden Woods" in world.boss_reqs.required_dungeons: + world.dungeons["Forbidden Woods"] = make_dungeon( + "Forbidden Woods", + item_factory("FW Big Key", world), + item_factory(["FW Small Key"] * 1, world), + item_factory(["FW Dungeon Map", "FW Compass"], world), + ) + + if not options.required_bosses or "Tower of the Gods" in world.boss_reqs.required_dungeons: + world.dungeons["Tower of the Gods"] = make_dungeon( + "Tower of the Gods", + item_factory("TotG Big Key", world), + item_factory(["TotG Small Key"] * 2, world), + item_factory(["TotG Dungeon Map", "TotG Compass"], world), + ) + + if not options.required_bosses or "Forsaken Fortress" in world.boss_reqs.required_dungeons: + world.dungeons["Forsaken Fortress"] = make_dungeon( + "Forsaken Fortress", + None, + [], + item_factory(["FF Dungeon Map", "FF Compass"], world), + ) + + if not options.required_bosses or "Earth Temple" in world.boss_reqs.required_dungeons: + world.dungeons["Earth Temple"] = make_dungeon( + "Earth Temple", + item_factory("ET Big Key", world), + item_factory(["ET Small Key"] * 3, world), + item_factory(["ET Dungeon Map", "ET Compass"], world), + ) + + if not options.required_bosses or "Wind Temple" in world.boss_reqs.required_dungeons: + world.dungeons["Wind Temple"] = make_dungeon( + "Wind Temple", + item_factory("WT Big Key", world), + item_factory(["WT Small Key"] * 2, world), + item_factory(["WT Dungeon Map", "WT Compass"], world), + ) + + +def get_dungeon_item_pool(multiworld: MultiWorld) -> list[Item]: + """ + Retrieve the item pool for all The Wind Waker dungeons in the multiworld. + + :param multiworld: The MultiWorld instance. + :return: List of dungeon items across all The Wind Waker dungeons. + """ + return [ + item for world in multiworld.get_game_worlds("The Wind Waker") for item in get_dungeon_item_pool_player(world) + ] + + +def get_dungeon_item_pool_player(world: "TWWWorld") -> list[Item]: + """ + Retrieve the item pool for all dungeons specific to a player. + + :param world: The Wind Waker game world. + :return: List of items in the player's dungeons. + """ + return [item for dungeon in world.dungeons.values() for item in dungeon.all_items] + + +def get_unfilled_dungeon_locations(multiworld: MultiWorld) -> list[Location]: + """ + Retrieve all unfilled The Wind Waker dungeon locations in the multiworld. + + :param multiworld: The MultiWorld instance. + :return: List of unfilled The Wind Waker dungeon locations. + """ + return [ + location + for world in multiworld.get_game_worlds("The Wind Waker") + for location in multiworld.get_locations(world.player) + if location.dungeon and not location.item + ] + + +def modify_dungeon_location_rules(multiworld: MultiWorld) -> None: + """ + Modify the rules for The Wind Waker dungeon locations based on specific player-requested constraints. + + :param multiworld: The MultiWorld instance. + """ + localized: set[tuple[int, str]] = set() + dungeon_specific: set[tuple[int, str]] = set() + for subworld in multiworld.get_game_worlds("The Wind Waker"): + player = subworld.player + if player not in multiworld.groups: + localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names} + dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names} + + if localized: + in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] + if in_dungeon_items: + locations = [location for location in get_unfilled_dungeon_locations(multiworld)] + + for location in locations: + if dungeon_specific: + # Special case: If Dragon Roost Cavern has its own small keys, then ensure the first chest isn't the + # Big Key. This is to avoid placing the Big Key there during fill and resulting in a costly swap. + if location.name == "Dragon Roost Cavern - First Room": + add_item_rule( + location, + lambda item: item.name != "DRC Big Key" + or (item.player, "DRC Small Key") in dungeon_specific, + ) + + # Add item rule to ensure dungeon items are in their own dungeon when they should be. + add_item_rule( + location, + lambda item, dungeon=location.dungeon: not (item.player, item.name) in dungeon_specific + or item.dungeon is dungeon, + ) + + +def fill_dungeons_restrictive(multiworld: MultiWorld) -> None: + """ + Correctly fill The Wind Waker dungeons in the multiworld. + + :param multiworld: The MultiWorld instance. + """ + localized: set[tuple[int, str]] = set() + dungeon_specific: set[tuple[int, str]] = set() + for subworld in multiworld.get_game_worlds("The Wind Waker"): + player = subworld.player + if player not in multiworld.groups: + localized |= {(player, item_name) for item_name in subworld.dungeon_local_item_names} + dungeon_specific |= {(player, item_name) for item_name in subworld.dungeon_specific_item_names} + + if localized: + in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] + if in_dungeon_items: + locations = [location for location in get_unfilled_dungeon_locations(multiworld)] + multiworld.random.shuffle(locations) + + # Dungeon-locked items have to be placed first so as not to run out of space for dungeon-locked items. + # Subsort in the order Big Key, Small Key, Other before placing dungeon items. + sort_order = {"Big Key": 3, "Small Key": 2} + in_dungeon_items.sort( + key=lambda item: sort_order.get(item.type, 1) + + (5 if (item.player, item.name) in dungeon_specific else 0) + ) + + # Construct a partial `all_state` that contains only the items from `get_pre_fill_items` that aren't in a + # dungeon. + in_dungeon_player_ids = {item.player for item in in_dungeon_items} + all_state_base = CollectionState(multiworld) + for item in multiworld.itempool: + multiworld.worlds[item.player].collect(all_state_base, item) + pre_fill_items = [] + for player in in_dungeon_player_ids: + pre_fill_items += multiworld.worlds[player].get_pre_fill_items() + for item in in_dungeon_items: + try: + pre_fill_items.remove(item) + except ValueError: + # `pre_fill_items` should be a subset of `in_dungeon_items`, but just in case. + pass + for item in pre_fill_items: + multiworld.worlds[item.player].collect(all_state_base, item) + all_state_base.sweep_for_advancements() + + # Remove the completion condition so that minimal-accessibility words place keys correctly. + for player in (item.player for item in in_dungeon_items): + if all_state_base.has("Victory", player): + all_state_base.remove(multiworld.worlds[player].create_item("Victory")) + + fill_restrictive( + multiworld, + all_state_base, + locations, + in_dungeon_items, + lock=True, + allow_excluded=True, + name="TWW Dungeon Items", + ) diff --git a/worlds/tww/randomizers/Entrances.py b/worlds/tww/randomizers/Entrances.py new file mode 100644 index 00000000..6a068397 --- /dev/null +++ b/worlds/tww/randomizers/Entrances.py @@ -0,0 +1,878 @@ +from collections import defaultdict +from collections.abc import Generator +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar, Optional + +from Fill import FillError +from Options import OptionError + +from .. import Macros +from ..Locations import LOCATION_TABLE, TWWFlag, split_location_name_by_zone + +if TYPE_CHECKING: + from .. import TWWWorld + + +@dataclass(frozen=True) +class ZoneEntrance: + """ + A data class that encapsulates information about a zone entrance. + """ + + entrance_name: str + island_name: Optional[str] = None + nested_in: Optional["ZoneExit"] = None + + @property + def is_nested(self) -> bool: + """ + Determine if this entrance is nested within another entrance. + + :return: `True` if the entrance is nested, `False` otherwise. + """ + return self.nested_in is not None + + def __repr__(self) -> str: + """ + Provide a string representation of the zone exit. + + :return: A string representing the zone exit. + """ + return f"ZoneEntrance('{self.entrance_name}')" + + all: ClassVar[dict[str, "ZoneEntrance"]] = {} + + def __post_init__(self) -> None: + ZoneEntrance.all[self.entrance_name] = self + + # Must be an island entrance XOR must be a nested entrance. + assert (self.island_name is None) ^ (self.nested_in is None) + + +@dataclass(frozen=True) +class ZoneExit: + """ + A data class that encapsulates information about a zone exit. + """ + + unique_name: str + zone_name: Optional[str] = None + + def __repr__(self) -> str: + """ + Provide a string representation of the zone exit. + + :return: A string representing the zone exit. + """ + return f"ZoneExit('{self.unique_name}')" + + all: ClassVar[dict[str, "ZoneExit"]] = {} + + def __post_init__(self) -> None: + ZoneExit.all[self.unique_name] = self + + +DUNGEON_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Dungeon Entrance on Dragon Roost Island", "Dragon Roost Island"), + ZoneEntrance("Dungeon Entrance in Forest Haven Sector", "Forest Haven"), + ZoneEntrance("Dungeon Entrance in Tower of the Gods Sector", "Tower of the Gods Sector"), + ZoneEntrance("Dungeon Entrance on Headstone Island", "Headstone Island"), + ZoneEntrance("Dungeon Entrance on Gale Isle", "Gale Isle"), +] +DUNGEON_EXITS: list[ZoneExit] = [ + ZoneExit("Dragon Roost Cavern", "Dragon Roost Cavern"), + ZoneExit("Forbidden Woods", "Forbidden Woods"), + ZoneExit("Tower of the Gods", "Tower of the Gods"), + ZoneExit("Earth Temple", "Earth Temple"), + ZoneExit("Wind Temple", "Wind Temple"), +] + +MINIBOSS_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Miniboss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]), + ZoneEntrance("Miniboss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]), + ZoneEntrance("Miniboss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]), + ZoneEntrance("Miniboss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]), + ZoneEntrance("Miniboss Entrance in Hyrule Castle", "Tower of the Gods Sector"), +] +MINIBOSS_EXITS: list[ZoneExit] = [ + ZoneExit("Forbidden Woods Miniboss Arena"), + ZoneExit("Tower of the Gods Miniboss Arena"), + ZoneExit("Earth Temple Miniboss Arena"), + ZoneExit("Wind Temple Miniboss Arena"), + ZoneExit("Master Sword Chamber"), +] + +BOSS_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Boss Entrance in Dragon Roost Cavern", nested_in=ZoneExit.all["Dragon Roost Cavern"]), + ZoneEntrance("Boss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]), + ZoneEntrance("Boss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]), + ZoneEntrance("Boss Entrance in Forsaken Fortress", "Forsaken Fortress Sector"), + ZoneEntrance("Boss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]), + ZoneEntrance("Boss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]), +] +BOSS_EXITS: list[ZoneExit] = [ + ZoneExit("Gohma Boss Arena"), + ZoneExit("Kalle Demos Boss Arena"), + ZoneExit("Gohdan Boss Arena"), + ZoneExit("Helmaroc King Boss Arena"), + ZoneExit("Jalhalla Boss Arena"), + ZoneExit("Molgera Boss Arena"), +] + +SECRET_CAVE_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Secret Cave Entrance on Outset Island", "Outset Island"), + ZoneEntrance("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"), + ZoneEntrance("Secret Cave Entrance on Fire Mountain", "Fire Mountain"), + ZoneEntrance("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"), + ZoneEntrance("Secret Cave Entrance on Private Oasis", "Private Oasis"), + ZoneEntrance("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"), + ZoneEntrance("Secret Cave Entrance on Angular Isles", "Angular Isles"), + ZoneEntrance("Secret Cave Entrance on Boating Course", "Boating Course"), + ZoneEntrance("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"), + ZoneEntrance("Secret Cave Entrance on Overlook Island", "Overlook Island"), + ZoneEntrance("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"), + ZoneEntrance("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"), + ZoneEntrance("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"), + ZoneEntrance("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"), + ZoneEntrance("Secret Cave Entrance on Bomb Island", "Bomb Island"), + ZoneEntrance("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"), + ZoneEntrance("Secret Cave Entrance on Shark Island", "Shark Island"), + ZoneEntrance("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"), + ZoneEntrance("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"), + ZoneEntrance("Secret Cave Entrance on Star Island", "Star Island"), +] +SECRET_CAVE_EXITS: list[ZoneExit] = [ + ZoneExit("Savage Labyrinth", zone_name="Outset Island"), + ZoneExit("Dragon Roost Island Secret Cave", zone_name="Dragon Roost Island"), + ZoneExit("Fire Mountain Secret Cave", zone_name="Fire Mountain"), + ZoneExit("Ice Ring Isle Secret Cave", zone_name="Ice Ring Isle"), + ZoneExit("Cabana Labyrinth", zone_name="Private Oasis"), + ZoneExit("Needle Rock Isle Secret Cave", zone_name="Needle Rock Isle"), + ZoneExit("Angular Isles Secret Cave", zone_name="Angular Isles"), + ZoneExit("Boating Course Secret Cave", zone_name="Boating Course"), + ZoneExit("Stone Watcher Island Secret Cave", zone_name="Stone Watcher Island"), + ZoneExit("Overlook Island Secret Cave", zone_name="Overlook Island"), + ZoneExit("Bird's Peak Rock Secret Cave", zone_name="Bird's Peak Rock"), + ZoneExit("Pawprint Isle Chuchu Cave", zone_name="Pawprint Isle"), + ZoneExit("Pawprint Isle Wizzrobe Cave"), + ZoneExit("Diamond Steppe Island Warp Maze Cave", zone_name="Diamond Steppe Island"), + ZoneExit("Bomb Island Secret Cave", zone_name="Bomb Island"), + ZoneExit("Rock Spire Isle Secret Cave", zone_name="Rock Spire Isle"), + ZoneExit("Shark Island Secret Cave", zone_name="Shark Island"), + ZoneExit("Cliff Plateau Isles Secret Cave", zone_name="Cliff Plateau Isles"), + ZoneExit("Horseshoe Island Secret Cave", zone_name="Horseshoe Island"), + ZoneExit("Star Island Secret Cave", zone_name="Star Island"), +] + +SECRET_CAVE_INNER_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Inner Entrance in Ice Ring Isle Secret Cave", nested_in=ZoneExit.all["Ice Ring Isle Secret Cave"]), + ZoneEntrance( + "Inner Entrance in Cliff Plateau Isles Secret Cave", nested_in=ZoneExit.all["Cliff Plateau Isles Secret Cave"] + ), +] +SECRET_CAVE_INNER_EXITS: list[ZoneExit] = [ + ZoneExit("Ice Ring Isle Inner Cave"), + ZoneExit("Cliff Plateau Isles Inner Cave"), +] + +FAIRY_FOUNTAIN_ENTRANCES: list[ZoneEntrance] = [ + ZoneEntrance("Fairy Fountain Entrance on Outset Island", "Outset Island"), + ZoneEntrance("Fairy Fountain Entrance on Thorned Fairy Island", "Thorned Fairy Island"), + ZoneEntrance("Fairy Fountain Entrance on Eastern Fairy Island", "Eastern Fairy Island"), + ZoneEntrance("Fairy Fountain Entrance on Western Fairy Island", "Western Fairy Island"), + ZoneEntrance("Fairy Fountain Entrance on Southern Fairy Island", "Southern Fairy Island"), + ZoneEntrance("Fairy Fountain Entrance on Northern Fairy Island", "Northern Fairy Island"), +] +FAIRY_FOUNTAIN_EXITS: list[ZoneExit] = [ + ZoneExit("Outset Fairy Fountain"), + ZoneExit("Thorned Fairy Fountain", zone_name="Thorned Fairy Island"), + ZoneExit("Eastern Fairy Fountain", zone_name="Eastern Fairy Island"), + ZoneExit("Western Fairy Fountain", zone_name="Western Fairy Island"), + ZoneExit("Southern Fairy Fountain", zone_name="Southern Fairy Island"), + ZoneExit("Northern Fairy Fountain", zone_name="Northern Fairy Island"), +] + +DUNGEON_INNER_EXITS: list[ZoneExit] = ( + MINIBOSS_EXITS + + BOSS_EXITS +) + +ALL_ENTRANCES: list[ZoneEntrance] = ( + DUNGEON_ENTRANCES + + MINIBOSS_ENTRANCES + + BOSS_ENTRANCES + + SECRET_CAVE_ENTRANCES + + SECRET_CAVE_INNER_ENTRANCES + + FAIRY_FOUNTAIN_ENTRANCES +) +ALL_EXITS: list[ZoneExit] = ( + DUNGEON_EXITS + + MINIBOSS_EXITS + + BOSS_EXITS + + SECRET_CAVE_EXITS + + SECRET_CAVE_INNER_EXITS + + FAIRY_FOUNTAIN_EXITS +) + +ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES: list[TWWFlag] = [ + TWWFlag.DUNGEON, + TWWFlag.PZL_CVE, + TWWFlag.CBT_CVE, + TWWFlag.SAVAGE, + TWWFlag.GRT_FRY, +] +ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES: dict[str, ZoneExit] = { + "Forbidden Woods - Mothula Miniboss Room": ZoneExit.all["Forbidden Woods Miniboss Arena"], + "Tower of the Gods - Darknut Miniboss Room": ZoneExit.all["Tower of the Gods Miniboss Arena"], + "Earth Temple - Stalfos Miniboss Room": ZoneExit.all["Earth Temple Miniboss Arena"], + "Wind Temple - Wizzrobe Miniboss Room": ZoneExit.all["Wind Temple Miniboss Arena"], + "Hyrule - Master Sword Chamber": ZoneExit.all["Master Sword Chamber"], + + "Dragon Roost Cavern - Gohma Heart Container": ZoneExit.all["Gohma Boss Arena"], + "Forbidden Woods - Kalle Demos Heart Container": ZoneExit.all["Kalle Demos Boss Arena"], + "Tower of the Gods - Gohdan Heart Container": ZoneExit.all["Gohdan Boss Arena"], + "Forsaken Fortress - Helmaroc King Heart Container": ZoneExit.all["Helmaroc King Boss Arena"], + "Earth Temple - Jalhalla Heart Container": ZoneExit.all["Jalhalla Boss Arena"], + "Wind Temple - Molgera Heart Container": ZoneExit.all["Molgera Boss Arena"], + + "Pawprint Isle - Wizzrobe Cave": ZoneExit.all["Pawprint Isle Wizzrobe Cave"], + + "Ice Ring Isle - Inner Cave - Chest": ZoneExit.all["Ice Ring Isle Inner Cave"], + "Cliff Plateau Isles - Highest Isle": ZoneExit.all["Cliff Plateau Isles Inner Cave"], + + "Outset Island - Great Fairy": ZoneExit.all["Outset Fairy Fountain"], +} + +MINIBOSS_EXIT_TO_DUNGEON: dict[str, str] = { + "Forbidden Woods Miniboss Arena": "Forbidden Woods", + "Tower of the Gods Miniboss Arena": "Tower of the Gods", + "Earth Temple Miniboss Arena": "Earth Temple", + "Wind Temple Miniboss Arena": "Wind Temple", +} + +BOSS_EXIT_TO_DUNGEON: dict[str, str] = { + "Gohma Boss Arena": "Dragon Roost Cavern", + "Kalle Demos Boss Arena": "Forbidden Woods", + "Gohdan Boss Arena": "Tower of the Gods", + "Helmaroc King Boss Arena": "Forsaken Fortress", + "Jalhalla Boss Arena": "Earth Temple", + "Molgera Boss Arena": "Wind Temple", +} + +VANILLA_ENTRANCES_TO_EXITS: dict[str, str] = { + "Dungeon Entrance on Dragon Roost Island": "Dragon Roost Cavern", + "Dungeon Entrance in Forest Haven Sector": "Forbidden Woods", + "Dungeon Entrance in Tower of the Gods Sector": "Tower of the Gods", + "Dungeon Entrance on Headstone Island": "Earth Temple", + "Dungeon Entrance on Gale Isle": "Wind Temple", + + "Miniboss Entrance in Forbidden Woods": "Forbidden Woods Miniboss Arena", + "Miniboss Entrance in Tower of the Gods": "Tower of the Gods Miniboss Arena", + "Miniboss Entrance in Earth Temple": "Earth Temple Miniboss Arena", + "Miniboss Entrance in Wind Temple": "Wind Temple Miniboss Arena", + "Miniboss Entrance in Hyrule Castle": "Master Sword Chamber", + + "Boss Entrance in Dragon Roost Cavern": "Gohma Boss Arena", + "Boss Entrance in Forbidden Woods": "Kalle Demos Boss Arena", + "Boss Entrance in Tower of the Gods": "Gohdan Boss Arena", + "Boss Entrance in Forsaken Fortress": "Helmaroc King Boss Arena", + "Boss Entrance in Earth Temple": "Jalhalla Boss Arena", + "Boss Entrance in Wind Temple": "Molgera Boss Arena", + + "Secret Cave Entrance on Outset Island": "Savage Labyrinth", + "Secret Cave Entrance on Dragon Roost Island": "Dragon Roost Island Secret Cave", + "Secret Cave Entrance on Fire Mountain": "Fire Mountain Secret Cave", + "Secret Cave Entrance on Ice Ring Isle": "Ice Ring Isle Secret Cave", + "Secret Cave Entrance on Private Oasis": "Cabana Labyrinth", + "Secret Cave Entrance on Needle Rock Isle": "Needle Rock Isle Secret Cave", + "Secret Cave Entrance on Angular Isles": "Angular Isles Secret Cave", + "Secret Cave Entrance on Boating Course": "Boating Course Secret Cave", + "Secret Cave Entrance on Stone Watcher Island": "Stone Watcher Island Secret Cave", + "Secret Cave Entrance on Overlook Island": "Overlook Island Secret Cave", + "Secret Cave Entrance on Bird's Peak Rock": "Bird's Peak Rock Secret Cave", + "Secret Cave Entrance on Pawprint Isle": "Pawprint Isle Chuchu Cave", + "Secret Cave Entrance on Pawprint Isle Side Isle": "Pawprint Isle Wizzrobe Cave", + "Secret Cave Entrance on Diamond Steppe Island": "Diamond Steppe Island Warp Maze Cave", + "Secret Cave Entrance on Bomb Island": "Bomb Island Secret Cave", + "Secret Cave Entrance on Rock Spire Isle": "Rock Spire Isle Secret Cave", + "Secret Cave Entrance on Shark Island": "Shark Island Secret Cave", + "Secret Cave Entrance on Cliff Plateau Isles": "Cliff Plateau Isles Secret Cave", + "Secret Cave Entrance on Horseshoe Island": "Horseshoe Island Secret Cave", + "Secret Cave Entrance on Star Island": "Star Island Secret Cave", + + "Inner Entrance in Ice Ring Isle Secret Cave": "Ice Ring Isle Inner Cave", + "Inner Entrance in Cliff Plateau Isles Secret Cave": "Cliff Plateau Isles Inner Cave", + + "Fairy Fountain Entrance on Outset Island": "Outset Fairy Fountain", + "Fairy Fountain Entrance on Thorned Fairy Island": "Thorned Fairy Fountain", + "Fairy Fountain Entrance on Eastern Fairy Island": "Eastern Fairy Fountain", + "Fairy Fountain Entrance on Western Fairy Island": "Western Fairy Fountain", + "Fairy Fountain Entrance on Southern Fairy Island": "Southern Fairy Fountain", + "Fairy Fountain Entrance on Northern Fairy Island": "Northern Fairy Fountain", +} + + +class EntranceRandomizer: + """ + This class handles the logic for The Wind Waker entrance randomizer. + + We reference the logic from the base randomizer with some modifications to suit it for Archipelago. + Reference: https://github.com/LagoLunatic/wwrando/blob/master/randomizers/entrances.py + + :param world: The Wind Waker game world. + """ + + def __init__(self, world: "TWWWorld"): + self.world = world + self.multiworld = world.multiworld + self.player = world.player + + self.item_location_to_containing_zone_exit: dict[str, ZoneExit] = {} + self.zone_exit_to_logically_dependent_item_locations: dict[ZoneExit, list[str]] = defaultdict(list) + self.register_mappings_between_item_locations_and_zone_exits() + + self.done_entrances_to_exits: dict[ZoneEntrance, ZoneExit] = {} + self.done_exits_to_entrances: dict[ZoneExit, ZoneEntrance] = {} + + for entrance_name, exit_name in VANILLA_ENTRANCES_TO_EXITS.items(): + zone_entrance = ZoneEntrance.all[entrance_name] + zone_exit = ZoneExit.all[exit_name] + self.done_entrances_to_exits[zone_entrance] = zone_exit + self.done_exits_to_entrances[zone_exit] = zone_entrance + + self.banned_exits: list[ZoneExit] = [] + self.islands_with_a_banned_dungeon: set[str] = set() + + def randomize_entrances(self) -> None: + """ + Randomize entrances for The Wind Waker. + """ + self.init_banned_exits() + + for relevant_entrances, relevant_exits in self.get_all_entrance_sets_to_be_randomized(): + self.randomize_one_set_of_entrances(relevant_entrances, relevant_exits) + + self.finalize_all_randomized_sets_of_entrances() + + def init_banned_exits(self) -> None: + """ + Initialize the list of banned exits for the randomizer. + + Dungeon exits in banned dungeons should be prohibited from being randomized. + Additionally, if dungeon entrances are not randomized, we can now note which island holds these banned dungeons. + """ + options = self.world.options + + if options.required_bosses: + for zone_exit in BOSS_EXITS: + assert zone_exit.unique_name.endswith(" Boss Arena") + boss_name = zone_exit.unique_name.removesuffix(" Boss Arena") + if boss_name in self.world.boss_reqs.banned_bosses: + self.banned_exits.append(zone_exit) + for zone_exit in DUNGEON_EXITS: + dungeon_name = zone_exit.unique_name + if dungeon_name in self.world.boss_reqs.banned_dungeons: + self.banned_exits.append(zone_exit) + for zone_exit in MINIBOSS_EXITS: + if zone_exit == ZoneExit.all["Master Sword Chamber"]: + # Hyrule cannot be chosen as a banned dungeon. + continue + assert zone_exit.unique_name.endswith(" Miniboss Arena") + dungeon_name = zone_exit.unique_name.removesuffix(" Miniboss Arena") + if dungeon_name in self.world.boss_reqs.banned_dungeons: + self.banned_exits.append(zone_exit) + + if not options.randomize_dungeon_entrances: + # If dungeon entrances are not randomized, `islands_with_a_banned_dungeon` can be initialized early since + # it's preset and won't be updated later since we won't randomize the dungeon entrances. + for en in DUNGEON_ENTRANCES: + if self.done_entrances_to_exits[en].unique_name in self.world.boss_reqs.banned_dungeons: + assert en.island_name is not None + self.islands_with_a_banned_dungeon.add(en.island_name) + + def randomize_one_set_of_entrances( + self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit] + ) -> None: + """ + Randomize a single set of entrances and their corresponding exits. + + :param relevant_entrances: A list of entrances to be randomized. + :param relevant_exits: A list of exits corresponding to the entrances. + """ + # Keep miniboss and boss entrances vanilla in non-required bosses' dungeons. + for zone_entrance in relevant_entrances.copy(): + zone_exit = self.done_entrances_to_exits[zone_entrance] + if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS: + relevant_entrances.remove(zone_entrance) + else: + del self.done_entrances_to_exits[zone_entrance] + for zone_exit in relevant_exits.copy(): + if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS: + relevant_exits.remove(zone_exit) + else: + del self.done_exits_to_entrances[zone_exit] + + self.multiworld.random.shuffle(relevant_entrances) + + # We calculate which exits are terminal (the end of a nested chain) per set instead of for all entrances. + # This is so that, for example, Ice Ring Isle counts as terminal when its inner cave is not being randomized. + non_terminal_exits = [] + for en in relevant_entrances: + if en.nested_in is not None and en.nested_in not in non_terminal_exits: + non_terminal_exits.append(en.nested_in) + terminal_exits = {ex for ex in relevant_exits if ex not in non_terminal_exits} + + remaining_entrances = relevant_entrances.copy() + remaining_exits = relevant_exits.copy() + + nonprogress_entrances, nonprogress_exits = self.split_nonprogress_entrances_and_exits( + remaining_entrances, remaining_exits + ) + if nonprogress_entrances: + for en in nonprogress_entrances: + remaining_entrances.remove(en) + for ex in nonprogress_exits: + remaining_exits.remove(ex) + self.randomize_one_set_of_exits(nonprogress_entrances, nonprogress_exits, terminal_exits) + + self.randomize_one_set_of_exits(remaining_entrances, remaining_exits, terminal_exits) + + def check_if_one_exit_is_progress(self, zone_exit: ZoneExit) -> bool: + """ + Determine if the zone exit leads to progress locations in the world. + + :param zone_exit: The zone exit to check. + :return: Whether the zone exit leads to progress locations. + """ + locs_for_exit = self.zone_exit_to_logically_dependent_item_locations[zone_exit] + assert locs_for_exit, f"Could not find any item locations corresponding to zone exit: {zone_exit.unique_name}" + + # Banned required bosses mode dungeons still technically count as progress locations, so filter them out + # separately first. + nonbanned_locs = [loc for loc in locs_for_exit if loc not in self.world.boss_reqs.banned_locations] + progress_locs = [loc for loc in nonbanned_locs if loc not in self.world.nonprogress_locations] + return bool(progress_locs) + + def split_nonprogress_entrances_and_exits( + self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit] + ) -> tuple[list[ZoneEntrance], list[ZoneExit]]: + """ + Splits the entrance and exit lists into two pairs: ones that should be considered nonprogress on this seed (will + never lead to any progress items) and ones that should be regarded as potentially required. + + This is so we can effectively randomize these two pairs separately without convoluted logic to ensure they don't + connect. + + :param relevant_entrances: A list of entrances. + :param relevant_exits: A list of exits corresponding to the entrances. + :raises FillError: If the number of randomizable entrances does not equal the number of randomizable exits. + """ + nonprogress_exits = [ex for ex in relevant_exits if not self.check_if_one_exit_is_progress(ex)] + nonprogress_entrances = [ + en + for en in relevant_entrances + if en.nested_in is not None + and ( + (en.nested_in in nonprogress_exits) + # The area this entrance is nested in is not randomized, but we still need to determine whether it's + # progression. + or (en.nested_in not in relevant_exits and not self.check_if_one_exit_is_progress(en.nested_in)) + ) + ] + + # At this point, `nonprogress_entrances` includes only the inner entrances nested inside the main exits, not any + # island entrances on the sea. So, we need to select `N` random island entrances to allow all of the nonprogress + # exits to be accessible, where `N` is the difference between the number of entrances and exits we currently + # have. + possible_island_entrances = [en for en in relevant_entrances if en.island_name is not None] + + # We need special logic to handle Forsaken Fortress, as it is the only island entrance inside a dungeon. + ff_boss_entrance = ZoneEntrance.all["Boss Entrance in Forsaken Fortress"] + if ff_boss_entrance in possible_island_entrances: + if self.world.options.progression_dungeons: + if "Forsaken Fortress" in self.world.boss_reqs.banned_dungeons: + ff_progress = False + else: + ff_progress = True + else: + ff_progress = False + + if ff_progress: + # If it's progress, don't allow it to be randomly chosen to lead to nonprogress exits. + possible_island_entrances.remove(ff_boss_entrance) + else: + # If it's not progress, manually mark it as such, and don't allow it to be chosen randomly. + nonprogress_entrances.append(ff_boss_entrance) + possible_island_entrances.remove(ff_boss_entrance) + + num_island_entrances_needed = len(nonprogress_exits) - len(nonprogress_entrances) + if num_island_entrances_needed > len(possible_island_entrances): + raise FillError("Not enough island entrances left to split entrances.") + + for _ in range(num_island_entrances_needed): + # Note: `relevant_entrances` is already shuffled, so we can just take the first result from + # `possible_island_entrances`—it's the same as picking one randomly. + nonprogress_island_entrance = possible_island_entrances.pop(0) + nonprogress_entrances.append(nonprogress_island_entrance) + + assert len(nonprogress_entrances) == len(nonprogress_exits) + + return nonprogress_entrances, nonprogress_exits + + def randomize_one_set_of_exits( + self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit], terminal_exits: set[ZoneExit] + ) -> None: + """ + Randomize a single set of entrances and their corresponding exits. + + :param relevant_entrances: A list of entrances to be randomized. + :param relevant_exits: A list of exits corresponding to the entrances. + :param terminal_exits: A set of exits which do not contain any entrances. + :raises FillError: If there are no valid exits to assign to an entrance. + """ + options = self.world.options + + remaining_entrances = relevant_entrances.copy() + remaining_exits = relevant_exits.copy() + + doing_banned = False + if any(ex in self.banned_exits for ex in relevant_exits): + doing_banned = True + + if options.required_bosses and not doing_banned: + # Prioritize entrances that share an island with an entrance randomized to lead into a + # required-bosses-mode-banned dungeon. (e.g., DRI, Pawprint, Outset, TotG sector.) + # This is because we need to prevent these islands from having a required boss or anything that could lead + # to a required boss. If we don't do this first, we can get backed into a corner where there is no other + # option left. + entrances_not_on_unique_islands = [] + for zone_entrance in relevant_entrances: + if zone_entrance.is_nested: + continue + if zone_entrance.island_name in self.islands_with_a_banned_dungeon: + # This island was already used on a previous call to `randomize_one_set_of_exits`. + entrances_not_on_unique_islands.append(zone_entrance) + continue + for zone_entrance in entrances_not_on_unique_islands: + remaining_entrances.remove(zone_entrance) + remaining_entrances = entrances_not_on_unique_islands + remaining_entrances + + while remaining_entrances: + # Filter out boss entrances that aren't yet accessible from the sea. + # We don't want to connect these to anything yet or we risk creating an infinite loop. + possible_remaining_entrances = [ + en for en in remaining_entrances if self.get_outermost_entrance_for_entrance(en) is not None + ] + zone_entrance = possible_remaining_entrances.pop(0) + remaining_entrances.remove(zone_entrance) + + possible_remaining_exits = remaining_exits.copy() + + if len(possible_remaining_entrances) == 0 and len(remaining_entrances) > 0: + # If this is the last entrance we have left to attach exits to, we can't place a terminal exit here. + # Terminal exits do not create another entrance, so one would leave us with no possible way to continue + # placing the remaining exits on future loops. + possible_remaining_exits = [ex for ex in possible_remaining_exits if ex not in terminal_exits] + + if options.required_bosses and zone_entrance.island_name is not None and not doing_banned: + # Prevent required bosses (and non-terminal exits, which could lead to required bosses) from appearing + # on islands where we already placed a banned boss or dungeon. + # This can happen with DRI and Pawprint, as these islands have two entrances. This would be bad because + # the required bosses mode's dungeon markers only tell you what island the required dungeons are on, not + # which of the two entrances to enter. + # So, if a banned dungeon is placed on DRI's main entrance, we will have to fill DRI's pit entrance with + # either a miniboss or one of the caves that does not have a nested entrance inside. We allow multiple + # banned and required dungeons on a single island. + if zone_entrance.island_name in self.islands_with_a_banned_dungeon: + possible_remaining_exits = [ + ex + for ex in possible_remaining_exits + if ex in terminal_exits and ex not in (DUNGEON_EXITS + BOSS_EXITS) + ] + + if not possible_remaining_exits: + raise FillError(f"No valid exits to place for entrance: {zone_entrance.entrance_name}") + + zone_exit = self.multiworld.random.choice(possible_remaining_exits) + remaining_exits.remove(zone_exit) + + self.done_entrances_to_exits[zone_entrance] = zone_exit + self.done_exits_to_entrances[zone_exit] = zone_entrance + + if zone_exit in self.banned_exits: + # Keep track of which islands have a required bosses mode banned dungeon to avoid marker overlap. + if zone_exit in DUNGEON_EXITS + BOSS_EXITS: + # We only keep track of dungeon exits and boss exits, not miniboss exits. + # Banned miniboss exits can share an island with required dungeons/bosses. + outer_entrance = self.get_outermost_entrance_for_entrance(zone_entrance) + + # Because we filter above so that we always assign entrances from the sea inwards, we can assume + # that when we assign an entrance, it has a path back to the sea. + # If we're assigning a non-terminal entrance, any nested entrances will get assigned after this one, + # and we'll run through this code again (so we can reason based on `zone_exit` only instead of + # having to recurse through the nested exits to find banned dungeons/bosses). + assert outer_entrance and outer_entrance.island_name is not None + self.islands_with_a_banned_dungeon.add(outer_entrance.island_name) + + def finalize_all_randomized_sets_of_entrances(self) -> None: + """ + Finalize all randomized entrance sets. + + For all entrance-exit pairs, this function adds a connection with the appropriate access rule to the world. + """ + + def get_access_rule(entrance: ZoneEntrance) -> str: + snake_case_region = entrance.entrance_name.lower().replace("'", "").replace(" ", "_") + return getattr(Macros, f"can_access_{snake_case_region}") + + # Connect each entrance-exit pair in the multiworld with the access rule for the entrance. + # The Great Sea is the parent_region for many entrances, so get it in advance. + great_sea_region = self.world.get_region("The Great Sea") + for zone_entrance, zone_exit in self.done_entrances_to_exits.items(): + # Get the parent region of the entrance. + if zone_entrance.island_name is not None: + # Entrances with an `island_name` are found in The Great Sea. + parent_region = great_sea_region + else: + # All other entrances must be nested within some other region. + parent_region = self.world.get_region(zone_entrance.nested_in.unique_name) + exit_region_name = zone_exit.unique_name + exit_region = self.world.get_region(exit_region_name) + parent_region.connect( + exit_region, + # The default name uses the "parent_region -> connecting_region", but the parent_region would not be + # useful for spoiler paths or debugging, so use the entrance name at the start. + f"{zone_entrance.entrance_name} -> {exit_region_name}", + rule=lambda state, rule=get_access_rule(zone_entrance): rule(state, self.player), + ) + + if __debug__ and self.world.options.required_bosses: + # Ensure we didn't accidentally place a banned boss and a required boss on the same island. + banned_island_names = set( + self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.banned_bosses + ) + required_island_names = set( + self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.required_bosses + ) + assert not banned_island_names & required_island_names + + def register_mappings_between_item_locations_and_zone_exits(self) -> None: + """ + Map item locations to their corresponding zone exits. + """ + for loc_name in list(LOCATION_TABLE.keys()): + zone_exit = self.get_zone_exit_for_item_location(loc_name) + if zone_exit is not None: + self.item_location_to_containing_zone_exit[loc_name] = zone_exit + self.zone_exit_to_logically_dependent_item_locations[zone_exit].append(loc_name) + + if loc_name == "The Great Sea - Withered Trees": + # This location isn't inside a zone exit, but it does logically require the player to be able to reach + # a different item location inside one. + sub_zone_exit = self.get_zone_exit_for_item_location("Cliff Plateau Isles - Highest Isle") + if sub_zone_exit is not None: + self.zone_exit_to_logically_dependent_item_locations[sub_zone_exit].append(loc_name) + + def get_all_entrance_sets_to_be_randomized( + self, + ) -> Generator[tuple[list[ZoneEntrance], list[ZoneExit]], None, None]: + """ + Retrieve all entrance-exit pairs that need to be randomized. + + :raises OptionError: If an invalid randomization option is set in the world's options. + :return: A generator that yields sets of entrances and exits to be randomized. + """ + options = self.world.options + + dungeons = bool(options.randomize_dungeon_entrances) + minibosses = bool(options.randomize_miniboss_entrances) + bosses = bool(options.randomize_boss_entrances) + secret_caves = bool(options.randomize_secret_cave_entrances) + inner_caves = bool(options.randomize_secret_cave_inner_entrances) + fountains = bool(options.randomize_fairy_fountain_entrances) + + mix_entrances = options.mix_entrances + if mix_entrances == "separate_pools": + if dungeons: + yield self.get_one_entrance_set(dungeons=dungeons) + if minibosses: + yield self.get_one_entrance_set(minibosses=minibosses) + if bosses: + yield self.get_one_entrance_set(bosses=bosses) + if secret_caves: + yield self.get_one_entrance_set(caves=secret_caves) + if inner_caves: + yield self.get_one_entrance_set(inner_caves=inner_caves) + if fountains: + yield self.get_one_entrance_set(fountains=fountains) + elif mix_entrances == "mix_pools": + yield self.get_one_entrance_set( + dungeons=dungeons, + minibosses=minibosses, + bosses=bosses, + caves=secret_caves, + inner_caves=inner_caves, + fountains=fountains, + ) + else: + raise OptionError(f"Invalid entrance randomization option: {mix_entrances}") + + def get_one_entrance_set( + self, + *, + dungeons: bool = False, + caves: bool = False, + minibosses: bool = False, + bosses: bool = False, + inner_caves: bool = False, + fountains: bool = False, + ) -> tuple[list[ZoneEntrance], list[ZoneExit]]: + """ + Retrieve a single set of entrance-exit pairs that need to be randomized. + + :param dungeons: Whether to include dungeon entrances and exits. Defaults to `False`. + :param caves: Whether to include secret cave entrances and exits. Defaults to `False`. + :param minibosses: Whether to include miniboss entrances and exits. Defaults to `False`. + :param bosses: Whether to include boss entrances and exits. Defaults to `False`. + :param inner_caves: Whether to include inner cave entrances and exits. Defaults to `False`. + :param fountains: Whether to include fairy fountain entrances and exits. Defaults to `False`. + :return: A tuple of lists of entrances and exits that should be randomized together. + """ + relevant_entrances: list[ZoneEntrance] = [] + relevant_exits: list[ZoneExit] = [] + if dungeons: + relevant_entrances += DUNGEON_ENTRANCES + relevant_exits += DUNGEON_EXITS + if minibosses: + relevant_entrances += MINIBOSS_ENTRANCES + relevant_exits += MINIBOSS_EXITS + if bosses: + relevant_entrances += BOSS_ENTRANCES + relevant_exits += BOSS_EXITS + if caves: + relevant_entrances += SECRET_CAVE_ENTRANCES + relevant_exits += SECRET_CAVE_EXITS + if inner_caves: + relevant_entrances += SECRET_CAVE_INNER_ENTRANCES + relevant_exits += SECRET_CAVE_INNER_EXITS + if fountains: + relevant_entrances += FAIRY_FOUNTAIN_ENTRANCES + relevant_exits += FAIRY_FOUNTAIN_EXITS + return relevant_entrances, relevant_exits + + def get_outermost_entrance_for_exit(self, zone_exit: ZoneExit) -> Optional[ZoneEntrance]: + """ + Unrecurses nested dungeons to determine a given exit's outermost (island) entrance. + + :param zone_exit: The given exit. + :return: The outermost (island) entrance for the exit, or `None` if entrances have yet to be randomized. + """ + zone_entrance = self.done_exits_to_entrances[zone_exit] + return self.get_outermost_entrance_for_entrance(zone_entrance) + + def get_outermost_entrance_for_entrance(self, zone_entrance: ZoneEntrance) -> Optional[ZoneEntrance]: + """ + Unrecurses nested dungeons to determine a given entrance's outermost (island) entrance. + + :param zone_exit: The given entrance. + :return: The outermost (island) entrance for the entrance, or `None` if entrances have yet to be randomized. + """ + seen_entrances = self.get_all_entrances_on_path_to_entrance(zone_entrance) + if seen_entrances is None: + # Undecided. + return None + outermost_entrance = seen_entrances[-1] + return outermost_entrance + + def get_all_entrances_on_path_to_entrance(self, zone_entrance: ZoneEntrance) -> Optional[list[ZoneEntrance]]: + """ + Unrecurses nested dungeons to build a list of all entrances leading to a given entrance. + + :param zone_exit: The given entrance. + :return: A list of entrances leading to the given entrance, or `None` if entrances have yet to be randomized. + """ + seen_entrances: list[ZoneEntrance] = [] + while zone_entrance.is_nested: + if zone_entrance in seen_entrances: + path_str = ", ".join([e.entrance_name for e in seen_entrances]) + raise FillError(f"Entrances are in an infinite loop: {path_str}") + seen_entrances.append(zone_entrance) + if zone_entrance.nested_in not in self.done_exits_to_entrances: + # Undecided. + return None + zone_entrance = self.done_exits_to_entrances[zone_entrance.nested_in] + seen_entrances.append(zone_entrance) + return seen_entrances + + def is_item_location_behind_randomizable_entrance(self, location_name: str) -> bool: + """ + Determine if the location is behind a randomizable entrance. + + :param location_name: The location to check. + :return: `True` if the location is behind a randomizable entrance, `False` otherwise. + """ + loc_zone_name, _ = split_location_name_by_zone(location_name) + if loc_zone_name in ["Ganon's Tower", "Mailbox"]: + # Ganon's Tower and the handful of Mailbox locations that depend on beating dungeon bosses are considered + # "Dungeon" location types by the logic, but the entrance randomizer does not need to consider them. + # Although the mail locations are technically locked behind dungeons, we can still ignore them here because + # if all of the locations in the dungeon itself are nonprogress, then any mail depending on that dungeon + # should also be enforced as nonprogress by other parts of the code. + return False + + types = LOCATION_TABLE[location_name].flags + is_boss = TWWFlag.BOSS in types + if loc_zone_name == "Forsaken Fortress" and not is_boss: + # Special case. FF is a dungeon that is not randomized, except for the boss arena. + return False + + is_big_octo = TWWFlag.BG_OCTO in types + if is_big_octo: + # The Big Octo Great Fairy is the only Great Fairy location that is not also a Fairy Fountain. + return False + + # In the general case, we check if the location has a type corresponding to exits that can be randomized. + if any(t in types for t in ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES): + return True + + return False + + def get_zone_exit_for_item_location(self, location_name: str) -> Optional[ZoneExit]: + """ + Retrieve the zone exit for a given location. + + :param location_name: The name of the location. + :raises Exception: If a location exit override should be used instead. + :return: The zone exit for the location or `None` if the location is not behind a randomizable entrance. + """ + if not self.is_item_location_behind_randomizable_entrance(location_name): + return None + + zone_exit = ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES.get(location_name, None) + if zone_exit is not None: + return zone_exit + + loc_zone_name, _ = split_location_name_by_zone(location_name) + possible_exits = [ex for ex in ZoneExit.all.values() if ex.zone_name == loc_zone_name] + if len(possible_exits) == 0: + return None + elif len(possible_exits) == 1: + return possible_exits[0] + else: + raise Exception( + f"Multiple zone exits share the same zone name: {loc_zone_name!r}. " + "Use a location exit override instead." + ) + + def get_entrance_zone_for_boss(self, boss_name: str) -> str: + """ + Retrieve the entrance zone for a given boss. + + :param boss_name: The name of the boss. + :return: The name of the island on which the boss is located. + """ + boss_arena_name = f"{boss_name} Boss Arena" + zone_exit = ZoneExit.all[boss_arena_name] + outermost_entrance = self.get_outermost_entrance_for_exit(zone_exit) + assert outermost_entrance is not None and outermost_entrance.island_name is not None + return outermost_entrance.island_name diff --git a/worlds/tww/randomizers/ItemPool.py b/worlds/tww/randomizers/ItemPool.py new file mode 100644 index 00000000..86c02f39 --- /dev/null +++ b/worlds/tww/randomizers/ItemPool.py @@ -0,0 +1,205 @@ +from typing import TYPE_CHECKING + +from BaseClasses import ItemClassification as IC +from Fill import FillError + +from ..Items import ITEM_TABLE, item_factory +from ..Options import DungeonItem +from .Dungeons import get_dungeon_item_pool_player + +if TYPE_CHECKING: + from .. import TWWWorld + +VANILLA_DUNGEON_ITEM_LOCATIONS: dict[str, list[str]] = { + "DRC Small Key": [ + "Dragon Roost Cavern - First Room", + "Dragon Roost Cavern - Boarded Up Chest", + "Dragon Roost Cavern - Rat Room Boarded Up Chest", + "Dragon Roost Cavern - Bird's Nest", + ], + "FW Small Key": [ + "Forbidden Woods - Vine Maze Right Chest" + ], + "TotG Small Key": [ + "Tower of the Gods - Hop Across Floating Boxes", + "Tower of the Gods - Floating Platforms Room" + ], + "ET Small Key": [ + "Earth Temple - Transparent Chest in First Crypt", + "Earth Temple - Casket in Second Crypt", + "Earth Temple - End of Foggy Room With Floormasters", + ], + "WT Small Key": [ + "Wind Temple - Spike Wall Room - First Chest", + "Wind Temple - Chest Behind Seven Armos" + ], + + "DRC Big Key": ["Dragon Roost Cavern - Big Key Chest"], + "FW Big Key": ["Forbidden Woods - Big Key Chest"], + "TotG Big Key": ["Tower of the Gods - Big Key Chest"], + "ET Big Key": ["Earth Temple - Big Key Chest"], + "WT Big Key": ["Wind Temple - Big Key Chest"], + + "DRC Dungeon Map": ["Dragon Roost Cavern - Alcove With Water Jugs"], + "FW Dungeon Map": ["Forbidden Woods - First Room"], + "TotG Dungeon Map": ["Tower of the Gods - Chest Behind Bombable Walls"], + "FF Dungeon Map": ["Forsaken Fortress - Chest Outside Upper Jail Cell"], + "ET Dungeon Map": ["Earth Temple - Transparent Chest In Warp Pot Room"], + "WT Dungeon Map": ["Wind Temple - Chest In Many Cyclones Room"], + + "DRC Compass": ["Dragon Roost Cavern - Rat Room"], + "FW Compass": ["Forbidden Woods - Vine Maze Left Chest"], + "TotG Compass": ["Tower of the Gods - Skulls Room Chest"], + "FF Compass": ["Forsaken Fortress - Chest Guarded By Bokoblin"], + "ET Compass": ["Earth Temple - Chest In Three Blocks Room"], + "WT Compass": ["Wind Temple - Chest In Middle Of Hub Room"], +} + + +def generate_itempool(world: "TWWWorld") -> None: + """ + Generate the item pool for the world. + + :param world: The Wind Waker game world. + """ + multiworld = world.multiworld + + # Get the core pool of items. + pool, precollected_items = get_pool_core(world) + + # Add precollected items to the multiworld's `precollected_items` list. + for item in precollected_items: + multiworld.push_precollected(item_factory(item, world)) + + # Place a "Victory" item on "Defeat Ganondorf" for the spoiler log. + world.get_location("Defeat Ganondorf").place_locked_item(item_factory("Victory", world)) + + # Create the pool of the remaining shuffled items. + items = item_factory(pool, world) + world.random.shuffle(items) + + multiworld.itempool += items + + # Dungeon items should already be created, so handle those separately. + handle_dungeon_items(world) + + +def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]: + """ + Get the core pool of items and precollected items for the world. + + :param world: The Wind Waker game world. + :return: A tuple of the item pool and precollected items. + """ + pool: list[str] = [] + precollected_items: list[str] = [] + + # Split items into three different pools: progression, useful, and filler. + progression_pool: list[str] = [] + useful_pool: list[str] = [] + filler_pool: list[str] = [] + for item, data in ITEM_TABLE.items(): + if data.type == "Item": + adjusted_classification = world.item_classification_overrides.get(item) + classification = data.classification if adjusted_classification is None else adjusted_classification + + if classification & IC.progression: + progression_pool.extend([item] * data.quantity) + elif classification & IC.useful: + useful_pool.extend([item] * data.quantity) + else: + filler_pool.extend([item] * data.quantity) + + # Assign useful and filler items to item pools in the world. + world.random.shuffle(useful_pool) + world.random.shuffle(filler_pool) + world.useful_pool = useful_pool + world.filler_pool = filler_pool + + # Add filler items to place into excluded locations. + pool.extend([world.get_filler_item_name() for _ in world.options.exclude_locations]) + + # The remaining of items left to place should be the same as the number of non-excluded locations in the world. + nonexcluded_locations = [ + location + for location in world.multiworld.get_locations(world.player) + if location.name not in world.options.exclude_locations + ] + num_items_left_to_place = len(nonexcluded_locations) - 1 + + # Account for the dungeon items that have already been created. + for dungeon in world.dungeons.values(): + num_items_left_to_place -= len(dungeon.all_items) + + # All progression items are added to the item pool. + if len(progression_pool) > num_items_left_to_place: + raise FillError( + "There are insufficient locations to place progression items! " + f"Trying to place {len(progression_pool)} items in only {num_items_left_to_place} locations." + ) + pool.extend(progression_pool) + num_items_left_to_place -= len(progression_pool) + + # If the player starts with a sword, add one to the precollected items list and remove one from the item pool. + if world.options.sword_mode == "start_with_sword": + precollected_items.append("Progressive Sword") + num_items_left_to_place += 1 + pool.remove("Progressive Sword") + # Or, if it's swordless mode, remove all swords from the item pool. + elif world.options.sword_mode == "swordless": + while "Progressive Sword" in pool: + num_items_left_to_place += 1 + pool.remove("Progressive Sword") + + # Place useful items, then filler items to fill out the remaining locations. + pool.extend([world.get_filler_item_name(strict=False) for _ in range(num_items_left_to_place)]) + + return pool, precollected_items + + +def handle_dungeon_items(world: "TWWWorld") -> None: + """ + Handle the placement of dungeon items in the world. + + :param world: The Wind Waker game world. + """ + player = world.player + multiworld = world.multiworld + options = world.options + + dungeon_items = [ + item + for item in get_dungeon_item_pool_player(world) + if item.name not in multiworld.worlds[player].dungeon_local_item_names + ] + + for x in range(len(dungeon_items) - 1, -1, -1): + item = dungeon_items[x] + + # Consider dungeon items in non-required dungeons as filler. + if item.dungeon.name in world.boss_reqs.banned_dungeons: + item.classification = IC.filler + + option: DungeonItem + if item.type == "Big Key": + option = options.randomize_bigkeys + elif item.type == "Small Key": + option = options.randomize_smallkeys + else: + option = options.randomize_mapcompass + + if option == "startwith": + dungeon_items.pop(x) + multiworld.push_precollected(item) + multiworld.itempool.append(item_factory(world.get_filler_item_name(), world)) + elif option == "vanilla": + for location_name in VANILLA_DUNGEON_ITEM_LOCATIONS[item.name]: + location = world.get_location(location_name) + if location.item is None: + dungeon_items.pop(x) + location.place_locked_item(item) + break + else: + raise FillError(f"Could not place dungeon item in vanilla location: {item}") + + multiworld.itempool.extend([item for item in dungeon_items]) diff --git a/worlds/tww/randomizers/RequiredBosses.py b/worlds/tww/randomizers/RequiredBosses.py new file mode 100644 index 00000000..04c4d7c5 --- /dev/null +++ b/worlds/tww/randomizers/RequiredBosses.py @@ -0,0 +1,121 @@ +from typing import TYPE_CHECKING + +from Options import OptionError + +from ..Locations import DUNGEON_NAMES, LOCATION_TABLE, TWWFlag, split_location_name_by_zone +from ..Options import TWWOptions + +if TYPE_CHECKING: + from .. import TWWWorld + + +class RequiredBossesRandomizer: + """ + This class handles the randomization of the required bosses in The Wind Waker game based on user options. + + If the option is on, the required bosses must be defeated as part of the unlock condition of Puppet Ganon's door. + The quadrants in which the bosses are located are marked on the player's Sea Chart. + + :param world: The Wind Waker game world. + """ + + def __init__(self, world: "TWWWorld"): + self.world = world + self.multiworld = world.multiworld + + self.required_boss_item_locations: list[str] = [] + self.required_dungeons: set[str] = set() + self.required_bosses: list[str] = [] + self.banned_locations: set[str] = set() + self.banned_dungeons: set[str] = set() + self.banned_bosses: list[str] = [] + + def validate_boss_options(self, options: TWWOptions) -> None: + """ + Validate the user-defined boss options to ensure logical consistency. + + :param options: The game options set by the user. + :raises OptionError: If the boss options are inconsistent. + """ + if not options.progression_dungeons: + raise OptionError("You cannot make bosses required when progression dungeons are disabled.") + + if len(options.included_dungeons.value & options.excluded_dungeons.value) != 0: + raise OptionError( + "A conflict was found in the lists of required and banned dungeons for required bosses mode." + ) + + def randomize_required_bosses(self) -> None: + """ + Randomize the required bosses based on user-defined constraints and options. + + :raises OptionError: If the randomization fails to meet user-defined constraints. + """ + options = self.world.options + + # Validate constraints on required bosses options. + self.validate_boss_options(options) + + # If the user enforces a dungeon location to be priority, consider that when selecting required bosses. + dungeon_names = set(DUNGEON_NAMES) + required_dungeons = options.included_dungeons.value + for location_name in options.priority_locations.value: + dungeon_name, _ = split_location_name_by_zone(location_name) + if dungeon_name in dungeon_names: + required_dungeons.add(dungeon_name) + + # Ensure we aren't prioritizing more dungeon locations than the requested number of required bosses. + num_required_bosses = options.num_required_bosses + if len(required_dungeons) > num_required_bosses: + raise OptionError( + "Could not select required bosses to satisfy options set by the user. " + "There are more dungeons with priority locations than the desired number of required bosses." + ) + + # Ensure that after removing excluded dungeons, we still have enough to satisfy user options. + num_remaining = num_required_bosses - len(required_dungeons) + remaining_dungeon_options = dungeon_names - required_dungeons - options.excluded_dungeons.value + if len(remaining_dungeon_options) < num_remaining: + raise OptionError( + "Could not select required bosses to satisfy options set by the user. " + "After removing the excluded dungeons, there are not enough to meet the desired number of required " + "bosses." + ) + + # Finish selecting required bosses. + required_dungeons.update(self.world.random.sample(sorted(remaining_dungeon_options), num_remaining)) + + # Exclude locations that are not in the dungeon of a required boss. + banned_dungeons = dungeon_names - required_dungeons + for location_name, location_data in LOCATION_TABLE.items(): + dungeon_name, _ = split_location_name_by_zone(location_name) + if dungeon_name in banned_dungeons and TWWFlag.DUNGEON in location_data.flags: + self.banned_locations.add(location_name) + elif location_name == "Mailbox - Letter from Orca" and "Forbidden Woods" in banned_dungeons: + self.banned_locations.add(location_name) + elif location_name == "Mailbox - Letter from Baito" and "Earth Temple" in banned_dungeons: + self.banned_locations.add(location_name) + elif location_name == "Mailbox - Letter from Aryll" and "Forsaken Fortress" in banned_dungeons: + self.banned_locations.add(location_name) + elif location_name == "Mailbox - Letter from Tingle" and "Forsaken Fortress" in banned_dungeons: + self.banned_locations.add(location_name) + for location_name in self.banned_locations: + self.world.nonprogress_locations.add(location_name) + + # Record the item location names for required bosses. + self.required_boss_item_locations = [] + self.required_bosses = [] + self.banned_bosses = [] + possible_boss_item_locations = [loc for loc, data in LOCATION_TABLE.items() if TWWFlag.BOSS in data.flags] + for location_name in possible_boss_item_locations: + dungeon_name, specific_location_name = split_location_name_by_zone(location_name) + assert specific_location_name.endswith(" Heart Container") + boss_name = specific_location_name.removesuffix(" Heart Container") + + if dungeon_name in required_dungeons: + self.required_boss_item_locations.append(location_name) + self.required_bosses.append(boss_name) + else: + self.banned_bosses.append(boss_name) + self.required_dungeons = required_dungeons + self.banned_dungeons = banned_dungeons diff --git a/worlds/tww/requirements.txt b/worlds/tww/requirements.txt new file mode 100644 index 00000000..30a95d98 --- /dev/null +++ b/worlds/tww/requirements.txt @@ -0,0 +1 @@ +dolphin-memory-engine>=1.3.0