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 00000000..e1e9ab32 Binary files /dev/null and b/worlds/tww/assets/icon.ico differ diff --git a/worlds/tww/assets/icon.png b/worlds/tww/assets/icon.png new file mode 100644 index 00000000..a67e99a9 Binary files /dev/null and b/worlds/tww/assets/icon.png differ 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