From cae2adbeada9c3ed544514dd8060e9491378a60e Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:32:15 -0500 Subject: [PATCH] TUNIC: Implement New Game (#2172) --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/tunic/__init__.py | 279 +++++++++ worlds/tunic/docs/en_Tunic.md | 59 ++ worlds/tunic/docs/setup_en.md | 67 ++ worlds/tunic/er_data.py | 1001 ++++++++++++++++++++++++++++++ worlds/tunic/er_rules.py | 984 +++++++++++++++++++++++++++++ worlds/tunic/er_scripts.py | 453 ++++++++++++++ worlds/tunic/items.py | 214 +++++++ worlds/tunic/locations.py | 337 ++++++++++ worlds/tunic/options.py | 147 +++++ worlds/tunic/regions.py | 25 + worlds/tunic/rules.py | 345 ++++++++++ worlds/tunic/test/__init__.py | 6 + worlds/tunic/test/test_access.py | 70 +++ 15 files changed, 3991 insertions(+) create mode 100644 worlds/tunic/__init__.py create mode 100644 worlds/tunic/docs/en_Tunic.md create mode 100644 worlds/tunic/docs/setup_en.md create mode 100644 worlds/tunic/er_data.py create mode 100644 worlds/tunic/er_rules.py create mode 100644 worlds/tunic/er_scripts.py create mode 100644 worlds/tunic/items.py create mode 100644 worlds/tunic/locations.py create mode 100644 worlds/tunic/options.py create mode 100644 worlds/tunic/regions.py create mode 100644 worlds/tunic/rules.py create mode 100644 worlds/tunic/test/__init__.py create mode 100644 worlds/tunic/test/test_access.py diff --git a/README.md b/README.md index a1e03293d587..ce2130ce8e7c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Currently, the following games are supported: * Heretic * Landstalker: The Treasures of King Nole * Final Fantasy Mystic Quest +* TUNIC For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index e221371b2417..95c0baea3a1f 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -164,6 +164,9 @@ # The Legend of Zelda (1) /worlds/tloz/ @Rosalie-A @t3hf1gm3nt +# TUNIC +/worlds/tunic/ @silent-destroyer + # Undertale /worlds/undertale/ @jonloveslegos diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py new file mode 100644 index 000000000000..b946ea8e3039 --- /dev/null +++ b/worlds/tunic/__init__.py @@ -0,0 +1,279 @@ +from typing import Dict, List, Any + +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification +from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names +from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations +from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon +from .er_rules import set_er_location_rules +from .regions import tunic_regions +from .er_scripts import create_er_regions +from .options import TunicOptions +from worlds.AutoWorld import WebWorld, World +from decimal import Decimal, ROUND_HALF_UP + + +class TunicWeb(WebWorld): + tutorials = [ + Tutorial( + tutorial_name="Multiworld Setup Guide", + description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.", + language="English", + file_name="setup_en.md", + link="setup/en", + authors=["SilentDestroyer"] + ) + ] + theme = "grassFlowers" + game = "Tunic" + + +class TunicItem(Item): + game: str = "Tunic" + + +class TunicLocation(Location): + game: str = "Tunic" + + +class TunicWorld(World): + """ + Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game + about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will + confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox! + """ + game = "Tunic" + web = TunicWeb() + + data_version = 2 + options: TunicOptions + options_dataclass = TunicOptions + item_name_groups = item_name_groups + location_name_groups = location_name_groups + + item_name_to_id = item_name_to_id + location_name_to_id = location_name_to_id + + ability_unlocks: Dict[str, int] + slot_data_items: List[TunicItem] + tunic_portal_pairs: Dict[str, str] + er_portal_hints: Dict[int, str] + + def generate_early(self) -> None: + if self.options.start_with_sword and "Sword" not in self.options.start_inventory: + self.options.start_inventory.value["Sword"] = 1 + + def create_item(self, name: str) -> TunicItem: + item_data = item_table[name] + return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + + def create_items(self) -> None: + keys_behind_bosses = self.options.keys_behind_bosses + hexagon_quest = self.options.hexagon_quest + sword_progression = self.options.sword_progression + + tunic_items: List[TunicItem] = [] + self.slot_data_items = [] + + items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} + + for money_fool in fool_tiers[self.options.fool_traps]: + items_to_create["Fool Trap"] += items_to_create[money_fool] + items_to_create[money_fool] = 0 + + if sword_progression: + items_to_create["Stick"] = 0 + items_to_create["Sword"] = 0 + else: + items_to_create["Sword Upgrade"] = 0 + + if self.options.laurels_location: + laurels = self.create_item("Hero's Laurels") + if self.options.laurels_location == "6_coins": + self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) + elif self.options.laurels_location == "10_coins": + self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) + elif self.options.laurels_location == "10_fairies": + self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) + self.slot_data_items.append(laurels) + items_to_create["Hero's Laurels"] = 0 + + if keys_behind_bosses: + for rgb_hexagon, location in hexagon_locations.items(): + hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon) + self.multiworld.get_location(location, self.player).place_locked_item(hex_item) + self.slot_data_items.append(hex_item) + items_to_create[rgb_hexagon] = 0 + items_to_create[gold_hexagon] -= 3 + + if hexagon_quest: + # Calculate number of hexagons in item pool + hexagon_goal = self.options.hexagon_goal + extra_hexagons = self.options.extra_hexagon_percentage + items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) + + # Replace pages and normal hexagons with filler + for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): + items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item] + items_to_create[replaced_item] = 0 + + # Filler items that are still in the item pool to swap out + available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and + item_table[filler].classification == ItemClassification.filler] + + # Remove filler to make room for extra hexagons + for i in range(0, items_to_create[gold_hexagon]): + fill = self.random.choice(available_filler) + items_to_create[fill] -= 1 + if items_to_create[fill] == 0: + available_filler.remove(fill) + + if self.options.maskless: + mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) + tunic_items.append(mask_item) + items_to_create["Scavenger Mask"] = 0 + + if self.options.lanternless: + mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) + tunic_items.append(mask_item) + items_to_create["Lantern"] = 0 + + for item, quantity in items_to_create.items(): + for i in range(0, quantity): + tunic_item: TunicItem = self.create_item(item) + if item in slot_data_item_names: + self.slot_data_items.append(tunic_item) + tunic_items.append(tunic_item) + + self.multiworld.itempool += tunic_items + + def create_regions(self) -> None: + self.tunic_portal_pairs = {} + self.er_portal_hints = {} + self.ability_unlocks = randomize_ability_unlocks(self.random, self.options) + if self.options.entrance_rando: + portal_pairs, portal_hints = create_er_regions(self) + for portal1, portal2 in portal_pairs.items(): + self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() + self.er_portal_hints = portal_hints + + else: + for region_name in tunic_regions: + region = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(region) + + for region_name, exits in tunic_regions.items(): + region = self.multiworld.get_region(region_name, self.player) + region.add_exits(exits) + + for location_name, location_id in self.location_name_to_id.items(): + region = self.multiworld.get_region(location_table[location_name].region, self.player) + location = TunicLocation(self.player, location_name, location_id, region) + region.locations.append(location) + + victory_region = self.multiworld.get_region("Spirit Arena", self.player) + victory_location = TunicLocation(self.player, "The Heir", None, victory_region) + victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + victory_region.locations.append(victory_location) + + def set_rules(self) -> None: + if self.options.entrance_rando: + set_er_location_rules(self, self.ability_unlocks) + else: + set_region_rules(self, self.ability_unlocks) + set_location_rules(self, self.ability_unlocks) + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_items) + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.options.entrance_rando: + hint_data[self.player] = self.er_portal_hints + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data: Dict[str, Any] = { + "seed": self.random.randint(0, 2147483647), + "start_with_sword": self.options.start_with_sword.value, + "keys_behind_bosses": self.options.keys_behind_bosses.value, + "sword_progression": self.options.sword_progression.value, + "ability_shuffling": self.options.ability_shuffling.value, + "hexagon_quest": self.options.hexagon_quest.value, + "fool_traps": self.options.fool_traps.value, + "entrance_rando": self.options.entrance_rando.value, + "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], + "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], + "Hexagon Quest Ice Rod": self.ability_unlocks["Pages 52-53 (Ice Rod)"], + "Hexagon Quest Goal": self.options.hexagon_goal.value, + "Entrance Rando": self.tunic_portal_pairs + } + + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): + if tunic_item.name not in slot_data: + slot_data[tunic_item.name] = [] + if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: + continue + slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + + for start_item in self.options.start_inventory_from_pool: + if start_item in slot_data_item_names: + if start_item not in slot_data: + slot_data[start_item] = [] + for i in range(0, self.options.start_inventory_from_pool[start_item]): + slot_data[start_item].extend(["Your Pocket", self.player]) + + for plando_item in self.multiworld.plando_items[self.player]: + if plando_item["from_pool"]: + items_to_find = set() + for item_type in [key for key in ["item", "items"] if key in plando_item]: + for item in plando_item[item_type]: + items_to_find.add(item) + for item in items_to_find: + if item in slot_data_item_names: + slot_data[item] = [] + for item_location in self.multiworld.find_item_locations(item, self.player): + slot_data[item].extend([item_location.name, item_location.player]) + + return slot_data + + # for the universal tracker, doesn't get called in standard gen + def interpret_slot_data(self, slot_data: Dict[str, Any]) -> None: + # bypassing random yaml settings + self.options.start_with_sword.value = slot_data["start_with_sword"] + self.options.keys_behind_bosses.value = slot_data["keys_behind_bosses"] + self.options.sword_progression.value = slot_data["sword_progression"] + self.options.ability_shuffling.value = slot_data["ability_shuffling"] + self.options.hexagon_quest.value = slot_data["hexagon_quest"] + self.ability_unlocks["Pages 24-25 (Prayer)"] = slot_data["Hexagon Quest Prayer"] + self.ability_unlocks["Pages 42-43 (Holy Cross)"] = slot_data["Hexagon Quest Holy Cross"] + self.ability_unlocks["Pages 52-53 (Ice Rod)"] = slot_data["Hexagon Quest Ice Rod"] + + # swapping entrances around so the mapping matches what was generated + if slot_data["entrance_rando"]: + from BaseClasses import Entrance + from .er_data import portal_mapping + entrance_dict: Dict[str, Entrance] = {entrance.name: entrance + for region in self.multiworld.get_regions(self.player) + for entrance in region.entrances} + slot_portals: Dict[str, str] = slot_data["Entrance Rando"] + for portal1, portal2 in slot_portals.items(): + portal_name1: str = "" + portal_name2: str = "" + entrance1 = None + entrance2 = None + for portal in portal_mapping: + if portal.scene_destination() == portal1: + portal_name1 = portal.name + if portal.scene_destination() == portal2: + portal_name2 = portal.name + + for entrance_name, entrance in entrance_dict.items(): + if entrance_name.startswith(portal_name1): + entrance1 = entrance + if entrance_name.startswith(portal_name2): + entrance2 = entrance + if entrance1 is None: + raise Exception("entrance1 not found, portal1 is " + portal1) + if entrance2 is None: + raise Exception("entrance2 not found, portal2 is " + portal2) + entrance1.connected_region = entrance2.parent_region + entrance2.connected_region = entrance1.parent_region diff --git a/worlds/tunic/docs/en_Tunic.md b/worlds/tunic/docs/en_Tunic.md new file mode 100644 index 000000000000..82569195caa8 --- /dev/null +++ b/worlds/tunic/docs/en_Tunic.md @@ -0,0 +1,59 @@ +# TUNIC + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics, +and other unique items are shuffled.
+ +Ability shuffling is an option available from the settings page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), +preventing them from being used until they are unlocked.
+ +Enemy randomization and other options are also available and can be turned on in the client mod. + +## What is the goal of TUNIC when randomized? +The standard goal is the same as the vanilla game, which is to find the three hexagon keys, at which point you may either Take Your +Rightful Place or seek another path and Share Your Wisdom. + +Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal +being to find the required amount of them and then Share Your Wisdom. + +## What items from TUNIC can appear in another player's world? +Every item has a chance to appear in another player's world. + +## How many checks are in TUNIC? +There are 302 checks located across the world of TUNIC. + +## What do items from other worlds look like in TUNIC? +Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a +chest with the original chest texture for that item. + +Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with +a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression. + +## Is there a tracker pack? +There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty. + +There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon. + +## What should I know regarding logic? +- Nighttime is not considered in logic. Every check in the game is obtainable during the day. +- The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. +- The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. + +For Entrance Rando specifically: +- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. +- The West Garden fuse can be activated from below. +- You can pray at the tree at the exterior of the Library. +- The elevators in the Rooted Ziggurat only go down. +- The portal in the trophy room of the Old House is active from the start. +- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing. + +## What item groups are there? +Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, ice rod, and progressive sword. + +## What location groups are there? +Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group. diff --git a/worlds/tunic/docs/setup_en.md b/worlds/tunic/docs/setup_en.md new file mode 100644 index 000000000000..087509240f31 --- /dev/null +++ b/worlds/tunic/docs/setup_en.md @@ -0,0 +1,67 @@ +# TUNIC Setup Guide + +## Installation + +### Required Software + +- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported) +- [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip) +- [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest) + +### Optional Software +- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest) (For use with EmoTracker/PopTracker) +- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest) + +### Find Your Relevant Game Directories + +Find your TUNIC game installation directory: + +- **Steam**: Right click TUNIC in your Steam Library, then *Manage → Browse local files*.
+ - **Steam Deck**: Hold down the power button, tap "Switch to Desktop", then launch Steam from Desktop Mode to access the above option. +- **PC Game Pass**: In the Xbox PC app, go to the TUNIC game page from your library, click the [...] button next to "Play", then +*Manage → Files → Browse...*
+- **Other platforms**: Follow a similar pattern of steps as above to locate your specific game directory. + +### Install BepInEx + +BepInEx is a general purpose framework for modding Unity games, and is used by the TUNIC Randomizer. + +Download [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip). + +If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html). + +Extract the contents of the BepInEx .zip file into your TUNIC game directory:
+- **Steam**: Steam\steamapps\common\TUNIC
+- **PC Game Pass**: XboxGames\Tunic\Content
+- **Other platforms**: Place into the same folder that the Tunic_Data/Secret Legend_Data folder is found. + +Launch the game once and close it to finish the BepInEx installation. + +### Install The TUNIC Randomizer Archipelago Client Mod + +Download the latest release of the [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest). + +The downloaded .zip will contain a folder called `Tunic Archipelago`. + +Copy the `Tunic Archipelago` folder into `BepInEx/plugins` in your TUNIC game installation directory. +The filepath to the mod should look like `BepInEx/plugins/Tunic Archipelago/TunicArchipelago.dll`
+ +Launch the game, and if everything was installed correctly you should see `Randomizer + Archipelago Mod Ver. x.y.z` in the top left corner of the title screen! + +## Configure Archipelago Settings + +### Configure Your YAML File + +Visit the [TUNIC settings page](/games/Tunic/player-settings) to generate a YAML with your selected settings. + +### Configure Your Mod Settings +Launch the game and click the button labeled `Open Settings File` on the Title Screen. +This will open the settings file in your default text editor, allowing you to edit your connection info. +At the top of the file, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room. +The rest of the settings that appear in this file can be changed in the `Randomizer Settings` submenu of the in-game options menu. + +Once your player settings have been saved, press `Connect`. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`. + +An error message will display if the game fails to connect to the server. + +Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization! diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py new file mode 100644 index 000000000000..2d3bcc025f4b --- /dev/null +++ b/worlds/tunic/er_data.py @@ -0,0 +1,1001 @@ +from typing import Dict, NamedTuple, List, Tuple +from enum import IntEnum + + +class Portal(NamedTuple): + name: str # human-readable name + region: str # AP region + destination: str # vanilla destination scene and tag + + def scene(self) -> str: # the actual scene name in Tunic + return tunic_er_regions[self.region].game_scene + + def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod + return self.scene() + ", " + self.destination + + +portal_mapping: List[Portal] = [ + Portal(name="Stick House Entrance", region="Overworld", + destination="Sword Cave_"), + Portal(name="Windmill Entrance", region="Overworld", + destination="Windmill_"), + Portal(name="Well Ladder Entrance", region="Overworld", + destination="Sewer_entrance"), + Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail", + destination="Sewer_west_aqueduct"), + Portal(name="Old House Door Entrance", region="Overworld Old House Door", + destination="Overworld Interiors_house"), + Portal(name="Old House Waterfall Entrance", region="Overworld", + destination="Overworld Interiors_under_checkpoint"), + Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail", + destination="Furnace_gyro_upper_north"), + Portal(name="Entrance to Furnace under Windmill", region="Overworld", + destination="Furnace_gyro_upper_east"), + Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace", + destination="Furnace_gyro_west"), + Portal(name="Entrance to Furnace from Beach", region="Overworld", + destination="Furnace_gyro_lower"), + Portal(name="Caustic Light Cave Entrance", region="Overworld", + destination="Overworld Cave_"), + Portal(name="Swamp Upper Entrance", region="Overworld Laurels", + destination="Swamp Redux 2_wall"), + Portal(name="Swamp Lower Entrance", region="Overworld", + destination="Swamp Redux 2_conduit"), + Portal(name="Ruined Passage Not-Door Entrance", region="Overworld", + destination="Ruins Passage_east"), + Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door", + destination="Ruins Passage_west"), + Portal(name="Atoll Upper Entrance", region="Overworld", + destination="Atoll Redux_upper"), + Portal(name="Atoll Lower Entrance", region="Overworld", + destination="Atoll Redux_lower"), + Portal(name="Special Shop Entrance", region="Overworld Laurels", + destination="ShopSpecial_"), + Portal(name="Maze Cave Entrance", region="Overworld", + destination="Maze Room_"), + Portal(name="West Garden Entrance near Belltower", region="Overworld Belltower", + destination="Archipelagos Redux_upper"), + Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", + destination="Archipelagos Redux_lower"), + Portal(name="West Garden Laurels Entrance", region="Overworld Laurels", + destination="Archipelagos Redux_lowest"), + Portal(name="Temple Door Entrance", region="Overworld Temple Door", + destination="Temple_main"), + Portal(name="Temple Rafters Entrance", region="Overworld", + destination="Temple_rafters"), + Portal(name="Ruined Shop Entrance", region="Overworld", + destination="Ruined Shop_"), + Portal(name="Patrol Cave Entrance", region="Overworld", + destination="PatrolCave_"), + Portal(name="Hourglass Cave Entrance", region="Overworld", + destination="Town Basement_beach"), + Portal(name="Changing Room Entrance", region="Overworld", + destination="Changing Room_"), + Portal(name="Cube Cave Entrance", region="Overworld", + destination="CubeRoom_"), + Portal(name="Stairs from Overworld to Mountain", region="Overworld", + destination="Mountain_"), + Portal(name="Overworld to Fortress", region="Overworld", + destination="Fortress Courtyard_"), + Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door", + destination="Town_FiligreeRoom_"), + Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door", + destination="EastFiligreeCache_"), + Portal(name="Overworld to Quarry Connector", region="Overworld", + destination="Darkwoods Tunnel_"), + Portal(name="Dark Tomb Main Entrance", region="Overworld", + destination="Crypt Redux_"), + Portal(name="Overworld to Forest Belltower", region="Overworld", + destination="Forest Belltower_"), + Portal(name="Town to Far Shore", region="Overworld Town Portal", + destination="Transit_teleporter_town"), + Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal", + destination="Transit_teleporter_starting island"), + Portal(name="Secret Gathering Place Entrance", region="Overworld", + destination="Waterfall_"), + + Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place", + destination="Overworld Redux_"), + + Portal(name="Windmill Exit", region="Windmill", + destination="Overworld Redux_"), + Portal(name="Windmill Shop", region="Windmill", + destination="Shop_"), + + Portal(name="Old House Door Exit", region="Old House Front", + destination="Overworld Redux_house"), + Portal(name="Old House to Glyph Tower", region="Old House Front", + destination="g_elements_"), + Portal(name="Old House Waterfall Exit", region="Old House Back", + destination="Overworld Redux_under_checkpoint"), + + Portal(name="Glyph Tower Exit", region="Relic Tower", + destination="Overworld Interiors_"), + + Portal(name="Changing Room Exit", region="Changing Room", + destination="Overworld Redux_"), + + Portal(name="Fountain HC Room Exit", region="Fountain Cross Room", + destination="Overworld Redux_"), + + Portal(name="Cube Cave Exit", region="Cube Cave", + destination="Overworld Redux_"), + + Portal(name="Guard Patrol Cave Exit", region="Patrol Cave", + destination="Overworld Redux_"), + + Portal(name="Ruined Shop Exit", region="Ruined Shop", + destination="Overworld Redux_"), + + Portal(name="Furnace Exit towards Well", region="Furnace Fuse", + destination="Overworld Redux_gyro_upper_north"), + Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path", + destination="Crypt Redux_"), + Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path", + destination="Overworld Redux_gyro_west"), + Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_lower"), + Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area", + destination="Overworld Redux_gyro_upper_east"), + + Portal(name="Stick House Exit", region="Stick House", + destination="Overworld Redux_"), + + Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage", + destination="Overworld Redux_east"), + Portal(name="Ruined Passage Door Exit", region="Ruined Passage", + destination="Overworld Redux_west"), + + Portal(name="Southeast HC Room Exit", region="Southeast Cross Room", + destination="Overworld Redux_"), + + Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave", + destination="Overworld Redux_"), + + Portal(name="Maze Cave Exit", region="Maze Cave", + destination="Overworld Redux_"), + + Portal(name="Hourglass Cave Exit", region="Hourglass Cave", + destination="Overworld Redux_beach"), + + Portal(name="Special Shop Exit", region="Special Shop", + destination="Overworld Redux_"), + + Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters", + destination="Overworld Redux_rafters"), + Portal(name="Temple Door Exit", region="Sealed Temple", + destination="Overworld Redux_main"), + + Portal(name="Well Ladder Exit", region="Beneath the Well Front", + destination="Overworld Redux_entrance"), + Portal(name="Well to Well Boss", region="Beneath the Well Back", + destination="Sewer_Boss_"), + Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", + destination="Overworld Redux_west_aqueduct"), + + Portal(name="Well Boss to Well", region="Well Boss", + destination="Sewer_"), + Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", + destination="Crypt Redux_"), + + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", + destination="Overworld Redux_"), + Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", + destination="Furnace_"), + Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", + destination="Sewer_Boss_"), + + Portal(name="West Garden Exit near Hero's Grave", region="West Garden", + destination="Overworld Redux_lower"), + Portal(name="West Garden to Magic Dagger House", region="West Garden", + destination="archipelagos_house_"), + Portal(name="West Garden Exit after Boss", region="West Garden after Boss", + destination="Overworld Redux_upper"), + Portal(name="West Garden Shop", region="West Garden", + destination="Shop_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit", + destination="Overworld Redux_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="West Garden to Far Shore", region="West Garden Portal", + destination="Transit_teleporter_archipelagos_teleporter"), + + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", + destination="Archipelagos Redux_"), + + Portal(name="Atoll Upper Exit", region="Ruined Atoll", + destination="Overworld Redux_upper"), + Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", + destination="Overworld Redux_lower"), + Portal(name="Atoll Shop", region="Ruined Atoll", + destination="Shop_"), + Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", + destination="Transit_teleporter_atoll"), + Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Portal", + destination="Library Exterior_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll", + destination="Frog Stairs_eye"), + Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", + destination="Frog Stairs_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog's Domain Entry", + destination="Atoll Redux_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog's Domain Entry", + destination="Atoll Redux_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog's Domain Entry", + destination="frog cave main_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog's Domain Entry", + destination="frog cave main_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain", + destination="Frog Stairs_Entrance"), + Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", + destination="Frog Stairs_Exit"), + + Portal(name="Library Exterior Tree", region="Library Exterior Tree", + destination="Atoll Redux_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder", + destination="Library Hall_"), + + Portal(name="Library Hall Bookshelf Exit", region="Library Hall", + destination="Library Exterior_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall", + destination="Library Rotunda_"), + + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda", + destination="Library Hall_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda", + destination="Library Lab_"), + + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", + destination="Library Rotunda_"), + Portal(name="Library to Far Shore", region="Library Portal", + destination="Transit_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab", + destination="Library Arena_"), + + Portal(name="Librarian Arena Exit", region="Library Arena", + destination="Library Lab_"), + + Portal(name="Forest to Belltower", region="East Forest", + destination="Forest Belltower_"), + Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", + destination="East Forest Redux Laddercave_lower"), + Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", + destination="East Forest Redux Laddercave_gate"), + Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", + destination="East Forest Redux Laddercave_upper"), + Portal(name="Forest to Far Shore", region="East Forest Portal", + destination="Transit_teleporter_forest teleporter"), + Portal(name="Forest Guard House 2 Lower Entrance", region="East Forest", + destination="East Forest Redux Interior_lower"), + Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", + destination="East Forest Redux Interior_upper"), + Portal(name="Forest Grave Path Lower Entrance", region="East Forest", + destination="Sword Access_lower"), + Portal(name="Forest Grave Path Upper Entrance", region="East Forest", + destination="Sword Access_upper"), + + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", + destination="East Forest Redux_upper"), + Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", + destination="East Forest Redux_lower"), + Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", + destination="East Forest Redux_gate"), + Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", + destination="Forest Boss Room_"), + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Guard House 2 Lower Exit", region="Guard House 2", + destination="East Forest Redux_lower"), + Portal(name="Guard House 2 Upper Exit", region="Guard House 2", + destination="East Forest Redux_upper"), + + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", + destination="East Forest Redux Laddercave_"), + Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", + destination="Forest Belltower_"), + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + destination="Overworld Redux_"), + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room_"), + + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", + destination="Fortress Reliquary_Lower"), + Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", + destination="Fortress Reliquary_Upper"), + Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", + destination="Fortress Main_Big Door"), + Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", + destination="Fortress East_"), + Portal(name="Fortress Courtyard to Beneath the Earth", region="Fortress Exterior near cave", + destination="Fortress Basement_"), + Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", + destination="Forest Belltower_"), + Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", + destination="Overworld Redux_"), + Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", + destination="Shop_"), + + Portal(name="Beneath the Earth to Fortress Interior", region="Beneath the Vault Back", + destination="Fortress Main_"), + Portal(name="Beneath the Earth to Fortress Courtyard", region="Beneath the Vault Front", + destination="Fortress Courtyard_"), + + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", + destination="Fortress Courtyard_Big Door"), + Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", + destination="Fortress Basement_"), + Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", + destination="Fortress Arena_"), + Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", + destination="Shop_"), + Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", + destination="Fortress East_upper"), + Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", + destination="Fortress East_lower"), + + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", + destination="Fortress Main_lower"), + Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", + destination="Fortress Courtyard_"), + Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", + destination="Fortress Main_upper"), + + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", + destination="Fortress Courtyard_Lower"), + Portal(name="Fortress Hero's Grave", region="Fortress Grave Path", + destination="RelicVoid_teleporter_relic plinth"), + Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", + destination="Fortress Courtyard_Upper"), + Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance", + destination="Dusty_"), + + Portal(name="Dusty Exit", region="Fortress Leaf Piles", + destination="Fortress Reliquary_"), + + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", + destination="Fortress Main_"), + Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", + destination="Transit_teleporter_spidertank"), + + Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", + destination="Mountaintop_"), + Portal(name="Mountain to Quarry", region="Lower Mountain", + destination="Quarry Redux_"), + Portal(name="Mountain to Overworld", region="Lower Mountain", + destination="Overworld Redux_"), + + Portal(name="Top of the Mountain Exit", region="Top of the Mountain", + destination="Mountain_"), + + Portal(name="Quarry Connector to Overworld", region="Quarry Connector", + destination="Overworld Redux_"), + Portal(name="Quarry Connector to Quarry", region="Quarry Connector", + destination="Quarry Redux_"), + + Portal(name="Quarry to Overworld Exit", region="Quarry Entry", + destination="Darkwoods Tunnel_"), + Portal(name="Quarry Shop", region="Quarry Entry", + destination="Shop_"), + Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry", + destination="Monastery_front"), + Portal(name="Quarry to Monastery Back", region="Monastery Rope", + destination="Monastery_back"), + Portal(name="Quarry to Mountain", region="Quarry Back", + destination="Mountain_"), + Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door", + destination="ziggurat2020_0_"), + Portal(name="Quarry to Far Shore", region="Quarry Portal", + destination="Transit_teleporter_quarry teleporter"), + + Portal(name="Monastery Rear Exit", region="Monastery Back", + destination="Quarry Redux_back"), + Portal(name="Monastery Front Exit", region="Monastery Front", + destination="Quarry Redux_front"), + Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry", + destination="Quarry Redux_"), + + Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry", + destination="ziggurat2020_0_"), + Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back", + destination="ziggurat2020_2_"), + + Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top", + destination="ziggurat2020_1_"), + Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", + destination="ziggurat2020_3_"), + + Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front", + destination="ziggurat2020_2_"), + Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", + destination="ziggurat2020_FTRoom_"), + + Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", + destination="ziggurat2020_3_"), + Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal", + destination="Transit_teleporter_ziggurat teleporter"), + + Portal(name="Swamp Lower Exit", region="Swamp", + destination="Overworld Redux_conduit"), + Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance", + destination="Cathedral Redux_main"), + Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room", + destination="Cathedral Redux_secret"), + Portal(name="Swamp to Gauntlet", region="Back of Swamp", + destination="Cathedral Arena_"), + Portal(name="Swamp Shop", region="Swamp", + destination="Shop_"), + Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area", + destination="Overworld Redux_wall"), + Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave", + destination="RelicVoid_teleporter_relic plinth"), + + Portal(name="Cathedral Main Exit", region="Cathedral", + destination="Swamp Redux 2_main"), + Portal(name="Cathedral Elevator", region="Cathedral", + destination="Cathedral Arena_"), + Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", + destination="Swamp Redux 2_secret"), + + Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit", + destination="Swamp Redux 2_"), + Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint", + destination="Cathedral Redux_"), + Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint", + destination="Shop_"), + + Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress", + destination="Fortress Reliquary_teleporter_relic plinth"), + Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry", + destination="Monastery_teleporter_relic plinth"), + Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden", + destination="Archipelagos Redux_teleporter_relic plinth"), + Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest", + destination="Sword Access_teleporter_relic plinth"), + Portal(name="Hero's Grave to Library", region="Hero Relic - Library", + destination="Library Hall_teleporter_relic plinth"), + Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp", + destination="Swamp Redux 2_teleporter_relic plinth"), + + Portal(name="Far Shore to West Garden", region="Far Shore to West Garden", + destination="Archipelagos Redux_teleporter_archipelagos_teleporter"), + Portal(name="Far Shore to Library", region="Far Shore to Library", + destination="Library Lab_teleporter_library teleporter"), + Portal(name="Far Shore to Quarry", region="Far Shore to Quarry", + destination="Quarry Redux_teleporter_quarry teleporter"), + Portal(name="Far Shore to East Forest", region="Far Shore to East Forest", + destination="East Forest Redux_teleporter_forest teleporter"), + Portal(name="Far Shore to Fortress", region="Far Shore to Fortress", + destination="Fortress Arena_teleporter_spidertank"), + Portal(name="Far Shore to Atoll", region="Far Shore", + destination="Atoll Redux_teleporter_atoll"), + Portal(name="Far Shore to Ziggurat", region="Far Shore", + destination="ziggurat2020_FTRoom_teleporter_ziggurat teleporter"), + Portal(name="Far Shore to Heir", region="Far Shore", + destination="Spirit Arena_teleporter_spirit arena"), + Portal(name="Far Shore to Town", region="Far Shore", + destination="Overworld Redux_teleporter_town"), + Portal(name="Far Shore to Spawn", region="Far Shore to Spawn", + destination="Overworld Redux_teleporter_starting island"), + + Portal(name="Heir Arena Exit", region="Spirit Arena", + destination="Transit_teleporter_spirit arena"), + + Portal(name="Purgatory Bottom Exit", region="Purgatory", + destination="Purgatory_bottom"), + Portal(name="Purgatory Top Exit", region="Purgatory", + destination="Purgatory_top"), +] + + +class RegionInfo(NamedTuple): + game_scene: str # the name of the scene in the actual game + dead_end: int = 0 # if a region has only one exit + hint: int = 0 # what kind of hint text you should have + + +class DeadEnd(IntEnum): + free = 0 # not a dead end + all_cats = 1 # dead end in every logic category + restricted = 2 # dead end only in restricted + # there's no dead ends that are only in unrestricted + + +class Hint(IntEnum): + none = 0 # big areas, empty hallways, etc. + region = 1 # at least one of the portals must not be a dead end + scene = 2 # multiple regions in the scene, so using region could mean no valid hints + special = 3 # for if there's a weird case of specific regions being viable + + +# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all +tunic_er_regions: Dict[str, RegionInfo] = { + "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld": RegionInfo("Overworld Redux"), + "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest + "Overworld Laurels": RegionInfo("Overworld Redux"), # all spots in Overworld that you need laurels to reach + "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region), + "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway + "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal + "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), + "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal + "Overworld Town Portal": RegionInfo("Overworld Redux"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux"), + "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Windmill": RegionInfo("Windmill"), + "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door + "Old House Front": RegionInfo("Overworld Interiors"), # part with the bedroom + "Relic Tower": RegionInfo("g_elements", dead_end=DeadEnd.all_cats), + "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace + "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder + "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden + "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Ruined Passage": RegionInfo("Ruins Passage", hint=Hint.region), + "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Sealed Temple": RegionInfo("Temple", hint=Hint.scene), + "Sealed Temple Rafters": RegionInfo("Temple", hint=Hint.scene), + "Forest Belltower Upper": RegionInfo("Forest Belltower", hint=Hint.region), + "Forest Belltower Main": RegionInfo("Forest Belltower"), + "Forest Belltower Lower": RegionInfo("Forest Belltower"), + "East Forest": RegionInfo("East Forest Redux"), + "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), + "East Forest Portal": RegionInfo("East Forest Redux"), + "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), + "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), + "Guard House 2": RegionInfo("East Forest Redux Interior"), + "Forest Boss Room": RegionInfo("Forest Boss Room"), + "Forest Grave Path Main": RegionInfo("Sword Access"), + "Forest Grave Path Upper": RegionInfo("Sword Access"), + "Forest Grave Path by Grave": RegionInfo("Sword Access"), + "Forest Hero's Grave": RegionInfo("Sword Access"), + "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits + "Dark Tomb Main": RegionInfo("Crypt Redux"), + "Dark Tomb Dark Exit": RegionInfo("Crypt Redux"), + "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"), # can laurels backwards + "Well Boss": RegionInfo("Sewer_Boss"), # can walk through (with bombs at least) + "Beneath the Well Front": RegionInfo("Sewer"), + "Beneath the Well Main": RegionInfo("Sewer"), + "Beneath the Well Back": RegionInfo("Sewer"), + "West Garden": RegionInfo("Archipelagos Redux"), + "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats, hint=Hint.region), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, hint=Hint.special), + "West Garden Laurels Exit": RegionInfo("Archipelagos Redux"), + "West Garden after Boss": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave": RegionInfo("Archipelagos Redux"), + "Ruined Atoll": RegionInfo("Atoll Redux"), + "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), + "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux"), + "Frog's Domain Entry": RegionInfo("Frog Stairs"), + "Frog's Domain": RegionInfo("frog cave main", hint=Hint.region), + "Frog's Domain Back": RegionInfo("frog cave main", hint=Hint.scene), + "Library Exterior Tree": RegionInfo("Library Exterior"), + "Library Exterior Ladder": RegionInfo("Library Exterior"), + "Library Hall": RegionInfo("Library Hall"), + "Library Hero's Grave": RegionInfo("Library Hall"), + "Library Rotunda": RegionInfo("Library Rotunda"), + "Library Lab": RegionInfo("Library Lab"), + "Library Lab Lower": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab"), + "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), + "Fortress Exterior from Overworld": RegionInfo("Fortress Courtyard"), + "Fortress Exterior near cave": RegionInfo("Fortress Courtyard"), # where the shop and beneath the earth entry are + "Fortress Courtyard": RegionInfo("Fortress Courtyard"), + "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"), + "Beneath the Vault Front": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla entry point + "Beneath the Vault Back": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla exit point + "Eastern Vault Fortress": RegionInfo("Fortress Main"), + "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"), + "Fortress East Shortcut Upper": RegionInfo("Fortress East"), + "Fortress East Shortcut Lower": RegionInfo("Fortress East"), + "Fortress Grave Path": RegionInfo("Fortress Reliquary"), + "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted, hint=Hint.region), + "Fortress Grave Path Dusty Entrance": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave": RegionInfo("Fortress Reliquary"), + "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Arena": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Lower Mountain": RegionInfo("Mountain"), + "Lower Mountain Stairs": RegionInfo("Mountain"), + "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Quarry Connector": RegionInfo("Darkwoods Tunnel"), + "Quarry Entry": RegionInfo("Quarry Redux"), + "Quarry": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Back": RegionInfo("Quarry Redux"), + "Quarry Monastery Entry": RegionInfo("Quarry Redux"), + "Monastery Front": RegionInfo("Monastery"), + "Monastery Back": RegionInfo("Monastery"), + "Monastery Hero's Grave": RegionInfo("Monastery"), + "Monastery Rope": RegionInfo("Quarry Redux"), + "Lower Quarry": RegionInfo("Quarry Redux"), + "Lower Quarry Zig Door": RegionInfo("Quarry Redux"), + "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), + "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), + "Rooted Ziggurat Upper Front": RegionInfo("ziggurat2020_1"), + "Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator + "Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"), + "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), + "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side + "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side + "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side + "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), + "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), + "Swamp": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), + "Swamp to Cathedral Main Entrance": RegionInfo("Swamp Redux 2"), + "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance + "Swamp Hero's Grave": RegionInfo("Swamp Redux 2"), + "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse + "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), + "Cathedral Gauntlet Exit": RegionInfo("Cathedral Arena"), + "Far Shore": RegionInfo("Transit"), + "Far Shore to Spawn": RegionInfo("Transit"), + "Far Shore to East Forest": RegionInfo("Transit"), + "Far Shore to Quarry": RegionInfo("Transit"), + "Far Shore to Fortress": RegionInfo("Transit"), + "Far Shore to Library": RegionInfo("Transit"), + "Far Shore to West Garden": RegionInfo("Transit"), + "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Purgatory": RegionInfo("Purgatory"), + "Shop Entrance 1": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 2": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 3": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 4": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 5": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop Entrance 6": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), + "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats) +} + + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways: Dict[str, str] = { + "Overworld Redux, Furnace_gyro_west": "Overworld Redux, Archipelagos Redux_lower", + "Overworld Redux, Furnace_gyro_upper_north": "Overworld Redux, Sewer_west_aqueduct", + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper: Dict[str, str] = {} +for p1, p2 in hallways.items(): + hallway_helper[p1] = p2 + hallway_helper[p2] = p1 + +# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints +hallways_nmg: Dict[str, str] = { + "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", + "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", + "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", + "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", + "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", + "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", + "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", + "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", +} +hallway_helper_nmg: Dict[str, str] = {} +for p1, p2 in hallways.items(): + hallway_helper[p1] = p2 + hallway_helper[p2] = p1 + + +# the key is the region you have, the value is the regions you get for having that region +# this is mostly so we don't have to do something overly complex to get this information +dependent_regions: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + ("Old House Front",): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Well Boss",): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"], + ("West Garden Portal", "West Garden Portal Item"): ["West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Courtyard Upper",): + ["Fortress Courtyard Upper", "Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper",): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress",): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front",): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Monastery Back", "Monastery Hero's Grave"): + ["Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Lower Quarry Zig Door"], + ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", + "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave", "Swamp", + "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} + + +dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { + # can use ladder storage to get to the well rail + ("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door"): + ["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Well to Furnace Rail"], + # can laurels through the gate + ("Old House Front", "Old House Back"): + ["Old House Front", "Old House Back"], + ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): + ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], + ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], + ("Forest Belltower Upper",): + ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], + ("Forest Belltower Main",): + ["Forest Belltower Main", "Forest Belltower Lower"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + # can use laurels, ice grapple, or ladder storage to traverse + ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): + ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Dark Tomb Checkpoint", "Well Boss"): + ["Dark Tomb Checkpoint", "Well Boss"], + # can ice grapple from portal area to the rest, and vice versa + ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"): + ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + "West Garden Portal", "West Garden Portal Item"], + ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"): + ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal"], + ("Frog's Domain",): + ["Frog's Domain", "Frog's Domain Back"], + ("Library Exterior Ladder", "Library Exterior Tree"): + ["Library Exterior Ladder", "Library Exterior Tree"], + ("Library Hall", "Library Hero's Grave"): + ["Library Hall", "Library Hero's Grave"], + ("Library Lab", "Library Lab Lower", "Library Portal"): + ["Library Lab", "Library Lab Lower", "Library Portal"], + # can use ice grapple or ladder storage to get from any ladder to upper + ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], + ("Beneath the Vault Front", "Beneath the Vault Back"): + ["Beneath the Vault Front", "Beneath the Vault Back"], + # can ice grapple up + ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): + ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], + ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): + ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + # can ice grapple down + ("Fortress Grave Path Upper",): + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", + "Fortress Hero's Grave"], + ("Fortress Arena", "Fortress Arena Portal"): + ["Fortress Arena", "Fortress Arena Portal"], + ("Lower Mountain", "Lower Mountain Stairs"): + ["Lower Mountain", "Lower Mountain Stairs"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + # can use ladder storage at any of the Quarry ladders to get to Monastery Rope + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope"): + ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Monastery Rope", "Lower Quarry Zig Door"], + ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): + ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], + ("Rooted Ziggurat Middle Top",): + ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], + ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): + ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], + ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): + ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], + ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"): + ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", + "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Cathedral Gauntlet Checkpoint",): + ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): + ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", + "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] +} diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py new file mode 100644 index 000000000000..5d88022dc159 --- /dev/null +++ b/worlds/tunic/er_rules.py @@ -0,0 +1,984 @@ +from typing import Dict, TYPE_CHECKING +from worlds.generic.Rules import set_rule, forbid_item +from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage +from .er_data import Portal +from BaseClasses import Region + +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +ice_rod = "Pages 52-53 (Ice Rod)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], + portal_pairs: Dict[Portal, Portal]) -> None: + player = world.player + options = world.options + + regions["Menu"].connect( + connecting_region=regions["Overworld"]) + + # Overworld + regions["Overworld"].connect( + connecting_region=regions["Overworld Holy Cross"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Belltower"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Belltower"].connect( + connecting_region=regions["Overworld"]) + + # nmg: can laurels through the ruined passage door + regions["Overworld"].connect( + connecting_region=regions["Overworld Ruined Passage Door"], + rule=lambda state: state.has(key, player, 2) + or (state.has(laurels, player) and options.logic_rules)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Laurels"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld Laurels"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player)) + + # nmg: can ice grapple through the door + regions["Overworld"].connect( + connecting_region=regions["Overworld Old House Door"], + rule=lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # not including ice grapple through this because it's very tedious to get an enemy here + regions["Overworld"].connect( + connecting_region=regions["Overworld Southeast Cross Door"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Overworld Southeast Cross Door"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # not including ice grapple through this because we're not including it on the other door + regions["Overworld"].connect( + connecting_region=regions["Overworld Fountain Cross Door"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Overworld Fountain Cross Door"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Town Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Overworld Town Portal"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Spawn Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Overworld Spawn Portal"].connect( + connecting_region=regions["Overworld"]) + + # nmg: ice grapple through temple door + regions["Overworld"].connect( + connecting_region=regions["Overworld Temple Door"], + name="Overworld Temple Door", + rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # Overworld side areas + regions["Old House Front"].connect( + connecting_region=regions["Old House Back"]) + # nmg: laurels through the gate + regions["Old House Back"].connect( + connecting_region=regions["Old House Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Sealed Temple"].connect( + connecting_region=regions["Sealed Temple Rafters"]) + regions["Sealed Temple Rafters"].connect( + connecting_region=regions["Sealed Temple"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Walking Path"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Walking Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Furnace Fuse"].connect( + connecting_region=regions["Furnace Ladder Area"], + rule=lambda state: state.has(laurels, player)) + regions["Furnace Ladder Area"].connect( + connecting_region=regions["Furnace Fuse"], + rule=lambda state: state.has(laurels, player)) + + # East Forest + regions["Forest Belltower Upper"].connect( + connecting_region=regions["Forest Belltower Main"]) + + regions["Forest Belltower Main"].connect( + connecting_region=regions["Forest Belltower Lower"]) + + # nmg: ice grapple up to dance fox spot, and vice versa + regions["East Forest"].connect( + connecting_region=regions["East Forest Dance Fox Spot"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["East Forest Dance Fox Spot"].connect( + connecting_region=regions["East Forest"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["East Forest"].connect( + connecting_region=regions["East Forest Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["East Forest Portal"].connect( + connecting_region=regions["East Forest"]) + + regions["Guard House 1 East"].connect( + connecting_region=regions["Guard House 1 West"]) + regions["Guard House 1 West"].connect( + connecting_region=regions["Guard House 1 East"], + rule=lambda state: state.has(laurels, player)) + + # nmg: ice grapple from upper grave path exit to the rest of it + regions["Forest Grave Path Upper"].connect( + connecting_region=regions["Forest Grave Path Main"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path Upper"], + rule=lambda state: state.has(laurels, player)) + + regions["Forest Grave Path Main"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + # nmg: ice grapple or laurels through the gate + regions["Forest Grave Path by Grave"].connect( + connecting_region=regions["Forest Grave Path Main"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + + regions["Forest Grave Path by Grave"].connect( + connecting_region=regions["Forest Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Forest Hero's Grave"].connect( + connecting_region=regions["Forest Grave Path by Grave"]) + + # Beneath the Well and Dark Tomb + regions["Beneath the Well Front"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + regions["Beneath the Well Main"].connect( + connecting_region=regions["Beneath the Well Front"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + + regions["Beneath the Well Back"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + regions["Beneath the Well Main"].connect( + connecting_region=regions["Beneath the Well Back"], + rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + + regions["Well Boss"].connect( + connecting_region=regions["Dark Tomb Checkpoint"]) + # nmg: can laurels through the gate + regions["Dark Tomb Checkpoint"].connect( + connecting_region=regions["Well Boss"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Dark Tomb Entry Point"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Entry Point"], + rule=lambda state: has_lantern(state, player, options)) + + regions["Dark Tomb Main"].connect( + connecting_region=regions["Dark Tomb Dark Exit"], + rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Dark Exit"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_lantern(state, player, options)) + + # West Garden + regions["West Garden Laurels Exit"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden"].connect( + connecting_region=regions["West Garden Laurels Exit"], + rule=lambda state: state.has(laurels, player)) + + # todo: can you wake the boss, then grapple to it, then kill it? + regions["West Garden after Boss"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden"].connect( + connecting_region=regions["West Garden after Boss"], + rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + + regions["West Garden"].connect( + connecting_region=regions["West Garden Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["West Garden Hero's Grave"].connect( + connecting_region=regions["West Garden"]) + + regions["West Garden Portal"].connect( + connecting_region=regions["West Garden Portal Item"], + rule=lambda state: state.has(laurels, player)) + regions["West Garden Portal Item"].connect( + connecting_region=regions["West Garden Portal"], + rule=lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + + # nmg: can ice grapple to and from the item behind the magic dagger house + regions["West Garden Portal Item"].connect( + connecting_region=regions["West Garden"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["West Garden"].connect( + connecting_region=regions["West Garden Portal Item"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # Atoll and Frog's Domain + # nmg: ice grapple the bird below the portal + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Lower Entry Area"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Ruined Atoll Lower Entry Area"].connect( + connecting_region=regions["Ruined Atoll"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Frog Mouth"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Ruined Atoll Frog Mouth"].connect( + connecting_region=regions["Ruined Atoll"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Ruined Atoll Portal"].connect( + connecting_region=regions["Ruined Atoll"]) + + regions["Frog's Domain"].connect( + connecting_region=regions["Frog's Domain Back"], + rule=lambda state: state.has(grapple, player)) + + # Library + regions["Library Exterior Tree"].connect( + connecting_region=regions["Library Exterior Ladder"], + rule=lambda state: state.has(grapple, player) or state.has(laurels, player)) + regions["Library Exterior Ladder"].connect( + connecting_region=regions["Library Exterior Tree"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + and (state.has(grapple, player) or state.has(laurels, player))) + + regions["Library Hall"].connect( + connecting_region=regions["Library Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Hero's Grave"].connect( + connecting_region=regions["Library Hall"]) + + regions["Library Lab Lower"].connect( + connecting_region=regions["Library Lab"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Library Lab"].connect( + connecting_region=regions["Library Lab Lower"], + rule=lambda state: state.has(laurels, player)) + + regions["Library Lab"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab"]) + + # Eastern Vault Fortress + regions["Fortress Exterior from East Forest"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Exterior from East Forest"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player)) + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Exterior near cave"], + rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + + regions["Fortress Courtyard"].connect( + connecting_region=regions["Fortress Exterior from Overworld"], + rule=lambda state: state.has(laurels, player)) + # nmg: can ice grapple an enemy in the courtyard + regions["Fortress Exterior from Overworld"].connect( + connecting_region=regions["Fortress Courtyard"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Courtyard Upper"].connect( + connecting_region=regions["Fortress Courtyard"]) + # nmg: can ice grapple to the upper ledge + regions["Fortress Courtyard"].connect( + connecting_region=regions["Fortress Courtyard Upper"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Courtyard Upper"].connect( + connecting_region=regions["Fortress Exterior from Overworld"]) + + regions["Beneath the Vault Front"].connect( + connecting_region=regions["Beneath the Vault Back"], + rule=lambda state: has_lantern(state, player, options)) + regions["Beneath the Vault Back"].connect( + connecting_region=regions["Beneath the Vault Front"]) + + regions["Fortress East Shortcut Upper"].connect( + connecting_region=regions["Fortress East Shortcut Lower"]) + # nmg: can ice grapple upwards + regions["Fortress East Shortcut Lower"].connect( + connecting_region=regions["Fortress East Shortcut Upper"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # nmg: ice grapple through the big gold door, can do it both ways + regions["Eastern Vault Fortress"].connect( + connecting_region=regions["Eastern Vault Fortress Gold Door"], + name="Fortress Gold Door", + rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", + "Activate Eastern Vault East Fuse"}, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Eastern Vault Fortress Gold Door"].connect( + connecting_region=regions["Eastern Vault Fortress"], + name="Fortress Gold Door", + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance"], + rule=lambda state: state.has(laurels, player)) + regions["Fortress Grave Path Dusty Entrance"].connect( + connecting_region=regions["Fortress Grave Path"], + rule=lambda state: state.has(laurels, player)) + + regions["Fortress Grave Path"].connect( + connecting_region=regions["Fortress Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Fortress Hero's Grave"].connect( + connecting_region=regions["Fortress Grave Path"]) + + # nmg: ice grapple from upper grave path to lower + regions["Fortress Grave Path Upper"].connect( + connecting_region=regions["Fortress Grave Path"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Fortress Arena"].connect( + connecting_region=regions["Fortress Arena Portal"], + name="Fortress Arena to Fortress Portal", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Fortress Arena Portal"].connect( + connecting_region=regions["Fortress Arena"]) + + # Quarry + regions["Lower Mountain"].connect( + connecting_region=regions["Lower Mountain Stairs"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Lower Mountain Stairs"].connect( + connecting_region=regions["Lower Mountain"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + regions["Quarry Entry"].connect( + connecting_region=regions["Quarry Portal"], + name="Quarry to Quarry Portal", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Quarry Portal"].connect( + connecting_region=regions["Quarry Entry"]) + + regions["Quarry Entry"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Entry"]) + + regions["Quarry Back"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Back"]) + + regions["Quarry Monastery Entry"].connect( + connecting_region=regions["Quarry"], + rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) + regions["Quarry"].connect( + connecting_region=regions["Quarry Monastery Entry"]) + + regions["Monastery Rope"].connect( + connecting_region=regions["Quarry Back"]) + + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry"], + rule=lambda state: has_mask(state, player, options)) + + # nmg: bring a scav over, then ice grapple through the door + regions["Lower Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + name="Quarry to Zig Door", + rule=lambda state: state.has("Activate Quarry Fuse", player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask + regions["Quarry"].connect( + connecting_region=regions["Lower Quarry Zig Door"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Monastery Front"].connect( + connecting_region=regions["Monastery Back"]) + # nmg: can laurels through the gate + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Front"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Monastery Back"].connect( + connecting_region=regions["Monastery Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Monastery Hero's Grave"].connect( + connecting_region=regions["Monastery Back"]) + + # Ziggurat + regions["Rooted Ziggurat Upper Entry"].connect( + connecting_region=regions["Rooted Ziggurat Upper Front"]) + + regions["Rooted Ziggurat Upper Front"].connect( + connecting_region=regions["Rooted Ziggurat Upper Back"], + rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + regions["Rooted Ziggurat Upper Back"].connect( + connecting_region=regions["Rooted Ziggurat Upper Front"], + rule=lambda state: state.has(laurels, player)) + + regions["Rooted Ziggurat Middle Top"].connect( + connecting_region=regions["Rooted Ziggurat Middle Bottom"]) + + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"], + rule=lambda state: state.has(laurels, player) + or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))) + # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"], + rule=lambda state: state.has(laurels, player) or can_ladder_storage(state, player, options)) + + regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Rooted Ziggurat Portal Room Entrance"].connect( + connecting_region=regions["Rooted Ziggurat Lower Back"]) + + regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room Exit"], + name="Zig Portal Room Exit", + rule=lambda state: state.has("Activate Ziggurat Fuse", player)) + regions["Rooted Ziggurat Portal Room Exit"].connect( + connecting_region=regions["Rooted Ziggurat Portal"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + + # Swamp and Cathedral + # nmg: ice grapple through cathedral door, can do it both ways + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Main Entrance"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Swamp to Cathedral Main Entrance"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + regions["Swamp"].connect( + connecting_region=regions["Swamp to Cathedral Treasure Room"], + rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + regions["Swamp to Cathedral Treasure Room"].connect( + connecting_region=regions["Swamp"]) + + regions["Back of Swamp"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: state.has(laurels, player)) + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Back of Swamp"], + rule=lambda state: state.has(laurels, player)) + + # nmg: can ice grapple down while you're on the pillars + regions["Back of Swamp Laurels Area"].connect( + connecting_region=regions["Swamp"], + rule=lambda state: state.has(laurels, player) + and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Back of Swamp"].connect( + connecting_region=regions["Swamp Hero's Grave"], + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + regions["Swamp Hero's Grave"].connect( + connecting_region=regions["Back of Swamp"]) + + regions["Cathedral Gauntlet Checkpoint"].connect( + connecting_region=regions["Cathedral Gauntlet"]) + + regions["Cathedral Gauntlet"].connect( + connecting_region=regions["Cathedral Gauntlet Exit"], + rule=lambda state: state.has(laurels, player)) + regions["Cathedral Gauntlet Exit"].connect( + connecting_region=regions["Cathedral Gauntlet"], + rule=lambda state: state.has(laurels, player)) + + # Far Shore + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Spawn"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to Spawn"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to East Forest"], + rule=lambda state: state.has(laurels, player)) + regions["Far Shore to East Forest"].connect( + connecting_region=regions["Far Shore"], + rule=lambda state: state.has(laurels, player)) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to West Garden"], + name="Far Shore to West Garden", + rule=lambda state: state.has("Activate West Garden Fuse", player)) + regions["Far Shore to West Garden"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Quarry"], + name="Far Shore to Quarry", + rule=lambda state: state.has("Activate Quarry Fuse", player)) + regions["Far Shore to Quarry"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Fortress"], + name="Far Shore to Fortress", + rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) + regions["Far Shore to Fortress"].connect( + connecting_region=regions["Far Shore"]) + + regions["Far Shore"].connect( + connecting_region=regions["Far Shore to Library"], + name="Far Shore to Library", + rule=lambda state: state.has("Activate Library Fuse", player)) + regions["Far Shore to Library"].connect( + connecting_region=regions["Far Shore"]) + + # Misc + regions["Shop Entrance 1"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 2"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 3"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 4"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 5"].connect( + connecting_region=regions["Shop"]) + regions["Shop Entrance 6"].connect( + connecting_region=regions["Shop"]) + + regions["Spirit Arena"].connect( + connecting_region=regions["Spirit Arena Victory"], + rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if + world.options.hexagon_quest else + state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player))) + + # connecting the regions portals are in to other portals you can access via ladder storage + # using has_stick instead of can_ladder_storage since it's already checking the logic rules + if options.logic_rules == "unrestricted": + def get_paired_region(portal_sd: str) -> str: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal2.region + if portal2.scene_destination() == portal_sd: + return portal1.region + raise Exception("no matches found in get_paired_region") + + # The upper Swamp entrance + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Swamp Redux 2_wall")], + rule=lambda state: has_stick(state, player)) + # Western Furnace entrance, next to the sign that leads to West Garden + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_west")], + rule=lambda state: has_stick(state, player)) + # Upper West Garden entry, by the belltower + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden entry by the Furnace + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lower")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels entrance, by the beach + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Archipelagos Redux_lowest")], + rule=lambda state: has_stick(state, player)) + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Sewer_west_aqueduct")], + rule=lambda state: has_stick(state, player)) + # Well rail, east side. Need some height from the temple stairs + regions["Overworld"].connect( + regions[get_paired_region("Overworld Redux, Furnace_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + + # Furnace ladder to the fuse entrance + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_upper_north")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to Dark Tomb + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Crypt Redux_")], + rule=lambda state: has_stick(state, player)) + # Furnace ladder to the West Garden connector + regions["Furnace Ladder Area"].connect( + regions[get_paired_region("Furnace, Overworld Redux_gyro_west")], + rule=lambda state: has_stick(state, player)) + + # West Garden exit after Garden Knight + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_upper")], + rule=lambda state: has_stick(state, player)) + # West Garden laurels exit + regions["West Garden"].connect( + regions[get_paired_region("Archipelagos Redux, Overworld Redux_lowest")], + rule=lambda state: has_stick(state, player)) + + # Frog mouth entrance + regions["Ruined Atoll"].connect( + regions[get_paired_region("Atoll Redux, Frog Stairs_mouth")], + rule=lambda state: has_stick(state, player)) + + # Entrance by the dancing fox holy cross spot + regions["East Forest"].connect( + regions[get_paired_region("East Forest Redux, East Forest Redux Laddercave_upper")], + rule=lambda state: has_stick(state, player)) + + # From the west side of guard house 1 to the east side + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, East Forest Redux_gate")], + rule=lambda state: has_stick(state, player)) + regions["Guard House 1 West"].connect( + regions[get_paired_region("East Forest Redux Laddercave, Forest Boss Room_")], + rule=lambda state: has_stick(state, player)) + + # Upper exit from the Forest Grave Path, use ls at the ladder by the gate switch + regions["Forest Grave Path Main"].connect( + regions[get_paired_region("Sword Access, East Forest Redux_upper")], + rule=lambda state: has_stick(state, player)) + + # Fortress exterior shop, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from Overworld"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the east side of the area + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Shop_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior from East Forest"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # same as above, except from the Beneath the Vault entrance ladder + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], + rule=lambda state: has_stick(state, player)) + regions["Fortress Exterior near cave"].connect( + regions[get_paired_region("Fortress Courtyard, Fortress East_")], + rule=lambda state: has_stick(state, player)) + + # ls at the ladder, need to gain a little height to get up the stairs + regions["Lower Mountain"].connect( + regions[get_paired_region("Mountain, Mountaintop_")], + rule=lambda state: has_stick(state, player)) + + # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword + regions["Quarry Monastery Entry"].connect( + regions[get_paired_region("Quarry Redux, Monastery_back")], + rule=lambda state: has_stick(state, player)) + + # Swamp to Gauntlet + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Arena_")], + rule=lambda state: has_stick(state, player)) + # Swamp to Overworld upper + regions["Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_wall")], + rule=lambda state: has_stick(state, player)) + # Ladder by the hero grave + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Overworld Redux_conduit")], + rule=lambda state: has_stick(state, player)) + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Shop_")], + rule=lambda state: has_stick(state, player)) + # Need to put the cathedral HC code mid-flight + regions["Back of Swamp"].connect( + regions[get_paired_region("Swamp Redux 2, Cathedral Redux_secret")], + rule=lambda state: has_stick(state, player) + and has_ability(state, player, holy_cross, options, ability_unlocks)) + + +def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + player = world.player + multiworld = world.multiworld + options = world.options + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + + # Ability Shuffle Exclusive Rules + set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + lambda state: state.has("Activate Furnace Fuse", player)) + set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + lambda state: state.has(fairies, player, 20)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + lambda state: state.has_all({grapple, laurels}, player)) + set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( + state.has_all({grapple, ice_dagger, fire_wand}, player) and + has_ability(state, player, ice_rod, options, ability_unlocks))) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options, + ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + lambda state: state.has(laurels, player)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, player)) + + # Eastern Vault Fortress + set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + lambda state: state.has(vault_key, player)) + + # Beneath the Vault + set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + lambda state: state.has_group("melee weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), + lambda state: has_lantern(state, player, options)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + + # Ziggurat + set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + lambda state: has_sword(state, player) or state.has(fire_wand, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player), + lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)) + + # Bosses + set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + lambda state: state.has(laurels, player)) + # these two swamp checks really want you to kill the big skeleton first + set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + lambda state: has_sword(state, player)) + + # Hero's Grave and Far Shore + set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + lambda state: state.has(laurels, player)) + + # Events + set_rule(multiworld.get_location("Eastern Bell", player), + lambda state: (has_stick(state, player) or state.has(fire_wand, player))) + set_rule(multiworld.get_location("Western Bell", player), + lambda state: (has_stick(state, player) or state.has(fire_wand, player))) + set_rule(multiworld.get_location("Furnace Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Vault Fuse", player), + lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) + set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + lambda state: state.has("Activate Beneath the Vault Fuse", player)) + set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", + "Activate South and West Fortress Exterior Fuses"}, player)) + set_rule(multiworld.get_location("Quarry Connector Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player)) + set_rule(multiworld.get_location("Quarry Fuse", player), + lambda state: state.has("Activate Quarry Connector Fuse", player)) + set_rule(multiworld.get_location("Ziggurat Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Fuse", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py new file mode 100644 index 000000000000..84b97e13daad --- /dev/null +++ b/worlds/tunic/er_scripts.py @@ -0,0 +1,453 @@ +from typing import Dict, List, Set, Tuple, TYPE_CHECKING +from BaseClasses import Region, ItemClassification, Item, Location +from .locations import location_table +from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_nmg, \ + dependent_regions, dependent_regions_nmg, dependent_regions_ur +from .er_rules import set_er_region_rules + +if TYPE_CHECKING: + from . import TunicWorld + + +class TunicERItem(Item): + game: str = "Tunic" + + +class TunicERLocation(Location): + game: str = "Tunic" + + +def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]: + regions: Dict[str, Region] = {} + portal_pairs: Dict[Portal, Portal] = pair_portals(world) + logic_rules = world.options.logic_rules + + # check if a portal leads to a hallway. if it does, update the hint text accordingly + def hint_helper(portal: Portal, hint_string: str = "") -> str: + # start by setting it as the name of the portal, for the case we're not using the hallway helper + if hint_string == "": + hint_string = portal.name + + if logic_rules: + hallways = hallway_helper_nmg + else: + hallways = hallway_helper + + if portal.scene_destination() in hallways: + # if we have a hallway, we want the region rather than the portal name + if hint_string == portal.name: + hint_string = portal.region + # library exterior is two regions, we just want to fix up the name + if hint_string in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_string = "Library Exterior" + + # search through the list for the other end of the hallway + for portala, portalb in portal_pairs.items(): + if portala.scene_destination() == hallways[portal.scene_destination()]: + # if we find that we have a chain of hallways, do recursion + if portalb.scene_destination() in hallways: + hint_region = portalb.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portalb, hint_string) + else: + # if we didn't find a chain, get the portal name for the end of the chain + hint_string = portalb.name + " then " + hint_string + return hint_string + # and then the same thing for the other portal, since we have to check each separately + if portalb.scene_destination() == hallways[portal.scene_destination()]: + if portala.scene_destination() in hallways: + hint_region = portala.region + if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: + hint_region = "Library Exterior" + hint_string = hint_region + " then " + hint_string + hint_string = hint_helper(portala, hint_string) + else: + hint_string = portala.name + " then " + hint_string + return hint_string + return hint_string + + # create our regions, give them hint text if they're in a spot where it makes sense to + for region_name, region_data in tunic_er_regions.items(): + hint_text = "error" + if region_data.hint == 1: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == region_name: + hint_text = hint_helper(portal2) + break + if portal2.region == region_name: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 2: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal2) + break + if portal2.scene() == tunic_er_regions[region_name].game_scene: + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + elif region_data.hint == 3: + # only the west garden portal item for now + if region_name == "West Garden Portal Item": + if world.options.logic_rules: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal2) + break + if portal2.scene() == "Archipelagos Redux": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + for portal1, portal2 in portal_pairs.items(): + if portal1.region == "West Garden Portal": + hint_text = hint_helper(portal2) + break + if portal2.region == "West Garden Portal": + hint_text = hint_helper(portal1) + break + regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) + else: + regions[region_name] = Region(region_name, world.player, world.multiworld) + + set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) + + er_hint_data: Dict[int, str] = {} + for location_name, location_id in world.location_name_to_id.items(): + region = regions[location_table[location_name].er_region] + location = TunicERLocation(world.player, location_name, location_id, region) + region.locations.append(location) + if region.name == region.hint_text: + continue + er_hint_data[location.address] = region.hint_text + + create_randomized_entrances(portal_pairs, regions) + + for region in regions.values(): + world.multiworld.regions.append(region) + + place_event_items(world, regions) + + victory_region = regions["Spirit Arena Victory"] + victory_location = TunicERLocation(world.player, "The Heir", None, victory_region) + victory_location.place_locked_item(TunicERItem("Victory", ItemClassification.progression, None, world.player)) + world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) + victory_region.locations.append(victory_location) + + portals_and_hints = (portal_pairs, er_hint_data) + + return portals_and_hints + + +tunic_events: Dict[str, str] = { + "Eastern Bell": "Forest Belltower Upper", + "Western Bell": "Overworld Belltower", + "Furnace Fuse": "Furnace Fuse", + "South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld", + "Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper", + "Beneath the Vault Fuse": "Beneath the Vault Back", + "Eastern Vault West Fuses": "Eastern Vault Fortress", + "Eastern Vault East Fuse": "Eastern Vault Fortress", + "Quarry Connector Fuse": "Quarry Connector", + "Quarry Fuse": "Quarry", + "Ziggurat Fuse": "Rooted Ziggurat Lower Back", + "West Garden Fuse": "West Garden", + "Library Fuse": "Library Lab", +} + + +def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: + for event_name, region_name in tunic_events.items(): + region = regions[region_name] + location = TunicERLocation(world.player, event_name, None, region) + if event_name.endswith("Bell"): + location.place_locked_item( + TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player)) + else: + location.place_locked_item( + TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player)) + region.locations.append(location) + + +# pairing off portals, starting with dead ends +def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: + # separate the portals into dead ends and non-dead ends + portal_pairs: Dict[Portal, Portal] = {} + dead_ends: List[Portal] = [] + two_plus: List[Portal] = [] + fixed_shop = False + logic_rules = world.options.logic_rules.value + + # create separate lists for dead ends and non-dead ends + if logic_rules: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end == 1: + dead_ends.append(portal) + else: + two_plus.append(portal) + else: + for portal in portal_mapping: + if tunic_er_regions[portal.region].dead_end: + dead_ends.append(portal) + else: + two_plus.append(portal) + + connected_regions: Set[str] = set() + # make better start region stuff when/if implementing random start + start_region = "Overworld" + connected_regions.update(add_dependent_regions(start_region, logic_rules)) + + # need to plando fairy cave, or it could end up laurels locked + # fix this later to be random? probably not? + if world.options.laurels_location == "10_fairies": + portal1 = None + portal2 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Waterfall_": + portal1 = portal + break + for portal in dead_ends: + if portal.scene_destination() == "Waterfall, Overworld Redux_": + portal2 = portal + break + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + dead_ends.remove(portal2) + + if world.options.fixed_shop: + fixed_shop = True + portal1 = None + for portal in two_plus: + if portal.scene_destination() == "Overworld Redux, Windmill_": + portal1 = portal + break + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance 2", destination="Previous Region_") + portal_pairs[portal1] = portal2 + two_plus.remove(portal1) + + # we want to start by making sure every region is accessible + non_dead_end_regions = set() + for region_name, region_info in tunic_er_regions.items(): + if not region_info.dead_end: + non_dead_end_regions.add(region_name) + elif region_info.dead_end == 2 and logic_rules: + non_dead_end_regions.add(region_name) + + world.random.shuffle(two_plus) + check_success = 0 + portal1 = None + portal2 = None + while len(connected_regions) < len(non_dead_end_regions): + # find a portal in an inaccessible region + if check_success == 0: + for portal in two_plus: + if portal.region in connected_regions: + # if there's risk of self-locking, start over + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal1 = portal + two_plus.remove(portal) + check_success = 1 + break + + # then we find a portal in a connected region + if check_success == 1: + for portal in two_plus: + if portal.region not in connected_regions: + # if there's risk of self-locking, shuffle and try again + if gate_before_switch(portal, two_plus): + world.random.shuffle(two_plus) + break + portal2 = portal + two_plus.remove(portal) + check_success = 2 + break + + # once we have both portals, connect them and add the new region(s) to connected_regions + if check_success == 2: + connected_regions.update(add_dependent_regions(portal2.region, logic_rules)) + portal_pairs[portal1] = portal2 + check_success = 0 + world.random.shuffle(two_plus) + + # add 6 shops, connect them to unique scenes + # this is due to a limitation in Tunic -- you wrong warp if there's multiple shops + shop_scenes: Set[str] = set() + shop_count = 6 + + if fixed_shop: + shop_count = 1 + shop_scenes.add("Overworld Redux") + + for i in range(shop_count): + portal1 = None + for portal in two_plus: + if portal.scene() not in shop_scenes: + shop_scenes.add(portal.scene()) + portal1 = portal + two_plus.remove(portal) + break + if portal1 is None: + raise Exception("Too many shops in the pool, or something else went wrong") + portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {i + 1}", destination="Previous Region_") + portal_pairs[portal1] = portal2 + + # connect dead ends to random non-dead ends + # none of the key events are in dead ends, so we don't need to do gate_before_switch + while len(dead_ends) > 0: + portal1 = two_plus.pop() + portal2 = dead_ends.pop() + portal_pairs[portal1] = portal2 + + # then randomly connect the remaining portals to each other + # every region is accessible, so gate_before_switch is not necessary + while len(two_plus) > 1: + portal1 = two_plus.pop() + portal2 = two_plus.pop() + portal_pairs[portal1] = portal2 + + if len(two_plus) == 1: + raise Exception("two plus had an odd number of portals, investigate this") + + for portal1, portal2 in portal_pairs.items(): + world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + + return portal_pairs + + +# loop through our list of paired portals and make two-way connections +def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None: + for portal1, portal2 in portal_pairs.items(): + region1 = regions[portal1.region] + region2 = regions[portal2.region] + region1.connect(region2, f"{portal1.name} -> {portal2.name}") + # prevent the logic from thinking you can get to any shop-connected region from the shop + if portal2.name != "Shop": + region2.connect(region1, f"{portal2.name} -> {portal1.name}") + + +# loop through the static connections, return regions you can reach from this region +def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]: + region_set = set() + if not logic_rules: + regions_to_add = dependent_regions + elif logic_rules == 1: + regions_to_add = dependent_regions_nmg + else: + regions_to_add = dependent_regions_ur + for origin_regions, destination_regions in regions_to_add.items(): + if region_name in origin_regions: + # if you matched something in the first set, you get the regions in its paired set + region_set.update(destination_regions) + return region_set + # if you didn't match anything in the first sets, just gives you the region + region_set = {region_name} + return region_set + + +# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are +# doing this ensures the keys will not be locked behind the event-locked portal +def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: + # the western belltower cannot be locked since you can access it with laurels + # so we only need to make sure the forest belltower isn't locked + if check_portal.scene_destination() == "Overworld Redux, Temple_main": + i = 0 + for portal in two_plus: + if portal.region == "Forest Belltower Upper": + i += 1 + break + if i == 1: + return True + + # fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard + elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_": + i = j = k = 0 + for portal in two_plus: + if portal.region == "Fortress Courtyard Upper": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 2 or j == 2 or k == 5: + return True + + # fortress teleporter needs only the left fuses + elif check_portal.scene_destination() in ["Fortress Arena, Transit_teleporter_spidertank", + "Transit, Fortress Arena_teleporter_spidertank"]: + i = j = k = 0 + for portal in two_plus: + if portal.scene() == "Fortress Courtyard": + i += 1 + if portal.scene() == "Fortress Basement": + j += 1 + if portal.region == "Eastern Vault Fortress": + k += 1 + if i == 8 or j == 2 or k == 5: + return True + + # Cathedral door needs Overworld and the front of Swamp + # Overworld is currently guaranteed, so no need to check it + elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main": + i = 0 + for portal in two_plus: + if portal.region == "Swamp": + i += 1 + if i == 4: + return True + + # Zig portal room exit needs Zig 3 to be accessible to hit the fuse + elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_": + i = 0 + for portal in two_plus: + if portal.scene() == "ziggurat2020_3": + i += 1 + if i == 2: + return True + + # Quarry teleporter needs you to hit the Darkwoods fuse + # Since it's physically in Quarry, we don't need to check for it + elif check_portal.scene_destination() in ["Quarry Redux, Transit_teleporter_quarry teleporter", + "Quarry Redux, ziggurat2020_0_"]: + i = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if i == 2: + return True + + # Same as above, but Quarry isn't guaranteed here + elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter": + i = j = 0 + for portal in two_plus: + if portal.scene() == "Darkwoods Tunnel": + i += 1 + if portal.scene() == "Quarry Redux": + j += 1 + if i == 2 or j == 7: + return True + + # Need Library fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Library Lab": + i += 1 + if i == 3: + return True + + # Need West Garden fuse to use this teleporter + elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter": + i = 0 + for portal in two_plus: + if portal.scene() == "Archipelagos Redux": + i += 1 + if i == 6: + return True + + # false means you're good to place the portal + return False diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py new file mode 100644 index 000000000000..16608620c6e3 --- /dev/null +++ b/worlds/tunic/items.py @@ -0,0 +1,214 @@ +from itertools import groupby +from typing import Dict, List, Set, NamedTuple +from BaseClasses import ItemClassification + + +class TunicItemData(NamedTuple): + classification: ItemClassification + quantity_in_item_pool: int + item_id_offset: int + item_group: str = "" + + +item_base_id = 509342400 + +item_table: Dict[str, TunicItemData] = { + "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "bombs"), + "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "bombs"), + "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "bombs"), + "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "bombs"), + "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "bombs"), + "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "bombs"), + "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "bombs"), + "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "bombs"), + "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "bombs"), + "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "bombs"), + "Lure": TunicItemData(ItemClassification.filler, 4, 10, "consumables"), + "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "consumables"), + "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "consumables"), + "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "consumables"), + "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "money"), + "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "consumables"), + "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "consumables"), + "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "consumables"), + "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "consumables"), + "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "consumables"), + "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "consumables"), + "Fairy": TunicItemData(ItemClassification.progression, 20, 21), + "Stick": TunicItemData(ItemClassification.progression, 1, 22, "weapons"), + "Sword": TunicItemData(ItemClassification.progression, 3, 23, "weapons"), + "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "weapons"), + "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "weapons"), + "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26), + "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27), + "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28), + "Lantern": TunicItemData(ItemClassification.progression, 1, 29), + "Gun": TunicItemData(ItemClassification.useful, 1, 30, "weapons"), + "Shield": TunicItemData(ItemClassification.useful, 1, 31), + "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32), + "Hourglass": TunicItemData(ItemClassification.useful, 1, 33), + "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "keys"), + "Key": TunicItemData(ItemClassification.progression, 2, 35, "keys"), + "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "keys"), + "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37, "potions"), + "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"), + "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39), + "Card Slot": TunicItemData(ItemClassification.useful, 4, 40), + "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "hexagons"), + "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "hexagons"), + "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "hexagons"), + "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "hexagons"), + "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "offerings"), + "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "offerings"), + "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "offerings"), + "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "offerings"), + "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "offerings"), + "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "offerings"), + "Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "hero relics"), + "Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"), + "Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"), + "Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"), + "Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"), + "Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "hero relics"), + "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "cards"), + "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "cards"), + "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "cards"), + "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "cards"), + "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "cards"), + "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "cards"), + "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "cards"), + "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "cards"), + "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "cards"), + "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "cards"), + "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "cards"), + "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "cards"), + "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "cards"), + "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "cards"), + "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "cards"), + "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "cards"), + "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "golden treasures"), + "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "golden treasures"), + "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "golden treasures"), + "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "golden treasures"), + "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "golden treasures"), + "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "golden treasures"), + "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "golden treasures"), + "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "golden treasures"), + "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "golden treasures"), + "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "golden treasures"), + "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "golden treasures"), + "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "golden treasures"), + "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85, "fool"), + "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "money"), + "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "money"), + "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "money"), + "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "money"), + "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "money"), + "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "money"), + "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "money"), + "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "money"), + "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "money"), + "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "money"), + "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "money"), + "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "money"), + "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "money"), + "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "money"), + "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "money"), + "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "money"), + "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "pages"), + "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "pages"), + "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "pages"), + "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "pages"), + "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "pages"), + "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "pages"), + "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "pages"), + "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "pages"), + "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "pages"), + "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "pages"), + "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "pages"), + "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "pages"), + "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "pages"), + "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "pages"), + "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "pages"), + "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "pages"), + "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "pages"), + "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "pages"), + "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "pages"), + "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "pages"), + "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "pages"), + "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "pages"), + "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "pages"), + "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "pages"), + "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "pages"), + "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"), + "Pages 52-53 (Ice Rod)": TunicItemData(ItemClassification.progression, 1, 128, "pages"), + "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"), +} + +fool_tiers: List[List[str]] = [ + [], + ["Money x1", "Money x10", "Money x15", "Money x16"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"], + ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], +] + +slot_data_item_names = [ + "Stick", + "Sword", + "Sword Upgrade", + "Magic Dagger", + "Magic Wand", + "Magic Orb", + "Hero's Laurels", + "Lantern", + "Gun", + "Scavenger Mask", + "Shield", + "Dath Stone", + "Hourglass", + "Old House Key", + "Fortress Vault Key", + "Hero Relic - ATT", + "Hero Relic - DEF", + "Hero Relic - POTION", + "Hero Relic - HP", + "Hero Relic - SP", + "Hero Relic - MP", + "Pages 24-25 (Prayer)", + "Pages 42-43 (Holy Cross)", + "Pages 52-53 (Ice Rod)", + "Red Questagon", + "Green Questagon", + "Blue Questagon", + "Gold Questagon", +] + +item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} + +filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler] + + +def get_item_group(item_name: str) -> str: + return item_table[item_name].item_group + + +item_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != "" +} + +# extra groups for the purpose of aliasing items +extra_groups: Dict[str, Set[str]] = { + "laurels": {"Hero's Laurels"}, + "orb": {"Magic Orb"}, + "dagger": {"Magic Dagger"}, + "magic rod": {"Magic Wand"}, + "holy cross": {"Pages 42-43 (Holy Cross)"}, + "prayer": {"Pages 24-25 (Prayer)"}, + "ice rod": {"Pages 52-53 (Ice Rod)"}, + "melee weapons": {"Stick", "Sword", "Sword Upgrade"}, + "progressive sword": {"Sword Upgrade"}, + "abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Ice Rod)"}, + "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"} +} + +item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py new file mode 100644 index 000000000000..1501fb7da24d --- /dev/null +++ b/worlds/tunic/locations.py @@ -0,0 +1,337 @@ +from typing import Dict, NamedTuple, Set +from itertools import groupby + + +class TunicLocationData(NamedTuple): + region: str + er_region: str # entrance rando region + location_group: str = "region" + + +location_base_id = 509342400 + +location_table: Dict[str, TunicLocationData] = { + "Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Third Room] Tentacle Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Entryway] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Save Room] Upper Floor Chest 1": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Save Room] Upper Floor Chest 2": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Second Room] Underwater Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"), + "Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Beneath the Well", "Dark Tomb Checkpoint"), + "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"), + "Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"), + "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), + "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", "holy cross"), + "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "East Forest", "holy cross"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), + "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), + "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), + "East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"), + "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"), + "East Forest - Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"), + "East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"), + "Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"), + "Forest Belltower - After Guard Captain": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"), + "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"), + "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", "holy cross"), + "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"), + "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"), + "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"), + "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"), + "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"), + "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"), + "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"), + "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", "holy cross"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"), + "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), + "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), + "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), + "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"), + "Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), + "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"), + "Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena"), + "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", "holy cross"), + "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Behind Chalkboard by Fuse": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"), + "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"), + "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"), + "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"), + "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"), + "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "Fortress Courtyard - Near Fuse": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Below Walkway": TunicLocationData("Overworld", "Fortress Exterior from Overworld"), + "Fortress Courtyard - Page Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), + "West Furnace - Lantern Pickup": TunicLocationData("Overworld", "Furnace Fuse"), + "Maze Cave - Maze Room Chest": TunicLocationData("Overworld", "Maze Cave"), + "Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"), + "Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"), + "Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"), + "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"), + "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"), + "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"), + "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"), + "Shop - Potion 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Potion 2": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 1": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Coin 2": TunicLocationData("Overworld", "Shop", "shop"), + "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"), + "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"), + "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"), + "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"), + "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"), + "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn"), + "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", "well"), + "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), + "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), + "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", "holy cross"), + "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", "holy cross"), + "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), + "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", "holy cross"), + "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", "holy cross"), + "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", "holy cross"), + "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", "holy cross"), + "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", "holy cross"), + "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave", "holy cross"), + "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", "holy cross"), + "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", "holy cross"), + "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", "holy cross"), + "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", "holy cross"), + "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", "holy cross"), + "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), + "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), + "Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), + "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), + "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"), + "Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"), + "Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"), + "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back"), + "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [East] Locked Room Upper Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"), + "Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"), + "Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"), + "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"), + "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"), + "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp"), + "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp"), + "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"), + "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), + "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), + "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"), + "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("West Garden", "West Garden after Boss"), + "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), + "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), + "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"), + "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), + "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), +} + +hexagon_locations: Dict[str, str] = { + "Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup", + "Green Questagon": "Librarian - Hexagon Green", + "Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue", +} + +location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)} + + +def get_loc_group(location_name: str) -> str: + loc_group = location_table[location_name].location_group + if loc_group == "region": + # set loc_group as the region name. Typically, location groups are lowercase + loc_group = location_table[location_name].region.lower() + return loc_group + + +location_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in groupby(sorted(location_table, key=get_loc_group), get_loc_group) +} diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py new file mode 100644 index 000000000000..77fa2cdaf5bc --- /dev/null +++ b/worlds/tunic/options.py @@ -0,0 +1,147 @@ +from dataclasses import dataclass + +from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, PerGameCommonOptions + + +class SwordProgression(DefaultOnToggle): + """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new + swords with increased range and attack power.""" + internal_name = "sword_progression" + display_name = "Sword Progression" + + +class StartWithSword(Toggle): + """Start with a sword in the player's inventory. Does not count towards Sword Progression.""" + internal_name = "start_with_sword" + display_name = "Start With Sword" + + +class KeysBehindBosses(Toggle): + """Places the three hexagon keys behind their respective boss fight in your world.""" + internal_name = "keys_behind_bosses" + display_name = "Keys Behind Bosses" + + +class AbilityShuffling(Toggle): + """Locks the usage of Prayer, Holy Cross*, and Ice Rod until the relevant pages of the manual have been found. + If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required + Hexagon goal amount. + *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other + player-facing codes. + """ + internal_name = "ability_shuffling" + display_name = "Ability Shuffling" + + +class LogicRules(Choice): + """Set which logic rules to use for your world. + Restricted: Standard logic, no glitches. + No Major Glitches: Ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. + Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. + *Special Shop is not in logic without the Hero's Laurels in Unrestricted due to soft lock potential. + *Using Ladder Storage to get to individual chests is not in logic to avoid tedium. + *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in + Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.""" + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + option_unrestricted = 2 + default = 0 + + +class Lanternless(Toggle): + """Choose whether you require the Lantern for dark areas. + When enabled, the Lantern is marked as Useful instead of Progression.""" + internal_name = "lanternless" + display_name = "Lanternless" + + +class Maskless(Toggle): + """Choose whether you require the Scavenger's Mask for Lower Quarry. + When enabled, the Scavenger's Mask is marked as Useful instead of Progression.""" + internal_name = "maskless" + display_name = "Maskless" + + +class FoolTraps(Choice): + """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative + effects to the player.""" + internal_name = "fool_traps" + display_name = "Fool Traps" + option_off = 0 + option_normal = 1 + option_double = 2 + option_onslaught = 3 + default = 1 + + +class HexagonQuest(Toggle): + """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed + after collecting the required number of them.""" + internal_name = "hexagon_quest" + display_name = "Hexagon Quest" + + +class HexagonGoal(Range): + """How many Gold Questagons are required to complete the game on Hexagon Quest.""" + internal_name = "hexagon_goal" + display_name = "Gold Hexagons Required" + range_start = 15 + range_end = 50 + default = 20 + + +class ExtraHexagonPercentage(Range): + """How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.""" + internal_name = "extra_hexagon_percentage" + display_name = "Percentage of Extra Gold Hexagons" + range_start = 0 + range_end = 100 + default = 50 + + +class EntranceRando(Toggle): + """Randomize the connections between scenes. + A small, very lost fox on a big adventure.""" + internal_name = "entrance_rando" + display_name = "Entrance Rando" + + +class FixedShop(Toggle): + """Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool. + Has no effect if Entrance Rando is not enabled.""" + internal_name = "fixed_shop" + display_name = "ER Fixed Shop" + + +class LaurelsLocation(Choice): + """Force the Hero's Laurels to be placed at a location in your world. + For if you want to avoid or specify early or late Laurels. + If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance.""" + internal_name = "laurels_location" + display_name = "Laurels Location" + option_anywhere = 0 + option_6_coins = 1 + option_10_coins = 2 + option_10_fairies = 3 + default = 0 + + +@dataclass +class TunicOptions(PerGameCommonOptions): + sword_progression: SwordProgression + start_with_sword: StartWithSword + keys_behind_bosses: KeysBehindBosses + ability_shuffling: AbilityShuffling + logic_rules: LogicRules + entrance_rando: EntranceRando + fixed_shop: FixedShop + fool_traps: FoolTraps + hexagon_quest: HexagonQuest + hexagon_goal: HexagonGoal + extra_hexagon_percentage: ExtraHexagonPercentage + lanternless: Lanternless + maskless: Maskless + laurels_location: LaurelsLocation + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py new file mode 100644 index 000000000000..5d5248f210d6 --- /dev/null +++ b/worlds/tunic/regions.py @@ -0,0 +1,25 @@ +from typing import Dict, Set + +tunic_regions: Dict[str, Set[str]] = { + "Menu": {"Overworld"}, + "Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden", + "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", + "Spirit Arena"}, + "Overworld Holy Cross": set(), + "East Forest": {"Eastern Vault Fortress"}, + "Dark Tomb": {"West Garden"}, + "Beneath the Well": {"Dark Tomb"}, + "West Garden": {"Overworld", "Dark Tomb"}, + "Ruined Atoll": {"Frog's Domain", "Library"}, + "Frog's Domain": set(), + "Library": set(), + "Eastern Vault Fortress": {"Beneath the Vault"}, + "Beneath the Vault": {"Eastern Vault Fortress"}, + "Quarry Back": {"Quarry"}, + "Quarry": {"Lower Quarry", "Rooted Ziggurat"}, + "Lower Quarry": {"Rooted Ziggurat"}, + "Rooted Ziggurat": set(), + "Swamp": {"Cathedral"}, + "Cathedral": set(), + "Spirit Arena": set() +} diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py new file mode 100644 index 000000000000..9906936a469f --- /dev/null +++ b/worlds/tunic/rules.py @@ -0,0 +1,345 @@ +from random import Random +from typing import Dict, TYPE_CHECKING + +from worlds.generic.Rules import set_rule, forbid_item +from BaseClasses import CollectionState +from .options import TunicOptions +if TYPE_CHECKING: + from . import TunicWorld + +laurels = "Hero's Laurels" +grapple = "Magic Orb" +ice_dagger = "Magic Dagger" +fire_wand = "Magic Wand" +lantern = "Lantern" +fairies = "Fairy" +coins = "Golden Coin" +prayer = "Pages 24-25 (Prayer)" +holy_cross = "Pages 42-43 (Holy Cross)" +ice_rod = "Pages 52-53 (Ice Rod)" +key = "Key" +house_key = "Old House Key" +vault_key = "Fortress Vault Key" +mask = "Scavenger Mask" +red_hexagon = "Red Questagon" +green_hexagon = "Green Questagon" +blue_hexagon = "Blue Questagon" +gold_hexagon = "Gold Questagon" + + +def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: + ability_requirement = [1, 1, 1] + if options.hexagon_quest.value: + hexagon_goal = options.hexagon_goal.value + # Set ability unlocks to 25, 50, and 75% of goal amount + ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4] + abilities = [prayer, holy_cross, ice_rod] + random.shuffle(abilities) + return dict(zip(abilities, ability_requirement)) + + +def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, + ability_unlocks: Dict[str, int]) -> bool: + if not options.ability_shuffling: + return True + if options.hexagon_quest: + return state.has(gold_hexagon, player, ability_unlocks[ability]) + return state.has(ability, player) + + +# a check to see if you can whack things in melee at all +def has_stick(state: CollectionState, player: int) -> bool: + return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player) + + +def has_sword(state: CollectionState, player: int) -> bool: + return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) + + +def has_ice_grapple_logic(long_range: bool, state: CollectionState, player: int, options: TunicOptions, + ability_unlocks: Dict[str, int]) -> bool: + if not options.logic_rules: + return False + + if not long_range: + return state.has_all({ice_dagger, grapple}, player) + else: + return state.has_all({ice_dagger, fire_wand, grapple}, player) and \ + has_ability(state, player, ice_rod, options, ability_unlocks) + + +def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.logic_rules == "unrestricted" and has_stick(state, player): + return True + else: + return False + + +def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.maskless: + return True + else: + return state.has(mask, player) + + +def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: + if options.lanternless: + return True + else: + return state.has(lantern, player) + + +def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \ + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ + lambda state: has_stick(state, player) or state.has(fire_wand, player) + multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or can_ladder_storage(state, player, options) + multiworld.get_entrance("Beneath the Well -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("West Garden -> Dark Tomb", player).access_rule = \ + lambda state: has_lantern(state, player, options) + multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ + or can_ladder_storage(state, player, options) + multiworld.get_entrance("East Forest -> Eastern Vault Fortress", player).access_rule = \ + lambda state: state.has(laurels, player) \ + or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ + or can_ladder_storage(state, player, options) + # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules + multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ + lambda state: has_lantern(state, player, options) and \ + has_ability(state, player, prayer, options, ability_unlocks) + multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ + lambda state: state.has_any({grapple, laurels}, player) and \ + has_ability(state, player, prayer, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ + lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ + and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options)) + multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ + lambda state: has_sword(state, player) or state.has(fire_wand, player) + multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ + lambda state: has_mask(state, player, options) + multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ + lambda state: (state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks)) \ + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Quarry -> Rooted Ziggurat", player).access_rule = \ + lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \ + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \ + lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value + else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)) and \ + has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) + + +def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + multiworld = world.multiworld + player = world.player + options = world.options + + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + + # Ability Shuffle Exclusive Rules + set_rule(multiworld.get_location("Far Shore - Page Pickup", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) + or can_ladder_storage(state, player, options) + or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_lantern(state, player, options))) + set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) + or can_ladder_storage(state, player, options) + or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_lantern(state, player, options))) + set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + + # Overworld + set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Old House - Normal Chest", player), + lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and + (state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules))) + set_rule(multiworld.get_location("Old House - Shield Pickup", player), + lambda state: state.has(house_key, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or (state.has(laurels, player) and options.logic_rules)) + set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and has_sword(state, player))) + set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), + lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and + (state.has(laurels, player) + or (has_lantern(state, player, options) and + (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + + set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + lambda state: state.has(fairies, player, 10)) + set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + lambda state: state.has(fairies, player, 20)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + lambda state: state.has(coins, player, 10)) + set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + lambda state: state.has(coins, player, 15)) + + # East Forest + set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + lambda state: state.has(grapple, player)) + set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + lambda state: state.has_all({grapple, laurels}, player)) + set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), + lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) + and has_ability(state, player, ice_rod, options, ability_unlocks)) + + # West Garden + set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + lambda state: state.has(laurels, player) + and has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), + lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + lambda state: has_sword(state, player) or state.has(laurels, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or can_ladder_storage(state, player, options)) + + # Ruined Atoll + set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + lambda state: state.has_any({laurels, key}, player)) + set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + lambda state: has_sword(state, player) or options.logic_rules) + + # Frog's Domain + set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + lambda state: state.has_any({grapple, laurels}, player)) + set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + lambda state: state.has_any({grapple, laurels}, player)) + + # Eastern Vault Fortress + set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + lambda state: has_sword(state, player) and + (has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + lambda state: state.has(vault_key, player) and + (has_ability(state, player, prayer, options, ability_unlocks) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + + # Beneath the Vault + set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) + set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), + lambda state: has_stick(state, player) and has_lantern(state, player, options)) + + # Quarry + set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player), + lambda state: has_mask(state, player, options)) + set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + lambda state: has_sword(state, player)) + + # Swamp + set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + lambda state: state.has(laurels, player) and state.has(fire_wand, player) and has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + lambda state: state.has(laurels, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), + lambda state: has_sword(state, player)) + + # Hero's Grave + set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py new file mode 100644 index 000000000000..f8ab99d67d24 --- /dev/null +++ b/worlds/tunic/test/__init__.py @@ -0,0 +1,6 @@ +from test.bases import WorldTestBase + + +class TunicTestBase(WorldTestBase): + game = "Tunic" + player: int = 1 \ No newline at end of file diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py new file mode 100644 index 000000000000..d74858bd27ef --- /dev/null +++ b/worlds/tunic/test/test_access.py @@ -0,0 +1,70 @@ +from . import TunicTestBase +from .. import options + + +class TestAccess(TunicTestBase): + # test whether you can get into the temple without laurels + def test_temple_access(self): + self.collect_all_but(["Hero's Laurels", "Lantern"]) + self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup")) + self.collect_by_name(["Lantern"]) + self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup")) + + # test that the wells function properly. Since fairies is written the same way, that should succeed too + def test_wells(self): + self.collect_all_but(["Golden Coin"]) + self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins")) + self.collect_by_name(["Golden Coin"]) + self.assertTrue(self.can_reach_location("Coins in the Well - 3 Coins")) + + +class TestStandardShuffle(TunicTestBase): + options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need to get holy cross to open the hc door in overworld + def test_hc_door(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Pages 42-43 (Holy Cross)") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true} + + # test that you need the gold questagons to open the hc door in overworld + def test_hc_door_hex_shuffle(self): + self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup")) + self.collect_by_name("Gold Questagon") + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestHexQuestNoShuffle(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false} + + # test that you can get the item behind the overworld hc door with nothing and no ability shuffle + def test_hc_door_no_shuffle(self): + self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup")) + + +class TestNormalGoal(TunicTestBase): + options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + # test that you need the three colored hexes to reach the Heir in standard + def test_normal_goal(self): + location = ["The Heir"] + items = [["Red Questagon", "Blue Questagon", "Green Questagon"]] + self.assertAccessDependency(location, items) + + +class TestER(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_true, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false} + + def test_overworld_hc_chest(self): + # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld + self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) + self.collect_by_name(["Pages 42-43 (Holy Cross)"]) + self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))