From d02b9b08a7734ad86c4b462b8d47ca2c3d755c12 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sun, 4 Aug 2024 13:03:00 -0400 Subject: [PATCH 01/51] add starting location option + implement in client --- worlds/zork_grand_inquisitor/client.py | 14 +++++++- .../zork_grand_inquisitor/data/item_data.py | 6 ---- worlds/zork_grand_inquisitor/data_funcs.py | 5 +++ worlds/zork_grand_inquisitor/enums.py | 14 +++++++- .../zork_grand_inquisitor/game_controller.py | 36 +++++++++++++++++-- worlds/zork_grand_inquisitor/options.py | 23 ++++++++++++ worlds/zork_grand_inquisitor/world.py | 5 +++ 7 files changed, 93 insertions(+), 10 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 11d6b7f8f183..c86c3bc356ad 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -6,7 +6,15 @@ from typing import Any, Dict, List, Optional, Set, Tuple -from .data_funcs import item_names_to_id, location_names_to_id, id_to_items, id_to_locations, id_to_goals +from .data_funcs import ( + item_names_to_id, + location_names_to_id, + id_to_items, + id_to_locations, + id_to_goals, + id_to_starting_locations, +) + from .enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations from .game_controller import GameController @@ -98,6 +106,10 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["grant_missable_location_checks"] == 1 ) + self.game_controller.option_starting_location = ( + id_to_starting_locations()[_args["slot_data"]["starting_location"]] + ) + async def controller(self): while not self.exit_event.is_set(): await asyncio.sleep(0.1) diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index c312bbce3d09..84ec548a488d 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -653,12 +653,6 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), - ZorkGrandInquisitorItems.SPELL_VOXAM: ZorkGrandInquisitorItemData( - statemap_keys=(191,), - archipelago_id=ITEM_OFFSET + 200 + 7, - classification=ItemClassification.useful, - tags=(ZorkGrandInquisitorTags.SPELL,), - ), # Subway Destinations ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData( statemap_keys=(13757, 13297, 13486, 13625), diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index 2a7bff1fbb6b..1d4178868a88 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -10,6 +10,7 @@ ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations, ZorkGrandInquisitorRegions, + ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorTags, ) @@ -54,6 +55,10 @@ def id_to_locations() -> Dict[int, ZorkGrandInquisitorLocations]: } +def id_to_starting_locations() -> Dict[int, ZorkGrandInquisitorStartingLocations]: + return {starting_location.value: starting_location for starting_location in ZorkGrandInquisitorStartingLocations} + + def item_groups() -> Dict[str, List[str]]: groups: Dict[str, List[str]] = dict() diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index ecbb38a949b4..241675e7a6e8 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -131,7 +131,6 @@ class ZorkGrandInquisitorItems(enum.Enum): SPELL_NARWILE = "Spell: NARWILE" SPELL_REZROV = "Spell: REZROV" SPELL_THROCK = "Spell: THROCK" - SPELL_VOXAM = "Spell: VOXAM" STUDENT_ID = "Student ID" SUBWAY_DESTINATION_FLOOD_CONTROL_DAM = "Subway Destination: Flood Control Dam #3" SUBWAY_DESTINATION_HADES = "Subway Destination: Hades" @@ -336,6 +335,19 @@ class ZorkGrandInquisitorRegions(enum.Enum): WHITE_HOUSE = "White House" +class ZorkGrandInquisitorStartingLocations(enum.Enum): + PORT_FOOZLE = 0 + CROSSROADS = 1 + DM_LAIR = 2 + DM_LAIR_HOUSE = 3 + GUE_TECH = 4 + SPELL_LAB = 5 + HADES_SHORE = 6 + FLOOD_CONTROL_DAM_3 = 7 + MONASTERY_TOTEMIZER = 8 + MONASTERY_EXHIBIT = 9 + + class ZorkGrandInquisitorTags(enum.Enum): CORE = "Core" DEATHSANITY = "Deathsanity" diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 7a60a1460829..bc552a0ac260 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -18,6 +18,7 @@ ZorkGrandInquisitorGoals, ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations, + ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorTags, ) @@ -48,6 +49,7 @@ class GameController: option_goal: Optional[ZorkGrandInquisitorGoals] option_deathsanity: Optional[bool] option_grant_missable_location_checks: Optional[bool] + option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] def __init__(self, logger=None) -> None: self.logger = logger @@ -81,6 +83,7 @@ def __init__(self, logger=None) -> None: self.option_goal = None self.option_deathsanity = None self.option_grant_missable_location_checks = None + self.option_starting_location = None @functools.cached_property def brog_items(self) -> Set[ZorkGrandInquisitorItems]: @@ -193,6 +196,8 @@ def update(self) -> None: try: self.game_state_manager.refresh_game_location() + self._apply_starting_location() + self._apply_permanent_game_state() self._apply_conditional_game_state() @@ -214,6 +219,31 @@ def update(self) -> None: except Exception as e: self.log_debug(e) + def _apply_starting_location(self, force: bool = False) -> None: + if self._read_game_state_value_for(19985) == 0 or force: + if self.option_starting_location == ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: + self.game_state_manager.set_game_location("ps10", 825) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.CROSSROADS: + self.game_state_manager.set_game_location("uc10", 1200) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR: + self.game_state_manager.set_game_location("dg10", 1410) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: + self.game_state_manager.set_game_location("dv10", 1673) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.GUE_TECH: + self.game_state_manager.set_game_location("tr10", 150) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SPELL_LAB: + self.game_state_manager.set_game_location("tp10", 0) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.HADES_SHORE: + self.game_state_manager.set_game_location("hp10", 534) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: + self.game_state_manager.set_game_location("ue10", 1578) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: + self.game_state_manager.set_game_location("mt10", 1483) + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: + self.game_state_manager.set_game_location("me10", 1023) + + self._write_game_state_value_for(19985, 1) + def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(10934, 1) # Rope Taken self._write_game_state_value_for(10418, 1) # Mead Light Taken @@ -265,6 +295,7 @@ def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(13384, 1) # Skip Meanwhile... Cutscene self._write_game_state_value_for(8620, 1) # First Coin Paid to Charon self._write_game_state_value_for(8731, 1) # First Coin Paid to Charon + self._write_game_state_value_for(191, 1) # VOXAM Learned def _apply_conditional_game_state(self): # Can teleport to Dungeon Master's Lair @@ -907,9 +938,10 @@ def _apply_conditional_teleports(self) -> None: if self._player_is_at("ej10"): self.game_state_manager.set_game_location("uc10", 1200) + # VOXAM Cast if self._read_game_state_value_for(9) == 224: self._write_game_state_value_for(9, 0) - self.game_state_manager.set_game_location("uc10", 1200) + self._apply_starting_location(force=True) def _check_for_victory(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS: @@ -951,7 +983,7 @@ def _determine_game_state_inventory(self) -> Set[ZorkGrandInquisitorItems]: # Spells i: int - for i in range(191, 203): + for i in range(192, 203): if self._read_game_state_value_for(i) == 1: if i in self.game_id_to_items: game_state_inventory.add(self.game_id_to_items[i]) diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index f06415199934..8937e54f8af5 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -52,6 +52,28 @@ class GrantMissableLocationChecks(Toggle): display_name: str = "Grant Missable Checks" +class StartingLocation(Choice): + """ + Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to + teleport back to the starting location at any time + """ + + display_name: str = "Starting Location" + + option_port_foozle: int = 0 + option_crossroads: int = 1 + option_dm_lair: int = 2 + option_dm_lair_house: int = 3 + option_gue_tech: int = 4 + option_spell_lab: int = 5 + option_hades_shore: int = 6 + option_flood_control_dam_3: int = 7 + option_monastery_totemizer: int = 8 + option_monastery_exhibit: int = 9 + + default = "random" + + @dataclass class ZorkGrandInquisitorOptions(PerGameCommonOptions): goal: Goal @@ -59,3 +81,4 @@ class ZorkGrandInquisitorOptions(PerGameCommonOptions): start_with_hotspot_items: StartWithHotspotItems deathsanity: Deathsanity grant_missable_location_checks: GrantMissableLocationChecks + starting_location: StartingLocation diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index a93f2c2134c1..c54ba64528e8 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -81,6 +81,10 @@ class ZorkGrandInquisitorWorld(World): filler_item_names: List[str] = item_groups()["Filler"] item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item() + def generate_early(self) -> None: + pass + # Set Starting Location + def create_regions(self) -> None: deathsanity: bool = bool(self.options.deathsanity) @@ -199,6 +203,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "start_with_hotspot_items", "deathsanity", "grant_missable_location_checks", + "starting_location", ) def get_filler_item_name(self) -> str: From 38050e2323ff9a2a61062b5602c39bdd880a50d1 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 1 Nov 2024 17:32:56 -0400 Subject: [PATCH 02/51] define logic helper items for starting locations; grant proper logic helper item to player depending on starting location --- .../zork_grand_inquisitor/data/item_data.py | 63 ++++++++++++++++++- .../data/mapping_data.py | 39 ++++++++++++ worlds/zork_grand_inquisitor/data_funcs.py | 7 +++ worlds/zork_grand_inquisitor/enums.py | 11 ++++ worlds/zork_grand_inquisitor/world.py | 16 ++++- 5 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 worlds/zork_grand_inquisitor/data/mapping_data.py diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index 84ec548a488d..49e043b86d4b 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -1,4 +1,4 @@ -from typing import Dict, NamedTuple, Optional, Tuple, Union +from typing import Dict, NamedTuple, Optional, Tuple from BaseClasses import ItemClassification @@ -783,4 +783,65 @@ class ZorkGrandInquisitorItemData(NamedTuple): tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), + # Logic Helpers - These virtual items are granted to the player conditionally to simplify logic where possible + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_CROSSROADS: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 0, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 1, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 2, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 3, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 4, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 5, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 6, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 7, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 8, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 9, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + ), } diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py new file mode 100644 index 000000000000..26946dce1c0c --- /dev/null +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -0,0 +1,39 @@ +from typing import Dict + +from ..enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorStartingLocations + + +starting_location_to_logic_helper_item: Dict[ + ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorItems +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE + ), + ZorkGrandInquisitorStartingLocations.CROSSROADS: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_CROSSROADS + ), + ZorkGrandInquisitorStartingLocations.DM_LAIR: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR + ), + ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE + ), + ZorkGrandInquisitorStartingLocations.GUE_TECH: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH + ), + ZorkGrandInquisitorStartingLocations.SPELL_LAB: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB + ), + ZorkGrandInquisitorStartingLocations.HADES_SHORE: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE + ), + ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3 + ), + ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER + ), + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT + ), +} diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index 1d4178868a88..6fa35a58b426 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -3,6 +3,7 @@ from .data.entrance_rule_data import entrance_rule_data from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData +from .data.mapping_data import starting_location_to_logic_item from .enums import ( ZorkGrandInquisitorEvents, @@ -143,6 +144,12 @@ def locations_with_tag(tag: ZorkGrandInquisitorTags) -> Set[ZorkGrandInquisitorL return {location for location, data in location_data.items() if data.tags is not None and tag in data.tags} +def starting_location_to_logic_helper_item( + starting_location: ZorkGrandInquisitorStartingLocations, +) -> ZorkGrandInquisitorItems: + return starting_location_to_logic_item[starting_location] + + def location_access_rule_for(location: ZorkGrandInquisitorLocations, player: int) -> str: data: ZorkGrandInquisitorLocationData = location_data[location] diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 241675e7a6e8..0422592ca170 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -105,6 +105,16 @@ class ZorkGrandInquisitorItems(enum.Enum): JAR_OF_HOTBUGS = "Jar of Hotbugs" LANTERN = "Lantern" LARGE_TELEGRAPH_HAMMER = "Large Telegraph Hammer" + LOGIC_HELPER_STARTING_LOCATION_CROSSROADS = "Starting Location: Crossroads" + LOGIC_HELPER_STARTING_LOCATION_DM_LAIR = "Starting Location: Dungeon Master's Lair" + LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE = "Starting Location: Dungeon Master's House" + LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3 = "Starting Location: Flood Control Dam #3" + LOGIC_HELPER_STARTING_LOCATION_GUE_TECH = "Starting Location: GUE Tech" + LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE = "Starting Location: Hades Shore" + LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT = "Starting Location: Monastery Exhibit" + LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER = "Starting Location: Monastery Totemizer" + LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE = "Starting Location: Port Foozle" + LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB = "Starting Location: Spell Lab" LUCYS_PLAYING_CARD_1 = "Lucy's Playing Card: 1 Pip" LUCYS_PLAYING_CARD_2 = "Lucy's Playing Card: 2 Pips" LUCYS_PLAYING_CARD_3 = "Lucy's Playing Card: 3 Pips" @@ -354,6 +364,7 @@ class ZorkGrandInquisitorTags(enum.Enum): FILLER = "Filler" HOTSPOT = "Hotspot" INVENTORY_ITEM = "Inventory Item" + LOGIC_HELPER = "Logic Helper" MISSABLE = "Missable" SPELL = "Spell" SUBWAY_DESTINATION = "Subway Destination" diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index c54ba64528e8..51e29e898c98 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -6,12 +6,14 @@ from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData +from .data.mapping_data import starting_location_to_logic_helper_item from .data.region_data import region_data from .data_funcs import ( item_names_to_id, item_names_to_item, location_names_to_id, + id_to_starting_locations, item_groups, items_with_tag, location_groups, @@ -25,6 +27,7 @@ ZorkGrandInquisitorItems, ZorkGrandInquisitorLocations, ZorkGrandInquisitorRegions, + ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorTags, ) @@ -74,16 +77,16 @@ class ZorkGrandInquisitorWorld(World): item_name_groups = item_groups() location_name_groups = location_groups() - required_client_version: Tuple[int, int, int] = (0, 4, 4) + required_client_version: Tuple[int, int, int] = (0, 5, 0) web = ZorkGrandInquisitorWebWorld() filler_item_names: List[str] = item_groups()["Filler"] item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item() + starting_location: ZorkGrandInquisitorStartingLocations def generate_early(self) -> None: - pass - # Set Starting Location + self.starting_location = id_to_starting_locations()[self.options.starting_location.value] def create_regions(self) -> None: deathsanity: bool = bool(self.options.deathsanity) @@ -158,6 +161,8 @@ def create_items(self) -> None: continue elif ZorkGrandInquisitorTags.HOTSPOT in tags and start_with_hotspot_items: continue + elif ZorkGrandInquisitorTags.LOGIC_HELPER in tags: + continue item_pool.append(self.create_item(item.value)) @@ -183,6 +188,11 @@ def create_items(self) -> None: for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT): self.multiworld.push_precollected(self.create_item(item.value)) + # Logic Helper Items + self.multiworld.push_precollected( + self.create_item(starting_location_to_logic_helper_item[self.starting_location].value) + ) + def create_item(self, name: str) -> ZorkGrandInquisitorItem: data: ZorkGrandInquisitorItemData = item_data[self.item_name_to_item[name]] From c7f5333bb44d4344a235688a0f9316151cf64e3d Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 1 Nov 2024 17:56:40 -0400 Subject: [PATCH 03/51] remove hardcoded connection data between menu and port foozle; connect menu to starting location region --- .../data/entrance_rule_data.py | 1 - .../data/mapping_data.py | 22 ++++++++++++++++++- .../zork_grand_inquisitor/data/region_data.py | 3 --- worlds/zork_grand_inquisitor/world.py | 10 ++++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py index f48be5eb6b6a..2a048cf906b9 100644 --- a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py +++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py @@ -242,7 +242,6 @@ (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY,), ), - (ZorkGrandInquisitorRegions.MENU, ZorkGrandInquisitorRegions.PORT_FOOZLE): None, (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.HADES_SHORE): ( ( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 26946dce1c0c..96bcb0247e19 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -1,6 +1,10 @@ from typing import Dict -from ..enums import ZorkGrandInquisitorItems, ZorkGrandInquisitorStartingLocations +from ..enums import ( + ZorkGrandInquisitorItems, + ZorkGrandInquisitorRegions, + ZorkGrandInquisitorStartingLocations, +) starting_location_to_logic_helper_item: Dict[ @@ -37,3 +41,19 @@ ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT ), } + +# TODO: Align Starting Locations with Regions +starting_location_to_region: Dict[ + ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorRegions +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ZorkGrandInquisitorRegions.PORT_FOOZLE, + ZorkGrandInquisitorStartingLocations.CROSSROADS: ZorkGrandInquisitorRegions.CROSSROADS, + ZorkGrandInquisitorStartingLocations.DM_LAIR: ZorkGrandInquisitorRegions.DM_LAIR, + ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, + ZorkGrandInquisitorStartingLocations.GUE_TECH: ZorkGrandInquisitorRegions.GUE_TECH, + ZorkGrandInquisitorStartingLocations.SPELL_LAB: ZorkGrandInquisitorRegions.SPELL_LAB, + ZorkGrandInquisitorStartingLocations.HADES_SHORE: ZorkGrandInquisitorRegions.HADES_SHORE, + ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, + ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: ZorkGrandInquisitorRegions.MONASTERY, + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, +} diff --git a/worlds/zork_grand_inquisitor/data/region_data.py b/worlds/zork_grand_inquisitor/data/region_data.py index 1aed160f3088..308d37f4536d 100644 --- a/worlds/zork_grand_inquisitor/data/region_data.py +++ b/worlds/zork_grand_inquisitor/data/region_data.py @@ -97,9 +97,6 @@ class ZorkGrandInquisitorRegionData(NamedTuple): ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ) ), - ZorkGrandInquisitorRegions.MENU: ZorkGrandInquisitorRegionData( - exits=(ZorkGrandInquisitorRegions.PORT_FOOZLE,) - ), ZorkGrandInquisitorRegions.MONASTERY: ZorkGrandInquisitorRegionData( exits=( ZorkGrandInquisitorRegions.HADES_SHORE, diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 51e29e898c98..69db3c4f1193 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -6,7 +6,7 @@ from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.mapping_data import starting_location_to_logic_helper_item +from .data.mapping_data import starting_location_to_logic_helper_item, starting_location_to_region from .data.region_data import region_data from .data_funcs import ( @@ -146,6 +146,14 @@ def create_regions(self) -> None: self.multiworld.regions.append(region) + # Connect "Menu" region to correct starting location + region_menu: Region = Region("Menu", self.player, self.multiworld) + region_starting_location: ZorkGrandInquisitorRegions = starting_location_to_region[self.starting_location] + + region_menu.connect(region_mapping[region_starting_location]) + + self.multiworld.regions.append(region_menu) + def create_items(self) -> None: quick_port_foozle: bool = bool(self.options.quick_port_foozle) start_with_hotspot_items: bool = bool(self.options.start_with_hotspot_items) From c96a5088f86f8c6a2d4df09b969c33a99d1887c3 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 1 Nov 2024 18:06:27 -0400 Subject: [PATCH 04/51] align internal starting location and region naming --- .../zork_grand_inquisitor/data/item_data.py | 14 +++++++------- .../data/mapping_data.py | 19 +++++++++---------- worlds/zork_grand_inquisitor/enums.py | 12 ++++++------ .../zork_grand_inquisitor/game_controller.py | 6 +++--- worlds/zork_grand_inquisitor/world.py | 2 +- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index 49e043b86d4b..1ffe8dd6b566 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -796,25 +796,25 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 2, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 3, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 4, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 5, classification=ItemClassification.progression, @@ -826,19 +826,19 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 7, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 8, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 9, classification=ItemClassification.progression, diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 96bcb0247e19..56ef627779aa 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -19,8 +19,8 @@ ZorkGrandInquisitorStartingLocations.DM_LAIR: ( ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR ), - ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR ), ZorkGrandInquisitorStartingLocations.GUE_TECH: ( ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH @@ -31,29 +31,28 @@ ZorkGrandInquisitorStartingLocations.HADES_SHORE: ( ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE ), - ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3 + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM ), - ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER + ZorkGrandInquisitorStartingLocations.MONASTERY: ( + ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY ), ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ( ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT ), } -# TODO: Align Starting Locations with Regions starting_location_to_region: Dict[ ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorRegions ] = { ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorStartingLocations.CROSSROADS: ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorStartingLocations.DM_LAIR: ZorkGrandInquisitorRegions.DM_LAIR, - ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorStartingLocations.GUE_TECH: ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorStartingLocations.SPELL_LAB: ZorkGrandInquisitorRegions.SPELL_LAB, ZorkGrandInquisitorStartingLocations.HADES_SHORE: ZorkGrandInquisitorRegions.HADES_SHORE, - ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, - ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: ZorkGrandInquisitorRegions.MONASTERY, + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, + ZorkGrandInquisitorStartingLocations.MONASTERY: ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, } diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 0422592ca170..4ef6af51d122 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -107,14 +107,14 @@ class ZorkGrandInquisitorItems(enum.Enum): LARGE_TELEGRAPH_HAMMER = "Large Telegraph Hammer" LOGIC_HELPER_STARTING_LOCATION_CROSSROADS = "Starting Location: Crossroads" LOGIC_HELPER_STARTING_LOCATION_DM_LAIR = "Starting Location: Dungeon Master's Lair" - LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_HOUSE = "Starting Location: Dungeon Master's House" - LOGIC_HELPER_STARTING_LOCATION_FLOOD_CONTROL_DAM_3 = "Starting Location: Flood Control Dam #3" + LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR = "Starting Location: Dungeon Master's House" LOGIC_HELPER_STARTING_LOCATION_GUE_TECH = "Starting Location: GUE Tech" LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE = "Starting Location: Hades Shore" + LOGIC_HELPER_STARTING_LOCATION_MONASTERY = "Starting Location: Monastery Totemizer" LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT = "Starting Location: Monastery Exhibit" - LOGIC_HELPER_STARTING_LOCATION_MONASTERY_TOTEMIZER = "Starting Location: Monastery Totemizer" LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE = "Starting Location: Port Foozle" LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB = "Starting Location: Spell Lab" + LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM = "Starting Location: Flood Control Dam #3" LUCYS_PLAYING_CARD_1 = "Lucy's Playing Card: 1 Pip" LUCYS_PLAYING_CARD_2 = "Lucy's Playing Card: 2 Pips" LUCYS_PLAYING_CARD_3 = "Lucy's Playing Card: 3 Pips" @@ -349,12 +349,12 @@ class ZorkGrandInquisitorStartingLocations(enum.Enum): PORT_FOOZLE = 0 CROSSROADS = 1 DM_LAIR = 2 - DM_LAIR_HOUSE = 3 + DM_LAIR_INTERIOR = 3 GUE_TECH = 4 SPELL_LAB = 5 HADES_SHORE = 6 - FLOOD_CONTROL_DAM_3 = 7 - MONASTERY_TOTEMIZER = 8 + SUBWAY_FLOOD_CONTROL_DAM = 7 + MONASTERY = 8 MONASTERY_EXHIBIT = 9 diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index bc552a0ac260..8743855c74ea 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -227,7 +227,7 @@ def _apply_starting_location(self, force: bool = False) -> None: self.game_state_manager.set_game_location("uc10", 1200) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR: self.game_state_manager.set_game_location("dg10", 1410) - elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR_HOUSE: + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: self.game_state_manager.set_game_location("dv10", 1673) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.GUE_TECH: self.game_state_manager.set_game_location("tr10", 150) @@ -235,9 +235,9 @@ def _apply_starting_location(self, force: bool = False) -> None: self.game_state_manager.set_game_location("tp10", 0) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.HADES_SHORE: self.game_state_manager.set_game_location("hp10", 534) - elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.FLOOD_CONTROL_DAM_3: + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: self.game_state_manager.set_game_location("ue10", 1578) - elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY_TOTEMIZER: + elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY: self.game_state_manager.set_game_location("mt10", 1483) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: self.game_state_manager.set_game_location("me10", 1023) diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 69db3c4f1193..20e4fb8ffa53 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -146,7 +146,7 @@ def create_regions(self) -> None: self.multiworld.regions.append(region) - # Connect "Menu" region to correct starting location + # Connect "Menu" region to starting location region_menu: Region = Region("Menu", self.player, self.multiworld) region_starting_location: ZorkGrandInquisitorRegions = starting_location_to_region[self.starting_location] From 6dc1ad2ff2a239193f0963cc63a09ea740c384c1 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 1 Nov 2024 20:22:50 -0400 Subject: [PATCH 05/51] comment the rough offsets of other managers --- worlds/zork_grand_inquisitor/game_state_manager.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/worlds/zork_grand_inquisitor/game_state_manager.py b/worlds/zork_grand_inquisitor/game_state_manager.py index 25b35969bf5e..f38d1097e5f1 100644 --- a/worlds/zork_grand_inquisitor/game_state_manager.py +++ b/worlds/zork_grand_inquisitor/game_state_manager.py @@ -93,6 +93,13 @@ def open_process_handle(self) -> bool: self.script_manager_struct_address = self._resolve_address(0x5276600, (0xC8, 0x0)) self.render_manager_struct_address = self._resolve_address(0x5276600, (0xD0, 0x120)) + # 0xD8 Cursor Manager + # 0xE0 String Manager + # 0xE8 Search Manager + # 0xF0 Text Renderer + # 0xF8 Midi Manager + # 0x100 Save Manager + # 0x108 Menu Handler except Exception: return False From cc602fa76e3bd1a9ce7a134f3537f002402850b3 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 16 Nov 2024 08:14:30 -0500 Subject: [PATCH 06/51] bulk commit -> first playable beta --- worlds/zork_grand_inquisitor/client.py | 23 +- .../data/entrance_rule_data.py | 215 +++--- .../zork_grand_inquisitor/data/item_data.py | 386 +++++----- .../data/location_data.py | 690 ++++++++++-------- .../data/mapping_data.py | 394 +++++++++- ...ions_data.py => missable_location_data.py} | 120 ++- .../zork_grand_inquisitor/data/region_data.py | 26 +- .../data/transform_data.py | 180 +++++ worlds/zork_grand_inquisitor/data_funcs.py | 174 ++++- worlds/zork_grand_inquisitor/enums.py | 152 ++-- .../zork_grand_inquisitor/game_controller.py | 276 ++++--- worlds/zork_grand_inquisitor/options.py | 87 ++- worlds/zork_grand_inquisitor/world.py | 297 ++++++-- 13 files changed, 2132 insertions(+), 888 deletions(-) rename worlds/zork_grand_inquisitor/data/{missable_location_grant_conditions_data.py => missable_location_data.py} (56%) create mode 100644 worlds/zork_grand_inquisitor/data/transform_data.py diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index c86c3bc356ad..87dd0b840257 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -8,8 +8,11 @@ from .data_funcs import ( item_names_to_id, + item_names_to_item, location_names_to_id, + id_to_deathsanity, id_to_items, + id_to_landmarksanity, id_to_locations, id_to_goals, id_to_starting_locations, @@ -100,15 +103,27 @@ def on_package(self, cmd: str, _args: Any) -> None: # Options self.game_controller.option_goal = id_to_goals()[_args["slot_data"]["goal"]] - self.game_controller.option_deathsanity = _args["slot_data"]["deathsanity"] == 1 + + self.game_controller.option_starting_location = ( + id_to_starting_locations()[_args["slot_data"]["starting_location"]] + ) + + self.game_controller.option_deathsanity = ( + id_to_deathsanity()[_args["slot_data"]["deathsanity"]] + ) + + self.game_controller.option_landmarksanity = ( + id_to_landmarksanity()[_args["slot_data"]["landmarksanity"]] + ) self.game_controller.option_grant_missable_location_checks = ( _args["slot_data"]["grant_missable_location_checks"] == 1 ) - self.game_controller.option_starting_location = ( - id_to_starting_locations()[_args["slot_data"]["starting_location"]] - ) + # Initial Totemizer Destination + self.game_controller.initial_totemizer_destination = item_names_to_item()[ + _args["slot_data"]["initial_totemizer_destination"] + ] async def controller(self): while not self.exit_event.is_set(): diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py index 2a048cf906b9..532bec7aafe4 100644 --- a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py +++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py @@ -1,6 +1,11 @@ from typing import Dict, Tuple, Union -from ..enums import ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems, ZorkGrandInquisitorRegions +from ..enums import ( + ZorkGrandInquisitorEvents, + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorItems, + ZorkGrandInquisitorRegions, +) entrance_rule_data: Dict[ @@ -33,7 +38,7 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, ), ), - (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH): ( + (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE): ( ( ZorkGrandInquisitorItems.SPELL_REZROV, ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, @@ -51,7 +56,12 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES, ), ), - (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.PORT_FOOZLE): None, + (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.PORT_FOOZLE): ( + ( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ), + ), (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): ( ( ZorkGrandInquisitorItems.MAP, @@ -101,18 +111,22 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, ), ), - (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.DM_LAIR): None, + (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.DM_LAIR): ( + ( + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, + ), + ), (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WALKING_CASTLE): ( ( ZorkGrandInquisitorItems.HOTSPOT_BLINDS, - ZorkGrandInquisitorEvents.KNOWS_OBIDIL, + ZorkGrandInquisitorItems.SPELL_OBIDIL, ), ), (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WHITE_HOUSE): ( ( ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, ZorkGrandInquisitorItems.SPELL_NARWILE, - ZorkGrandInquisitorEvents.KNOWS_YASTARD, + ZorkGrandInquisitorItems.SPELL_YASTARD, ), ), (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON): ( @@ -123,30 +137,9 @@ ), (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.HADES_BEYOND_GATES): None, (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO): None, - (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.ENDGAME): ( - ( - ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, - ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, - ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, - ZorkGrandInquisitorRegions.WHITE_HOUSE, - ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, - ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, - ZorkGrandInquisitorItems.BROGS_PLANK, - ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, - ), + (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE): ( + (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), ), - (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.CROSSROADS): None, (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): ( ( ZorkGrandInquisitorItems.SPELL_IGRAM, @@ -156,6 +149,10 @@ (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): ( (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR,), ), + (ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.CROSSROADS): None, + (ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.GUE_TECH): ( + (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), + ), (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.GUE_TECH): None, (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): ( ( @@ -164,7 +161,10 @@ ), ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.CROSSROADS): ( - (ZorkGrandInquisitorItems.MAP,), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ), ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.DM_LAIR): ( ( @@ -172,7 +172,9 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, ), ), - (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.GUE_TECH): None, + (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.GUE_TECH): ( + (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), + ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.HADES_SHORE): ( ( ZorkGrandInquisitorItems.MAP, @@ -193,7 +195,7 @@ ), (ZorkGrandInquisitorRegions.HADES, ZorkGrandInquisitorRegions.HADES_BEYOND_GATES): ( ( - ZorkGrandInquisitorEvents.KNOWS_SNAVIG, + ZorkGrandInquisitorItems.SPELL_SNAVIG, ZorkGrandInquisitorItems.TOTEM_BROG, # Visually hiding this totem is tied to owning it; no choice ), ), @@ -203,12 +205,15 @@ (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO): ( ( ZorkGrandInquisitorItems.SPELL_NARWILE, - ZorkGrandInquisitorEvents.KNOWS_YASTARD, + ZorkGrandInquisitorItems.SPELL_YASTARD, ), ), (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.HADES): None, (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.CROSSROADS): ( - (ZorkGrandInquisitorItems.MAP,), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ), ), (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.DM_LAIR): ( ( @@ -235,7 +240,9 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB, ), ), - (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None, + (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): ( + (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS,), + ), (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM,), ), @@ -264,19 +271,16 @@ ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER, ZorkGrandInquisitorItems.SPELL_NARWILE, - ZorkGrandInquisitorEvents.KNOWS_YASTARD, + ZorkGrandInquisitorItems.SPELL_YASTARD, ), ), (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.CROSSROADS): ( - ( - ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE, - ZorkGrandInquisitorItems.ROPE, - ZorkGrandInquisitorItems.HOTSPOT_WELL, - ), + (ZorkGrandInquisitorItems.WELL_ROPE,), ), (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP): ( ( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, + ZorkGrandInquisitorItems.CIGAR, + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, ), ), @@ -288,33 +292,13 @@ ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, ), ), - (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, ZorkGrandInquisitorRegions.ENDGAME): ( - ( - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, - ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, - ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, - ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, - ZorkGrandInquisitorRegions.WHITE_HOUSE, - ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, - ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, - ZorkGrandInquisitorItems.BROGS_PLANK, - ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, - ), - ), (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST): None, (ZorkGrandInquisitorRegions.SPELL_LAB, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): None, (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.CROSSROADS): ( - (ZorkGrandInquisitorItems.MAP,), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ), ), (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.DM_LAIR): ( ( @@ -328,7 +312,11 @@ ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH, ), ), - (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): None, + (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): ( + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ), + ), (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.HADES_SHORE): ( ( ZorkGrandInquisitorItems.MAP, @@ -372,47 +360,88 @@ (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.HADES_SHORE): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES,), ), - (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None, + (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): ( + (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS,), + ), (ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY,), ), + (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.CROSSROADS): ( + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ), + ), (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.HADES_SHORE): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES,), ), (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.MONASTERY): ( - ( - ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorEvents.ROPE_GLORFABLE, - ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT, - ), + (ZorkGrandInquisitorItems.MONASTERY_ROPE,), + ), + (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): ( + (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS,), ), - (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): None, (ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM): ( (ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM,), ), (ZorkGrandInquisitorRegions.WALKING_CASTLE, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR): None, (ZorkGrandInquisitorRegions.WHITE_HOUSE, ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR): None, - (ZorkGrandInquisitorRegions.WHITE_HOUSE, ZorkGrandInquisitorRegions.ENDGAME): ( + (ZorkGrandInquisitorRegions.WHITE_HOUSE, ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR): ( ( - ZorkGrandInquisitorItems.TOTEM_BROG, # Needed here since White House is not broken down in 2 regions + ZorkGrandInquisitorItems.TOTEM_BROG, ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, - ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, - ZorkGrandInquisitorItems.BROGS_PLANK, - ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, - ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, - ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, - ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, - ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, - ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, - ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, ), ), + (ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, ZorkGrandInquisitorRegions.WHITE_HOUSE): None, +} + +endgame_entrance_data_by_goal: Dict[ + ZorkGrandInquisitorGoals, + Dict[ + Tuple[ + ZorkGrandInquisitorRegions, + ZorkGrandInquisitorRegions, + ], + Union[ + Tuple[ + Tuple[ + Union[ + ZorkGrandInquisitorEvents, + ZorkGrandInquisitorItems, + ZorkGrandInquisitorRegions, + ], + ..., + ], + ..., + ], + None, + ], + ], +] = { + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: { + (ZorkGrandInquisitorRegions.MENU, ZorkGrandInquisitorRegions.ENDGAME): ( + ( + ZorkGrandInquisitorItems.COCONUT_OF_QUENDOR, + ZorkGrandInquisitorItems.CUBE_OF_FOUNDATION, + ZorkGrandInquisitorItems.SKULL_OF_YORUK, + ), + ) + }, + # ZorkGrandInquisitorGoals.SPELL_HEIST: { + # (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.ENDGAME): ( + # ( + # ZorkGrandInquisitorItems.SPELL_BEBURTT, + # ZorkGrandInquisitorItems.SPELL_GLORF, + # ZorkGrandInquisitorItems.SPELL_GOLGATEM, + # ZorkGrandInquisitorItems.SPELL_IGRAM, + # ZorkGrandInquisitorItems.SPELL_KENDALL, + # ZorkGrandInquisitorItems.SPELL_OBIDIL, + # ZorkGrandInquisitorItems.SPELL_NARWILE, + # ZorkGrandInquisitorItems.SPELL_REZROV, + # ZorkGrandInquisitorItems.SPELL_SNAVIG, + # ZorkGrandInquisitorItems.SPELL_THROCK, + # ZorkGrandInquisitorItems.SPELL_YASTARD, + # ), + # ) + # }, } diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index 1ffe8dd6b566..010781d38271 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -41,207 +41,195 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), - ZorkGrandInquisitorItems.FLATHEADIA_FUDGE: ZorkGrandInquisitorItemData( - statemap_keys=(54,), + ZorkGrandInquisitorItems.CIGAR: ZorkGrandInquisitorItemData( + statemap_keys=(1,), archipelago_id=ITEM_OFFSET + 4, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), + ZorkGrandInquisitorItems.COCOA_INGREDIENTS: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 5, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), + ), ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP: ZorkGrandInquisitorItemData( statemap_keys=(86,), - archipelago_id=ITEM_OFFSET + 5, + archipelago_id=ITEM_OFFSET + 6, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH: ZorkGrandInquisitorItemData( statemap_keys=(84,), - archipelago_id=ITEM_OFFSET + 6, + archipelago_id=ITEM_OFFSET + 7, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT: ZorkGrandInquisitorItemData( statemap_keys=(9,), - archipelago_id=ITEM_OFFSET + 7, + archipelago_id=ITEM_OFFSET + 8, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN: ZorkGrandInquisitorItemData( statemap_keys=(16,), - archipelago_id=ITEM_OFFSET + 8, + archipelago_id=ITEM_OFFSET + 9, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.HAMMER: ZorkGrandInquisitorItemData( statemap_keys=(23,), - archipelago_id=ITEM_OFFSET + 9, + archipelago_id=ITEM_OFFSET + 10, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.HUNGUS_LARD: ZorkGrandInquisitorItemData( statemap_keys=(55,), - archipelago_id=ITEM_OFFSET + 10, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), - ), - ZorkGrandInquisitorItems.JAR_OF_HOTBUGS: ZorkGrandInquisitorItemData( - statemap_keys=(56,), archipelago_id=ITEM_OFFSET + 11, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), - ZorkGrandInquisitorItems.LANTERN: ZorkGrandInquisitorItemData( - statemap_keys=(4,), - archipelago_id=ITEM_OFFSET + 12, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), - ), ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER: ZorkGrandInquisitorItemData( statemap_keys=(88,), - archipelago_id=ITEM_OFFSET + 13, + archipelago_id=ITEM_OFFSET + 12, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1: ZorkGrandInquisitorItemData( statemap_keys=(116,), # With fly = 120 - archipelago_id=ITEM_OFFSET + 14, + archipelago_id=ITEM_OFFSET + 13, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2: ZorkGrandInquisitorItemData( statemap_keys=(117,), # With fly = 121 - archipelago_id=ITEM_OFFSET + 15, + archipelago_id=ITEM_OFFSET + 14, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3: ZorkGrandInquisitorItemData( statemap_keys=(118,), # With fly = 122 - archipelago_id=ITEM_OFFSET + 16, + archipelago_id=ITEM_OFFSET + 15, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4: ZorkGrandInquisitorItemData( statemap_keys=(119,), # With fly = 123 - archipelago_id=ITEM_OFFSET + 17, + archipelago_id=ITEM_OFFSET + 16, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.MAP: ZorkGrandInquisitorItemData( statemap_keys=(6,), - archipelago_id=ITEM_OFFSET + 18, + archipelago_id=ITEM_OFFSET + 17, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.MEAD_LIGHT: ZorkGrandInquisitorItemData( statemap_keys=(2,), - archipelago_id=ITEM_OFFSET + 19, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), - ), - ZorkGrandInquisitorItems.MOSS_OF_MAREILON: ZorkGrandInquisitorItemData( - statemap_keys=(57,), - archipelago_id=ITEM_OFFSET + 20, + archipelago_id=ITEM_OFFSET + 18, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), - ZorkGrandInquisitorItems.MUG: ZorkGrandInquisitorItemData( - statemap_keys=(35,), - archipelago_id=ITEM_OFFSET + 21, + ZorkGrandInquisitorItems.MONASTERY_ROPE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 19, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.OLD_SCRATCH_CARD: ZorkGrandInquisitorItemData( statemap_keys=(17,), - archipelago_id=ITEM_OFFSET + 22, + archipelago_id=ITEM_OFFSET + 20, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE: ZorkGrandInquisitorItemData( statemap_keys=(36,), - archipelago_id=ITEM_OFFSET + 23, + archipelago_id=ITEM_OFFSET + 21, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER: ZorkGrandInquisitorItemData( statemap_keys=(3,), - archipelago_id=ITEM_OFFSET + 24, + archipelago_id=ITEM_OFFSET + 22, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS: ZorkGrandInquisitorItemData( - statemap_keys=(5827,), - archipelago_id=ITEM_OFFSET + 25, + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 23, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.PROZORK_TABLET: ZorkGrandInquisitorItemData( statemap_keys=(65,), - archipelago_id=ITEM_OFFSET + 26, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), - ), - ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB: ZorkGrandInquisitorItemData( - statemap_keys=(53,), - archipelago_id=ITEM_OFFSET + 27, + archipelago_id=ITEM_OFFSET + 24, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), - ZorkGrandInquisitorItems.ROPE: ZorkGrandInquisitorItemData( - statemap_keys=(83,), - archipelago_id=ITEM_OFFSET + 28, + ZorkGrandInquisitorItems.SANDWITCH_WRAPPER: ZorkGrandInquisitorItemData( + statemap_keys=(34,), + archipelago_id=ITEM_OFFSET + 25, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS: ZorkGrandInquisitorItemData( statemap_keys=(101,), # SNA = 41 - archipelago_id=ITEM_OFFSET + 29, + archipelago_id=ITEM_OFFSET + 26, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV: ZorkGrandInquisitorItemData( statemap_keys=(102,), # VIG = 48 - archipelago_id=ITEM_OFFSET + 30, + archipelago_id=ITEM_OFFSET + 27, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SHOVEL: ZorkGrandInquisitorItemData( statemap_keys=(49,), - archipelago_id=ITEM_OFFSET + 31, + archipelago_id=ITEM_OFFSET + 28, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SNAPDRAGON: ZorkGrandInquisitorItemData( statemap_keys=(50,), - archipelago_id=ITEM_OFFSET + 32, + archipelago_id=ITEM_OFFSET + 29, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.STUDENT_ID: ZorkGrandInquisitorItemData( statemap_keys=(39,), - archipelago_id=ITEM_OFFSET + 33, + archipelago_id=ITEM_OFFSET + 30, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SUBWAY_TOKEN: ZorkGrandInquisitorItemData( statemap_keys=(20,), - archipelago_id=ITEM_OFFSET + 34, + archipelago_id=ITEM_OFFSET + 31, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.SWORD: ZorkGrandInquisitorItemData( statemap_keys=(21,), - archipelago_id=ITEM_OFFSET + 35, + archipelago_id=ITEM_OFFSET + 32, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), + ), + ZorkGrandInquisitorItems.WELL_ROPE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 33, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.ZIMDOR_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=(25,), - archipelago_id=ITEM_OFFSET + 36, + archipelago_id=ITEM_OFFSET + 34, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), ZorkGrandInquisitorItems.ZORK_ROCKS: ZorkGrandInquisitorItemData( statemap_keys=(37,), - archipelago_id=ITEM_OFFSET + 37, + archipelago_id=ITEM_OFFSET + 35, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.INVENTORY_ITEM,), ), @@ -270,129 +258,141 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), + ZorkGrandInquisitorItems.HOTSPOT_BUCKET: ZorkGrandInquisitorItemData( + statemap_keys=(13928,), + archipelago_id=ITEM_OFFSET + 100 + 4, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT,), + ), ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS: ZorkGrandInquisitorItemData( statemap_keys=(12691, 12692, 12693, 12694, 12695, 12696, 12697, 12698, 12699, 12700, 12701), - archipelago_id=ITEM_OFFSET + 100 + 4, + archipelago_id=ITEM_OFFSET + 100 + 5, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(12702,), - archipelago_id=ITEM_OFFSET + 100 + 5, + archipelago_id=ITEM_OFFSET + 100 + 6, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(12909,), - archipelago_id=ITEM_OFFSET + 100 + 6, + archipelago_id=ITEM_OFFSET + 100 + 7, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(12900,), - archipelago_id=ITEM_OFFSET + 100 + 7, + archipelago_id=ITEM_OFFSET + 100 + 8, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(5010,), - archipelago_id=ITEM_OFFSET + 100 + 8, + archipelago_id=ITEM_OFFSET + 100 + 9, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(9539,), - archipelago_id=ITEM_OFFSET + 100 + 9, + archipelago_id=ITEM_OFFSET + 100 + 10, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER: ZorkGrandInquisitorItemData( statemap_keys=(19712,), - archipelago_id=ITEM_OFFSET + 100 + 10, + archipelago_id=ITEM_OFFSET + 100 + 11, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT: ZorkGrandInquisitorItemData( statemap_keys=(2586,), - archipelago_id=ITEM_OFFSET + 100 + 11, + archipelago_id=ITEM_OFFSET + 100 + 12, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER: ZorkGrandInquisitorItemData( statemap_keys=(11878,), - archipelago_id=ITEM_OFFSET + 100 + 12, + archipelago_id=ITEM_OFFSET + 100 + 13, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND: ZorkGrandInquisitorItemData( statemap_keys=(11751,), - archipelago_id=ITEM_OFFSET + 100 + 13, + archipelago_id=ITEM_OFFSET + 100 + 14, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH: ZorkGrandInquisitorItemData( statemap_keys=(15147, 15153), - archipelago_id=ITEM_OFFSET + 100 + 14, + archipelago_id=ITEM_OFFSET + 100 + 15, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW: ZorkGrandInquisitorItemData( statemap_keys=(1705,), - archipelago_id=ITEM_OFFSET + 100 + 15, + archipelago_id=ITEM_OFFSET + 100 + 16, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS: ZorkGrandInquisitorItemData( statemap_keys=(1425, 1426), - archipelago_id=ITEM_OFFSET + 100 + 16, + archipelago_id=ITEM_OFFSET + 100 + 17, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT,), + ), + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT: ZorkGrandInquisitorItemData( + statemap_keys=(4791,), + archipelago_id=ITEM_OFFSET + 100 + 18, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE: ZorkGrandInquisitorItemData( statemap_keys=(13106,), - archipelago_id=ITEM_OFFSET + 100 + 17, + archipelago_id=ITEM_OFFSET + 100 + 19, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS: ZorkGrandInquisitorItemData( statemap_keys=(13219, 13220, 13221, 13222), - archipelago_id=ITEM_OFFSET + 100 + 18, + archipelago_id=ITEM_OFFSET + 100 + 20, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS: ZorkGrandInquisitorItemData( statemap_keys=(14327, 14332, 14337, 14342), - archipelago_id=ITEM_OFFSET + 100 + 19, + archipelago_id=ITEM_OFFSET + 100 + 21, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(12528,), - archipelago_id=ITEM_OFFSET + 100 + 20, + archipelago_id=ITEM_OFFSET + 100 + 22, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS: ZorkGrandInquisitorItemData( statemap_keys=(12523, 12524, 12525), - archipelago_id=ITEM_OFFSET + 100 + 21, + archipelago_id=ITEM_OFFSET + 100 + 23, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE: ZorkGrandInquisitorItemData( statemap_keys=(13002,), - archipelago_id=ITEM_OFFSET + 100 + 22, + archipelago_id=ITEM_OFFSET + 100 + 24, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL: ZorkGrandInquisitorItemData( statemap_keys=(10726,), - archipelago_id=ITEM_OFFSET + 100 + 23, + archipelago_id=ITEM_OFFSET + 100 + 25, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(12280,), - archipelago_id=ITEM_OFFSET + 100 + 24, + archipelago_id=ITEM_OFFSET + 100 + 26, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), @@ -420,286 +420,322 @@ class ZorkGrandInquisitorItemData(NamedTuple): 17726, 17727 ), - archipelago_id=ITEM_OFFSET + 100 + 25, + archipelago_id=ITEM_OFFSET + 100 + 27, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT,), + ), + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS: ZorkGrandInquisitorItemData( + statemap_keys=(11543, 12256, 11720), + archipelago_id=ITEM_OFFSET + 100 + 28, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS: ZorkGrandInquisitorItemData( statemap_keys=(8448, 8449, 8450, 8451, 8452, 8453, 8454, 8455, 8456, 8457, 8458, 8459), - archipelago_id=ITEM_OFFSET + 100 + 26, + archipelago_id=ITEM_OFFSET + 100 + 29, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER: ZorkGrandInquisitorItemData( statemap_keys=(8446,), - archipelago_id=ITEM_OFFSET + 100 + 27, + archipelago_id=ITEM_OFFSET + 100 + 30, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_HARRY: ZorkGrandInquisitorItemData( statemap_keys=(4260,), - archipelago_id=ITEM_OFFSET + 100 + 28, + archipelago_id=ITEM_OFFSET + 100 + 31, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY: ZorkGrandInquisitorItemData( statemap_keys=(18026,), - archipelago_id=ITEM_OFFSET + 100 + 29, + archipelago_id=ITEM_OFFSET + 100 + 32, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH: ZorkGrandInquisitorItemData( statemap_keys=(17623,), - archipelago_id=ITEM_OFFSET + 100 + 30, + archipelago_id=ITEM_OFFSET + 100 + 33, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(13140,), - archipelago_id=ITEM_OFFSET + 100 + 31, + archipelago_id=ITEM_OFFSET + 100 + 34, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(10441,), - archipelago_id=ITEM_OFFSET + 100 + 32, + archipelago_id=ITEM_OFFSET + 100 + 35, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS: ZorkGrandInquisitorItemData( statemap_keys=(19632, 19627), - archipelago_id=ITEM_OFFSET + 100 + 33, + archipelago_id=ITEM_OFFSET + 100 + 36, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(3025,), - archipelago_id=ITEM_OFFSET + 100 + 34, + archipelago_id=ITEM_OFFSET + 100 + 37, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG: ZorkGrandInquisitorItemData( statemap_keys=(3036,), - archipelago_id=ITEM_OFFSET + 100 + 35, + archipelago_id=ITEM_OFFSET + 100 + 38, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_MIRROR: ZorkGrandInquisitorItemData( statemap_keys=(5031,), - archipelago_id=ITEM_OFFSET + 100 + 36, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.HOTSPOT,), - ), - ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT: ZorkGrandInquisitorItemData( - statemap_keys=(13597,), - archipelago_id=ITEM_OFFSET + 100 + 37, + archipelago_id=ITEM_OFFSET + 100 + 39, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE: ZorkGrandInquisitorItemData( statemap_keys=(13390,), - archipelago_id=ITEM_OFFSET + 100 + 38, + archipelago_id=ITEM_OFFSET + 100 + 40, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR: ZorkGrandInquisitorItemData( statemap_keys=(2455, 2447), - archipelago_id=ITEM_OFFSET + 100 + 39, + archipelago_id=ITEM_OFFSET + 100 + 41, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS: ZorkGrandInquisitorItemData( statemap_keys=(12389, 12390), - archipelago_id=ITEM_OFFSET + 100 + 40, + archipelago_id=ITEM_OFFSET + 100 + 42, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE: ZorkGrandInquisitorItemData( statemap_keys=(4302,), - archipelago_id=ITEM_OFFSET + 100 + 41, + archipelago_id=ITEM_OFFSET + 100 + 43, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE: ZorkGrandInquisitorItemData( statemap_keys=(16383, 16384), - archipelago_id=ITEM_OFFSET + 100 + 42, + archipelago_id=ITEM_OFFSET + 100 + 44, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE: ZorkGrandInquisitorItemData( statemap_keys=(2769,), - archipelago_id=ITEM_OFFSET + 100 + 43, + archipelago_id=ITEM_OFFSET + 100 + 45, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON: ZorkGrandInquisitorItemData( statemap_keys=(4149,), - archipelago_id=ITEM_OFFSET + 100 + 44, + archipelago_id=ITEM_OFFSET + 100 + 46, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS: ZorkGrandInquisitorItemData( statemap_keys=(12584, 12585, 12586, 12587), - archipelago_id=ITEM_OFFSET + 100 + 45, + archipelago_id=ITEM_OFFSET + 100 + 47, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(12574,), - archipelago_id=ITEM_OFFSET + 100 + 46, + archipelago_id=ITEM_OFFSET + 100 + 48, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(13412,), - archipelago_id=ITEM_OFFSET + 100 + 47, + archipelago_id=ITEM_OFFSET + 100 + 49, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER: ZorkGrandInquisitorItemData( statemap_keys=(12170,), - archipelago_id=ITEM_OFFSET + 100 + 48, + archipelago_id=ITEM_OFFSET + 100 + 50, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT,), + ), + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT: ZorkGrandInquisitorItemData( + statemap_keys=(12045,), + archipelago_id=ITEM_OFFSET + 100 + 51, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM: ZorkGrandInquisitorItemData( statemap_keys=(16382,), - archipelago_id=ITEM_OFFSET + 100 + 49, + archipelago_id=ITEM_OFFSET + 100 + 52, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM: ZorkGrandInquisitorItemData( statemap_keys=(4209,), - archipelago_id=ITEM_OFFSET + 100 + 50, + archipelago_id=ITEM_OFFSET + 100 + 53, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE: ZorkGrandInquisitorItemData( statemap_keys=(11973,), - archipelago_id=ITEM_OFFSET + 100 + 51, + archipelago_id=ITEM_OFFSET + 100 + 54, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT: ZorkGrandInquisitorItemData( statemap_keys=(13168,), - archipelago_id=ITEM_OFFSET + 100 + 52, + archipelago_id=ITEM_OFFSET + 100 + 55, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY: ZorkGrandInquisitorItemData( statemap_keys=(15396,), - archipelago_id=ITEM_OFFSET + 100 + 53, + archipelago_id=ITEM_OFFSET + 100 + 56, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH: ZorkGrandInquisitorItemData( statemap_keys=(9706,), - archipelago_id=ITEM_OFFSET + 100 + 54, + archipelago_id=ITEM_OFFSET + 100 + 57, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS: ZorkGrandInquisitorItemData( statemap_keys=(9728, 9729, 9730), - archipelago_id=ITEM_OFFSET + 100 + 55, + archipelago_id=ITEM_OFFSET + 100 + 58, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), - ZorkGrandInquisitorItems.HOTSPOT_WELL: ZorkGrandInquisitorItemData( - statemap_keys=(10314,), - archipelago_id=ITEM_OFFSET + 100 + 56, + # Spells + ZorkGrandInquisitorItems.SPELL_BEBURTT: ZorkGrandInquisitorItemData( + statemap_keys=(194,), + archipelago_id=ITEM_OFFSET + 200 + 0, classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.HOTSPOT,), + tags=(ZorkGrandInquisitorTags.SPELL,), ), - # Spells ZorkGrandInquisitorItems.SPELL_GLORF: ZorkGrandInquisitorItemData( statemap_keys=(202,), - archipelago_id=ITEM_OFFSET + 200 + 0, + archipelago_id=ITEM_OFFSET + 200 + 1, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_GOLGATEM: ZorkGrandInquisitorItemData( statemap_keys=(192,), - archipelago_id=ITEM_OFFSET + 200 + 1, + archipelago_id=ITEM_OFFSET + 200 + 2, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_IGRAM: ZorkGrandInquisitorItemData( statemap_keys=(199,), - archipelago_id=ITEM_OFFSET + 200 + 2, + archipelago_id=ITEM_OFFSET + 200 + 3, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_KENDALL: ZorkGrandInquisitorItemData( statemap_keys=(196,), - archipelago_id=ITEM_OFFSET + 200 + 3, + archipelago_id=ITEM_OFFSET + 200 + 4, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.SPELL,), + ), + ZorkGrandInquisitorItems.SPELL_OBIDIL: ZorkGrandInquisitorItemData( + statemap_keys=(193,), + archipelago_id=ITEM_OFFSET + 200 + 5, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_NARWILE: ZorkGrandInquisitorItemData( statemap_keys=(197,), - archipelago_id=ITEM_OFFSET + 200 + 4, + archipelago_id=ITEM_OFFSET + 200 + 6, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_REZROV: ZorkGrandInquisitorItemData( statemap_keys=(195,), - archipelago_id=ITEM_OFFSET + 200 + 5, + archipelago_id=ITEM_OFFSET + 200 + 7, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.SPELL,), + ), + ZorkGrandInquisitorItems.SPELL_SNAVIG: ZorkGrandInquisitorItemData( + statemap_keys=(201,), + archipelago_id=ITEM_OFFSET + 200 + 8, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), ZorkGrandInquisitorItems.SPELL_THROCK: ZorkGrandInquisitorItemData( statemap_keys=(200,), - archipelago_id=ITEM_OFFSET + 200 + 6, + archipelago_id=ITEM_OFFSET + 200 + 9, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.SPELL,), + ), + ZorkGrandInquisitorItems.SPELL_YASTARD: ZorkGrandInquisitorItemData( + statemap_keys=(198,), + archipelago_id=ITEM_OFFSET + 200 + 10, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SPELL,), ), # Subway Destinations + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS: ZorkGrandInquisitorItemData( + statemap_keys=(13760, 13323, 13512, 13651), + archipelago_id=ITEM_OFFSET + 300 + 0, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,), + ), ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData( statemap_keys=(13757, 13297, 13486, 13625), - archipelago_id=ITEM_OFFSET + 300 + 0, + archipelago_id=ITEM_OFFSET + 300 + 1, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,), ), ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES: ZorkGrandInquisitorItemData( statemap_keys=(13758, 13309, 13498, 13637), - archipelago_id=ITEM_OFFSET + 300 + 1, + archipelago_id=ITEM_OFFSET + 300 + 2, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,), ), ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY: ZorkGrandInquisitorItemData( statemap_keys=(13759, 13316, 13505, 13644), - archipelago_id=ITEM_OFFSET + 300 + 2, + archipelago_id=ITEM_OFFSET + 300 + 3, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.SUBWAY_DESTINATION,), ), - # Teleporter Destinations + # Teleporter Destination + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS: ZorkGrandInquisitorItemData( + statemap_keys=(12918,), + archipelago_id=ITEM_OFFSET + 400 + 0, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), + ), ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR: ZorkGrandInquisitorItemData( statemap_keys=(2203,), - archipelago_id=ITEM_OFFSET + 400 + 0, + archipelago_id=ITEM_OFFSET + 400 + 1, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), ), ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH: ZorkGrandInquisitorItemData( statemap_keys=(7132,), - archipelago_id=ITEM_OFFSET + 400 + 1, + archipelago_id=ITEM_OFFSET + 400 + 2, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), ), ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES: ZorkGrandInquisitorItemData( statemap_keys=(7119,), - archipelago_id=ITEM_OFFSET + 400 + 2, + archipelago_id=ITEM_OFFSET + 400 + 3, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), ), ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY: ZorkGrandInquisitorItemData( statemap_keys=(7148,), - archipelago_id=ITEM_OFFSET + 400 + 3, + archipelago_id=ITEM_OFFSET + 400 + 4, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), ), ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB: ZorkGrandInquisitorItemData( statemap_keys=(16545,), - archipelago_id=ITEM_OFFSET + 400 + 4, + archipelago_id=ITEM_OFFSET + 400 + 5, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TELEPORTER_DESTINATION,), ), @@ -713,19 +749,25 @@ class ZorkGrandInquisitorItemData(NamedTuple): ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: ZorkGrandInquisitorItemData( statemap_keys=(9666,), archipelago_id=ITEM_OFFSET + 500 + 1, - classification=ItemClassification.filler, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,), + ), + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY: ZorkGrandInquisitorItemData( + statemap_keys=(9664,), + archipelago_id=ITEM_OFFSET + 500 + 2, + classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,), ), ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: ZorkGrandInquisitorItemData( statemap_keys=(9668,), - archipelago_id=ITEM_OFFSET + 500 + 2, + archipelago_id=ITEM_OFFSET + 500 + 3, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,), ), ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: ZorkGrandInquisitorItemData( statemap_keys=(9662,), - archipelago_id=ITEM_OFFSET + 500 + 3, - classification=ItemClassification.filler, + archipelago_id=ITEM_OFFSET + 500 + 4, + classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION,), ), # Totems @@ -783,65 +825,23 @@ class ZorkGrandInquisitorItemData(NamedTuple): tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), - # Logic Helpers - These virtual items are granted to the player conditionally to simplify logic where possible - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_CROSSROADS: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 0, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 1, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 2, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 3, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 4, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 5, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT: ZorkGrandInquisitorItemData( - statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 6, - classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), - ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE: ZorkGrandInquisitorItemData( + # Goal Items + ZorkGrandInquisitorItems.COCONUT_OF_QUENDOR: ZorkGrandInquisitorItemData( statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 7, + archipelago_id=ITEM_OFFSET + 800 + 0, classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + tags=(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.CUBE_OF_FOUNDATION: ZorkGrandInquisitorItemData( statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 8, + archipelago_id=ITEM_OFFSET + 800 + 1, classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + tags=(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS,), ), - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.SKULL_OF_YORUK: ZorkGrandInquisitorItemData( statemap_keys=None, - archipelago_id=ITEM_OFFSET + 900 + 9, + archipelago_id=ITEM_OFFSET + 800 + 2, classification=ItemClassification.progression, - tags=(ZorkGrandInquisitorTags.LOGIC_HELPER,), + tags=(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS,), ), } diff --git a/worlds/zork_grand_inquisitor/data/location_data.py b/worlds/zork_grand_inquisitor/data/location_data.py index 8b4e57392de8..e259a467bf29 100644 --- a/worlds/zork_grand_inquisitor/data/location_data.py +++ b/worlds/zork_grand_inquisitor/data/location_data.py @@ -16,6 +16,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): Tuple[str, str], Tuple[int, int], Tuple[int, Tuple[int, ...]], + Tuple[Tuple[int, ...], int], ], ..., ] @@ -61,8 +62,8 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.CIGAR, ), ), ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED: ZorkGrandInquisitorLocationData( @@ -76,7 +77,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 3, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=(ZorkGrandInquisitorEvents.KNOWS_OBIDIL,), + requirements=(ZorkGrandInquisitorItems.SPELL_OBIDIL,), ), ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE: ZorkGrandInquisitorLocationData( game_state_trigger=((9124, 1),), @@ -155,46 +156,34 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorLocations.BROG_DO_GOOD: ZorkGrandInquisitorLocationData( game_state_trigger=((2644, 1),), archipelago_id=LOCATION_OFFSET + 12, - region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.TOTEM_BROG, ZorkGrandInquisitorItems.BROGS_GRUE_EGG, ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, ) ), ZorkGrandInquisitorLocations.BROG_EAT_ROCKS: ZorkGrandInquisitorLocationData( game_state_trigger=((2629, 1),), archipelago_id=LOCATION_OFFSET + 13, - region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=( - ZorkGrandInquisitorItems.TOTEM_BROG, - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, - ) ), ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB: ZorkGrandInquisitorLocationData( game_state_trigger=((2650, 1),), archipelago_id=LOCATION_OFFSET + 14, - region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=( - ZorkGrandInquisitorItems.TOTEM_BROG, - ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, - ) + requirements=(ZorkGrandInquisitorItems.BROGS_GRUE_EGG,), ), ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME: ZorkGrandInquisitorLocationData( game_state_trigger=((15715, 1),), archipelago_id=LOCATION_OFFSET + 15, - region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.TOTEM_BROG, ZorkGrandInquisitorItems.BROGS_GRUE_EGG, ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, - ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, ZorkGrandInquisitorItems.BROGS_PLANK, ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, ) @@ -218,9 +207,22 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "cd6k"), (1673, 1), (1660, 1), (1312, 1)), + archipelago_id=LOCATION_OFFSET + 19, + region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, + ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, + ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, + ), + ), ZorkGrandInquisitorLocations.CRISIS_AVERTED: ZorkGrandInquisitorLocationData( game_state_trigger=((11769, 1),), - archipelago_id=LOCATION_OFFSET + 19, + archipelago_id=LOCATION_OFFSET + 20, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -232,13 +234,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.CUT_THAT_OUT_YOU_LITTLE_CREEP: ZorkGrandInquisitorLocationData( game_state_trigger=((19350, 1),), - archipelago_id=LOCATION_OFFSET + 20, + archipelago_id=LOCATION_OFFSET + 21, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER: ZorkGrandInquisitorLocationData( game_state_trigger=((17632, 1),), - archipelago_id=LOCATION_OFFSET + 21, + archipelago_id=LOCATION_OFFSET + 22, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -246,12 +248,6 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SPELL_GOLGATEM, ), ), - ZorkGrandInquisitorLocations.DESPERATELY_SEEKING_TUTOR: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "tr2q"),), - archipelago_id=LOCATION_OFFSET + 22, - region=ZorkGrandInquisitorRegions.GUE_TECH, - tags=(ZorkGrandInquisitorTags.CORE,), - ), ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "hp5e"), (8919, 2), (9, 100)), archipelago_id=LOCATION_OFFSET + 23, @@ -335,8 +331,8 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.CIGAR, ), ), ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE: ZorkGrandInquisitorLocationData( @@ -379,33 +375,27 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), - ZorkGrandInquisitorLocations.GUE_TECH_DEANS_LIST: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "tr2k"),), + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "sg6e"), (17103, 1), (15715, 1), (15707, 1)), archipelago_id=LOCATION_OFFSET + 39, - region=ZorkGrandInquisitorRegions.GUE_TECH, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ZorkGrandInquisitorItems.BROGS_GRUE_EGG, + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ZorkGrandInquisitorItems.BROGS_PLANK, + ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ) ), ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM: ZorkGrandInquisitorLocationData( game_state_trigger=((11082, 1), (11307, 1), (11536, 1)), archipelago_id=LOCATION_OFFSET + 40, - region=ZorkGrandInquisitorRegions.GUE_TECH, - tags=(ZorkGrandInquisitorTags.CORE,), - ), - ZorkGrandInquisitorLocations.GUE_TECH_HEALTH_MEMO: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "tr2j"),), - archipelago_id=LOCATION_OFFSET + 41, - region=ZorkGrandInquisitorRegions.GUE_TECH, - tags=(ZorkGrandInquisitorTags.CORE,), - ), - ZorkGrandInquisitorLocations.GUE_TECH_MAGEMEISTERS: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "tr2n"),), - archipelago_id=LOCATION_OFFSET + 42, - region=ZorkGrandInquisitorRegions.GUE_TECH, + region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY: ZorkGrandInquisitorLocationData( game_state_trigger=((8443, 1),), - archipelago_id=LOCATION_OFFSET + 43, + archipelago_id=LOCATION_OFFSET + 41, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -415,13 +405,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING: ZorkGrandInquisitorLocationData( game_state_trigger=((4698, 1),), - archipelago_id=LOCATION_OFFSET + 44, + archipelago_id=LOCATION_OFFSET + 42, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE: ZorkGrandInquisitorLocationData( game_state_trigger=((10421, 1),), - archipelago_id=LOCATION_OFFSET + 45, + archipelago_id=LOCATION_OFFSET + 43, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -431,7 +421,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.HEY_FREE_DIRT: ZorkGrandInquisitorLocationData( game_state_trigger=((11747, 1),), - archipelago_id=LOCATION_OFFSET + 46, + archipelago_id=LOCATION_OFFSET + 44, region=ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -439,40 +429,28 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SHOVEL, ), ), - ZorkGrandInquisitorLocations.HI_MY_NAME_IS_DOUG: ZorkGrandInquisitorLocationData( - game_state_trigger=((4698, 2),), - archipelago_id=LOCATION_OFFSET + 47, - region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, - tags=(ZorkGrandInquisitorTags.CORE,), - ), ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "mt2h"),), - archipelago_id=LOCATION_OFFSET + 48, + archipelago_id=LOCATION_OFFSET + 45, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), ), - ZorkGrandInquisitorLocations.HOLD_ON_FOR_AN_IMPORTANT_MESSAGE: ZorkGrandInquisitorLocationData( - game_state_trigger=((4698, 5),), - archipelago_id=LOCATION_OFFSET + 49, - region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, - tags=(ZorkGrandInquisitorTags.CORE,), - ), ZorkGrandInquisitorLocations.HOW_TO_HYPNOTIZE_YOURSELF: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "uh1e"),), - archipelago_id=LOCATION_OFFSET + 50, + archipelago_id=LOCATION_OFFSET + 46, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HOW_TO_WIN_AT_DOUBLE_FANUCCI: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3s"),), - archipelago_id=LOCATION_OFFSET + 51, + archipelago_id=LOCATION_OFFSET + 47, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.IMBUE_BEBURTT: ZorkGrandInquisitorLocationData( - game_state_trigger=((194, 1),), - archipelago_id=LOCATION_OFFSET + 52, + game_state_trigger=((12166, 1),), + archipelago_id=LOCATION_OFFSET + 48, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -482,13 +460,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.IM_COMPLETELY_NUDE: ZorkGrandInquisitorLocationData( game_state_trigger=((19344, 1),), - archipelago_id=LOCATION_OFFSET + 53, + archipelago_id=LOCATION_OFFSET + 49, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE: ZorkGrandInquisitorLocationData( game_state_trigger=((13060, 1),), - archipelago_id=LOCATION_OFFSET + 54, + archipelago_id=LOCATION_OFFSET + 50, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -498,14 +476,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((12967, 1),), - archipelago_id=LOCATION_OFFSET + 55, + archipelago_id=LOCATION_OFFSET + 51, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_IGRAM,), ), ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE: ZorkGrandInquisitorLocationData( game_state_trigger=((12931, 1),), - archipelago_id=LOCATION_OFFSET + 56, + archipelago_id=LOCATION_OFFSET + 52, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -515,7 +493,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST: ZorkGrandInquisitorLocationData( game_state_trigger=((13062, 1),), - archipelago_id=LOCATION_OFFSET + 57, + archipelago_id=LOCATION_OFFSET + 53, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -525,13 +503,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe3j"),), - archipelago_id=LOCATION_OFFSET + 58, + archipelago_id=LOCATION_OFFSET + 54, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY: ZorkGrandInquisitorLocationData( game_state_trigger=((3816, 1008),), - archipelago_id=LOCATION_OFFSET + 59, + archipelago_id=LOCATION_OFFSET + 55, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -541,24 +519,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.I_DONT_WANT_NO_TROUBLE: ZorkGrandInquisitorLocationData( game_state_trigger=((10694, 1),), - archipelago_id=LOCATION_OFFSET + 60, + archipelago_id=LOCATION_OFFSET + 56, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), - ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE: ZorkGrandInquisitorLocationData( - game_state_trigger=((9637, 1),), - archipelago_id=LOCATION_OFFSET + 61, - region=ZorkGrandInquisitorRegions.SUBWAY_MONASTERY, - tags=(ZorkGrandInquisitorTags.CORE,), - requirements=( - ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorEvents.ROPE_GLORFABLE, - ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT, - ), - ), ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE: ZorkGrandInquisitorLocationData( game_state_trigger=((16374, 1),), - archipelago_id=LOCATION_OFFSET + 62, + archipelago_id=LOCATION_OFFSET + 57, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -571,32 +538,32 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tp1e"), (9, 87), (1011, 1)), - archipelago_id=LOCATION_OFFSET + 63, + archipelago_id=LOCATION_OFFSET + 58, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,), ), ZorkGrandInquisitorLocations.LIT_SUNFLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((4129, 1),), - archipelago_id=LOCATION_OFFSET + 64, + archipelago_id=LOCATION_OFFSET + 59, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_THROCK,), ), - ZorkGrandInquisitorLocations.MAGIC_FOREVER: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "pc1e"), (10304, 1), (5221, 1)), - archipelago_id=LOCATION_OFFSET + 65, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + ZorkGrandInquisitorLocations.LOOK_AN_ICE_CREAM_BAR: ZorkGrandInquisitorLocationData( + game_state_trigger=((12517, 1),), + archipelago_id=LOCATION_OFFSET + 60, + region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE, - ZorkGrandInquisitorItems.ROPE, - ZorkGrandInquisitorItems.HOTSPOT_WELL, - ), + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ) ), ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((2498, (1, 2)),), - archipelago_id=LOCATION_OFFSET + 66, + archipelago_id=LOCATION_OFFSET + 61, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -606,8 +573,8 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR: ZorkGrandInquisitorLocationData( - game_state_trigger=((8623, 21),), - archipelago_id=LOCATION_OFFSET + 67, + game_state_trigger=(((8623, 8734), 21),), + archipelago_id=LOCATION_OFFSET + 62, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -617,7 +584,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.MEAD_LIGHT: ZorkGrandInquisitorLocationData( game_state_trigger=((10485, 1),), - archipelago_id=LOCATION_OFFSET + 68, + archipelago_id=LOCATION_OFFSET + 63, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -627,13 +594,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.MIKES_PANTS: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tr2p"),), - archipelago_id=LOCATION_OFFSET + 69, + archipelago_id=LOCATION_OFFSET + 64, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData( game_state_trigger=((4217, 1),), - archipelago_id=LOCATION_OFFSET + 70, + archipelago_id=LOCATION_OFFSET + 65, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -643,7 +610,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NATIONAL_TREASURE: ZorkGrandInquisitorLocationData( game_state_trigger=((14318, 1),), - archipelago_id=LOCATION_OFFSET + 71, + archipelago_id=LOCATION_OFFSET + 66, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -654,13 +621,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1p"),), - archipelago_id=LOCATION_OFFSET + 72, + archipelago_id=LOCATION_OFFSET + 67, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO: ZorkGrandInquisitorLocationData( game_state_trigger=((12706, 1),), - archipelago_id=LOCATION_OFFSET + 73, + archipelago_id=LOCATION_OFFSET + 68, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -671,51 +638,51 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE: ZorkGrandInquisitorLocationData( game_state_trigger=((4237, 1),), - archipelago_id=LOCATION_OFFSET + 74, + archipelago_id=LOCATION_OFFSET + 69, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.CIGAR, ), ), ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT: ZorkGrandInquisitorLocationData( game_state_trigger=((8935, 1),), - archipelago_id=LOCATION_OFFSET + 75, + archipelago_id=LOCATION_OFFSET + 70, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,), + requirements=(ZorkGrandInquisitorItems.SPELL_SNAVIG,), ), ZorkGrandInquisitorLocations.NO_AUTOGRAPHS: ZorkGrandInquisitorLocationData( game_state_trigger=((10476, 1),), - archipelago_id=LOCATION_OFFSET + 76, + archipelago_id=LOCATION_OFFSET + 71, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,), ), ZorkGrandInquisitorLocations.NO_BONDAGE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe2e"), (10262, 2), (15150, 83)), - archipelago_id=LOCATION_OFFSET + 77, + archipelago_id=LOCATION_OFFSET + 72, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( - ZorkGrandInquisitorItems.ROPE, + ZorkGrandInquisitorEvents.ROPE_GLORFABLE, ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, ), ), ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP: ZorkGrandInquisitorLocationData( game_state_trigger=((12164, 1),), - archipelago_id=LOCATION_OFFSET + 78, + archipelago_id=LOCATION_OFFSET + 73, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL, ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.SANDWITCH_WRAPPER, ), ), ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=((1300, 1),), - archipelago_id=LOCATION_OFFSET + 79, + archipelago_id=LOCATION_OFFSET + 74, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -727,7 +694,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS: ZorkGrandInquisitorLocationData( game_state_trigger=((2448, 1),), - archipelago_id=LOCATION_OFFSET + 80, + archipelago_id=LOCATION_OFFSET + 75, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -737,41 +704,40 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU: ZorkGrandInquisitorLocationData( game_state_trigger=((4869, 1),), - archipelago_id=LOCATION_OFFSET + 81, + archipelago_id=LOCATION_OFFSET + 76, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.FLATHEADIA_FUDGE, + ZorkGrandInquisitorItems.COCOA_INGREDIENTS, ZorkGrandInquisitorItems.HUNGUS_LARD, - ZorkGrandInquisitorItems.JAR_OF_HOTBUGS, - ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB, - ZorkGrandInquisitorItems.MOSS_OF_MAREILON, - ZorkGrandInquisitorItems.MUG, ), ), ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER: ZorkGrandInquisitorLocationData( game_state_trigger=((4512, 32),), - archipelago_id=LOCATION_OFFSET + 82, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, # This can be done anywhere if the item requirement is met + archipelago_id=LOCATION_OFFSET + 77, + region=ZorkGrandInquisitorRegions.ANYWHERE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,), ), ZorkGrandInquisitorLocations.ONLY_YOU_CAN_PREVENT_FOOZLE_FIRES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe5n"),), - archipelago_id=LOCATION_OFFSET + 83, + archipelago_id=LOCATION_OFFSET + 78, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((8730, 1),), - archipelago_id=LOCATION_OFFSET + 84, + archipelago_id=LOCATION_OFFSET + 79, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,), + requirements=( + ZorkGrandInquisitorItems.SPELL_SNAVIG, + ZorkGrandInquisitorItems.TOTEM_BROG, # Visually hiding this totem is tied to owning it; no choice + ), ), ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES: ZorkGrandInquisitorLocationData( game_state_trigger=((4241, 1),), - archipelago_id=LOCATION_OFFSET + 85, + archipelago_id=LOCATION_OFFSET + 80, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -782,25 +748,25 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.PERMASEAL: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "mt1g"),), - archipelago_id=LOCATION_OFFSET + 86, + archipelago_id=LOCATION_OFFSET + 81, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PLANETFALL: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pp1j"),), - archipelago_id=LOCATION_OFFSET + 87, + archipelago_id=LOCATION_OFFSET + 82, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PLEASE_DONT_THROCK_THE_GRASS: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "te1g"),), - archipelago_id=LOCATION_OFFSET + 88, - region=ZorkGrandInquisitorRegions.GUE_TECH, + archipelago_id=LOCATION_OFFSET + 83, + region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL: ZorkGrandInquisitorLocationData( game_state_trigger=((9404, 1),), - archipelago_id=LOCATION_OFFSET + 89, + archipelago_id=LOCATION_OFFSET + 84, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -812,7 +778,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.PROZORKED: ZorkGrandInquisitorLocationData( game_state_trigger=((4115, 1),), - archipelago_id=LOCATION_OFFSET + 90, + archipelago_id=LOCATION_OFFSET + 85, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -822,7 +788,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG: ZorkGrandInquisitorLocationData( game_state_trigger=((4512, 98),), - archipelago_id=LOCATION_OFFSET + 91, + archipelago_id=LOCATION_OFFSET + 86, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -831,27 +797,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.HOTSPOT_MIRROR, ), ), - ZorkGrandInquisitorLocations.RESTOCKED_ON_GRUESDAY: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "tr2h"),), - archipelago_id=LOCATION_OFFSET + 92, - region=ZorkGrandInquisitorRegions.GUE_TECH, - tags=(ZorkGrandInquisitorTags.CORE,), - ), ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE: ZorkGrandInquisitorLocationData( game_state_trigger=((4698, 3),), - archipelago_id=LOCATION_OFFSET + 93, - region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, - tags=(ZorkGrandInquisitorTags.CORE,), - ), - ZorkGrandInquisitorLocations.RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE: ZorkGrandInquisitorLocationData( - game_state_trigger=((4698, 4),), - archipelago_id=LOCATION_OFFSET + 94, + archipelago_id=LOCATION_OFFSET + 87, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.SNAVIG_REPAIRED: ZorkGrandInquisitorLocationData( - game_state_trigger=((201, 1),), - archipelago_id=LOCATION_OFFSET + 95, + game_state_trigger=((12161, 1),), + archipelago_id=LOCATION_OFFSET + 88, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -861,7 +815,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.SOUVENIR: ZorkGrandInquisitorLocationData( game_state_trigger=((13408, 1),), - archipelago_id=LOCATION_OFFSET + 96, + archipelago_id=LOCATION_OFFSET + 89, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -871,7 +825,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((9719, 1),), - archipelago_id=LOCATION_OFFSET + 97, + archipelago_id=LOCATION_OFFSET + 90, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -882,7 +836,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData( game_state_trigger=((14511, 1), (14524, 5)), - archipelago_id=LOCATION_OFFSET + 98, + archipelago_id=LOCATION_OFFSET + 91, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -896,31 +850,33 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.SUCKING_ROCKS: ZorkGrandInquisitorLocationData( game_state_trigger=((12859, 1),), - archipelago_id=LOCATION_OFFSET + 99, + archipelago_id=LOCATION_OFFSET + 92, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE, ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT, ), ), ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR: ZorkGrandInquisitorLocationData( game_state_trigger=((10299, 1),), - archipelago_id=LOCATION_OFFSET + 100, + archipelago_id=LOCATION_OFFSET + 93, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,), ), ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1h"),), - archipelago_id=LOCATION_OFFSET + 101, + archipelago_id=LOCATION_OFFSET + 94, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THAR_SHE_BLOWS: ZorkGrandInquisitorLocationData( game_state_trigger=((1311, 1), (1312, 1)), - archipelago_id=LOCATION_OFFSET + 102, + archipelago_id=LOCATION_OFFSET + 95, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -933,61 +889,71 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THATS_A_ROPE: ZorkGrandInquisitorLocationData( game_state_trigger=((10486, 1),), - archipelago_id=LOCATION_OFFSET + 103, + archipelago_id=LOCATION_OFFSET + 96, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( - ZorkGrandInquisitorItems.ROPE, + ZorkGrandInquisitorEvents.ROPE_GLORFABLE, ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, ), ), ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS: ZorkGrandInquisitorLocationData( game_state_trigger=((13805, 1),), - archipelago_id=LOCATION_OFFSET + 104, + archipelago_id=LOCATION_OFFSET + 97, region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), ), ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tp1e"), (9, 83), (1011, 1)), - archipelago_id=LOCATION_OFFSET + 105, + archipelago_id=LOCATION_OFFSET + 98, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorEvents.ROPE_GLORFABLE,), ), ZorkGrandInquisitorLocations.THATS_THE_SPIRIT: ZorkGrandInquisitorLocationData( game_state_trigger=((10341, 95),), - archipelago_id=LOCATION_OFFSET + 106, + archipelago_id=LOCATION_OFFSET + 99, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS,), ), ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE: ZorkGrandInquisitorLocationData( game_state_trigger=((9459, 1),), - archipelago_id=LOCATION_OFFSET + 107, + archipelago_id=LOCATION_OFFSET + 100, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE: ZorkGrandInquisitorLocationData( game_state_trigger=((9473, 1),), - archipelago_id=LOCATION_OFFSET + 108, + archipelago_id=LOCATION_OFFSET + 101, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO: ZorkGrandInquisitorLocationData( game_state_trigger=((9520, 1),), - archipelago_id=LOCATION_OFFSET + 109, + archipelago_id=LOCATION_OFFSET + 102, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), + ZorkGrandInquisitorLocations.THE_ONLY_WAY_TO_WIN_IS_NOT_TO_PLAY: ZorkGrandInquisitorLocationData( + game_state_trigger=((16286, 1),), + archipelago_id=LOCATION_OFFSET + 103, + region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE, + ZorkGrandInquisitorItems.SPELL_KENDALL, + ), + ), ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "me1j"),), - archipelago_id=LOCATION_OFFSET + 110, + archipelago_id=LOCATION_OFFSET + 104, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND: ZorkGrandInquisitorLocationData( game_state_trigger=((13167, 1),), - archipelago_id=LOCATION_OFFSET + 111, + archipelago_id=LOCATION_OFFSET + 105, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -997,14 +963,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "cd60"), (1524, 1)), - archipelago_id=LOCATION_OFFSET + 112, + archipelago_id=LOCATION_OFFSET + 106, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.TOTEM_LUCY,), ), ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData( game_state_trigger=((4219, 1),), - archipelago_id=LOCATION_OFFSET + 113, + archipelago_id=LOCATION_OFFSET + 107, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1015,34 +981,34 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3z"),), - archipelago_id=LOCATION_OFFSET + 114, + archipelago_id=LOCATION_OFFSET + 108, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.TOTEMIZED_DAILY_BILLBOARD: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "px1h"),), - archipelago_id=LOCATION_OFFSET + 115, + archipelago_id=LOCATION_OFFSET + 109, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "cd60"), (1520, 1)), - archipelago_id=LOCATION_OFFSET + 116, + archipelago_id=LOCATION_OFFSET + 110, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.TOTEM_BROG,), ), ZorkGrandInquisitorLocations.UMBRELLA_FLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((12926, 1),), - archipelago_id=LOCATION_OFFSET + 117, + archipelago_id=LOCATION_OFFSET + 111, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=(ZorkGrandInquisitorEvents.KNOWS_BEBURTT,), + requirements=(ZorkGrandInquisitorItems.SPELL_BEBURTT,), ), ZorkGrandInquisitorLocations.UP: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 5200),), - archipelago_id=LOCATION_OFFSET + 118, + archipelago_id=LOCATION_OFFSET + 112, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1052,14 +1018,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.USELESS_BUT_FUN: ZorkGrandInquisitorLocationData( game_state_trigger=((14321, 1),), - archipelago_id=LOCATION_OFFSET + 119, + archipelago_id=LOCATION_OFFSET + 113, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_GOLGATEM,), ), ZorkGrandInquisitorLocations.UUUUUP: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 3500),), - archipelago_id=LOCATION_OFFSET + 120, + archipelago_id=LOCATION_OFFSET + 114, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1069,13 +1035,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "uh1h"),), - archipelago_id=LOCATION_OFFSET + 121, + archipelago_id=LOCATION_OFFSET + 115, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO: ZorkGrandInquisitorLocationData( game_state_trigger=((4034, 1),), - archipelago_id=LOCATION_OFFSET + 122, + archipelago_id=LOCATION_OFFSET + 116, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1087,7 +1053,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE: ZorkGrandInquisitorLocationData( game_state_trigger=((2461, 1),), - archipelago_id=LOCATION_OFFSET + 123, + archipelago_id=LOCATION_OFFSET + 117, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1097,7 +1063,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER: ZorkGrandInquisitorLocationData( game_state_trigger=((15472, 1),), - archipelago_id=LOCATION_OFFSET + 124, + archipelago_id=LOCATION_OFFSET + 118, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1111,7 +1077,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID: ZorkGrandInquisitorLocationData( game_state_trigger=((10484, 1),), - archipelago_id=LOCATION_OFFSET + 125, + archipelago_id=LOCATION_OFFSET + 119, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1121,7 +1087,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL: ZorkGrandInquisitorLocationData( game_state_trigger=((4983, 1),), - archipelago_id=LOCATION_OFFSET + 126, + archipelago_id=LOCATION_OFFSET + 120, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1131,20 +1097,20 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dc10"), (1596, 1)), - archipelago_id=LOCATION_OFFSET + 127, + archipelago_id=LOCATION_OFFSET + 121, region=ZorkGrandInquisitorRegions.WALKING_CASTLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dm2g"),), - archipelago_id=LOCATION_OFFSET + 128, + archipelago_id=LOCATION_OFFSET + 122, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.HOTSPOT_MIRROR,), ), ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dg4e"), (4266, 1), (9, 21), (4035, 1)), - archipelago_id=LOCATION_OFFSET + 129, + archipelago_id=LOCATION_OFFSET + 123, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1154,14 +1120,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: ZorkGrandInquisitorLocationData( game_state_trigger=((16405, 1),), - archipelago_id=LOCATION_OFFSET + 130, + archipelago_id=LOCATION_OFFSET + 124, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.SPELL_REZROV,), ), ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS: ZorkGrandInquisitorLocationData( game_state_trigger=((16342, 1),), - archipelago_id=LOCATION_OFFSET + 131, + archipelago_id=LOCATION_OFFSET + 125, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1169,15 +1135,29 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, ), ), + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "qs1e"), (14511, 1), (14524, 5)), + archipelago_id=LOCATION_OFFSET + 126, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_1, + ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, + ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, + ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ), + ), ZorkGrandInquisitorLocations.YOU_ONE_OF_THEM_AGITATORS_AINT_YA: ZorkGrandInquisitorLocationData( game_state_trigger=((10586, 1),), - archipelago_id=LOCATION_OFFSET + 132, + archipelago_id=LOCATION_OFFSET + 127, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY: ZorkGrandInquisitorLocationData( game_state_trigger=((15151, 1),), - archipelago_id=LOCATION_OFFSET + 133, + archipelago_id=LOCATION_OFFSET + 128, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH,), @@ -1189,8 +1169,8 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.CIGAR, ), ), ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES: ZorkGrandInquisitorLocationData( @@ -1208,21 +1188,24 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 200 + 2, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=(ZorkGrandInquisitorItems.WELL_ROPE,), ), ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 18)), archipelago_id=LOCATION_OFFSET + 200 + 3, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, - tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), + region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( - ZorkGrandInquisitorItems.ROPE, - ZorkGrandInquisitorItems.HOTSPOT_WELL, + ( + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.TOTEM_LUCY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_JUMPED_IN_BOTTOMLESS_PIT: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 3)), archipelago_id=LOCATION_OFFSET + 200 + 4, - region=ZorkGrandInquisitorRegions.GUE_TECH, + region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.DEATHSANITY,), ), ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData( @@ -1242,7 +1225,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 23)), archipelago_id=LOCATION_OFFSET + 200 + 6, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + region=ZorkGrandInquisitorRegions.ANYWHERE, tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,), ), @@ -1288,44 +1271,253 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 34)), archipelago_id=LOCATION_OFFSET + 200 + 11, - region=ZorkGrandInquisitorRegions.GUE_TECH, + region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.SPELL_THROCK, ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS, ), ), - ZorkGrandInquisitorLocations.DEATH_TOTEMIZED: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "gjde"), (2201, (9, 32, 33))), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_INFINITY: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 9)), archipelago_id=LOCATION_OFFSET + 200 + 12, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, ), ), - ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "gjde"), (2201, (5, 6, 7, 8, 13))), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_NEWARK_NEW_JERSEY: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 33)), archipelago_id=LOCATION_OFFSET + 200 + 13, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.DEATHSANITY,), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_HALLS_OF_INQUISITION: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 8)), + archipelago_id=LOCATION_OFFSET + 200 + 14, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_INFINITY: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 7)), + archipelago_id=LOCATION_OFFSET + 200 + 15, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_NEWARK_NEW_JERSEY: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 6)), + archipelago_id=LOCATION_OFFSET + 200 + 16, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 5)), + archipelago_id=LOCATION_OFFSET + 200 + 17, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_SURFACE_OF_MERZ: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 13)), + archipelago_id=LOCATION_OFFSET + 200 + 18, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), + ), + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_SURFACE_OF_MERZ: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "gjde"), (2201, 32)), + archipelago_id=LOCATION_OFFSET + 200 + 19, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.DEATHSANITY,), + requirements=( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), ), ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 10)), - archipelago_id=LOCATION_OFFSET + 200 + 14, + archipelago_id=LOCATION_OFFSET + 200 + 20, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), - requirements=(ZorkGrandInquisitorEvents.KNOWS_SNAVIG,), + requirements=(ZorkGrandInquisitorItems.SPELL_SNAVIG,), ), ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 19)), - archipelago_id=LOCATION_OFFSET + 200 + 15, + archipelago_id=LOCATION_OFFSET + 200 + 21, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED,), ), + # Landmarksanity + ZorkGrandInquisitorLocations.LANDMARK_DRAGON_ARCHIPELAGO: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "cd60"),), + archipelago_id=LOCATION_OFFSET + 300 + 0, + region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + requirements=( + ( + ZorkGrandInquisitorItems.TOTEM_BROG, + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.TOTEM_LUCY, + ), + ), + ), + ZorkGrandInquisitorLocations.LANDMARK_DUNGEON_MASTERS_HOUSE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "dg40"),), + archipelago_id=LOCATION_OFFSET + 300 + 1, + region=ZorkGrandInquisitorRegions.DM_LAIR, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_FLOOD_CONTROL_DAM_3: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "ue1e"),), + archipelago_id=LOCATION_OFFSET + 300 + 2, + region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_GATES_OF_HELL: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "hp50"),), + archipelago_id=LOCATION_OFFSET + 300 + 3, + region=ZorkGrandInquisitorRegions.HADES, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_GREAT_UNDERGROUND_EMPIRE_ENTRANCE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "uw10"),), + archipelago_id=LOCATION_OFFSET + 300 + 4, + region=ZorkGrandInquisitorRegions.CROSSROADS, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_INSIDE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "tr10"),), + archipelago_id=LOCATION_OFFSET + 300 + 5, + region=ZorkGrandInquisitorRegions.GUE_TECH, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_OUTSIDE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "te50"),), + archipelago_id=LOCATION_OFFSET + 300 + 6, + region=ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_HADES_SHORE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "hp10"),), + archipelago_id=LOCATION_OFFSET + 300 + 7, + region=ZorkGrandInquisitorRegions.HADES_SHORE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_INFINITE_CORRIDOR: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "th10"),), + archipelago_id=LOCATION_OFFSET + 300 + 8, + region=ZorkGrandInquisitorRegions.GUE_TECH, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_INQUISITION_HEADQUARTERS: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "px10"),), + archipelago_id=LOCATION_OFFSET + 300 + 9, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_JACKS_SHOP: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "pp10"),), + archipelago_id=LOCATION_OFFSET + 300 + 10, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_MIRROR_ROOM: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "dm10"),), + archipelago_id=LOCATION_OFFSET + 300 + 11, + region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + requirements=(ZorkGrandInquisitorItems.HOTSPOT_MIRROR,), + ), + ZorkGrandInquisitorLocations.LANDMARK_PAST_PORT_FOOZLE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "qe10"),), + archipelago_id=LOCATION_OFFSET + 300 + 12, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + requirements=( + ( + ZorkGrandInquisitorItems.TOTEM_BROG, + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.TOTEM_LUCY, + ), + ), + ), + ZorkGrandInquisitorLocations.LANDMARK_PORT_FOOZLE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "pe10"),), + archipelago_id=LOCATION_OFFSET + 300 + 13, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_SPELL_CHECKER: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "tp40"),), + archipelago_id=LOCATION_OFFSET + 300 + 14, + region=ZorkGrandInquisitorRegions.SPELL_LAB, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_TOTEMIZER: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "mt10"),), + archipelago_id=LOCATION_OFFSET + 300 + 15, + region=ZorkGrandInquisitorRegions.MONASTERY, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_UMBRELLA_TREE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "uc1e"),), + archipelago_id=LOCATION_OFFSET + 300 + 16, + region=ZorkGrandInquisitorRegions.CROSSROADS, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_UNDERGROUND_UNDERGROUND_ENTRANCE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "uc60"),), + archipelago_id=LOCATION_OFFSET + 300 + 17, + region=ZorkGrandInquisitorRegions.CROSSROADS, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_WALKING_CASTLES_HEART: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "dc1h"), (1596, 1)), + archipelago_id=LOCATION_OFFSET + 300 + 18, + region=ZorkGrandInquisitorRegions.WALKING_CASTLE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + ), + ZorkGrandInquisitorLocations.LANDMARK_WHITE_HOUSE: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "sw40"),), + archipelago_id=LOCATION_OFFSET + 300 + 19, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE, + tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), + requirements=( + ( + ZorkGrandInquisitorItems.TOTEM_BROG, + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.TOTEM_LUCY, + ), + ), + ), # Events ZorkGrandInquisitorEvents.CHARON_CALLED: ZorkGrandInquisitorLocationData( game_state_trigger=None, @@ -1337,16 +1529,6 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), event_item_name=ZorkGrandInquisitorEvents.CHARON_CALLED.value, ), - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, - requirements=( - ZorkGrandInquisitorItems.LANTERN, - ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, - ), - event_item_name=ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE.value, - ), ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE: ZorkGrandInquisitorLocationData( game_state_trigger=None, archipelago_id=None, @@ -1386,8 +1568,8 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=None, region=ZorkGrandInquisitorRegions.DM_LAIR, requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.CIGAR, ), event_item_name=ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value, ), @@ -1424,65 +1606,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), event_item_name=ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value, ), - ZorkGrandInquisitorEvents.KNOWS_BEBURTT: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.SPELL_LAB, - requirements=( - ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, - ), - event_item_name=ZorkGrandInquisitorEvents.KNOWS_BEBURTT.value, - ), - ZorkGrandInquisitorEvents.KNOWS_OBIDIL: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.SPELL_LAB, - requirements=( - ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, - ), - event_item_name=ZorkGrandInquisitorEvents.KNOWS_OBIDIL.value, - ), - ZorkGrandInquisitorEvents.KNOWS_SNAVIG: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.SPELL_LAB, - requirements=( - ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, - ), - event_item_name=ZorkGrandInquisitorEvents.KNOWS_SNAVIG.value, - ), - ZorkGrandInquisitorEvents.KNOWS_YASTARD: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, - requirements=( - ZorkGrandInquisitorItems.FLATHEADIA_FUDGE, - ZorkGrandInquisitorItems.HUNGUS_LARD, - ZorkGrandInquisitorItems.JAR_OF_HOTBUGS, - ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB, - ZorkGrandInquisitorItems.MOSS_OF_MAREILON, - ZorkGrandInquisitorItems.MUG, - ), - event_item_name=ZorkGrandInquisitorEvents.KNOWS_YASTARD.value, - ), - ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, - requirements=( - ZorkGrandInquisitorEvents.CIGAR_ACCESSIBLE, - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, - ), - event_item_name=ZorkGrandInquisitorEvents.LANTERN_DALBOZ_ACCESSIBLE.value, - ), ZorkGrandInquisitorEvents.ROPE_GLORFABLE: ZorkGrandInquisitorLocationData( game_state_trigger=None, archipelago_id=None, region=ZorkGrandInquisitorRegions.CROSSROADS, - requirements=(ZorkGrandInquisitorItems.SPELL_GLORF,), + requirements=( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.SPELL_GLORF, + ), event_item_name=ZorkGrandInquisitorEvents.ROPE_GLORFABLE.value, ), ZorkGrandInquisitorEvents.VICTORY: ZorkGrandInquisitorLocationData( @@ -1505,7 +1636,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE: ZorkGrandInquisitorLocationData( game_state_trigger=None, archipelago_id=None, - region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + region=ZorkGrandInquisitorRegions.ANYWHERE, requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,), event_item_name=ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE.value, ), @@ -1521,15 +1652,4 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), event_item_name=ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value, ), - ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE: ZorkGrandInquisitorLocationData( - game_state_trigger=None, - archipelago_id=None, - region=ZorkGrandInquisitorRegions.GUE_TECH, - requirements=( - ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, - ), - event_item_name=ZorkGrandInquisitorEvents.ZORK_ROCKS_SUCKABLE.value, - ), } diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 56ef627779aa..0c920e2864ac 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -1,44 +1,412 @@ -from typing import Dict +from typing import Dict, Optional, Tuple from ..enums import ( + ZorkGrandInquisitorGoals, ZorkGrandInquisitorItems, ZorkGrandInquisitorRegions, ZorkGrandInquisitorStartingLocations, ) +# Avoid spells in early items to prevent clash with craftable spells +early_items_for_starting_location: Dict[ + ZorkGrandInquisitorStartingLocations, Optional[Tuple[Tuple[ZorkGrandInquisitorItems, ...], ...]] +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ( + ( + ZorkGrandInquisitorItems.WELL_ROPE, + ), + ), + ZorkGrandInquisitorStartingLocations.CROSSROADS: None, + ZorkGrandInquisitorStartingLocations.DM_LAIR: None, + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ( + ( + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, + ), + ), + ZorkGrandInquisitorStartingLocations.GUE_TECH: None, + ZorkGrandInquisitorStartingLocations.SPELL_LAB: None, + ZorkGrandInquisitorStartingLocations.HADES_SHORE: None, + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: None, + ZorkGrandInquisitorStartingLocations.MONASTERY: None, + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: None, +} + +endgame_connecting_regions_for_goal: Dict[ + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorRegions, +] = { + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: ZorkGrandInquisitorRegions.MENU, + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: ZorkGrandInquisitorRegions.WALKING_CASTLE, + ZorkGrandInquisitorGoals.SPELL_HEIST: ZorkGrandInquisitorRegions.PORT_FOOZLE, + ZorkGrandInquisitorGoals.ZORK_TOUR: ZorkGrandInquisitorRegions.PORT_FOOZLE, + ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: ( + ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, + ), +} -starting_location_to_logic_helper_item: Dict[ - ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorItems +starter_kits_for_starting_location: Dict[ + ZorkGrandInquisitorStartingLocations, Optional[Tuple[Tuple[ZorkGrandInquisitorItems, ...], ...]] ] = { ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE + ( + ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ), ), ZorkGrandInquisitorStartingLocations.CROSSROADS: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_CROSSROADS + ( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ), + ( + ZorkGrandInquisitorItems.SPELL_BEBURTT, + ZorkGrandInquisitorItems.SUBWAY_TOKEN, + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ), + ( + ZorkGrandInquisitorItems.SPELL_REZROV, + ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ), + ( + ZorkGrandInquisitorItems.HAMMER, + ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE, + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.SPELL_THROCK, + ), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS, + ), ), ZorkGrandInquisitorStartingLocations.DM_LAIR: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR + ( + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.HUNGUS_LARD, + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ), + ( + ZorkGrandInquisitorItems.HAMMER, + ZorkGrandInquisitorItems.SPELL_THROCK, + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ), + ( + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.CIGAR, + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES, + ), + ( + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH, + ZorkGrandInquisitorItems.SHOVEL, + ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND, + ), + ( + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.CIGAR, + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.MEAD_LIGHT, + ZorkGrandInquisitorItems.ZIMDOR_SCROLL, + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ), ), ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR + ( + ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.SPELL_OBIDIL, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS, + ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV, + ZorkGrandInquisitorItems.COCOA_INGREDIENTS, + ZorkGrandInquisitorItems.HUNGUS_LARD, + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ZorkGrandInquisitorItems.TOTEM_LUCY, + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ZorkGrandInquisitorItems.TOTEM_BROG, + ZorkGrandInquisitorItems.BROGS_FLICKERING_TORCH, + ZorkGrandInquisitorItems.BROGS_GRUE_EGG, + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ), ), ZorkGrandInquisitorStartingLocations.GUE_TECH: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_GUE_TECH + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND, + ZorkGrandInquisitorItems.SHOVEL, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ), + ( + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ), + ( + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT, + ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.ZORK_ROCKS, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ), + ( + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ), + ( + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.ZORK_ROCKS, + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER, + ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE, + ZorkGrandInquisitorItems.STUDENT_ID, + ), ), ZorkGrandInquisitorStartingLocations.SPELL_LAB: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, + ZorkGrandInquisitorItems.SANDWITCH_WRAPPER, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MONASTERY_ROPE, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.SANDWITCH_WRAPPER, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.SWORD, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM, + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.SANDWITCH_WRAPPER, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ZorkGrandInquisitorItems.SPELL_REZROV, + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ), ), ZorkGrandInquisitorStartingLocations.HADES_SHORE: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.SPELL_SNAVIG, + ZorkGrandInquisitorItems.TOTEM_BROG, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.SPELL_OBIDIL, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM, + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ), + ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ZorkGrandInquisitorItems.SPELL_KENDALL, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MONASTERY_ROPE, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, + ), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, + ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE, + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.HAMMER, + ), ), ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM + ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ZorkGrandInquisitorItems.SPELL_REZROV, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB, + ), + ( + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ), + ( + ZorkGrandInquisitorItems.SPELL_THROCK, + ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ), + ( + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ), + ( + ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.SWORD, + ), ), ZorkGrandInquisitorStartingLocations.MONASTERY: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION, + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, + ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, + ZorkGrandInquisitorItems.OLD_SCRATCH_CARD, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS, + ZorkGrandInquisitorItems.SUBWAY_TOKEN, + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB, + ZorkGrandInquisitorItems.SPELL_REZROV, + ), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.SWORD, + ), ), ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ( - ZorkGrandInquisitorItems.LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, + ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ), + ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES, + ), + ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM, + ), + ( + ZorkGrandInquisitorItems.MAP, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ), ), } diff --git a/worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py b/worlds/zork_grand_inquisitor/data/missable_location_data.py similarity index 56% rename from worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py rename to worlds/zork_grand_inquisitor/data/missable_location_data.py index ef6eacb78ceb..67271e7c28d8 100644 --- a/worlds/zork_grand_inquisitor/data/missable_location_grant_conditions_data.py +++ b/worlds/zork_grand_inquisitor/data/missable_location_data.py @@ -4,7 +4,8 @@ class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple): - location_condition: ZorkGrandInquisitorLocations + game_location_condition: Optional[str] + location_condition: Tuple[ZorkGrandInquisitorLocations, ...] item_conditions: Optional[Tuple[ZorkGrandInquisitorItems, ...]] @@ -13,187 +14,226 @@ class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple): ] = { ZorkGrandInquisitorLocations.BOING_BOING_BOING: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON, + game_location_condition="dg3e", + location_condition=(ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON,), item_conditions=None, ) , ZorkGrandInquisitorLocations.BONK: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.PROZORKED, + game_location_condition="dg2f", + location_condition=(ZorkGrandInquisitorLocations.PROZORKED,), item_conditions=(ZorkGrandInquisitorItems.HAMMER,), ) , ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.ARREST_THE_VANDAL, + game_location_condition="pe6e", + location_condition=(ZorkGrandInquisitorLocations.ARREST_THE_VANDAL,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES, - item_conditions=None, - ) - , - ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE: - ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.MAGIC_FOREVER, + game_location_condition="dg4f", + location_condition=(ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER, + game_location_condition="qs1e", + location_condition=(ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER, + game_location_condition=None, + location_condition=(ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES, + game_location_condition="dg4f", + location_condition=(ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS, + game_location_condition="tp1e", + location_condition=(ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.A_SMALLWAY, + game_location_condition="th10", + location_condition=(ZorkGrandInquisitorLocations.A_SMALLWAY,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.THAR_SHE_BLOWS, + game_location_condition="cd20", + location_condition=(ZorkGrandInquisitorLocations.THAR_SHE_BLOWS,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL, + game_location_condition="hp60", + location_condition=(ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.CRISIS_AVERTED, + game_location_condition="th3j", + location_condition=(ZorkGrandInquisitorLocations.CRISIS_AVERTED,), item_conditions=None, ) , ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE, + game_location_condition="dc10", + location_condition=(ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE,), item_conditions=(ZorkGrandInquisitorItems.SPELL_GOLGATEM,), ) , ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED, + game_location_condition="th10", + location_condition=(ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED,), item_conditions=None, ) , ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS, + game_location_condition="tp1e", + location_condition=(ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,), item_conditions=(ZorkGrandInquisitorItems.SPELL_IGRAM,), ) , ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.PROZORKED, + game_location_condition="dg2f", + location_condition=(ZorkGrandInquisitorLocations.PROZORKED,), item_conditions=(ZorkGrandInquisitorItems.SPELL_THROCK,), ) , ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS, + game_location_condition="tp1e", + location_condition=(ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,), item_conditions=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,), ) , ZorkGrandInquisitorLocations.MEAD_LIGHT: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE, + game_location_condition="pe1e", + location_condition=( + ZorkGrandInquisitorLocations.FIRE_FIRE, + ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO, + ), item_conditions=(ZorkGrandInquisitorItems.MEAD_LIGHT,), ) , ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED, + game_location_condition="dg3e", + location_condition=(ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED,), item_conditions=None, ) , ZorkGrandInquisitorLocations.NO_AUTOGRAPHS: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE, + game_location_condition="pe1e", + location_condition=(ZorkGrandInquisitorLocations.FIRE_FIRE,), item_conditions=None, ) , ZorkGrandInquisitorLocations.NO_BONDAGE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE, - item_conditions=(ZorkGrandInquisitorItems.ROPE,), + game_location_condition="pe20", + location_condition=(ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE,), + item_conditions=( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.SPELL_GLORF, + ), ) , ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE, + game_location_condition="pe5e", + location_condition=(ZorkGrandInquisitorLocations.FIRE_FIRE,), item_conditions=None, ) , ZorkGrandInquisitorLocations.THATS_A_ROPE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE, - item_conditions=(ZorkGrandInquisitorItems.ROPE,), + game_location_condition="pe1e", + location_condition=(ZorkGrandInquisitorLocations.FIRE_FIRE,), + item_conditions=( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.SPELL_GLORF, + ), ) , ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP, + game_location_condition="us2e", + location_condition=(ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP,), item_conditions=None, ) , ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS, - item_conditions=(ZorkGrandInquisitorItems.SPELL_GLORF,), + game_location_condition="tp1e", + location_condition=(ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,), + item_conditions=( + ZorkGrandInquisitorItems.WELL_ROPE, + ZorkGrandInquisitorItems.SPELL_GLORF, + ), ) , ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.FIRE_FIRE, + game_location_condition="pe1e", + location_condition=( + ZorkGrandInquisitorLocations.FIRE_FIRE, + ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE, + ), item_conditions=(ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER,), ) , ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG, + game_location_condition="dw10", + location_condition=(ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG,), item_conditions=None, ) , ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO, + game_location_condition="dv10", + location_condition=(ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO,), item_conditions=(ZorkGrandInquisitorItems.SWORD, ZorkGrandInquisitorItems.HOTSPOT_HARRY), ) , ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS, + game_location_condition="tp1e", + location_condition=(ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS,), item_conditions=(ZorkGrandInquisitorItems.SPELL_REZROV,), ) , ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY: ZorkGrandInquisitorMissableLocationGrantConditionsData( - location_condition=ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE, + game_location_condition="pe20", + location_condition=(ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE,), item_conditions=None, ) , diff --git a/worlds/zork_grand_inquisitor/data/region_data.py b/worlds/zork_grand_inquisitor/data/region_data.py index 308d37f4536d..14f2c992f73e 100644 --- a/worlds/zork_grand_inquisitor/data/region_data.py +++ b/worlds/zork_grand_inquisitor/data/region_data.py @@ -8,10 +8,11 @@ class ZorkGrandInquisitorRegionData(NamedTuple): region_data: Dict[ZorkGrandInquisitorRegions, ZorkGrandInquisitorRegionData] = { + ZorkGrandInquisitorRegions.ANYWHERE: ZorkGrandInquisitorRegionData(exits=None), ZorkGrandInquisitorRegions.CROSSROADS: ZorkGrandInquisitorRegionData( exits=( ZorkGrandInquisitorRegions.DM_LAIR, - ZorkGrandInquisitorRegions.GUE_TECH, + ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.PORT_FOOZLE, @@ -44,19 +45,22 @@ class ZorkGrandInquisitorRegionData(NamedTuple): ) ), ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON: ZorkGrandInquisitorRegionData( - exits=( - ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, - ZorkGrandInquisitorRegions.ENDGAME, - ) + exits=(ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO,) ), ZorkGrandInquisitorRegions.ENDGAME: ZorkGrandInquisitorRegionData(exits=None), ZorkGrandInquisitorRegions.GUE_TECH: ZorkGrandInquisitorRegionData( exits=( - ZorkGrandInquisitorRegions.CROSSROADS, + ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ) ), + ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE: ZorkGrandInquisitorRegionData( + exits=( + ZorkGrandInquisitorRegions.CROSSROADS, + ZorkGrandInquisitorRegions.GUE_TECH, + ) + ), ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY: ZorkGrandInquisitorRegionData( exits=( ZorkGrandInquisitorRegions.GUE_TECH, @@ -126,10 +130,7 @@ class ZorkGrandInquisitorRegionData(NamedTuple): ) ), ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN: ZorkGrandInquisitorRegionData( - exits=( - ZorkGrandInquisitorRegions.ENDGAME, - ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, - ) + exits=(ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST,) ), ZorkGrandInquisitorRegions.SPELL_LAB: ZorkGrandInquisitorRegionData( exits=(ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE,) @@ -174,7 +175,10 @@ class ZorkGrandInquisitorRegionData(NamedTuple): ZorkGrandInquisitorRegions.WHITE_HOUSE: ZorkGrandInquisitorRegionData( exits=( ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, - ZorkGrandInquisitorRegions.ENDGAME, + ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, ) ), + ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR: ZorkGrandInquisitorRegionData( + exits=(ZorkGrandInquisitorRegions.WHITE_HOUSE,) + ), } diff --git a/worlds/zork_grand_inquisitor/data/transform_data.py b/worlds/zork_grand_inquisitor/data/transform_data.py new file mode 100644 index 000000000000..f6b5228d4062 --- /dev/null +++ b/worlds/zork_grand_inquisitor/data/transform_data.py @@ -0,0 +1,180 @@ +from typing import Dict, Optional, Tuple, Union + +from ..enums import ( + ZorkGrandInquisitorDeathsanity, + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorItemTransforms, + ZorkGrandInquisitorItems, + ZorkGrandInquisitorLandmarksanity, + ZorkGrandInquisitorLocations, + ZorkGrandInquisitorLocationTransforms, + ZorkGrandInquisitorStartingLocations, +) + + +item_data_transforms: Dict[ + Union[ + ZorkGrandInquisitorStartingLocations, + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorDeathsanity, + ZorkGrandInquisitorLandmarksanity, + ], + Optional[Dict[ZorkGrandInquisitorItemTransforms, Tuple[ZorkGrandInquisitorItems, ...]]] +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ) + }, + ZorkGrandInquisitorStartingLocations.CROSSROADS: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ) + }, + ZorkGrandInquisitorStartingLocations.DM_LAIR: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, + ) + }, + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR, + ) + }, + ZorkGrandInquisitorStartingLocations.GUE_TECH: None, + ZorkGrandInquisitorStartingLocations.SPELL_LAB: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB, + ) + }, + ZorkGrandInquisitorStartingLocations.HADES_SHORE: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_HADES, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES, + ) + }, + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM, + ) + }, + ZorkGrandInquisitorStartingLocations.MONASTERY: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ) + }, + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ) + }, + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: None, + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: None, + ZorkGrandInquisitorGoals.SPELL_HEIST: None, + ZorkGrandInquisitorGoals.ZORK_TOUR: None, + ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: None, + ZorkGrandInquisitorDeathsanity.OFF: { + ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, + ) + }, + ZorkGrandInquisitorDeathsanity.ON: None, + ZorkGrandInquisitorLandmarksanity.OFF: None, + ZorkGrandInquisitorLandmarksanity.ON: None, +} + +location_data_transforms: Dict[ + Union[ + ZorkGrandInquisitorStartingLocations, + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorDeathsanity, + ZorkGrandInquisitorLandmarksanity, + ], + Optional[Dict[ZorkGrandInquisitorLocationTransforms, Tuple[ZorkGrandInquisitorLocations, ...]]] +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: None, + ZorkGrandInquisitorStartingLocations.CROSSROADS: None, + ZorkGrandInquisitorStartingLocations.DM_LAIR: None, + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: None, + ZorkGrandInquisitorStartingLocations.GUE_TECH: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_INSIDE, + ) + }, + ZorkGrandInquisitorStartingLocations.SPELL_LAB: None, + ZorkGrandInquisitorStartingLocations.HADES_SHORE: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.LANDMARK_HADES_SHORE, + ) + }, + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: None, + ZorkGrandInquisitorStartingLocations.MONASTERY: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.LANDMARK_TOTEMIZER, + ) + }, + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: None, + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: None, + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: None, # TODO: Remember to remove 3 locations of artifacts + ZorkGrandInquisitorGoals.SPELL_HEIST: None, + ZorkGrandInquisitorGoals.ZORK_TOUR: None, + ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: None, + ZorkGrandInquisitorDeathsanity.OFF: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK, + ZorkGrandInquisitorLocations.DEATH_ATTACKED_THE_QUELBEES, + ZorkGrandInquisitorLocations.DEATH_CLIMBED_OUT_OF_THE_WELL, + ZorkGrandInquisitorLocations.DEATH_EATEN_BY_A_GRUE, + ZorkGrandInquisitorLocations.DEATH_JUMPED_IN_BOTTOMLESS_PIT, + ZorkGrandInquisitorLocations.DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER, + ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH, + ZorkGrandInquisitorLocations.DEATH_OUTSMARTED_BY_THE_QUELBEES, + ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD, + ZorkGrandInquisitorLocations.DEATH_STEPPED_INTO_THE_INFINITE, + ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON, + ZorkGrandInquisitorLocations.DEATH_THROCKED_THE_GRASS, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_INFINITY, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_HALLS_OF_INQUISITION, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_INFINITY, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_STRAIGHT_TO_HELL, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_SURFACE_OF_MERZ, + ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_SURFACE_OF_MERZ, + ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON, + ZorkGrandInquisitorLocations.DEATH_ZORK_ROCKS_EXPLODED, + ), + }, + ZorkGrandInquisitorDeathsanity.ON: None, + ZorkGrandInquisitorLandmarksanity.OFF: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.LANDMARK_DRAGON_ARCHIPELAGO, + ZorkGrandInquisitorLocations.LANDMARK_DUNGEON_MASTERS_HOUSE, + ZorkGrandInquisitorLocations.LANDMARK_FLOOD_CONTROL_DAM_3, + ZorkGrandInquisitorLocations.LANDMARK_GATES_OF_HELL, + ZorkGrandInquisitorLocations.LANDMARK_GREAT_UNDERGROUND_EMPIRE_ENTRANCE, + ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_INSIDE, + ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_OUTSIDE, + ZorkGrandInquisitorLocations.LANDMARK_HADES_SHORE, + ZorkGrandInquisitorLocations.LANDMARK_INFINITE_CORRIDOR, + ZorkGrandInquisitorLocations.LANDMARK_INQUISITION_HEADQUARTERS, + ZorkGrandInquisitorLocations.LANDMARK_JACKS_SHOP, + ZorkGrandInquisitorLocations.LANDMARK_MIRROR_ROOM, + ZorkGrandInquisitorLocations.LANDMARK_PAST_PORT_FOOZLE, + ZorkGrandInquisitorLocations.LANDMARK_PORT_FOOZLE, + ZorkGrandInquisitorLocations.LANDMARK_SPELL_CHECKER, + ZorkGrandInquisitorLocations.LANDMARK_TOTEMIZER, + ZorkGrandInquisitorLocations.LANDMARK_UMBRELLA_TREE, + ZorkGrandInquisitorLocations.LANDMARK_UNDERGROUND_UNDERGROUND_ENTRANCE, + ZorkGrandInquisitorLocations.LANDMARK_WALKING_CASTLES_HEART, + ZorkGrandInquisitorLocations.LANDMARK_WHITE_HOUSE, + ), + }, + ZorkGrandInquisitorLandmarksanity.ON: None, +} diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index 6fa35a58b426..e3c48b641f1d 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -1,15 +1,22 @@ -from typing import Dict, List, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union -from .data.entrance_rule_data import entrance_rule_data +from BaseClasses import ItemClassification + +from .data.entrance_rule_data import entrance_rule_data, endgame_entrance_data_by_goal from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.mapping_data import starting_location_to_logic_item +from .data.transform_data import item_data_transforms, location_data_transforms from .enums import ( + ZorkGrandInquisitorCraftableSpellBehaviors, + ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorEvents, ZorkGrandInquisitorGoals, ZorkGrandInquisitorItems, + ZorkGrandInquisitorItemTransforms, + ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorLocations, + ZorkGrandInquisitorLocationTransforms, ZorkGrandInquisitorRegions, ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorTags, @@ -24,7 +31,7 @@ def item_names_to_item() -> Dict[str, ZorkGrandInquisitorItems]: return {item.value: item for item in item_data} -def location_names_to_id() -> Dict[str, int]: +def location_names_to_id() -> Dict[Any, int]: return { location.value: data.archipelago_id for location, data in location_data.items() @@ -32,7 +39,7 @@ def location_names_to_id() -> Dict[str, int]: } -def location_names_to_location() -> Dict[str, ZorkGrandInquisitorLocations]: +def location_names_to_location() -> Dict[Any, ZorkGrandInquisitorLocations]: return { location.value: location for location, data in location_data.items() @@ -40,6 +47,14 @@ def location_names_to_location() -> Dict[str, ZorkGrandInquisitorLocations]: } +def id_to_craftable_spell_behaviors() -> Dict[int, ZorkGrandInquisitorCraftableSpellBehaviors]: + return {behavior.value: behavior for behavior in ZorkGrandInquisitorCraftableSpellBehaviors} + + +def id_to_deathsanity() -> Dict[int, ZorkGrandInquisitorDeathsanity]: + return {deathsanity.value: deathsanity for deathsanity in ZorkGrandInquisitorDeathsanity} + + def id_to_goals() -> Dict[int, ZorkGrandInquisitorGoals]: return {goal.value: goal for goal in ZorkGrandInquisitorGoals} @@ -48,6 +63,10 @@ def id_to_items() -> Dict[int, ZorkGrandInquisitorItems]: return {data.archipelago_id: item for item, data in item_data.items()} +def id_to_landmarksanity() -> Dict[int, ZorkGrandInquisitorLandmarksanity]: + return {landmarksanity.value: landmarksanity for landmarksanity in ZorkGrandInquisitorLandmarksanity} + + def id_to_locations() -> Dict[int, ZorkGrandInquisitorLocations]: return { data.archipelago_id: location @@ -57,7 +76,10 @@ def id_to_locations() -> Dict[int, ZorkGrandInquisitorLocations]: def id_to_starting_locations() -> Dict[int, ZorkGrandInquisitorStartingLocations]: - return {starting_location.value: starting_location for starting_location in ZorkGrandInquisitorStartingLocations} + return { + starting_location.value: starting_location + for starting_location in ZorkGrandInquisitorStartingLocations + } def item_groups() -> Dict[str, List[str]]: @@ -115,9 +137,12 @@ def location_groups() -> Dict[str, List[str]]: return {k: v for k, v in groups.items() if len(v)} -def locations_by_region(include_deathsanity: bool = False) -> Dict[ - ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations] -]: +def locations_by_region_for_world( + world_location_data: Dict[ + Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], + ZorkGrandInquisitorLocationData, + ] +) -> Dict[ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]]: mapping: Dict[ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]] = dict() region: ZorkGrandInquisitorRegions @@ -126,12 +151,7 @@ def locations_by_region(include_deathsanity: bool = False) -> Dict[ location: ZorkGrandInquisitorLocations data: ZorkGrandInquisitorLocationData - for location, data in location_data.items(): - if not include_deathsanity and ZorkGrandInquisitorTags.DEATHSANITY in ( - data.tags or tuple() - ): - continue - + for location, data in world_location_data.items(): mapping[data.region].append(location) return mapping @@ -144,10 +164,86 @@ def locations_with_tag(tag: ZorkGrandInquisitorTags) -> Set[ZorkGrandInquisitorL return {location for location, data in location_data.items() if data.tags is not None and tag in data.tags} -def starting_location_to_logic_helper_item( +def prepare_item_data( starting_location: ZorkGrandInquisitorStartingLocations, -) -> ZorkGrandInquisitorItems: - return starting_location_to_logic_item[starting_location] + goal: ZorkGrandInquisitorGoals, + deathsanity: ZorkGrandInquisitorDeathsanity, + landmarksanity: ZorkGrandInquisitorLandmarksanity, +) -> Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData]: + transformed_item_data: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData] = dict() + + # Filter items + item: ZorkGrandInquisitorItems + data: ZorkGrandInquisitorItemData + for item, data in item_data.items(): + # Filter here... + transformed_item_data[item] = data + + # Apply transformations + items_to_make_filler: Set[ZorkGrandInquisitorItems] = set() + + for context in (starting_location, goal, deathsanity, landmarksanity): + if item_data_transforms[context] is not None: + transform: ZorkGrandInquisitorItemTransforms + items: Tuple[ZorkGrandInquisitorItems, ...] + for transform, items in item_data_transforms[context].items(): + if transform == ZorkGrandInquisitorItemTransforms.MAKE_FILLER: + item: ZorkGrandInquisitorItems + for item in items: + items_to_make_filler.add(item) + + item: ZorkGrandInquisitorItems + for item in items_to_make_filler: + transformed_item_data[item] = transformed_item_data[item]._replace( + classification=ItemClassification.filler + ) + + return transformed_item_data + + +def prepare_location_data( + starting_location: ZorkGrandInquisitorStartingLocations, + goal: ZorkGrandInquisitorGoals, + deathsanity: ZorkGrandInquisitorDeathsanity, + landmarksanity: ZorkGrandInquisitorLandmarksanity, +) -> Dict[ + Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], ZorkGrandInquisitorLocationData +]: + transformed_location_data: Dict[ + Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], ZorkGrandInquisitorLocationData + ] = dict() + + # Force certain options depending on goal + if goal == ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: + deathsanity = ZorkGrandInquisitorDeathsanity.ON + elif goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + landmarksanity = ZorkGrandInquisitorLandmarksanity.ON + + # Filter locations + location: Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents] + data: ZorkGrandInquisitorLocationData + for location, data in location_data.items(): + # Filter here... + transformed_location_data[location] = data + + # Apply transformations + locations_to_remove: List[ZorkGrandInquisitorLocations] = list() + + for context in (starting_location, goal, deathsanity, landmarksanity): + if location_data_transforms[context] is not None: + transform: ZorkGrandInquisitorLocationTransforms + locations: Tuple[ZorkGrandInquisitorLocations, ...] + for transform, locations in location_data_transforms[context].items(): + if transform == ZorkGrandInquisitorLocationTransforms.REMOVE: + location: ZorkGrandInquisitorLocations + for location in locations: + locations_to_remove.append(location) + + location: ZorkGrandInquisitorLocations + for location in locations_to_remove: + del transformed_location_data[location] + + return transformed_location_data def location_access_rule_for(location: ZorkGrandInquisitorLocations, player: int) -> str: @@ -196,8 +292,33 @@ def location_access_rule_for(location: ZorkGrandInquisitorLocations, player: int def entrance_access_rule_for( region_origin: ZorkGrandInquisitorRegions, region_destination: ZorkGrandInquisitorRegions, - player: int + player: int, + dataset: Optional[ + Dict[ + Tuple[ + ZorkGrandInquisitorRegions, + ZorkGrandInquisitorRegions, + ], + Union[ + Tuple[ + Tuple[ + Union[ + ZorkGrandInquisitorEvents, + ZorkGrandInquisitorItems, + ZorkGrandInquisitorRegions, + ], + ..., + ], + ..., + ], + None, + ], + ] + ] = None ) -> str: + if dataset is None: + dataset = entrance_rule_data + data: Union[ Tuple[ Tuple[ @@ -211,7 +332,7 @@ def entrance_access_rule_for( ..., ], None, - ] = entrance_rule_data[(region_origin, region_destination)] + ] = dataset[(region_origin, region_destination)] if data is None: return "lambda state: True" @@ -257,3 +378,16 @@ def entrance_access_rule_for( lambda_string += " or " return lambda_string + + +def goal_access_rule_for( + region: ZorkGrandInquisitorRegions, + goal: ZorkGrandInquisitorGoals, + player: int, +) -> str: + return entrance_access_rule_for( + region, + ZorkGrandInquisitorRegions.ENDGAME, + player, + dataset=endgame_entrance_data_by_goal[goal], + ) diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 4ef6af51d122..82ec8853c980 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -1,9 +1,19 @@ import enum +class ZorkGrandInquisitorCraftableSpellBehaviors(enum.Enum): + VANILLA = 0 + ANY_SPELL = 1 + ANYTHING = 2 + + +class ZorkGrandInquisitorDeathsanity(enum.Enum): + OFF = 0 + ON = 1 + + class ZorkGrandInquisitorEvents(enum.Enum): CHARON_CALLED = "Event: Charon Called" - CIGAR_ACCESSIBLE = "Event: Cigar Accessible" DALBOZ_LOCKER_OPENABLE = "Event: Dalboz Locker Openable" DAM_DESTROYED = "Event: Dam Destroyed" DOOR_DRANK_MEAD = "Event: Door Drank Mead" @@ -11,21 +21,19 @@ class ZorkGrandInquisitorEvents(enum.Enum): DUNCE_LOCKER_OPENABLE = "Event: Dunce Locker Openable" HAS_REPAIRABLE_OBIDIL = "Event: Has Repairable OBIDIL" HAS_REPAIRABLE_SNAVIG = "Event: Has Repairable SNAVIG" - KNOWS_BEBURTT = "Event: Knows BEBURTT" - KNOWS_OBIDIL = "Event: Knows OBIDIL" - KNOWS_SNAVIG = "Event: Knows SNAVIG" - KNOWS_YASTARD = "Event: Knows YASTARD" - LANTERN_DALBOZ_ACCESSIBLE = "Event: Lantern (Dalboz) Accessible" ROPE_GLORFABLE = "Event: Rope GLORFable" VICTORY = "Victory" WHITE_HOUSE_LETTER_MAILABLE = "Event: White House Letter Mailable" ZORKMID_BILL_ACCESSIBLE = "Event: 500 Zorkmid Bill Accessible" ZORK_ROCKS_ACTIVATED = "Event: Zork Rocks Activated" - ZORK_ROCKS_SUCKABLE = "Event: Zork Rocks Suckable" class ZorkGrandInquisitorGoals(enum.Enum): THREE_ARTIFACTS = 0 + ARTIFACT_OF_MAGIC_HUNT = 1 + SPELL_HEIST = 2 + ZORK_TOUR = 3 + NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE = 4 class ZorkGrandInquisitorItems(enum.Enum): @@ -33,12 +41,15 @@ class ZorkGrandInquisitorItems(enum.Enum): BROGS_FLICKERING_TORCH = "Brog's Flickering Torch" BROGS_GRUE_EGG = "Brog's Grue Egg" BROGS_PLANK = "Brog's Plank" + CIGAR = "Cigar" + COCOA_INGREDIENTS = "Cocoa Ingredients" + COCONUT_OF_QUENDOR = "Coconut of Quendor" + CUBE_OF_FOUNDATION = "Cube of Foundation" FILLER_FROBOZZ_ELECTRIC_GADGET = "Frobozz Electric Gadget" FILLER_INQUISITION_PROPAGANDA_FLYER = "Inquisition Propaganda Flyer" FILLER_MAGIC_CONTRABAND = "Magic Contraband" FILLER_NONSENSICAL_INQUISITION_PAPERWORK = "Nonsensical Inquisition Paperwork" FILLER_UNREADABLE_SPELL_SCROLL = "Unreadable Spell Scroll" - FLATHEADIA_FUDGE = "Flatheadia Fudge" GRIFFS_AIR_PUMP = "Griff's Air Pump" GRIFFS_DRAGON_TOOTH = "Griff's Dragon Tooth" GRIFFS_INFLATABLE_RAFT = "Griff's Inflatable Raft" @@ -48,6 +59,7 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS = "Hotspot: Alpine's Quandry Card Slots" HOTSPOT_BLANK_SCROLL_BOX = "Hotspot: Blank Scroll Box" HOTSPOT_BLINDS = "Hotspot: Blinds" + HOTSPOT_BUCKET = "Hotspot: Bucket" HOTSPOT_CANDY_MACHINE_BUTTONS = "Hotspot: Candy Machine Buttons" HOTSPOT_CANDY_MACHINE_COIN_SLOT = "Hotspot: Candy Machine Coin Slot" HOTSPOT_CANDY_MACHINE_VACUUM_SLOT = "Hotspot: Candy Machine Vacuum Slot" @@ -62,6 +74,7 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_DRAGON_CLAW = "Hotspot: Dragon Claw" HOTSPOT_DRAGON_NOSTRILS = "Hotspot: Dragon Nostrils" HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE = "Hotspot: Dungeon Master's Lair Entrance" + HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT = "Hotspot: Dungeon Master's House Exit" HOTSPOT_FLOOD_CONTROL_BUTTONS = "Hotspot: Flood Control Buttons" HOTSPOT_FLOOD_CONTROL_DOORS = "Hotspot: Flood Control Doors" HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT = "Hotspot: Frozen Treat Machine Coin Slot" @@ -70,6 +83,7 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_GRAND_INQUISITOR_DOLL = "Hotspot: Grand Inquisitor Doll" HOTSPOT_GUE_TECH_DOOR = "Hotspot: GUE Tech Door" HOTSPOT_GUE_TECH_GRASS = "Hotspot: GUE Tech Grass" + HOTSPOT_GUE_TECH_WINDOWS = "Hotspot: GUE Tech Windows" HOTSPOT_HADES_PHONE_BUTTONS = "Hotspot: Hades Phone Buttons" HOTSPOT_HADES_PHONE_RECEIVER = "Hotspot: Hades Phone Receiver" HOTSPOT_HARRY = "Hotspot: Harry" @@ -81,7 +95,6 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_MAILBOX_DOOR = "Hotspot: Mailbox Door" HOTSPOT_MAILBOX_FLAG = "Hotspot: Mailbox Flag" HOTSPOT_MIRROR = "Hotspot: Mirror" - HOTSPOT_MONASTERY_VENT = "Hotspot: Monastery Vent" HOTSPOT_MOSSY_GRATE = "Hotspot: Mossy Grate" HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR = "Hotspot: Port Foozle Past Tavern Door" HOTSPOT_PURPLE_WORDS = "Hotspot: Purple Words" @@ -93,6 +106,7 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_SODA_MACHINE_COIN_SLOT = "Hotspot: Soda Machine Coin Slot" HOTSPOT_SOUVENIR_COIN_SLOT = "Hotspot: Souvenir Coin Slot" HOTSPOT_SPELL_CHECKER = "Hotspot: Spell Checker" + HOTSPOT_SPELL_LAB_BRIDGE_EXIT = "Hotspot: Spell Lab Bridge Exit" HOTSPOT_SPELL_LAB_CHASM = "Hotspot: Spell Lab Chasm" HOTSPOT_SPRING_MUSHROOM = "Hotspot: Spring Mushroom" HOTSPOT_STUDENT_ID_MACHINE = "Hotspot: Student ID Machine" @@ -100,53 +114,45 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_TAVERN_FLY = "Hotspot: Tavern Fly" HOTSPOT_TOTEMIZER_SWITCH = "Hotspot: Totemizer Switch" HOTSPOT_TOTEMIZER_WHEELS = "Hotspot: Totemizer Wheels" - HOTSPOT_WELL = "Hotspot: Well" HUNGUS_LARD = "Hungus Lard" - JAR_OF_HOTBUGS = "Jar of Hotbugs" - LANTERN = "Lantern" LARGE_TELEGRAPH_HAMMER = "Large Telegraph Hammer" - LOGIC_HELPER_STARTING_LOCATION_CROSSROADS = "Starting Location: Crossroads" - LOGIC_HELPER_STARTING_LOCATION_DM_LAIR = "Starting Location: Dungeon Master's Lair" - LOGIC_HELPER_STARTING_LOCATION_DM_LAIR_INTERIOR = "Starting Location: Dungeon Master's House" - LOGIC_HELPER_STARTING_LOCATION_GUE_TECH = "Starting Location: GUE Tech" - LOGIC_HELPER_STARTING_LOCATION_HADES_SHORE = "Starting Location: Hades Shore" - LOGIC_HELPER_STARTING_LOCATION_MONASTERY = "Starting Location: Monastery Totemizer" - LOGIC_HELPER_STARTING_LOCATION_MONASTERY_EXHIBIT = "Starting Location: Monastery Exhibit" - LOGIC_HELPER_STARTING_LOCATION_PORT_FOOZLE = "Starting Location: Port Foozle" - LOGIC_HELPER_STARTING_LOCATION_SPELL_LAB = "Starting Location: Spell Lab" - LOGIC_HELPER_STARTING_LOCATION_SUBWAY_FLOOD_CONTROL_DAM = "Starting Location: Flood Control Dam #3" LUCYS_PLAYING_CARD_1 = "Lucy's Playing Card: 1 Pip" LUCYS_PLAYING_CARD_2 = "Lucy's Playing Card: 2 Pips" LUCYS_PLAYING_CARD_3 = "Lucy's Playing Card: 3 Pips" LUCYS_PLAYING_CARD_4 = "Lucy's Playing Card: 4 Pips" MAP = "Map" MEAD_LIGHT = "Mead Light" - MOSS_OF_MAREILON = "Moss of Mareilon" - MUG = "Mug" + MONASTERY_ROPE = "Monastery Rope" OLD_SCRATCH_CARD = "Old Scratch Card" PERMA_SUCK_MACHINE = "Perma-Suck Machine" PLASTIC_SIX_PACK_HOLDER = "Plastic Six-Pack Holder" POUCH_OF_ZORKMIDS = "Pouch of Zorkmids" PROZORK_TABLET = "Prozork Tablet" - QUELBEE_HONEYCOMB = "Quelbee Honeycomb" - ROPE = "Rope" + SANDWITCH_WRAPPER = "Sandwitch Wrapper" SCROLL_FRAGMENT_ANS = "Scroll Fragment: ANS" SCROLL_FRAGMENT_GIV = "Scroll Fragment: GIV" SHOVEL = "Shovel" + SKULL_OF_YORUK = "Skull of Yoruk" SNAPDRAGON = "Snapdragon" + SPELL_BEBURTT = "Spell: BEBURTT" SPELL_GLORF = "Spell: GLORF" SPELL_GOLGATEM = "Spell: GOLGATEM" SPELL_IGRAM = "Spell: IGRAM" SPELL_KENDALL = "Spell: KENDALL" + SPELL_OBIDIL = "Spell: OBIDIL" SPELL_NARWILE = "Spell: NARWILE" SPELL_REZROV = "Spell: REZROV" + SPELL_SNAVIG = "Spell: SNAVIG" SPELL_THROCK = "Spell: THROCK" + SPELL_YASTARD = "Spell: YASTARD" STUDENT_ID = "Student ID" + SUBWAY_DESTINATION_CROSSROADS = "Subway Destination: Crossroads" SUBWAY_DESTINATION_FLOOD_CONTROL_DAM = "Subway Destination: Flood Control Dam #3" SUBWAY_DESTINATION_HADES = "Subway Destination: Hades" SUBWAY_DESTINATION_MONASTERY = "Subway Destination: Monastery" SUBWAY_TOKEN = "Subway Token" SWORD = "Sword" + TELEPORTER_DESTINATION_CROSSROADS = "Teleporter Destination: Crossroads" TELEPORTER_DESTINATION_DM_LAIR = "Teleporter Destination: Dungeon Master's Lair" TELEPORTER_DESTINATION_GUE_TECH = "Teleporter Destination: GUE Tech" TELEPORTER_DESTINATION_HADES = "Teleporter Destination: Hades" @@ -157,12 +163,23 @@ class ZorkGrandInquisitorItems(enum.Enum): TOTEM_LUCY = "Totem: Lucy" TOTEMIZER_DESTINATION_HALL_OF_INQUISITION = "Totemizer Destination: Hall of Inquisition" TOTEMIZER_DESTINATION_INFINITY = "Totemizer Destination: Infinity" + TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY = "Totemizer Destination: Newark, New Jersey" TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL = "Totemizer Destination: Straight to Hell" TOTEMIZER_DESTINATION_SURFACE_OF_MERZ = "Totemizer Destination: Surface of Merz" + WELL_ROPE = "Well Rope" ZIMDOR_SCROLL = "ZIMDOR Scroll" ZORK_ROCKS = "Zork Rocks" +class ZorkGrandInquisitorItemTransforms(enum.Enum): + MAKE_FILLER = "Make Filler" + + +class ZorkGrandInquisitorLandmarksanity(enum.Enum): + OFF = 0 + ON = 1 + + class ZorkGrandInquisitorLocations(enum.Enum): ALARM_SYSTEM_IS_DOWN = "Alarm System is Down" ARREST_THE_VANDAL = "Arrest the Vandal!" @@ -183,26 +200,32 @@ class ZorkGrandInquisitorLocations(enum.Enum): CASTLE_WATCHING_A_FIELD_GUIDE = "Castle Watching: A Field Guide" CAVES_NOTES = "Cave's Notes" CLOSING_THE_TIME_TUNNELS = "Closing the Time Tunnels" + COME_TO_PAPA_YOU_NUT = "Come to Papa. You Nut" CRISIS_AVERTED = "Crisis Averted" CUT_THAT_OUT_YOU_LITTLE_CREEP = "Cut That Out You Little Creep!" - DEATH_ARRESTED_WITH_JACK = "Death: Arrested With Jack" - DEATH_ATTACKED_THE_QUELBEES = "Death: Attacked the Quelbees" - DEATH_CLIMBED_OUT_OF_THE_WELL = "Death: Climbed Out of the Well" - DEATH_EATEN_BY_A_GRUE = "Death: Eaten by a Grue" - DEATH_JUMPED_IN_BOTTOMLESS_PIT = "Death: Jumped in Bottomless Pit" - DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER = "Death: Lost Game of Strip Grue, Fire, Water" - DEATH_LOST_SOUL_TO_OLD_SCRATCH = "Death: Lost Soul to Old Scratch" - DEATH_OUTSMARTED_BY_THE_QUELBEES = "Death: Outsmarted by the Quelbees" - DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD = "Death: Sliced up by the Invisible Guard" - DEATH_STEPPED_INTO_THE_INFINITE = "Death: Step Into the Infinite" - DEATH_SWALLOWED_BY_A_DRAGON = "Death: Swallowed by a Dragon" - DEATH_THROCKED_THE_GRASS = "Death: THROCKed the Grass" - DEATH_TOTEMIZED = "Death: Totemized?" - DEATH_TOTEMIZED_PERMANENTLY = "Death: Totemized... Permanently" - DEATH_YOURE_NOT_CHARON = "Death: You're Not Charon!?" - DEATH_ZORK_ROCKS_EXPLODED = "Death: Zork Rocks Exploded" + DEATH_ARRESTED_WITH_JACK = "Death: Interesting Philosophical Thought" + DEATH_ATTACKED_THE_QUELBEES = "Death: Just Under Half a Second" + DEATH_CLIMBED_OUT_OF_THE_WELL = "Death: Breaking Curfew" + DEATH_EATEN_BY_A_GRUE = "Death: Pitch Black Cave in a Zork Game" + DEATH_JUMPED_IN_BOTTOMLESS_PIT = "Death: Old Age" + DEATH_LOST_GAME_OF_STRIP_GRUE_FIRE_WATER = "Death: Mind-Boggling Unlucky Streak" + DEATH_LOST_SOUL_TO_OLD_SCRATCH = "Death: Contractually Obligated" + DEATH_OUTSMARTED_BY_THE_QUELBEES = "Death: Antennae Nose Plugs" + DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD = "Death: Chop, Slice, Puree" + DEATH_STEPPED_INTO_THE_INFINITE = "Death: Tiny Step for Mankind" + DEATH_SWALLOWED_BY_A_DRAGON = "Death: Spare Loft Behind Uvula" + DEATH_THROCKED_THE_GRASS = "Death: Expressly Forbidden" + DEATH_TOTEMIZED_INFINITY = "Death: Airless Expanse of the Cosmos" + DEATH_TOTEMIZED_NEWARK_NEW_JERSEY = "Death: Arteriosclerosis" + DEATH_TOTEMIZED_PERMANENTLY_HALLS_OF_INQUISITION = "Death: Ms. Peeper's Paperweight" + DEATH_TOTEMIZED_PERMANENTLY_INFINITY = "Death: S.S. Feinstein's Tractor Beam" + DEATH_TOTEMIZED_PERMANENTLY_NEWARK_NEW_JERSEY = "Death: Manhole Cover in New Jersey" + DEATH_TOTEMIZED_PERMANENTLY_STRAIGHT_TO_HELL = "Death: Evil Spawn's Plaything" + DEATH_TOTEMIZED_PERMANENTLY_SURFACE_OF_MERZ = "Death: Eternity Gazing at the Scenic Vista" + DEATH_TOTEMIZED_SURFACE_OF_MERZ = "Death: Very, Very Pretty" + DEATH_YOURE_NOT_CHARON = "Death: Not Charon" + DEATH_ZORK_ROCKS_EXPLODED = "Death: Pretty Painless" DENIED_BY_THE_LAKE_MONSTER = "Denied by the Lake Monster" - DESPERATELY_SEEKING_TUTOR = "Desperately Seeking Tutor" DONT_EVEN_START_WITH_US_SPARKY = "Don't Even Start With Us, Sparky" DOOOOOOWN = "Doooooown" DOWN = "Down" @@ -219,17 +242,13 @@ class ZorkGrandInquisitorLocations(enum.Enum): FROBUARY_3_UNDERGROUNDHOG_DAY = "Frobruary 3 - Undergroundhog Day" GETTING_SOME_CHANGE = "Getting Some Change" GO_AWAY = "GO AWAY!" - GUE_TECH_DEANS_LIST = "GUE Tech Dean's List" + GOOD_PUZZLE_SMART_BROG = "Good Puzzle. Smart Brog" GUE_TECH_ENTRANCE_EXAM = "GUE Tech Entrance Exam" - GUE_TECH_HEALTH_MEMO = "GUE Tech Health Memo" - GUE_TECH_MAGEMEISTERS = "GUE Tech Magemeisters" HAVE_A_HELL_OF_A_DAY = "Have a Hell of a Day!" HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING = "Hello, This is Shona from Gurth Publishing" HELP_ME_CANT_BREATHE = "Help... Me. Can't... Breathe" HEY_FREE_DIRT = "Hey, Free Dirt!" - HI_MY_NAME_IS_DOUG = "Hi, My Name is Doug" HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING = "Hmmm. Informative. Yet Deeply Disturbing" - HOLD_ON_FOR_AN_IMPORTANT_MESSAGE = "Hold on for an Important Message" HOW_TO_HYPNOTIZE_YOURSELF = "How to Hypnotize Yourself" HOW_TO_WIN_AT_DOUBLE_FANUCCI = "How to Win at Double Fanucci" IMBUE_BEBURTT = "Imbue BEBURTT" @@ -241,11 +260,30 @@ class ZorkGrandInquisitorLocations(enum.Enum): ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN = "It's One of Those Adventurers Again..." I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY = "I Don't Think You Would've Wanted That to Work Anyway" I_DONT_WANT_NO_TROUBLE = "I Don't Want No Trouble!" - I_HOPE_YOU_CAN_CLIMB_UP_THERE = "I Hope You Can Climb Up There With All This Junk" I_LIKE_YOUR_STYLE = "I Like Your Style!" I_SPIT_ON_YOUR_FILTHY_COINAGE = "I Spit on Your Filthy Coinage" + LANDMARK_DRAGON_ARCHIPELAGO = "Landmark Visited: Dragon Archipelago" + LANDMARK_DUNGEON_MASTERS_HOUSE = "Landmark Visited: Dungeon Master's House" + LANDMARK_FLOOD_CONTROL_DAM_3 = "Landmark Visited: Flood Control Dam #3" + LANDMARK_GATES_OF_HELL = "Landmark Visited: Gates of Hell" + LANDMARK_GREAT_UNDERGROUND_EMPIRE_ENTRANCE = "Landmark Visited: Great Underground Empire Entrance" + LANDMARK_GUE_TECH_FOUNTAIN_INSIDE = "Landmark Visited: GUE Tech Fountain (Inside)" + LANDMARK_GUE_TECH_FOUNTAIN_OUTSIDE = "Landmark Visited: GUE Tech Fountain (Outside)" + LANDMARK_HADES_SHORE = "Landmark Visited: Hades Shore" + LANDMARK_INFINITE_CORRIDOR = "Landmark Visited: Infinite Corridor" + LANDMARK_INQUISITION_HEADQUARTERS = "Landmark Visited: Inquisition Headquarters" + LANDMARK_JACKS_SHOP = "Landmark Visited: Jack's Shop" + LANDMARK_MIRROR_ROOM = "Landmark Visited: Mirror Room" + LANDMARK_PAST_PORT_FOOZLE = "Landmark Visited: Past Port Foozle" + LANDMARK_PORT_FOOZLE = "Landmark Visited: Port Foozle" + LANDMARK_SPELL_CHECKER = "Landmark Visited: Spell Checker" + LANDMARK_TOTEMIZER = "Landmark Visited: Totemizer" + LANDMARK_UMBRELLA_TREE = "Landmark Visited: Umbrella Tree" + LANDMARK_UNDERGROUND_UNDERGROUND_ENTRANCE = "Landmark Visited: Underground Underground Entrance" + LANDMARK_WALKING_CASTLES_HEART = "Landmark Visited: Walking Castle's Heart" + LANDMARK_WHITE_HOUSE = "Landmark Visited: White House" LIT_SUNFLOWERS = "Lit Sunflowers" - MAGIC_FOREVER = "Magic Forever!" + LOOK_AN_ICE_CREAM_BAR = "Look! An Ice Cream Bar" MAILED_IT_TO_HELL = "Mailed it to Hell" MAKE_LOVE_NOT_WAR = "Make Love, Not War" MEAD_LIGHT = "Mead Light?" @@ -272,9 +310,7 @@ class ZorkGrandInquisitorLocations(enum.Enum): PORT_FOOZLE_TIME_TUNNEL = "Port Foozle Time Tunnel" PROZORKED = "Prozorked" REASSEMBLE_SNAVIG = "Reassemble SNAVIG" - RESTOCKED_ON_GRUESDAY = "Restocked on Gruesday" RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE = "Right. Hello. Yes. Uh, This is Sneffle" - RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE = "Right. Uh, Sorry. It's Me Again. Sneffle" SNAVIG_REPAIRED = "SNAVIG, Repaired" SOUVENIR = "Souvenir" STRAIGHT_TO_HELL = "Straight to Hell" @@ -290,6 +326,7 @@ class ZorkGrandInquisitorLocations(enum.Enum): THE_ALCHEMICAL_DEBACLE = "The Alchemical Debacle" THE_ENDLESS_FIRE = "The Endless Fire" THE_FLATHEADIAN_FUDGE_FIASCO = "The Flatheadian Fudge Fiasco" + THE_ONLY_WAY_TO_WIN_IS_NOT_TO_PLAY = "The Only Way to Win is Not to Play" THE_PERILS_OF_MAGIC = "The Perils of Magic" THE_UNDERGROUND_UNDERGROUND = "The Underground Underground" THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE = "This Doesn't Look Anything Like the Brochure" @@ -312,11 +349,17 @@ class ZorkGrandInquisitorLocations(enum.Enum): YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY = "Your Puny Weapons Don't Phase Me, Baby!" YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER = "You Don't Go Messing With a Man's Zipper" YOU_GAINED_86_EXPERIENCE_POINTS = "You Gained 86 Experience Points" + YOU_LOSE_MUFFET_ANTE_UP = "You Lose, Muffet. Ante Up" YOU_ONE_OF_THEM_AGITATORS_AINT_YA = "You One of Them Agitators, Ain't Ya?" YOU_WANT_A_PIECE_OF_ME_DOCK_BOY = "You Want a Piece of Me, Dock Boy? or Girl" +class ZorkGrandInquisitorLocationTransforms(enum.Enum): + REMOVE = "Remove" + + class ZorkGrandInquisitorRegions(enum.Enum): + ANYWHERE = "Anywhere" CROSSROADS = "Crossroads" DM_LAIR = "Dungeon Master's Lair" DM_LAIR_INTERIOR = "Dungeon Master's Lair - Interior" @@ -324,6 +367,7 @@ class ZorkGrandInquisitorRegions(enum.Enum): DRAGON_ARCHIPELAGO_DRAGON = "Dragon Archipelago - Dragon" ENDGAME = "Endgame" GUE_TECH = "GUE Tech" + GUE_TECH_ENTRANCE = "GUE Tech - Entrance" GUE_TECH_HALLWAY = "GUE Tech - Hallway" GUE_TECH_OUTSIDE = "GUE Tech - Outside" HADES = "Hades" @@ -343,6 +387,7 @@ class ZorkGrandInquisitorRegions(enum.Enum): SUBWAY_MONASTERY = "Subway Platform - Monastery" WALKING_CASTLE = "Walking Castle" WHITE_HOUSE = "White House" + WHITE_HOUSE_INTERIOR = "White House - Interior" class ZorkGrandInquisitorStartingLocations(enum.Enum): @@ -362,9 +407,10 @@ class ZorkGrandInquisitorTags(enum.Enum): CORE = "Core" DEATHSANITY = "Deathsanity" FILLER = "Filler" + GOAL_THREE_ARTIFACTS = "Goal: Three Artifacts" HOTSPOT = "Hotspot" INVENTORY_ITEM = "Inventory Item" - LOGIC_HELPER = "Logic Helper" + LANDMARKSANITY = "Landmarksanity" MISSABLE = "Missable" SPELL = "Spell" SUBWAY_DESTINATION = "Subway Destination" diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 8743855c74ea..cb7933518ec7 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1,13 +1,14 @@ import collections import functools import logging +import traceback # TODO: Only in dev -from typing import Dict, Optional, Set, Tuple, Union +from typing import Dict, List, Optional, Set, Tuple, Union from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.missable_location_grant_conditions_data import ( +from .data.missable_location_data import ( missable_location_grant_conditions_data, ZorkGrandInquisitorMissableLocationGrantConditionsData, ) @@ -15,8 +16,10 @@ from .data_funcs import game_id_to_items, items_with_tag, locations_with_tag from .enums import ( + ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, ZorkGrandInquisitorItems, + ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorLocations, ZorkGrandInquisitorStartingLocations, ZorkGrandInquisitorTags, @@ -47,9 +50,12 @@ class GameController: goal_completed: bool option_goal: Optional[ZorkGrandInquisitorGoals] - option_deathsanity: Optional[bool] - option_grant_missable_location_checks: Optional[bool] option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] + option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] + option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] + option_grant_missable_location_checks: Optional[bool] + + initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] def __init__(self, logger=None) -> None: self.logger = logger @@ -81,9 +87,12 @@ def __init__(self, logger=None) -> None: self.goal_completed = False self.option_goal = None + self.option_starting_location = None self.option_deathsanity = None + self.option_landmarksanity = None self.option_grant_missable_location_checks = None - self.option_starting_location = None + + self.initial_totemizer_destination = None @functools.cached_property def brog_items(self) -> Set[ZorkGrandInquisitorItems]: @@ -120,6 +129,10 @@ def totem_items(self) -> Set[ZorkGrandInquisitorItems]: def missable_locations(self) -> Set[ZorkGrandInquisitorLocations]: return locations_with_tag(ZorkGrandInquisitorTags.MISSABLE) + @functools.cached_property + def is_deathsanity(self) -> bool: + return self.option_deathsanity == ZorkGrandInquisitorDeathsanity.ON + def log(self, message) -> None: if self.logger: self.logger.info(message) @@ -197,6 +210,7 @@ def update(self) -> None: self.game_state_manager.refresh_game_location() self._apply_starting_location() + self._apply_initial_totemizer_destination() self._apply_permanent_game_state() self._apply_conditional_game_state() @@ -218,8 +232,12 @@ def update(self) -> None: self._check_for_victory() except Exception as e: self.log_debug(e) + traceback.print_exc() def _apply_starting_location(self, force: bool = False) -> None: + if self.option_starting_location is None: + return None + if self._read_game_state_value_for(19985) == 0 or force: if self.option_starting_location == ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: self.game_state_manager.set_game_location("ps10", 825) @@ -232,7 +250,7 @@ def _apply_starting_location(self, force: bool = False) -> None: elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.GUE_TECH: self.game_state_manager.set_game_location("tr10", 150) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SPELL_LAB: - self.game_state_manager.set_game_location("tp10", 0) + self.game_state_manager.set_game_location("tp20", 1244) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.HADES_SHORE: self.game_state_manager.set_game_location("hp10", 534) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: @@ -244,10 +262,28 @@ def _apply_starting_location(self, force: bool = False) -> None: self._write_game_state_value_for(19985, 1) + def _apply_initial_totemizer_destination(self) -> None: + if self.initial_totemizer_destination is None: + return None + + if self._read_game_state_value_for(19986) == 0: + mapping: Dict[ZorkGrandInquisitorItems, int] = { + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION: 0, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: 1, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY: 2, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: 3, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: 4, + } + + self._write_game_state_value_for(9617, mapping[self.initial_totemizer_destination]) + self._write_game_state_value_for(19986, 1) + def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(10934, 1) # Rope Taken self._write_game_state_value_for(10418, 1) # Mead Light Taken self._write_game_state_value_for(10275, 0) # Lantern in Crate + self._write_game_state_value_for(10297, 0) # Lantern on Jack's Table + self._write_game_state_value_for(5221, 1) # Player has Lantern self._write_game_state_value_for(13929, 1) # Great Underground Door Open self._write_game_state_value_for(13968, 1) # Subway Token Taken self._write_game_state_value_for(12930, 1) # Hammer Taken @@ -293,55 +329,57 @@ def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(13934, 1) # Skip Well Cutscenes self._write_game_state_value_for(13935, 1) # Skip Well Cutscenes self._write_game_state_value_for(13384, 1) # Skip Meanwhile... Cutscene + self._write_game_state_value_for(18275, 1) # Skip Flashback Cutscene self._write_game_state_value_for(8620, 1) # First Coin Paid to Charon self._write_game_state_value_for(8731, 1) # First Coin Paid to Charon self._write_game_state_value_for(191, 1) # VOXAM Learned + self._write_game_state_value_for(15384, 0) # Never Consider All Artifacts to be Placed def _apply_conditional_game_state(self): - # Can teleport to Dungeon Master's Lair + # Teleporter Destinations + if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_CROSSROADS): + self._write_game_state_value_for(12918, 1) + else: + self._write_game_state_value_for(12918, 0) + if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_DM_LAIR): self._write_game_state_value_for(2203, 1) else: self._write_game_state_value_for(2203, 0) - # Can teleport to GUE Tech if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_GUE_TECH): self._write_game_state_value_for(7132, 1) else: self._write_game_state_value_for(7132, 0) - # Can Teleport to Spell Lab if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_SPELL_LAB): self._write_game_state_value_for(16545, 1) else: self._write_game_state_value_for(16545, 0) - # Can Teleport to Hades if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_HADES): self._write_game_state_value_for(7119, 1) else: self._write_game_state_value_for(7119, 0) - # Can Teleport to Monastery Station if self._player_has(ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY): self._write_game_state_value_for(7148, 1) else: self._write_game_state_value_for(7148, 0) - # Initial Totemizer Destination - should_force_initial_totemizer_destination: bool = True - - if self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION): - should_force_initial_totemizer_destination = False - elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL): - should_force_initial_totemizer_destination = False - elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY): - should_force_initial_totemizer_destination = False - elif self._player_has(ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ): - should_force_initial_totemizer_destination = False + # Monastery Rope + if self._player_has(ZorkGrandInquisitorItems.MONASTERY_ROPE): + self._write_game_state_value_for(9637, 1) + else: + self._write_game_state_value_for(9637, 0) - if should_force_initial_totemizer_destination: - self._write_game_state_value_for(9617, 2) + # Well Rope + if self._player_has(ZorkGrandInquisitorItems.WELL_ROPE): + self._write_game_state_value_for(10304, 1) + self._write_game_state_value_for(13938, 0) + else: + self._write_game_state_value_for(10304, 0) + self._write_game_state_value_for(13938, 1) # Pouch of Zorkmids if self._player_has(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS): @@ -349,6 +387,22 @@ def _apply_conditional_game_state(self): else: self._write_game_state_value_for(5827, 0) + # Cocoa Ingredients + is_cocoa_brewed: bool = ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU in self.completed_locations + + if self._player_has(ZorkGrandInquisitorItems.COCOA_INGREDIENTS) and not is_cocoa_brewed: + self._write_game_state_value_for(4750, 1) # Jar of Hotbugs + self._write_game_state_value_for(4763, 1) # Moss of Mareilon + self._write_game_state_value_for(4766, 1) # Flatheadia Fudge + self._write_game_state_value_for(4772, 1) # Mug + self._write_game_state_value_for(4769, 1) # Quelbee Honeycomb + else: + self._write_game_state_value_for(4750, 0) + self._write_game_state_value_for(4763, 0) + self._write_game_state_value_for(4766, 0) + self._write_game_state_value_for(4772, 0) + self._write_game_state_value_for(4769, 0) + # Brog Torches if self._player_is_brog() and self._player_has(ZorkGrandInquisitorItems.BROGS_BICKERING_TORCH): self._write_game_state_value_for(10999, 1) @@ -360,22 +414,26 @@ def _apply_conditional_game_state(self): else: self._write_game_state_value_for(10998, 0) - # Monastery Rope - if ZorkGrandInquisitorLocations.I_HOPE_YOU_CAN_CLIMB_UP_THERE in self.completed_locations: - self._write_game_state_value_for(9637, 1) - def _apply_permanent_game_flags(self) -> None: + self._write_game_flags_value_for(13597, 2) # Monastery Vent self._write_game_flags_value_for(9437, 2) # Monastery Exhibit Door to Outside self._write_game_flags_value_for(3074, 2) # White House Door self._write_game_flags_value_for(13005, 2) # Map self._write_game_flags_value_for(13006, 2) # Sword self._write_game_flags_value_for(13007, 2) # Sword + self._write_game_flags_value_for(4854, 2) # Hungus Lard self._write_game_flags_value_for(13389, 2) # Moss of Mareilon self._write_game_flags_value_for(4301, 2) # Quelbee Honeycomb self._write_game_flags_value_for(12895, 2) # Change Machine Money self._write_game_flags_value_for(4150, 2) # Prozorked Snapdragon self._write_game_flags_value_for(13413, 2) # Letter Opener self._write_game_flags_value_for(15403, 2) # Lucy's Cards + self._write_game_flags_value_for(4876, 2) # Cocoa Ingredient - Jar of Hotbugs + self._write_game_flags_value_for(4877, 2) # Cocoa Ingredient - Moss of Mareilon + self._write_game_flags_value_for(4874, 2) # Cocoa Ingredient - Flatheadia Fudge + self._write_game_flags_value_for(4875, 2) # Cocoa Ingredient - Mug + self._write_game_flags_value_for(4873, 2) # Cocoa Ingredient - Quelbee Honeycomb + self._write_game_flags_value_for(10809, 2) # Back of Jack's Shop def _check_for_completed_locations(self) -> None: location: ZorkGrandInquisitorLocations @@ -388,7 +446,7 @@ def _check_for_completed_locations(self) -> None: is_location_completed: bool = True - trigger: [Union[str, int]] + trigger: Union[str, int, Tuple[int, ...]] value: Union[str, int, Tuple[int, ...]] for trigger, value in data.game_state_trigger: if trigger == "location": @@ -407,6 +465,12 @@ def _check_for_completed_locations(self) -> None: else: is_location_completed = False break + elif isinstance(trigger, tuple): + game_state_values: List[int] = [self._read_game_state_value_for(key) for key in trigger] + + if value not in game_state_values: + is_location_completed = False + break else: is_location_completed = False break @@ -415,6 +479,15 @@ def _check_for_completed_locations(self) -> None: self.completed_locations.add(location) self.completed_locations_queue.append(location) + self._after_location_completed(location) + + def _after_location_completed(self, location: ZorkGrandInquisitorLocations) -> None: + # Write certain events to unused game state that otherwise don't have a permanent way to track + if location == ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP: + self._write_game_state_value_for(19951, 1) + elif location == ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG: + self._write_game_state_value_for(19952, 1) + def _check_for_missable_locations_to_grant(self) -> None: missable_location: ZorkGrandInquisitorLocations for missable_location in self.missable_locations: @@ -423,7 +496,7 @@ def _check_for_missable_locations_to_grant(self) -> None: data: ZorkGrandInquisitorLocationData = location_data[missable_location] - if ZorkGrandInquisitorTags.DEATHSANITY in data.tags and not self.option_deathsanity: + if ZorkGrandInquisitorTags.DEATHSANITY in data.tags and not self.is_deathsanity: continue condition_data: ZorkGrandInquisitorMissableLocationGrantConditionsData = ( @@ -434,7 +507,15 @@ def _check_for_missable_locations_to_grant(self) -> None: self.log_debug(f"Missable Location {missable_location.value} has no grant conditions") continue - if condition_data.location_condition in self.completed_locations: + if condition_data.game_location_condition is not None: + if not self._player_is_at(condition_data.game_location_condition): + continue + + location_condition_intersection: Set[ZorkGrandInquisitorLocations] = ( + set(condition_data.location_condition) & self.completed_locations + ) + + if len(location_condition_intersection): grant_location: bool = True item: ZorkGrandInquisitorItems @@ -505,6 +586,11 @@ def _manage_hotspots(self) -> None: self._write_game_flags_value_for(4799, 0) else: self._write_game_flags_value_for(4799, 2) + elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_BUCKET: + has_well_rope: bool = self._player_has(ZorkGrandInquisitorItems.WELL_ROPE) + + if self.game_state_manager.game_location == "uw10" and has_well_rope: + self._write_game_flags_value_for(13928, 0) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS: if self.game_state_manager.game_location == "tr5g": key: int @@ -595,6 +681,9 @@ def _manage_hotspots(self) -> None: self._write_game_flags_value_for(1426, 2) else: self._write_game_flags_value_for(1426, 0) + elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT: + if self.game_state_manager.game_location == "dv10": + self._write_game_flags_value_for(4791, 0) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE: if self.game_state_manager.game_location == "uc3e": if self._read_game_state_value_for(13060) == 0: @@ -661,6 +750,14 @@ def _manage_hotspots(self) -> None: key: int for key in data.statemap_keys: self._write_game_flags_value_for(key, 0) + elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS: + if self.game_state_manager.game_location == "te3e": + if self._read_game_state_value_for(11536) == 1: + self._write_game_flags_value_for(11543, 0) + elif self.game_state_manager.game_location == "tr1g": + self._write_game_flags_value_for(12256, 0) + elif self.game_state_manager.game_location == "te40": + self._write_game_flags_value_for(11720, 0) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS: if self.game_state_manager.game_location == "hp1e": if self._read_game_state_value_for(8431) == 1: @@ -723,12 +820,6 @@ def _manage_hotspots(self) -> None: elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MIRROR: if self.game_state_manager.game_location == "dw1f": self._write_game_flags_value_for(5031, 0) - elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MONASTERY_VENT: - if self.game_state_manager.game_location == "um1e": - if self._read_game_state_value_for(9637) == 0: - self._write_game_flags_value_for(13597, 0) - else: - self._write_game_flags_value_for(13597, 2) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE: if self.game_state_manager.game_location == "ue2g": if self._read_game_state_value_for(13278) == 0: @@ -798,6 +889,9 @@ def _manage_hotspots(self) -> None: elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER: if self.game_state_manager.game_location == "tp4g": self._write_game_flags_value_for(12170, 0) + elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT: + if self.game_state_manager.game_location == "tp10": + self._write_game_flags_value_for(12045, 0) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM: if self.game_state_manager.game_location == "tp1e": if self._read_game_state_value_for(16342) == 1 and self._read_game_state_value_for(16374) == 0: @@ -827,9 +921,15 @@ def _manage_hotspots(self) -> None: self._write_game_flags_value_for(9728, 0) self._write_game_flags_value_for(9729, 0) self._write_game_flags_value_for(9730, 0) - elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_WELL: - if self.game_state_manager.game_location == "pc1e": - self._write_game_flags_value_for(10314, 0) + elif hotspot_item == ZorkGrandInquisitorItems.SUBWAY_DESTINATION_CROSSROADS: + if self.game_state_manager.game_location == "us2e": + self._write_game_flags_value_for(13760, 0) + elif self.game_state_manager.game_location == "ue2e": + self._write_game_flags_value_for(13323, 0) + elif self.game_state_manager.game_location == "uh2e": + self._write_game_flags_value_for(13512, 0) + elif self.game_state_manager.game_location == "um2e": + self._write_game_flags_value_for(13651, 0) elif hotspot_item == ZorkGrandInquisitorItems.SUBWAY_DESTINATION_FLOOD_CONTROL_DAM: if self.game_state_manager.game_location == "us2e": self._write_game_flags_value_for(13757, 0) @@ -860,15 +960,18 @@ def _manage_hotspots(self) -> None: elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION: if self.game_state_manager.game_location == "mt1f": self._write_game_flags_value_for(9660, 0) + elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: + if self.game_state_manager.game_location == "mt1f": + self._write_game_flags_value_for(9662, 0) + elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY: + if self.game_state_manager.game_location == "mt1f": + self._write_game_flags_value_for(9664, 0) elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: if self.game_state_manager.game_location == "mt1f": self._write_game_flags_value_for(9666, 0) elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: if self.game_state_manager.game_location == "mt1f": self._write_game_flags_value_for(9668, 0) - elif hotspot_item == ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: - if self.game_state_manager.game_location == "mt1f": - self._write_game_flags_value_for(9662, 0) def _manage_items(self) -> None: if self._player_is_afgncaap(): @@ -926,22 +1029,34 @@ def _manage_items(self) -> None: seen_items.add(item) def _apply_conditional_teleports(self) -> None: + # Skip Well Cutscene if self._player_is_at("uw1x"): self.game_state_manager.set_game_location("uw10", 0) - if self._player_is_at("uw1k") and self._read_game_state_value_for(13938) == 0: - self.game_state_manager.set_game_location("pc10", 250) + # Skip Y'Gael Cutscene + if self._player_is_at("ej10"): + self.game_state_manager.set_game_location("uc10", 1200) + # Skip Power Outage Cutscene if self._player_is_at("ue1q"): self.game_state_manager.set_game_location("ue1e", 0) - if self._player_is_at("ej10"): - self.game_state_manager.set_game_location("uc10", 1200) + # Bucket -> Surface + if self._player_is_at("uw1k") and self._read_game_state_value_for(13938) == 0: + self.game_state_manager.set_game_location("pc10", 250) + + # Monastery Subway Station -> Monastery + if self._player_is_at("um1e") and self._read_game_state_value_for(9637) == 1: + self.game_state_manager.set_game_location("mt10", 1531) # VOXAM Cast + zork_rocks_inert = self._read_game_state_value_for(11767) == 0 + if self._read_game_state_value_for(9) == 224: self._write_game_state_value_for(9, 0) - self._apply_starting_location(force=True) + + if zork_rocks_inert: + self._apply_starting_location(force=True) def _check_for_victory(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS: @@ -1001,11 +1116,11 @@ def _determine_game_state_inventory(self) -> Set[ZorkGrandInquisitorItems]: return game_state_inventory def _add_to_inventory(self, item: ZorkGrandInquisitorItems) -> None: - if item == ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS: - return None - data: ZorkGrandInquisitorItemData = item_data[item] + if data.statemap_keys is None: + return None + if ZorkGrandInquisitorTags.INVENTORY_ITEM in data.tags: if len(self.available_inventory_slots): # Inventory slot overflow protection inventory_slot: int = self.available_inventory_slots.pop() @@ -1016,11 +1131,11 @@ def _add_to_inventory(self, item: ZorkGrandInquisitorItems) -> None: self._write_game_state_value_for(data.statemap_keys[0], 1) def _remove_from_inventory(self, item: ZorkGrandInquisitorItems) -> None: - if item == ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS: - return None - data: ZorkGrandInquisitorItemData = item_data[item] + if data.statemap_keys is None: + return None + if ZorkGrandInquisitorTags.INVENTORY_ITEM in data.tags: inventory_slot: Optional[int] = self._inventory_slot_for(item) @@ -1092,12 +1207,7 @@ def _filter_received_inventory_items( item: ZorkGrandInquisitorItems for item in received_inventory_items: - if item == ZorkGrandInquisitorItems.FLATHEADIA_FUDGE: - if self._read_game_state_value_for(4766) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(4869) == 1: - to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.HUNGUS_LARD: + if item == ZorkGrandInquisitorItems.HUNGUS_LARD: if self._read_game_state_value_for(4870) == 1: to_filter_inventory_items.add(item) elif ( @@ -1105,16 +1215,6 @@ def _filter_received_inventory_items( and self._read_game_state_value_for(4309) == 0 ): to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.JAR_OF_HOTBUGS: - if self._read_game_state_value_for(4750) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(4869) == 1: - to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.LANTERN: - if self._read_game_state_value_for(10477) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(5221) == 1: - to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER: if self._read_game_state_value_for(9491) == 3: to_filter_inventory_items.add(item) @@ -1128,16 +1228,6 @@ def _filter_received_inventory_items( to_filter_inventory_items.add(item) elif self._read_game_state_value_for(4034) == 1: to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.MOSS_OF_MAREILON: - if self._read_game_state_value_for(4763) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(4869) == 1: - to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.MUG: - if self._read_game_state_value_for(4772) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(4869) == 1: - to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.OLD_SCRATCH_CARD: if 32 in inventory_item_values: to_filter_inventory_items.add(item) @@ -1154,36 +1244,18 @@ def _filter_received_inventory_items( elif item == ZorkGrandInquisitorItems.PROZORK_TABLET: if self._read_game_state_value_for(4115) == 1: to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.QUELBEE_HONEYCOMB: - if self._read_game_state_value_for(4769) == 1: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(4869) == 1: - to_filter_inventory_items.add(item) - elif item == ZorkGrandInquisitorItems.ROPE: - if 22 in inventory_item_values: - to_filter_inventory_items.add(item) - elif 111 in inventory_item_values: - to_filter_inventory_items.add(item) - elif ( - self._read_game_state_value_for(10304) == 1 - and not self._read_game_state_value_for(13938) == 1 - ): - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(15150) == 83: + elif item == ZorkGrandInquisitorItems.SANDWITCH_WRAPPER: + if self._read_game_state_value_for(19951) == 1: to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS: if 41 in inventory_item_values: to_filter_inventory_items.add(item) - elif 98 in inventory_item_values: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(201) == 1: + elif self._read_game_state_value_for(19952) == 1: to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV: if 48 in inventory_item_values: to_filter_inventory_items.add(item) - elif 98 in inventory_item_values: - to_filter_inventory_items.add(item) - elif self._read_game_state_value_for(201) == 1: + elif self._read_game_state_value_for(19952) == 1: to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.SNAPDRAGON: if self._read_game_state_value_for(4199) == 1: diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index 8937e54f8af5..cd97c4c21a5f 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -11,14 +11,32 @@ class Goal(Choice): """ display_name: str = "Goal" - default: int = 0 option_three_artifacts: int = 0 + default = 0 -class QuickPortFoozle(DefaultOnToggle): - """If true, the items needed to go down the well will be found in early locations for a smoother early game""" - display_name: str = "Quick Port Foozle" +class StartingLocation(Choice): + """ + Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to + teleport back to the starting location at any time. Depending on the starting location, the player may also be given + a starter kit of items to help them get going + """ + + display_name: str = "Starting Location" + + option_port_foozle: int = 0 + option_crossroads: int = 1 + option_dm_lair: int = 2 + option_dm_lair_house: int = 3 + option_gue_tech: int = 4 + option_spell_lab: int = 5 + option_hades_shore: int = 6 + option_flood_control_dam_3: int = 7 + option_monastery_totemizer: int = 8 + option_monastery_exhibit: int = 9 + + default = 0 class StartWithHotspotItems(DefaultOnToggle): @@ -33,12 +51,43 @@ class StartWithHotspotItems(DefaultOnToggle): display_name: str = "Start with Hotspot Items" +class CraftableSpells(Choice): + """ + Determines the behavior when craftable spells (BEBURTT, OBIDIL, SNAVIG, YASTARD) are obtained. + Spells in a starting location's starter kit always have precedence over this option + + Vanilla: After crafting a spell, the player will be given that exact spell + Any Spell: After crafting a spell, the player will be given a random spell + Anything: After crafting a spell, a random item from the multiworld will be unlocked + """ + + display_name: str = "Craftable Spells" + + option_vanilla: int = 0 + option_any_spell: int = 1 + option_anything: int = 2 + + default = 2 + + class Deathsanity(Toggle): - """If true, adds 16 player death locations to the world""" + """If true, adds 22 unique player death locations to the world""" # TODO: Add note about it being forced in Necro goal display_name: str = "Deathsanity" +class Landmarksanity(DefaultOnToggle): + """If true, adds 20 landmark locations to the world""" # TODO: Add note about it being forced in Zork Tour goal + + display_name: str = "Landmarksanity" + + +class PlaceEarlyItemsLocally(Toggle): + """If true, items to be placed early in the multiworld (when applicable) will be placed locally""" + + display_name: str = "Place Early Items Locally" + + class GrantMissableLocationChecks(Toggle): """ If true, performing an irreversible action will grant the locations checks that would have become unobtainable as a @@ -52,33 +101,13 @@ class GrantMissableLocationChecks(Toggle): display_name: str = "Grant Missable Checks" -class StartingLocation(Choice): - """ - Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to - teleport back to the starting location at any time - """ - - display_name: str = "Starting Location" - - option_port_foozle: int = 0 - option_crossroads: int = 1 - option_dm_lair: int = 2 - option_dm_lair_house: int = 3 - option_gue_tech: int = 4 - option_spell_lab: int = 5 - option_hades_shore: int = 6 - option_flood_control_dam_3: int = 7 - option_monastery_totemizer: int = 8 - option_monastery_exhibit: int = 9 - - default = "random" - - @dataclass class ZorkGrandInquisitorOptions(PerGameCommonOptions): goal: Goal - quick_port_foozle: QuickPortFoozle + starting_location: StartingLocation start_with_hotspot_items: StartWithHotspotItems + craftable_spells: CraftableSpells deathsanity: Deathsanity + landmarksanity: Landmarksanity + place_early_items_locally: PlaceEarlyItemsLocally grant_missable_location_checks: GrantMissableLocationChecks - starting_location: StartingLocation diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 20e4fb8ffa53..413089b4c708 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -1,30 +1,48 @@ -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Set, Tuple, Union from BaseClasses import Item, ItemClassification, Location, Region, Tutorial from worlds.AutoWorld import WebWorld, World -from .data.item_data import item_data, ZorkGrandInquisitorItemData -from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.mapping_data import starting_location_to_logic_helper_item, starting_location_to_region +from .data.item_data import ZorkGrandInquisitorItemData +from .data.location_data import ZorkGrandInquisitorLocationData + +from .data.mapping_data import ( + early_items_for_starting_location, + endgame_connecting_regions_for_goal, + starter_kits_for_starting_location, + starting_location_to_region, +) + from .data.region_data import region_data from .data_funcs import ( item_names_to_id, item_names_to_item, location_names_to_id, + id_to_craftable_spell_behaviors, + id_to_deathsanity, + id_to_goals, + id_to_landmarksanity, id_to_starting_locations, item_groups, items_with_tag, location_groups, - locations_by_region, + locations_by_region_for_world, + prepare_item_data, + prepare_location_data, location_access_rule_for, entrance_access_rule_for, + goal_access_rule_for, ) from .enums import ( + ZorkGrandInquisitorCraftableSpellBehaviors, + ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorEvents, + ZorkGrandInquisitorGoals, ZorkGrandInquisitorItems, + ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorLocations, ZorkGrandInquisitorRegions, ZorkGrandInquisitorStartingLocations, @@ -81,16 +99,74 @@ class ZorkGrandInquisitorWorld(World): web = ZorkGrandInquisitorWebWorld() + craftable_spells: ZorkGrandInquisitorCraftableSpellBehaviors + deathsanity: ZorkGrandInquisitorDeathsanity + early_items: Tuple[ZorkGrandInquisitorItems, ...] filler_item_names: List[str] = item_groups()["Filler"] + goal: ZorkGrandInquisitorGoals + grant_missable_location_checks: bool + initial_totemizer_destination: ZorkGrandInquisitorItems + item_data: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData] item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item() + landmarksanity: ZorkGrandInquisitorLandmarksanity + + location_data: Dict[ + Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], ZorkGrandInquisitorLocationData + ] + + locked_items: Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems] + place_early_items_locally: bool + start_with_hotspot_items: bool + starter_kit: Tuple[ZorkGrandInquisitorItems, ...] starting_location: ZorkGrandInquisitorStartingLocations def generate_early(self) -> None: + self.goal = id_to_goals()[self.options.goal.value] self.starting_location = id_to_starting_locations()[self.options.starting_location.value] - def create_regions(self) -> None: - deathsanity: bool = bool(self.options.deathsanity) + self.starter_kit = tuple() + + if starter_kits_for_starting_location[self.starting_location] is not None: + self.starter_kit = self.random.choice( + starter_kits_for_starting_location[self.starting_location] + ) + + self.early_items = tuple() + + if early_items_for_starting_location[self.starting_location] is not None: + self.early_items = self.random.choice( + early_items_for_starting_location[self.starting_location] + ) + + self.start_with_hotspot_items = bool(self.options.start_with_hotspot_items) + + self.craftable_spells = id_to_craftable_spell_behaviors()[self.options.craftable_spells.value] + + self.deathsanity = id_to_deathsanity()[self.options.deathsanity] + self.landmarksanity = id_to_landmarksanity()[self.options.landmarksanity] + self.place_early_items_locally = bool(self.options.place_early_items_locally) + self.grant_missable_location_checks = bool(self.options.grant_missable_location_checks) + + self.item_data = prepare_item_data( + self.starting_location, + self.goal, + self.deathsanity, + self.landmarksanity, + ) + + self.location_data = prepare_location_data( + self.starting_location, + self.goal, + self.deathsanity, + self.landmarksanity, + ) + + self.locked_items = self._prepare_locked_items() + + self.initial_totemizer_destination = self._select_initial_totemizer_destination() + + def create_regions(self) -> None: region_mapping: Dict[ZorkGrandInquisitorRegions, Region] = dict() region_enum_item: ZorkGrandInquisitorRegions @@ -98,7 +174,9 @@ def create_regions(self) -> None: region_mapping[region_enum_item] = Region(region_enum_item.value, self.player, self.multiworld) region_locations_mapping: Dict[ZorkGrandInquisitorRegions, List[ZorkGrandInquisitorLocations]] - region_locations_mapping = locations_by_region(include_deathsanity=deathsanity) + region_locations_mapping = locations_by_region_for_world(self.location_data) + + region_connecting_endgame: ZorkGrandInquisitorRegions = endgame_connecting_regions_for_goal[self.goal] region_enum_item: ZorkGrandInquisitorRegions region: Region @@ -108,7 +186,7 @@ def create_regions(self) -> None: # Locations location_enum_item: ZorkGrandInquisitorLocations for location_enum_item in regions_locations: - data: ZorkGrandInquisitorLocationData = location_data[location_enum_item] + data: ZorkGrandInquisitorLocationData = self.location_data[location_enum_item] location: ZorkGrandInquisitorLocation = ZorkGrandInquisitorLocation( self.player, @@ -117,7 +195,10 @@ def create_regions(self) -> None: region_mapping[data.region], ) - if isinstance(location_enum_item, ZorkGrandInquisitorEvents): + # Locked Items + if location_enum_item in self.locked_items: + location.place_locked_item(self.create_item(self.locked_items[location_enum_item].value)) + elif isinstance(location_enum_item, ZorkGrandInquisitorEvents): location.place_locked_item( ZorkGrandInquisitorItem( data.event_item_name, @@ -127,6 +208,7 @@ def create_regions(self) -> None: ) ) + # Access Rules location_access_rule: str = location_access_rule_for(location_enum_item, self.player) if location_access_rule != "lambda state: True": @@ -144,32 +226,64 @@ def create_regions(self) -> None: else: region.connect(region_mapping[region_exit], rule=eval(entrance_access_rule)) + if region_enum_item == region_connecting_endgame: + goal_access_rule: str = goal_access_rule_for(region_enum_item, self.goal, self.player) + region.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) + self.multiworld.regions.append(region) - # Connect "Menu" region to starting location + # Connect "Menu" region to starting location and to endgame when applicable region_menu: Region = Region("Menu", self.player, self.multiworld) region_starting_location: ZorkGrandInquisitorRegions = starting_location_to_region[self.starting_location] + region_menu.connect(region_mapping[ZorkGrandInquisitorRegions.ANYWHERE]) region_menu.connect(region_mapping[region_starting_location]) + if region_connecting_endgame == ZorkGrandInquisitorRegions.MENU: + goal_access_rule: str = goal_access_rule_for(ZorkGrandInquisitorRegions.MENU, self.goal, self.player) + region_menu.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) + self.multiworld.regions.append(region_menu) def create_items(self) -> None: - quick_port_foozle: bool = bool(self.options.quick_port_foozle) - start_with_hotspot_items: bool = bool(self.options.start_with_hotspot_items) + items_to_ignore: Set[ZorkGrandInquisitorItems] = set() + items_to_precollect: Set[ZorkGrandInquisitorItems] = set() + items_to_place_early: Set[ZorkGrandInquisitorItems] + item: ZorkGrandInquisitorItems + + for item in items_with_tag(ZorkGrandInquisitorTags.FILLER): + items_to_ignore.add(item) + + for item in items_with_tag(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS): + items_to_ignore.add(item) + + for item in self.locked_items.values(): + items_to_ignore.add(item) + + for item in self.starter_kit: + items_to_precollect.add(item) + + if self.start_with_hotspot_items: + for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT): + items_to_precollect.add(item) + + items_to_precollect.add(self.initial_totemizer_destination) + + if self.starting_location != ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: + items_to_precollect.add(ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT) + + if self.starting_location != ZorkGrandInquisitorStartingLocations.SPELL_LAB: + items_to_precollect.add(ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT) + + items_to_place_early = set(self.early_items) - items_to_precollect + + # Create Item Pool item_pool: List[ZorkGrandInquisitorItem] = list() - item: ZorkGrandInquisitorItems data: ZorkGrandInquisitorItemData - for item, data in item_data.items(): - tags: Tuple[ZorkGrandInquisitorTags, ...] = data.tags or tuple() - - if ZorkGrandInquisitorTags.FILLER in tags: - continue - elif ZorkGrandInquisitorTags.HOTSPOT in tags and start_with_hotspot_items: - continue - elif ZorkGrandInquisitorTags.LOGIC_HELPER in tags: + for item, data in self.item_data.items(): + if item in items_to_ignore or item in items_to_precollect: continue item_pool.append(self.create_item(item.value)) @@ -179,30 +293,21 @@ def create_items(self) -> None: self.multiworld.itempool += item_pool - if quick_port_foozle: - self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.ROPE.value] = 1 - self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.LANTERN.value] = 1 + # Precollect Items + for item in items_to_precollect: + self.multiworld.push_precollected(self.create_item(item.value)) - if not start_with_hotspot_items: - self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.HOTSPOT_WELL.value] = 1 - self.multiworld.early_items[self.player][ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR.value] = 1 - - self.multiworld.early_items[self.player][ - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL.value - ] = 1 - - if start_with_hotspot_items: - item: ZorkGrandInquisitorItems - for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT): - self.multiworld.push_precollected(self.create_item(item.value)) + # Set Early Items + # TODO: Does this even work? Needs testing + if len(items_to_place_early): + early: Dict[int, Dict[str, int]] + early = self.multiworld.local_early_items if self.place_early_items_locally else self.multiworld.early_items - # Logic Helper Items - self.multiworld.push_precollected( - self.create_item(starting_location_to_logic_helper_item[self.starting_location].value) - ) + for item in items_to_place_early: + early[self.player][item.value] = 1 def create_item(self, name: str) -> ZorkGrandInquisitorItem: - data: ZorkGrandInquisitorItemData = item_data[self.item_name_to_item[name]] + data: ZorkGrandInquisitorItemData = self.item_data[self.item_name_to_item[name]] return ZorkGrandInquisitorItem( name, @@ -215,14 +320,116 @@ def generate_basic(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) def fill_slot_data(self) -> Dict[str, Any]: - return self.options.as_dict( + slot_data: Dict[str, Any] = self.options.as_dict( "goal", - "quick_port_foozle", + "starting_location", "start_with_hotspot_items", + "craftable_spells", "deathsanity", + "landmarksanity", "grant_missable_location_checks", - "starting_location", ) + slot_data["initial_totemizer_destination"] = self.initial_totemizer_destination.value + + return slot_data + def get_filler_item_name(self) -> str: return self.random.choice(self.filler_item_names) + + def _prepare_locked_items( + self, + ) -> Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems]: + locked_items: Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems] = dict() + + # Goal Items + if self.goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS: + locked_items[ + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT + ] = ZorkGrandInquisitorItems.COCONUT_OF_QUENDOR + + locked_items[ + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG + ] = ZorkGrandInquisitorItems.SKULL_OF_YORUK + + locked_items[ + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP + ] = ZorkGrandInquisitorItems.CUBE_OF_FOUNDATION + + # Craftable Spells + if self.craftable_spells == ZorkGrandInquisitorCraftableSpellBehaviors.VANILLA: + if ZorkGrandInquisitorItems.SPELL_BEBURTT not in self.starter_kit: + locked_items[ + ZorkGrandInquisitorLocations.IMBUE_BEBURTT + ] = ZorkGrandInquisitorItems.SPELL_BEBURTT + + if ZorkGrandInquisitorItems.SPELL_OBIDIL not in self.starter_kit: + locked_items[ + ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP + ] = ZorkGrandInquisitorItems.SPELL_OBIDIL + + if ZorkGrandInquisitorItems.SPELL_SNAVIG not in self.starter_kit: + locked_items[ + ZorkGrandInquisitorLocations.SNAVIG_REPAIRED + ] = ZorkGrandInquisitorItems.SPELL_SNAVIG + + if ZorkGrandInquisitorItems.SPELL_YASTARD not in self.starter_kit: + locked_items[ + ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU + ] = ZorkGrandInquisitorItems.SPELL_YASTARD + elif self.craftable_spells == ZorkGrandInquisitorCraftableSpellBehaviors.ANY_SPELL: + allowable_spells: Set[ZorkGrandInquisitorItems] = { + ZorkGrandInquisitorItems.SPELL_BEBURTT, + ZorkGrandInquisitorItems.SPELL_GLORF, + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ZorkGrandInquisitorItems.SPELL_KENDALL, + ZorkGrandInquisitorItems.SPELL_OBIDIL, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_REZROV, + ZorkGrandInquisitorItems.SPELL_SNAVIG, + ZorkGrandInquisitorItems.SPELL_THROCK, + ZorkGrandInquisitorItems.SPELL_YASTARD, + } + + allowable_spells -= set(self.starter_kit) + + allowable_spells_yastard: List[str] = sorted([item.value for item in allowable_spells]) + + spell_yastard: ZorkGrandInquisitorItems = self.item_name_to_item[ + self.random.choice(allowable_spells_yastard) + ] + + locked_items[ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU] = spell_yastard + + allowable_spells -= {spell_yastard} + + if self.starting_location != ZorkGrandInquisitorStartingLocations.SPELL_LAB: + allowable_spells -= { + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.SPELL_REZROV, + } + + allowable_spells_spell_lab: List[str] = sorted( + [item.value for item in allowable_spells] + ) + + spells_to_lock: List[ZorkGrandInquisitorItems] = [ + self.item_name_to_item[item] + for item in self.random.sample(allowable_spells_spell_lab, 3) + ] + + locked_items[ZorkGrandInquisitorLocations.IMBUE_BEBURTT] = spells_to_lock[0] + locked_items[ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP] = spells_to_lock[1] + locked_items[ZorkGrandInquisitorLocations.SNAVIG_REPAIRED] = spells_to_lock[2] + + return locked_items + + def _select_initial_totemizer_destination(self) -> ZorkGrandInquisitorItems: + return self.random.choice(( + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, + )) From f94816ea43338f069f6ed19b674ad94bf36a3cc1 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Mon, 18 Nov 2024 20:17:52 -0500 Subject: [PATCH 07/51] add a small delay before accessing memory after teleporting to starting location --- worlds/zork_grand_inquisitor/game_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index cb7933518ec7..f943f7b11f28 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -2,6 +2,7 @@ import functools import logging import traceback # TODO: Only in dev +import time from typing import Dict, List, Optional, Set, Tuple, Union @@ -209,8 +210,8 @@ def update(self) -> None: try: self.game_state_manager.refresh_game_location() - self._apply_starting_location() self._apply_initial_totemizer_destination() + self._apply_starting_location() self._apply_permanent_game_state() self._apply_conditional_game_state() @@ -261,6 +262,7 @@ def _apply_starting_location(self, force: bool = False) -> None: self.game_state_manager.set_game_location("me10", 1023) self._write_game_state_value_for(19985, 1) + time.sleep(0.1) def _apply_initial_totemizer_destination(self) -> None: if self.initial_totemizer_destination is None: From c5607a35f440a88345e69255a8fdbdd6573fe35e Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Tue, 19 Nov 2024 18:07:50 -0500 Subject: [PATCH 08/51] fix skull cage boards being usable without the skull cage hotspot --- worlds/zork_grand_inquisitor/data/item_data.py | 2 +- worlds/zork_grand_inquisitor/game_controller.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index 010781d38271..ff9a374e5173 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -527,7 +527,7 @@ class ZorkGrandInquisitorItemData(NamedTuple): tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE: ZorkGrandInquisitorItemData( - statemap_keys=(2769,), + statemap_keys=(2769, 2761, 2764, 2767), archipelago_id=ITEM_OFFSET + 100 + 45, classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index f943f7b11f28..eaf0b501d9c0 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -865,8 +865,14 @@ def _manage_hotspots(self) -> None: if self.game_state_manager.game_location == "sg6e": if self._read_game_state_value_for(15715) == 1: self._write_game_flags_value_for(2769, 2) + self._write_game_flags_value_for(2761, 2) + self._write_game_flags_value_for(2764, 2) + self._write_game_flags_value_for(2767, 2) else: self._write_game_flags_value_for(2769, 0) + self._write_game_flags_value_for(2761, 0) + self._write_game_flags_value_for(2764, 0) + self._write_game_flags_value_for(2767, 0) elif hotspot_item == ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON: if self.game_state_manager.game_location == "dg2f": if self._read_game_state_value_for(4114) == 1 or self._read_game_state_value_for(4115) == 1: From cb5cde27ea4f19f99d659194cdf8266b2c6553d5 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Wed, 20 Nov 2024 00:01:46 -0500 Subject: [PATCH 09/51] hotspots option + regional hotspots --- worlds/zork_grand_inquisitor/client.py | 5 + .../data/entrance_rule_data.py | 146 ++++- .../zork_grand_inquisitor/data/item_data.py | 66 ++ .../data/location_data.py | 617 ++++++++++++++---- .../data/mapping_data.py | 270 +++++++- .../data/transform_data.py | 2 + worlds/zork_grand_inquisitor/data_funcs.py | 17 + worlds/zork_grand_inquisitor/enums.py | 18 + .../zork_grand_inquisitor/game_controller.py | 20 +- worlds/zork_grand_inquisitor/options.py | 20 +- worlds/zork_grand_inquisitor/world.py | 28 +- 11 files changed, 1041 insertions(+), 168 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 87dd0b840257..524c22021863 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -11,6 +11,7 @@ item_names_to_item, location_names_to_id, id_to_deathsanity, + id_to_hotspots, id_to_items, id_to_landmarksanity, id_to_locations, @@ -108,6 +109,10 @@ def on_package(self, cmd: str, _args: Any) -> None: id_to_starting_locations()[_args["slot_data"]["starting_location"]] ) + self.game_controller.option_hotspots = ( + id_to_hotspots()[_args["slot_data"]["hotspots"]] + ) + self.game_controller.option_deathsanity = ( id_to_deathsanity()[_args["slot_data"]["deathsanity"]] ) diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py index 532bec7aafe4..5c7e512c98f0 100644 --- a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py +++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py @@ -31,7 +31,10 @@ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.DM_LAIR): ( ( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ( + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ( ZorkGrandInquisitorItems.MAP, @@ -41,7 +44,10 @@ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE): ( ( ZorkGrandInquisitorItems.SPELL_REZROV, - ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): ( @@ -59,7 +65,10 @@ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.PORT_FOOZLE): ( ( ZorkGrandInquisitorItems.WELL_ROPE, - ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ( + ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): ( @@ -71,7 +80,10 @@ (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS): ( ( ZorkGrandInquisitorItems.SUBWAY_TOKEN, - ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), (ZorkGrandInquisitorRegions.CROSSROADS, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): ( @@ -113,18 +125,27 @@ ), (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.DM_LAIR): ( ( - ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, + ( + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WALKING_CASTLE): ( ( - ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ( + ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.SPELL_OBIDIL, ), ), (ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, ZorkGrandInquisitorRegions.WHITE_HOUSE): ( ( - ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.SPELL_NARWILE, ZorkGrandInquisitorItems.SPELL_YASTARD, ), @@ -132,32 +153,54 @@ (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON): ( ( ZorkGrandInquisitorItems.TOTEM_GRIFF, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW, + ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO, + ), ), ), (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, ZorkGrandInquisitorRegions.HADES_BEYOND_GATES): None, (ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO): None, (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE): ( - (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), + ( + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ), ), (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): ( ( ZorkGrandInquisitorItems.SPELL_IGRAM, - ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ( + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), (ZorkGrandInquisitorRegions.GUE_TECH, ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE): ( - (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR,), + ( + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ), ), (ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.CROSSROADS): None, (ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, ZorkGrandInquisitorRegions.GUE_TECH): ( - (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.GUE_TECH): None, (ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE): ( ( ZorkGrandInquisitorItems.STUDENT_ID, - ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE, + ( + ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.CROSSROADS): ( @@ -173,7 +216,12 @@ ), ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.GUE_TECH): ( - (ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS,), + ( + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ), ), (ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, ZorkGrandInquisitorRegions.HADES_SHORE): ( ( @@ -229,8 +277,14 @@ ), (ZorkGrandInquisitorRegions.HADES_SHORE, ZorkGrandInquisitorRegions.HADES): ( ( - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, ), ), @@ -252,23 +306,41 @@ (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.HADES_SHORE): ( ( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT): ( ( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), (ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): None, (ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, ZorkGrandInquisitorRegions.MONASTERY): None, (ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST): ( ( - ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, - ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER, ZorkGrandInquisitorItems.SPELL_NARWILE, ZorkGrandInquisitorItems.SPELL_YASTARD, @@ -280,8 +352,14 @@ (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP): ( ( ZorkGrandInquisitorItems.CIGAR, - ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ( + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ), ), (ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP, ZorkGrandInquisitorRegions.PORT_FOOZLE): None, @@ -289,7 +367,10 @@ (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN): ( ( ZorkGrandInquisitorItems.TOTEM_LUCY, - ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), (ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST): None, @@ -314,7 +395,10 @@ ), (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY): ( ( - ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.HADES_SHORE): ( @@ -326,10 +410,16 @@ (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.SPELL_LAB): ( ( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ( + ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ZorkGrandInquisitorEvents.DAM_DESTROYED, ZorkGrandInquisitorItems.SPELL_GOLGATEM, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), (ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, ZorkGrandInquisitorRegions.SUBWAY_MONASTERY): ( diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index ff9a374e5173..41f46f5b8b79 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -610,6 +610,72 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.HOTSPOT,), ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 0, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 1, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 2, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 3, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 4, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 5, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 6, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 7, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 8, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 9, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 100 + 80 + 10, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL,), + ), # Spells ZorkGrandInquisitorItems.SPELL_BEBURTT: ZorkGrandInquisitorItemData( statemap_keys=(194,), diff --git a/worlds/zork_grand_inquisitor/data/location_data.py b/worlds/zork_grand_inquisitor/data/location_data.py index e259a467bf29..6a3019b03e39 100644 --- a/worlds/zork_grand_inquisitor/data/location_data.py +++ b/worlds/zork_grand_inquisitor/data/location_data.py @@ -62,7 +62,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ( + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ZorkGrandInquisitorItems.CIGAR, ), ), @@ -86,7 +89,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE, - ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX, + ( + ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), ), ), ZorkGrandInquisitorLocations.A_SMALLWAY: ZorkGrandInquisitorLocationData( @@ -95,7 +101,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ( + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ZorkGrandInquisitorItems.SPELL_IGRAM, ), ), @@ -105,7 +114,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE, + ( + ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), ZorkGrandInquisitorItems.SPELL_THROCK, ), ), @@ -134,7 +146,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorItems.HAMMER, ZorkGrandInquisitorItems.SNAPDRAGON, - ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.BONK: ZorkGrandInquisitorLocationData( @@ -144,7 +159,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.HAMMER, - ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ( + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.BRAVE_SOULS_WANTED: ZorkGrandInquisitorLocationData( @@ -160,7 +178,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ( + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ) ), ZorkGrandInquisitorLocations.BROG_EAT_ROCKS: ZorkGrandInquisitorLocationData( @@ -183,9 +204,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ( + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ZorkGrandInquisitorItems.BROGS_PLANK, - ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ( + ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ) ), ZorkGrandInquisitorLocations.CASTLE_WATCHING_A_FIELD_GUIDE: ZorkGrandInquisitorLocationData( @@ -216,7 +243,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO, + ), ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, ), ), @@ -228,8 +258,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED, ZorkGrandInquisitorItems.SPELL_IGRAM, - ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, - ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER, + ( + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.CUT_THAT_OUT_YOU_LITTLE_CREEP: ZorkGrandInquisitorLocationData( @@ -244,7 +280,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ( + ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.SPELL_GOLGATEM, ), ), @@ -262,7 +301,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_GRIFF, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), ), ZorkGrandInquisitorLocations.DOWN: ZorkGrandInquisitorLocationData( @@ -272,7 +314,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_LUCY, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), ), ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL: ZorkGrandInquisitorLocationData( @@ -289,8 +334,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.EGGPLANTS: ZorkGrandInquisitorLocationData( @@ -331,7 +382,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ( + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ZorkGrandInquisitorItems.CIGAR, ), ), @@ -350,7 +404,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SPELL_THROCK, ZorkGrandInquisitorItems.SNAPDRAGON, ZorkGrandInquisitorItems.HAMMER, - ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.FROBUARY_3_UNDERGROUNDHOG_DAY: ZorkGrandInquisitorLocationData( @@ -366,7 +423,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorEvents.ZORKMID_BILL_ACCESSIBLE, - ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.GO_AWAY: ZorkGrandInquisitorLocationData( @@ -382,9 +442,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.BROGS_GRUE_EGG, - ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ( + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ZorkGrandInquisitorItems.BROGS_PLANK, - ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ( + ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ) ), ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM: ZorkGrandInquisitorLocationData( @@ -399,8 +465,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), ) ), ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING: ZorkGrandInquisitorLocationData( @@ -415,7 +487,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER, ), ), @@ -425,7 +500,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND, + ( + ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ZorkGrandInquisitorItems.SHOVEL, ), ), @@ -454,8 +532,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ( + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), ZorkGrandInquisitorLocations.IM_COMPLETELY_NUDE: ZorkGrandInquisitorLocationData( @@ -471,7 +555,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ( + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS: ZorkGrandInquisitorLocationData( @@ -488,7 +575,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.HAMMER, - ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE, + ( + ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST: ZorkGrandInquisitorLocationData( @@ -498,7 +588,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SPELL_REZROV, - ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), ZorkGrandInquisitorLocations.ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN: ZorkGrandInquisitorLocationData( @@ -514,7 +607,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.SPELL_THROCK, - ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ( + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.I_DONT_WANT_NO_TROUBLE: ZorkGrandInquisitorLocationData( @@ -530,10 +626,16 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ( + ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ZorkGrandInquisitorEvents.DAM_DESTROYED, ZorkGrandInquisitorItems.SPELL_GOLGATEM, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE: ZorkGrandInquisitorLocationData( @@ -557,8 +659,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ( + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ) ), ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL: ZorkGrandInquisitorLocationData( @@ -568,8 +676,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( (ZorkGrandInquisitorItems.TOTEM_GRIFF, ZorkGrandInquisitorItems.TOTEM_LUCY), - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), ), ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR: ZorkGrandInquisitorLocationData( @@ -589,7 +703,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.MEAD_LIGHT, - ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ), ), ZorkGrandInquisitorLocations.MIKES_PANTS: ZorkGrandInquisitorLocationData( @@ -605,7 +722,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.HAMMER, - ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.NATIONAL_TREASURE: ZorkGrandInquisitorLocationData( @@ -615,8 +735,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SPELL_REZROV, - ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, - ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), ), ), ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR: ZorkGrandInquisitorLocationData( @@ -632,8 +758,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE: ZorkGrandInquisitorLocationData( @@ -642,7 +774,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ( + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.CIGAR, ), ), @@ -658,7 +793,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 71, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), + ), ), ZorkGrandInquisitorLocations.NO_BONDAGE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe2e"), (10262, 2), (15150, 83)), @@ -667,7 +807,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorEvents.ROPE_GLORFABLE, - ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ), ), ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP: ZorkGrandInquisitorLocationData( @@ -676,7 +819,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ZorkGrandInquisitorItems.SANDWITCH_WRAPPER, ), ), @@ -689,7 +835,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO, + ), ), ), ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS: ZorkGrandInquisitorLocationData( @@ -699,7 +848,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_BROG, - ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU: ZorkGrandInquisitorLocationData( @@ -743,7 +895,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorItems.HUNGUS_LARD, ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ( + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.PERMASEAL: ZorkGrandInquisitorLocationData( @@ -770,8 +925,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, - ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ZorkGrandInquisitorItems.LARGE_TELEGRAPH_HAMMER, ZorkGrandInquisitorItems.SPELL_NARWILE, ), @@ -783,7 +944,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.PROZORK_TABLET, - ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ( + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG: ZorkGrandInquisitorLocationData( @@ -794,7 +958,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS, ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV, - ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ( + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE: ZorkGrandInquisitorLocationData( @@ -810,7 +977,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG, - ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), ZorkGrandInquisitorLocations.SOUVENIR: ZorkGrandInquisitorLocationData( @@ -820,7 +990,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), ), ), ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData( @@ -829,9 +1002,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData( @@ -844,8 +1023,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ( + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.SUCKING_ROCKS: ZorkGrandInquisitorLocationData( @@ -855,10 +1040,19 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ZorkGrandInquisitorItems.PERMA_SUCK_MACHINE, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR: ZorkGrandInquisitorLocationData( @@ -866,7 +1060,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 93, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), + ), ), ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1h"),), @@ -883,7 +1082,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO, + ), ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, ), ), @@ -894,7 +1096,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorEvents.ROPE_GLORFABLE, - ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ), ), ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS: ZorkGrandInquisitorLocationData( @@ -915,7 +1120,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 99, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), + ), ), ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE: ZorkGrandInquisitorLocationData( game_state_trigger=((9459, 1),), @@ -958,7 +1168,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SUBWAY_TOKEN, - ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS, + ), ), ), ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE: ZorkGrandInquisitorLocationData( @@ -976,7 +1189,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorItems.HAMMER, ZorkGrandInquisitorItems.SPELL_THROCK, - ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES: ZorkGrandInquisitorLocationData( @@ -1013,7 +1229,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_LUCY, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), ), ZorkGrandInquisitorLocations.USELESS_BUT_FUN: ZorkGrandInquisitorLocationData( @@ -1030,7 +1249,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_GRIFF, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), ), ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB: ZorkGrandInquisitorLocationData( @@ -1048,7 +1270,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR, ZorkGrandInquisitorItems.MEAD_LIGHT, ZorkGrandInquisitorItems.ZIMDOR_SCROLL, - ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ( + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE: ZorkGrandInquisitorLocationData( @@ -1058,7 +1283,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.TOTEM_GRIFF, - ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER: ZorkGrandInquisitorLocationData( @@ -1071,8 +1299,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ( + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID: ZorkGrandInquisitorLocationData( @@ -1082,7 +1316,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.PLASTIC_SIX_PACK_HOLDER, - ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ), ), ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL: ZorkGrandInquisitorLocationData( @@ -1091,7 +1328,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.SPELL_NARWILE, ), ), @@ -1106,7 +1346,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 122, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_MIRROR,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), + ), ), ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dg4e"), (4266, 1), (9, 21), (4035, 1)), @@ -1115,7 +1360,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ( + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: ZorkGrandInquisitorLocationData( @@ -1132,7 +1380,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.CORE,), requirements=( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ( + ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), ), ), ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP: ZorkGrandInquisitorLocationData( @@ -1145,8 +1396,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ( + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.YOU_ONE_OF_THEM_AGITATORS_AINT_YA: ZorkGrandInquisitorLocationData( @@ -1160,7 +1417,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 128, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), + ), ), # Deathsanity ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK: ZorkGrandInquisitorLocationData( @@ -1169,7 +1431,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=( - ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ( + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE, + ), ZorkGrandInquisitorItems.CIGAR, ), ), @@ -1180,7 +1445,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.SWORD, - ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ( + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.DEATH_CLIMBED_OUT_OF_THE_WELL: ZorkGrandInquisitorLocationData( @@ -1218,8 +1486,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_2, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_3, ZorkGrandInquisitorItems.LUCYS_PLAYING_CARD_4, - ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, - ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ( + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST, + ), ), ), ZorkGrandInquisitorLocations.DEATH_LOST_SOUL_TO_OLD_SCRATCH: ZorkGrandInquisitorLocationData( @@ -1236,7 +1510,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.HUNGUS_LARD, - ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ( + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), ), ZorkGrandInquisitorLocations.DEATH_SLICED_UP_BY_THE_INVISIBLE_GUARD: ZorkGrandInquisitorLocationData( @@ -1252,7 +1529,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY, ZorkGrandInquisitorTags.MISSABLE), requirements=( ZorkGrandInquisitorItems.SPELL_IGRAM, - ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ( + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.DEATH_SWALLOWED_BY_A_DRAGON: ZorkGrandInquisitorLocationData( @@ -1264,7 +1544,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.GRIFFS_AIR_PUMP, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_RAFT, ZorkGrandInquisitorItems.GRIFFS_INFLATABLE_SEA_CAPTAIN, - ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO, + ), ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, ), ), @@ -1275,7 +1558,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.SPELL_THROCK, - ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS, + ( + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_INFINITY: ZorkGrandInquisitorLocationData( @@ -1285,8 +1571,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_NEWARK_NEW_JERSEY: ZorkGrandInquisitorLocationData( @@ -1296,8 +1588,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_HALLS_OF_INQUISITION: ZorkGrandInquisitorLocationData( @@ -1307,7 +1605,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_INFINITY: ZorkGrandInquisitorLocationData( @@ -1317,7 +1618,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_NEWARK_NEW_JERSEY: ZorkGrandInquisitorLocationData( @@ -1327,7 +1631,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData( @@ -1337,7 +1644,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_PERMANENTLY_SURFACE_OF_MERZ: ZorkGrandInquisitorLocationData( @@ -1347,7 +1657,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_TOTEMIZED_SURFACE_OF_MERZ: ZorkGrandInquisitorLocationData( @@ -1357,8 +1670,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): tags=(ZorkGrandInquisitorTags.DEATHSANITY,), requirements=( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, - ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY, + ), ), ), ZorkGrandInquisitorLocations.DEATH_YOURE_NOT_CHARON: ZorkGrandInquisitorLocationData( @@ -1454,7 +1773,12 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=LOCATION_OFFSET + 300 + 11, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.LANDMARKSANITY,), - requirements=(ZorkGrandInquisitorItems.HOTSPOT_MIRROR,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), + ), ), ZorkGrandInquisitorLocations.LANDMARK_PAST_PORT_FOOZLE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "qe10"),), @@ -1524,8 +1848,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=None, region=ZorkGrandInquisitorRegions.HADES_SHORE, requirements=( - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, - ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES, + ), ), event_item_name=ZorkGrandInquisitorEvents.CHARON_CALLED.value, ), @@ -1535,8 +1865,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), event_item_name=ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE.value, ), @@ -1546,8 +1882,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, requirements=( ZorkGrandInquisitorItems.SPELL_REZROV, - ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, - ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM, + ), ), event_item_name=ZorkGrandInquisitorEvents.DAM_DESTROYED.value, ), @@ -1559,7 +1901,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR, ZorkGrandInquisitorItems.MEAD_LIGHT, ZorkGrandInquisitorItems.ZIMDOR_SCROLL, - ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ( + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), event_item_name=ZorkGrandInquisitorEvents.DOOR_DRANK_MEAD.value, ), @@ -1568,7 +1913,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): archipelago_id=None, region=ZorkGrandInquisitorRegions.DM_LAIR, requirements=( - ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ( + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ZorkGrandInquisitorItems.CIGAR, ), event_item_name=ZorkGrandInquisitorEvents.DOOR_SMOKED_CIGAR.value, @@ -1579,8 +1927,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), event_item_name=ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE.value, ), @@ -1590,8 +1944,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, - ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ( + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), event_item_name=ZorkGrandInquisitorEvents.HAS_REPAIRABLE_OBIDIL.value, ), @@ -1602,7 +1962,10 @@ class ZorkGrandInquisitorLocationData(NamedTuple): requirements=( ZorkGrandInquisitorItems.SCROLL_FRAGMENT_ANS, ZorkGrandInquisitorItems.SCROLL_FRAGMENT_GIV, - ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ( + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), ), event_item_name=ZorkGrandInquisitorEvents.HAS_REPAIRABLE_SNAVIG.value, ), @@ -1628,8 +1991,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.WHITE_HOUSE, requirements=( (ZorkGrandInquisitorItems.TOTEM_GRIFF, ZorkGrandInquisitorItems.TOTEM_LUCY), - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, - ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE, + ), ), event_item_name=ZorkGrandInquisitorEvents.WHITE_HOUSE_LETTER_MAILABLE.value, ), @@ -1646,9 +2015,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, requirements=( ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS, - ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ZorkGrandInquisitorItems.ZORK_ROCKS, - ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS, + ( + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH, + ), ), event_item_name=ZorkGrandInquisitorEvents.ZORK_ROCKS_ACTIVATED.value, ), diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 0c920e2864ac..3b10ffb2711a 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -18,11 +18,7 @@ ), ZorkGrandInquisitorStartingLocations.CROSSROADS: None, ZorkGrandInquisitorStartingLocations.DM_LAIR: None, - ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ( - ( - ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, - ), - ), + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: None, ZorkGrandInquisitorStartingLocations.GUE_TECH: None, ZorkGrandInquisitorStartingLocations.SPELL_LAB: None, ZorkGrandInquisitorStartingLocations.HADES_SHORE: None, @@ -44,6 +40,270 @@ ), } +hotspot_to_regional_hotspot: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItems] = { + ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES + ), + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST + ), + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB + ), + ZorkGrandInquisitorItems.HOTSPOT_BLINDS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_BUCKET: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS + ), + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY + ), + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY + ), + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE + ), + ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE + ), + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO + ), + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO + ), + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS + ), + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM + ), + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM + ), + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS + ), + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE + ), + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES + ), + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES + ), + ZorkGrandInquisitorItems.HOTSPOT_HARRY: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS + ), + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE + ), + ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE + ), + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE + ), + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE + ), + ZorkGrandInquisitorItems.HOTSPOT_MIRROR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM + ), + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST + ), + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB + ), + ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE + ), + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM + ), + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB + ), + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB + ), + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB + ), + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR + ), + ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH + ), + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS + ), + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST + ), + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY + ), + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS: ( + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY + ), +} + +hotspots_for_regional_hotspot: Dict[ZorkGrandInquisitorItems, Tuple[ZorkGrandInquisitorItems, ...]] = { + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_CROSSROADS: ( + ZorkGrandInquisitorItems.HOTSPOT_BUCKET, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_LAIR_ENTRANCE, + ZorkGrandInquisitorItems.HOTSPOT_GLASS_CASE, + ZorkGrandInquisitorItems.HOTSPOT_IN_MAGIC_WE_TRUST_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_SUBWAY_TOKEN_SLOT, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR: ( + ZorkGrandInquisitorItems.HOTSPOT_BLINDS, + ZorkGrandInquisitorItems.HOTSPOT_CLOSET_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT, + ZorkGrandInquisitorItems.HOTSPOT_HARRY, + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_ASHTRAY, + ZorkGrandInquisitorItems.HOTSPOT_HARRYS_BIRD_BATH, + ZorkGrandInquisitorItems.HOTSPOT_MIRROR, + ZorkGrandInquisitorItems.HOTSPOT_QUELBEE_HIVE, + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_SPRING_MUSHROOM, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO: ( + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_CLAW, + ZorkGrandInquisitorItems.HOTSPOT_DRAGON_NOSTRILS, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM: ( + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_FLOOD_CONTROL_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_MOSSY_GRATE, + ZorkGrandInquisitorItems.HOTSPOT_SOUVENIR_COIN_SLOT, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_GUE_TECH: ( + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CANDY_MACHINE_VACUUM_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CHANGE_MACHINE_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_DENTED_LOCKER, + ZorkGrandInquisitorItems.HOTSPOT_DIRT_MOUND, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_FROZEN_TREAT_MACHINE_DOORS, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_GRASS, + ZorkGrandInquisitorItems.HOTSPOT_GUE_TECH_WINDOWS, + ZorkGrandInquisitorItems.HOTSPOT_PURPLE_WORDS, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_SODA_MACHINE_COIN_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_STUDENT_ID_MACHINE, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_HADES: ( + ZorkGrandInquisitorItems.HOTSPOT_666_MAILBOX, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_BUTTONS, + ZorkGrandInquisitorItems.HOTSPOT_HADES_PHONE_RECEIVER, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_MONASTERY: ( + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_HAMMER_SLOT, + ZorkGrandInquisitorItems.HOTSPOT_CLOSING_THE_TIME_TUNNELS_LEVER, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_SWITCH, + ZorkGrandInquisitorItems.HOTSPOT_TOTEMIZER_WHEELS, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE: ( + ZorkGrandInquisitorItems.HOTSPOT_DOCK_WINCH, + ZorkGrandInquisitorItems.HOTSPOT_GRAND_INQUISITOR_DOLL, + ZorkGrandInquisitorItems.HOTSPOT_JACKS_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_LOUDSPEAKER_VOLUME_BUTTONS, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_PORT_FOOZLE_PAST: ( + ZorkGrandInquisitorItems.HOTSPOT_ALPINES_QUANDRY_CARD_SLOTS, + ZorkGrandInquisitorItems.HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_TAVERN_FLY, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB: ( + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, + ZorkGrandInquisitorItems.HOTSPOT_ROPE_BRIDGE, + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT, + ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_CHASM, + ), + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_WHITE_HOUSE: ( + ZorkGrandInquisitorItems.HOTSPOT_COOKING_POT, + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_DOOR, + ZorkGrandInquisitorItems.HOTSPOT_MAILBOX_FLAG, + ZorkGrandInquisitorItems.HOTSPOT_SKULL_CAGE, + ), +} + starter_kits_for_starting_location: Dict[ ZorkGrandInquisitorStartingLocations, Optional[Tuple[Tuple[ZorkGrandInquisitorItems, ...], ...]] ] = { diff --git a/worlds/zork_grand_inquisitor/data/transform_data.py b/worlds/zork_grand_inquisitor/data/transform_data.py index f6b5228d4062..1390a7f1bebc 100644 --- a/worlds/zork_grand_inquisitor/data/transform_data.py +++ b/worlds/zork_grand_inquisitor/data/transform_data.py @@ -64,12 +64,14 @@ ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MONASTERY_ROPE, ) }, ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: { ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( ZorkGrandInquisitorItems.SUBWAY_DESTINATION_MONASTERY, ZorkGrandInquisitorItems.TELEPORTER_DESTINATION_MONASTERY, + ZorkGrandInquisitorItems.MONASTERY_ROPE, ) }, ZorkGrandInquisitorGoals.THREE_ARTIFACTS: None, diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index e3c48b641f1d..be3d1bf8c99a 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -12,6 +12,7 @@ ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorEvents, ZorkGrandInquisitorGoals, + ZorkGrandInquisitorHotspots, ZorkGrandInquisitorItems, ZorkGrandInquisitorItemTransforms, ZorkGrandInquisitorLandmarksanity, @@ -59,6 +60,10 @@ def id_to_goals() -> Dict[int, ZorkGrandInquisitorGoals]: return {goal.value: goal for goal in ZorkGrandInquisitorGoals} +def id_to_hotspots() -> Dict[int, ZorkGrandInquisitorHotspots]: + return {hotspot.value: hotspot for hotspot in ZorkGrandInquisitorHotspots} + + def id_to_items() -> Dict[int, ZorkGrandInquisitorItems]: return {data.archipelago_id: item for item, data in item_data.items()} @@ -368,6 +373,18 @@ def entrance_access_rule_for( lambda_string += f"state.has(\"{requirement.value}\", {player})" elif requirement_type == ZorkGrandInquisitorRegions: lambda_string += f"state.can_reach(\"{requirement.value}\", \"Region\", {player})" + elif isinstance(requirement, tuple): + lambda_string += "(" + + iii: int + sub_requirement: ZorkGrandInquisitorItems + for iii, sub_requirement in enumerate(requirement): + lambda_string += f"state.has(\"{sub_requirement.value}\", {player})" + + if iii < len(requirement) - 1: + lambda_string += " or " + + lambda_string += ")" if ii < len(requirement_group) - 1: lambda_string += " and " diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 82ec8853c980..30b0a1b8609e 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -36,6 +36,12 @@ class ZorkGrandInquisitorGoals(enum.Enum): NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE = 4 +class ZorkGrandInquisitorHotspots(enum.Enum): + ENABLED = 0 + REQUIRE_ITEM_PER_REGION = 1 + REQUIRE_ITEM_PER_HOTSPOT = 2 + + class ZorkGrandInquisitorItems(enum.Enum): BROGS_BICKERING_TORCH = "Brog's Bickering Torch" BROGS_FLICKERING_TORCH = "Brog's Flickering Torch" @@ -99,6 +105,17 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_PORT_FOOZLE_PAST_TAVERN_DOOR = "Hotspot: Port Foozle Past Tavern Door" HOTSPOT_PURPLE_WORDS = "Hotspot: Purple Words" HOTSPOT_QUELBEE_HIVE = "Hotspot: Quelbee Hive" + HOTSPOT_REGIONAL_CROSSROADS = "Hotspots: Crossroads" + HOTSPOT_REGIONAL_DM_LAIR = "Hotspots: Dungeon Master's Lair" + HOTSPOT_REGIONAL_DRAGON_ARCHIPELAGO = "Hotspots: Dragon Archipelago" + HOTSPOT_REGIONAL_FLOOD_CONTROL_DAM = "Hotspots: Flood Control Dam #3" + HOTSPOT_REGIONAL_GUE_TECH = "Hotspots: GUE Tech" + HOTSPOT_REGIONAL_HADES = "Hotspots: Hades" + HOTSPOT_REGIONAL_MONASTERY = "Hotspots: Monastery" + HOTSPOT_REGIONAL_PORT_FOOZLE = "Hotspots: Port Foozle" + HOTSPOT_REGIONAL_PORT_FOOZLE_PAST = "Hotspots: Past Port Foozle" + HOTSPOT_REGIONAL_SPELL_LAB = "Hotspots: Spell Lab" + HOTSPOT_REGIONAL_WHITE_HOUSE = "Hotspots: White House" HOTSPOT_ROPE_BRIDGE = "Hotspot: Rope Bridge" HOTSPOT_SKULL_CAGE = "Hotspot: Skull Cage" HOTSPOT_SNAPDRAGON = "Hotspot: Snapdragon" @@ -409,6 +426,7 @@ class ZorkGrandInquisitorTags(enum.Enum): FILLER = "Filler" GOAL_THREE_ARTIFACTS = "Goal: Three Artifacts" HOTSPOT = "Hotspot" + HOTSPOT_REGIONAL = "Regional Hotspot" INVENTORY_ITEM = "Inventory Item" LANDMARKSANITY = "Landmarksanity" MISSABLE = "Missable" diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index eaf0b501d9c0..58489e6a8ef8 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -8,6 +8,7 @@ from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData +from .data.mapping_data import hotspots_for_regional_hotspot from .data.missable_location_data import ( missable_location_grant_conditions_data, @@ -19,6 +20,7 @@ from .enums import ( ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, + ZorkGrandInquisitorHotspots, ZorkGrandInquisitorItems, ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorLocations, @@ -52,6 +54,7 @@ class GameController: option_goal: Optional[ZorkGrandInquisitorGoals] option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] + option_hotspots: Optional[ZorkGrandInquisitorHotspots] option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] @@ -89,6 +92,7 @@ def __init__(self, logger=None) -> None: self.option_goal = None self.option_starting_location = None + self.option_hotspots = None self.option_deathsanity = None self.option_landmarksanity = None self.option_grant_missable_location_checks = None @@ -191,11 +195,20 @@ def list_received_lucy_items(self) -> None: self.log(f" {item}") def list_received_hotspots(self) -> None: + if self.option_hotspots == ZorkGrandInquisitorHotspots.ENABLED: + self.log("Hotspots are enabled for this seed and don't require items") + return + self.log("Received Hotspots:") self._process_received_items() - hotspot_items: Set[ZorkGrandInquisitorItems] = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT) + hotspot_items: Set[ZorkGrandInquisitorItems] = set() + if self.option_hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_REGION: + hotspot_items = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT_REGIONAL) + elif self.option_hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_HOTSPOT: + hotspot_items = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT) + received_hotspots: Set[ZorkGrandInquisitorItems] = self.received_items & hotspot_items if not len(received_hotspots): @@ -539,6 +552,11 @@ def _process_received_items(self) -> None: self.received_items.add(item) + if ZorkGrandInquisitorTags.HOTSPOT_REGIONAL in data.tags: + hotspot_item: ZorkGrandInquisitorItems + for hotspot_item in hotspots_for_regional_hotspot[item]: + self.received_items.add(hotspot_item) + def _manage_hotspots(self) -> None: hotspot_item: ZorkGrandInquisitorItems for hotspot_item in self.all_hotspot_items: diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index cd97c4c21a5f..f3cf1743024e 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -39,16 +39,22 @@ class StartingLocation(Choice): default = 0 -class StartWithHotspotItems(DefaultOnToggle): +class Hotspots(Choice): """ - If true, the player will be given all the hotspot items at the start of the game, effectively removing the need - to enable the important hotspots in the game before interacting with them. Recommended for beginners + Determines the behavior of hotspots (interactable areas of the screen) in the game. - Note: The spots these hotspot items would have occupied in the item pool will instead be filled with junk items. - Expect a higher volume of filler items if you enable this option + Enabled: All hotspots will be enabled at the start of the game + Require Item per Region: An item will enable all hotspots for a given region (e.g. Hotspots: Crossroads) + Require Item per Hotspot: An item will enable a specific hotspot (e.g. Hotspot: Subway Token Slot) """ - display_name: str = "Start with Hotspot Items" + display_name: str = "Hotspots" + + option_enabled: int = 0 + option_require_item_per_region: int = 1 + option_require_item_per_hotspot: int = 2 + + default = 0 class CraftableSpells(Choice): @@ -105,7 +111,7 @@ class GrantMissableLocationChecks(Toggle): class ZorkGrandInquisitorOptions(PerGameCommonOptions): goal: Goal starting_location: StartingLocation - start_with_hotspot_items: StartWithHotspotItems + hotspots: Hotspots craftable_spells: CraftableSpells deathsanity: Deathsanity landmarksanity: Landmarksanity diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 413089b4c708..a5c7e9c5e971 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -23,6 +23,7 @@ id_to_craftable_spell_behaviors, id_to_deathsanity, id_to_goals, + id_to_hotspots, id_to_landmarksanity, id_to_starting_locations, item_groups, @@ -41,6 +42,7 @@ ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorEvents, ZorkGrandInquisitorGoals, + ZorkGrandInquisitorHotspots, ZorkGrandInquisitorItems, ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorLocations, @@ -105,6 +107,7 @@ class ZorkGrandInquisitorWorld(World): filler_item_names: List[str] = item_groups()["Filler"] goal: ZorkGrandInquisitorGoals grant_missable_location_checks: bool + hotspots: ZorkGrandInquisitorHotspots initial_totemizer_destination: ZorkGrandInquisitorItems item_data: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData] item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item() @@ -116,7 +119,6 @@ class ZorkGrandInquisitorWorld(World): locked_items: Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems] place_early_items_locally: bool - start_with_hotspot_items: bool starter_kit: Tuple[ZorkGrandInquisitorItems, ...] starting_location: ZorkGrandInquisitorStartingLocations @@ -138,9 +140,8 @@ def generate_early(self) -> None: early_items_for_starting_location[self.starting_location] ) - self.start_with_hotspot_items = bool(self.options.start_with_hotspot_items) - self.craftable_spells = id_to_craftable_spell_behaviors()[self.options.craftable_spells.value] + self.hotspots = id_to_hotspots()[self.options.hotspots] self.deathsanity = id_to_deathsanity()[self.options.deathsanity] self.landmarksanity = id_to_landmarksanity()[self.options.landmarksanity] @@ -264,9 +265,24 @@ def create_items(self) -> None: for item in self.starter_kit: items_to_precollect.add(item) - if self.start_with_hotspot_items: - for item in items_with_tag(ZorkGrandInquisitorTags.HOTSPOT): + hotspot_items: Set[ZorkGrandInquisitorItems] = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT) + + hotspot_regional_items: Set[ZorkGrandInquisitorItems] = items_with_tag( + ZorkGrandInquisitorTags.HOTSPOT_REGIONAL + ) + + if self.hotspots == ZorkGrandInquisitorHotspots.ENABLED: + for item in hotspot_items: + items_to_ignore.add(item) + + for item in hotspot_regional_items: items_to_precollect.add(item) + elif self.hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_REGION: + for item in hotspot_items: + items_to_ignore.add(item) + elif self.hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_HOTSPOT: + for item in hotspot_regional_items: + items_to_ignore.add(item) items_to_precollect.add(self.initial_totemizer_destination) @@ -323,7 +339,7 @@ def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = self.options.as_dict( "goal", "starting_location", - "start_with_hotspot_items", + "hotspots", "craftable_spells", "deathsanity", "landmarksanity", From e7718007d31e71c527fbaa7352e480f946e1decc Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Thu, 21 Nov 2024 22:23:41 -0500 Subject: [PATCH 10/51] 4 new goals + 19 new checks + new thematic filler items --- worlds/zork_grand_inquisitor/client.py | 25 + .../data/entrance_rule_data.py | 61 ++- .../zork_grand_inquisitor/data/item_data.py | 497 +++++++++++++++++- .../data/location_data.py | 413 ++++++++++----- .../data/mapping_data.py | 48 +- .../data/missable_location_data.py | 36 +- .../data/transform_data.py | 48 +- worlds/zork_grand_inquisitor/data_funcs.py | 45 +- worlds/zork_grand_inquisitor/enums.py | 107 +++- .../zork_grand_inquisitor/game_controller.py | 96 +++- worlds/zork_grand_inquisitor/options.py | 63 ++- worlds/zork_grand_inquisitor/world.py | 77 ++- 12 files changed, 1301 insertions(+), 215 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 524c22021863..6b859d4bc839 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -10,6 +10,7 @@ item_names_to_id, item_names_to_item, location_names_to_id, + id_to_craftable_spell_behaviors, id_to_deathsanity, id_to_hotspots, id_to_items, @@ -31,6 +32,8 @@ def _cmd_zork(self) -> None: if result: self.ctx.process_attached_at_least_once = True self.output("Successfully attached to Zork Grand Inquisitor process.") + + self.ctx.game_controller.output_seed_information() else: self.output("Failed to attach to Zork Grand Inquisitor process.") @@ -105,6 +108,14 @@ def on_package(self, cmd: str, _args: Any) -> None: # Options self.game_controller.option_goal = id_to_goals()[_args["slot_data"]["goal"]] + self.game_controller.option_artifacts_of_magic_required = ( + _args["slot_data"]["artifacts_of_magic_required"] + ) + + self.game_controller.option_artifacts_of_magic_total = ( + _args["slot_data"]["artifacts_of_magic_total"] + ) + self.game_controller.option_starting_location = ( id_to_starting_locations()[_args["slot_data"]["starting_location"]] ) @@ -113,6 +124,10 @@ def on_package(self, cmd: str, _args: Any) -> None: id_to_hotspots()[_args["slot_data"]["hotspots"]] ) + self.game_controller.option_craftable_spells = ( + id_to_craftable_spell_behaviors()[_args["slot_data"]["craftable_spells"]] + ) + self.game_controller.option_deathsanity = ( id_to_deathsanity()[_args["slot_data"]["deathsanity"]] ) @@ -135,14 +150,24 @@ async def controller(self): await asyncio.sleep(0.1) # Enqueue Received Item Delta + goal_item_count: int = 0 + network_item: NetUtils.NetworkItem for network_item in self.items_received: item: ZorkGrandInquisitorItems = self.id_to_items[network_item.item] + if item in self.game_controller.all_goal_items: + goal_item_count += 1 + continue + if item not in self.game_controller.received_items: if item not in self.game_controller.received_items_queue: self.game_controller.received_items_queue.append(item) + if goal_item_count > self.game_controller.goal_item_count: + self.game_controller.goal_item_count = goal_item_count + self.game_controller.output_goal_item_update() + # Game Controller Update if self.game_controller.is_process_running(): self.game_controller.update() diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py index 5c7e512c98f0..7ebbaeec6b30 100644 --- a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py +++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py @@ -1,4 +1,4 @@ -from typing import Dict, Tuple, Union +from typing import Dict, List, Tuple, Union from ..enums import ( ZorkGrandInquisitorEvents, @@ -20,6 +20,7 @@ ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems, ZorkGrandInquisitorRegions, + List[Union[ZorkGrandInquisitorItems, int]], ], ..., ], @@ -499,6 +500,7 @@ ZorkGrandInquisitorEvents, ZorkGrandInquisitorItems, ZorkGrandInquisitorRegions, + List[Union[ZorkGrandInquisitorItems, int]] ], ..., ], @@ -506,7 +508,7 @@ ], None, ], - ], + ] ] = { ZorkGrandInquisitorGoals.THREE_ARTIFACTS: { (ZorkGrandInquisitorRegions.MENU, ZorkGrandInquisitorRegions.ENDGAME): ( @@ -517,21 +519,42 @@ ), ) }, - # ZorkGrandInquisitorGoals.SPELL_HEIST: { - # (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.ENDGAME): ( - # ( - # ZorkGrandInquisitorItems.SPELL_BEBURTT, - # ZorkGrandInquisitorItems.SPELL_GLORF, - # ZorkGrandInquisitorItems.SPELL_GOLGATEM, - # ZorkGrandInquisitorItems.SPELL_IGRAM, - # ZorkGrandInquisitorItems.SPELL_KENDALL, - # ZorkGrandInquisitorItems.SPELL_OBIDIL, - # ZorkGrandInquisitorItems.SPELL_NARWILE, - # ZorkGrandInquisitorItems.SPELL_REZROV, - # ZorkGrandInquisitorItems.SPELL_SNAVIG, - # ZorkGrandInquisitorItems.SPELL_THROCK, - # ZorkGrandInquisitorItems.SPELL_YASTARD, - # ), - # ) - # }, + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: { + (ZorkGrandInquisitorRegions.WALKING_CASTLE, ZorkGrandInquisitorRegions.ENDGAME): ( + ( + [ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC, 999], # Will get replaced with the actual number + ), + ) + }, + ZorkGrandInquisitorGoals.SPELL_HEIST: { + (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.ENDGAME): ( + ( + ZorkGrandInquisitorItems.SPELL_BEBURTT, + ZorkGrandInquisitorItems.SPELL_GLORF, + ZorkGrandInquisitorItems.SPELL_GOLGATEM, + ZorkGrandInquisitorItems.SPELL_IGRAM, + ZorkGrandInquisitorItems.SPELL_KENDALL, + ZorkGrandInquisitorItems.SPELL_OBIDIL, + ZorkGrandInquisitorItems.SPELL_NARWILE, + ZorkGrandInquisitorItems.SPELL_REZROV, + ZorkGrandInquisitorItems.SPELL_SNAVIG, + ZorkGrandInquisitorItems.SPELL_THROCK, + ZorkGrandInquisitorItems.SPELL_YASTARD, + ), + ) + }, + ZorkGrandInquisitorGoals.ZORK_TOUR: { + (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.ENDGAME): ( + ( + [ZorkGrandInquisitorItems.LANDMARK, 20], + ), + ), + }, + ZorkGrandInquisitorGoals.GRIM_JOURNEY: { + (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.ENDGAME): ( + ( + [ZorkGrandInquisitorItems.DEATH, 22], + ), + ), + }, } diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index 41f46f5b8b79..a91e4d82aa70 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -856,41 +856,510 @@ class ZorkGrandInquisitorItemData(NamedTuple): tags=(ZorkGrandInquisitorTags.TOTEM,), ), # Filler - ZorkGrandInquisitorItems.FILLER_INQUISITION_PROPAGANDA_FLYER: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.FILLER_AIMFIZ_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 700 + 0, classification=ItemClassification.filler, tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), - ZorkGrandInquisitorItems.FILLER_UNREADABLE_SPELL_SCROLL: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.FILLER_BAYALA_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 700 + 1, classification=ItemClassification.filler, tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), - ZorkGrandInquisitorItems.FILLER_MAGIC_CONTRABAND: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.FILLER_BITTYJOO_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 700 + 2, classification=ItemClassification.filler, tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), - ZorkGrandInquisitorItems.FILLER_FROBOZZ_ELECTRIC_GADGET: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.FILLER_BLORB_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 700 + 3, classification=ItemClassification.filler, tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), - ZorkGrandInquisitorItems.FILLER_NONSENSICAL_INQUISITION_PAPERWORK: ZorkGrandInquisitorItemData( + ZorkGrandInquisitorItems.FILLER_BLORPLE_SCROLL: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 700 + 4, classification=ItemClassification.filler, tags=(ZorkGrandInquisitorTags.FILLER,), maximum_quantity=None, ), + ZorkGrandInquisitorItems.FILLER_BOOZNIK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 5, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_BORCH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 6, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_CASKLY_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 7, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_CLEESH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 8, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_CONBAK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 9, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_DABHHU_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 10, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_DRILBO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 11, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_ESPNIS_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 12, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_EXEX_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 13, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FAIFT_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 14, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FILFRE_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 15, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FIZMO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 16, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FOBLUB_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 17, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FRIPPLE_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 18, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FROTZ_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 19, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_FWEEP_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 20, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GASPAR_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 21, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GILCH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 22, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GIRGOL_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 23, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GHELOOH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 24, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GIZGUM_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 25, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GLOTH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 26, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GNUSTO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 27, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GOLMAC_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 28, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GONDAR_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 29, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GORCH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 30, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_GUNCHO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 31, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_IMALI_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 32, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_IZYUK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 33, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_JINDAK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 34, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_KEPMKOMN_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 35, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_KOAASST_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 36, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_KRAK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 37, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_KREBF_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 38, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_KULCAD_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 39, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LESOCH_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 40, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LEXDOM_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 41, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LIDIBO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 42, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LISKON_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 43, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LOBAL_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 44, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_LOKTAR_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 45, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_MALYON_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 46, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_MEEF_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 47, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_MELBOR_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 48, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_MUSDEX_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 49, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_NERZO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 50, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_NIKMO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 51, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_NITFOL_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 52, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_OTSUNG_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 53, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_OZMOO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 54, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_PAXTEN_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 55, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_PULVER_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 56, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_QUELBO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 57, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_STEGAW_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 58, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_SWANZO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 59, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_TINSOT_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 60, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_TOSSIO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 61, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_UMBOZ_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 62, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_VARDIK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 63, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_VAXUM_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 64, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_VEZZA_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 65, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_YOMIN_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 66, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_YONK_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 67, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_YOZOZZO_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 68, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_ZIMBOR_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 69, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_ZOOKA_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 70, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), + ZorkGrandInquisitorItems.FILLER_ZUGTHUG_SCROLL: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 700 + 71, + classification=ItemClassification.filler, + tags=(ZorkGrandInquisitorTags.FILLER,), + maximum_quantity=None, + ), # Goal Items ZorkGrandInquisitorItems.COCONUT_OF_QUENDOR: ZorkGrandInquisitorItemData( statemap_keys=None, @@ -910,4 +1379,22 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS,), ), + ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 800 + 3, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.GOAL_ARTIFACT_OF_MAGIC_HUNT,), + ), + ZorkGrandInquisitorItems.LANDMARK: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 800 + 4, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.GOAL_ZORK_TOUR,), + ), + ZorkGrandInquisitorItems.DEATH: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 800 + 5, + classification=ItemClassification.progression, + tags=(ZorkGrandInquisitorTags.GOAL_GRIM_JOURNEY,), + ), } diff --git a/worlds/zork_grand_inquisitor/data/location_data.py b/worlds/zork_grand_inquisitor/data/location_data.py index 6a3019b03e39..1dda0568d35a 100644 --- a/worlds/zork_grand_inquisitor/data/location_data.py +++ b/worlds/zork_grand_inquisitor/data/location_data.py @@ -56,9 +56,19 @@ class ZorkGrandInquisitorLocationData(NamedTuple): region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), ), + ZorkGrandInquisitorLocations.AN_EXCELLENT_POPPING_UTENSIL: ZorkGrandInquisitorLocationData( + game_state_trigger=((2196, 84),), + archipelago_id=LOCATION_OFFSET + 1, + region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ZorkGrandInquisitorItems.TOTEM_GRIFF, + ZorkGrandInquisitorItems.GRIFFS_DRAGON_TOOTH, + ), + ), ZorkGrandInquisitorLocations.ARREST_THE_VANDAL: ZorkGrandInquisitorLocationData( game_state_trigger=((10789, 1),), - archipelago_id=LOCATION_OFFSET + 1, + archipelago_id=LOCATION_OFFSET + 2, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -71,20 +81,20 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.ARTIFACTS_EXPLAINED: ZorkGrandInquisitorLocationData( game_state_trigger=((11787, 1), (11788, 1), (11789, 1)), - archipelago_id=LOCATION_OFFSET + 2, + archipelago_id=LOCATION_OFFSET + 3, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.A_BIG_FAT_SASSY_2_HEADED_MONSTER: ZorkGrandInquisitorLocationData( game_state_trigger=((8929, 1),), - archipelago_id=LOCATION_OFFSET + 3, + archipelago_id=LOCATION_OFFSET + 4, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_OBIDIL,), ), ZorkGrandInquisitorLocations.A_LETTER_FROM_THE_WHITE_HOUSE: ZorkGrandInquisitorLocationData( game_state_trigger=((9124, 1),), - archipelago_id=LOCATION_OFFSET + 4, + archipelago_id=LOCATION_OFFSET + 5, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -97,7 +107,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.A_SMALLWAY: ZorkGrandInquisitorLocationData( game_state_trigger=((11777, 1),), - archipelago_id=LOCATION_OFFSET + 5, + archipelago_id=LOCATION_OFFSET + 6, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -110,7 +120,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BEAUTIFUL_THATS_PLENTY: ZorkGrandInquisitorLocationData( game_state_trigger=((13278, 1),), - archipelago_id=LOCATION_OFFSET + 6, + archipelago_id=LOCATION_OFFSET + 7, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -123,7 +133,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BEBURTT_DEMYSTIFIED: ZorkGrandInquisitorLocationData( game_state_trigger=((16315, 1),), - archipelago_id=LOCATION_OFFSET + 7, + archipelago_id=LOCATION_OFFSET + 8, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -133,14 +143,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BETTER_SPELL_MANUFACTURING_IN_UNDER_10_MINUTES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3x"),), - archipelago_id=LOCATION_OFFSET + 8, + archipelago_id=LOCATION_OFFSET + 9, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.BOING_BOING_BOING: ZorkGrandInquisitorLocationData( game_state_trigger=((4220, 1),), - archipelago_id=LOCATION_OFFSET + 9, + archipelago_id=LOCATION_OFFSET + 10, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -154,7 +164,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BONK: ZorkGrandInquisitorLocationData( game_state_trigger=((19491, 1),), - archipelago_id=LOCATION_OFFSET + 10, + archipelago_id=LOCATION_OFFSET + 11, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -167,13 +177,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BRAVE_SOULS_WANTED: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "us2g"),), - archipelago_id=LOCATION_OFFSET + 11, + archipelago_id=LOCATION_OFFSET + 12, region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.BROG_DO_GOOD: ZorkGrandInquisitorLocationData( game_state_trigger=((2644, 1),), - archipelago_id=LOCATION_OFFSET + 12, + archipelago_id=LOCATION_OFFSET + 13, region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -186,20 +196,20 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.BROG_EAT_ROCKS: ZorkGrandInquisitorLocationData( game_state_trigger=((2629, 1),), - archipelago_id=LOCATION_OFFSET + 13, + archipelago_id=LOCATION_OFFSET + 14, region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.BROG_KNOW_DUMB_THAT_DUMB: ZorkGrandInquisitorLocationData( game_state_trigger=((2650, 1),), - archipelago_id=LOCATION_OFFSET + 14, + archipelago_id=LOCATION_OFFSET + 15, region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.BROGS_GRUE_EGG,), ), ZorkGrandInquisitorLocations.BROG_MUCH_BETTER_AT_THIS_GAME: ZorkGrandInquisitorLocationData( game_state_trigger=((15715, 1),), - archipelago_id=LOCATION_OFFSET + 15, + archipelago_id=LOCATION_OFFSET + 16, region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -217,26 +227,26 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.CASTLE_WATCHING_A_FIELD_GUIDE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1t"),), - archipelago_id=LOCATION_OFFSET + 16, + archipelago_id=LOCATION_OFFSET + 17, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.CAVES_NOTES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3y"),), - archipelago_id=LOCATION_OFFSET + 17, + archipelago_id=LOCATION_OFFSET + 18, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.CLOSING_THE_TIME_TUNNELS: ZorkGrandInquisitorLocationData( game_state_trigger=((9543, 1),), - archipelago_id=LOCATION_OFFSET + 18, + archipelago_id=LOCATION_OFFSET + 19, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "cd6k"), (1673, 1), (1660, 1), (1312, 1)), - archipelago_id=LOCATION_OFFSET + 19, + archipelago_id=LOCATION_OFFSET + 20, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -252,7 +262,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.CRISIS_AVERTED: ZorkGrandInquisitorLocationData( game_state_trigger=((11769, 1),), - archipelago_id=LOCATION_OFFSET + 20, + archipelago_id=LOCATION_OFFSET + 21, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -270,13 +280,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.CUT_THAT_OUT_YOU_LITTLE_CREEP: ZorkGrandInquisitorLocationData( game_state_trigger=((19350, 1),), - archipelago_id=LOCATION_OFFSET + 21, + archipelago_id=LOCATION_OFFSET + 22, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.DENIED_BY_THE_LAKE_MONSTER: ZorkGrandInquisitorLocationData( game_state_trigger=((17632, 1),), - archipelago_id=LOCATION_OFFSET + 22, + archipelago_id=LOCATION_OFFSET + 23, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -287,16 +297,29 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SPELL_GOLGATEM, ), ), + ZorkGrandInquisitorLocations.DINGWHACKER_DELUXE: ZorkGrandInquisitorLocationData( + game_state_trigger=((2417, 1),), + archipelago_id=LOCATION_OFFSET + 24, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.DONT_EVEN_START_WITH_US_SPARKY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "hp5e"), (8919, 2), (9, 100)), - archipelago_id=LOCATION_OFFSET + 23, + archipelago_id=LOCATION_OFFSET + 25, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SWORD,), ), + ZorkGrandInquisitorLocations.DONT_GO_SPENDING_IT_ALL_IN_ONE_PLACE: ZorkGrandInquisitorLocationData( + game_state_trigger=((4512, 87),), + archipelago_id=LOCATION_OFFSET + 26, + region=ZorkGrandInquisitorRegions.ANYWHERE, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,), + ), ZorkGrandInquisitorLocations.DOOOOOOWN: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 3600),), - archipelago_id=LOCATION_OFFSET + 24, + archipelago_id=LOCATION_OFFSET + 27, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -309,7 +332,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.DOWN: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 5300),), - archipelago_id=LOCATION_OFFSET + 25, + archipelago_id=LOCATION_OFFSET + 28, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -322,14 +345,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.DRAGON_ARCHIPELAGO_TIME_TUNNEL: ZorkGrandInquisitorLocationData( game_state_trigger=((9216, 1),), - archipelago_id=LOCATION_OFFSET + 26, + archipelago_id=LOCATION_OFFSET + 29, region=ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_NARWILE,), ), ZorkGrandInquisitorLocations.DUNCE_LOCKER: ZorkGrandInquisitorLocationData( game_state_trigger=((11851, 1),), - archipelago_id=LOCATION_OFFSET + 27, + archipelago_id=LOCATION_OFFSET + 30, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -346,39 +369,39 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.EGGPLANTS: ZorkGrandInquisitorLocationData( game_state_trigger=((3816, 11000),), - archipelago_id=LOCATION_OFFSET + 28, + archipelago_id=LOCATION_OFFSET + 31, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.ELSEWHERE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pc1e"),), - archipelago_id=LOCATION_OFFSET + 29, + archipelago_id=LOCATION_OFFSET + 32, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.EMERGENCY_MAGICATRONIC_MESSAGE: ZorkGrandInquisitorLocationData( game_state_trigger=((11784, 1),), - archipelago_id=LOCATION_OFFSET + 30, + archipelago_id=LOCATION_OFFSET + 33, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), ), ZorkGrandInquisitorLocations.ENJOY_YOUR_TRIP: ZorkGrandInquisitorLocationData( game_state_trigger=((13743, 1),), - archipelago_id=LOCATION_OFFSET + 31, + archipelago_id=LOCATION_OFFSET + 34, region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_KENDALL,), ), ZorkGrandInquisitorLocations.FAT_LOT_OF_GOOD_THATLL_DO_YA: ZorkGrandInquisitorLocationData( game_state_trigger=((16368, 1),), - archipelago_id=LOCATION_OFFSET + 32, + archipelago_id=LOCATION_OFFSET + 35, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.SPELL_IGRAM,), ), ZorkGrandInquisitorLocations.FIRE_FIRE: ZorkGrandInquisitorLocationData( - game_state_trigger=((10277, 1),), - archipelago_id=LOCATION_OFFSET + 33, + game_state_trigger=((10277, (1, 2)),), + archipelago_id=LOCATION_OFFSET + 36, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -390,14 +413,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ZorkGrandInquisitorLocations.FLOOD_CONTROL_DAM_3_THE_NOT_REMOTELY_BORING_TALE: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "ue1h"),), - archipelago_id=LOCATION_OFFSET + 34, + game_state_trigger=((13259, 1),), + archipelago_id=LOCATION_OFFSET + 37, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.FLYING_SNAPDRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=((4222, 1),), - archipelago_id=LOCATION_OFFSET + 35, + archipelago_id=LOCATION_OFFSET + 38, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -412,13 +435,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.FROBUARY_3_UNDERGROUNDHOG_DAY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dw2g"),), - archipelago_id=LOCATION_OFFSET + 36, + archipelago_id=LOCATION_OFFSET + 39, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.GETTING_SOME_CHANGE: ZorkGrandInquisitorLocationData( game_state_trigger=((12892, 1),), - archipelago_id=LOCATION_OFFSET + 37, + archipelago_id=LOCATION_OFFSET + 40, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -431,13 +454,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.GO_AWAY: ZorkGrandInquisitorLocationData( game_state_trigger=((10654, 1),), - archipelago_id=LOCATION_OFFSET + 38, + archipelago_id=LOCATION_OFFSET + 41, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "sg6e"), (17103, 1), (15715, 1), (15707, 1)), - archipelago_id=LOCATION_OFFSET + 39, + archipelago_id=LOCATION_OFFSET + 42, region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -455,13 +478,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.GUE_TECH_ENTRANCE_EXAM: ZorkGrandInquisitorLocationData( game_state_trigger=((11082, 1), (11307, 1), (11536, 1)), - archipelago_id=LOCATION_OFFSET + 40, + archipelago_id=LOCATION_OFFSET + 43, region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HAVE_A_HELL_OF_A_DAY: ZorkGrandInquisitorLocationData( game_state_trigger=((8443, 1),), - archipelago_id=LOCATION_OFFSET + 41, + archipelago_id=LOCATION_OFFSET + 44, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -477,13 +500,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING: ZorkGrandInquisitorLocationData( game_state_trigger=((4698, 1),), - archipelago_id=LOCATION_OFFSET + 42, + archipelago_id=LOCATION_OFFSET + 45, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HELP_ME_CANT_BREATHE: ZorkGrandInquisitorLocationData( game_state_trigger=((10421, 1),), - archipelago_id=LOCATION_OFFSET + 43, + archipelago_id=LOCATION_OFFSET + 46, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -496,7 +519,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.HEY_FREE_DIRT: ZorkGrandInquisitorLocationData( game_state_trigger=((11747, 1),), - archipelago_id=LOCATION_OFFSET + 44, + archipelago_id=LOCATION_OFFSET + 47, region=ZorkGrandInquisitorRegions.GUE_TECH_OUTSIDE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -507,28 +530,35 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SHOVEL, ), ), + ZorkGrandInquisitorLocations.HMMM_BIG_TOOTHPICK: ZorkGrandInquisitorLocationData( + game_state_trigger=((2194, 69),), + archipelago_id=LOCATION_OFFSET + 48, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=(ZorkGrandInquisitorItems.BROGS_PLANK,), + ), ZorkGrandInquisitorLocations.HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "mt2h"),), - archipelago_id=LOCATION_OFFSET + 45, + archipelago_id=LOCATION_OFFSET + 49, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HOW_TO_HYPNOTIZE_YOURSELF: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "uh1e"),), - archipelago_id=LOCATION_OFFSET + 46, + archipelago_id=LOCATION_OFFSET + 50, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.HOW_TO_WIN_AT_DOUBLE_FANUCCI: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3s"),), - archipelago_id=LOCATION_OFFSET + 47, + archipelago_id=LOCATION_OFFSET + 51, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DALBOZ_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.IMBUE_BEBURTT: ZorkGrandInquisitorLocationData( game_state_trigger=((12166, 1),), - archipelago_id=LOCATION_OFFSET + 48, + archipelago_id=LOCATION_OFFSET + 52, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -544,13 +574,19 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.IM_COMPLETELY_NUDE: ZorkGrandInquisitorLocationData( game_state_trigger=((19344, 1),), - archipelago_id=LOCATION_OFFSET + 49, + archipelago_id=LOCATION_OFFSET + 53, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), + ZorkGrandInquisitorLocations.INFLATUS_THE_ETERNAL: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "cd4h"),), + archipelago_id=LOCATION_OFFSET + 54, + region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.INTO_THE_FOLIAGE: ZorkGrandInquisitorLocationData( game_state_trigger=((13060, 1),), - archipelago_id=LOCATION_OFFSET + 50, + archipelago_id=LOCATION_OFFSET + 55, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -563,14 +599,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.INVISIBLE_FLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((12967, 1),), - archipelago_id=LOCATION_OFFSET + 51, + archipelago_id=LOCATION_OFFSET + 56, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_IGRAM,), ), ZorkGrandInquisitorLocations.IN_CASE_OF_ADVENTURE: ZorkGrandInquisitorLocationData( game_state_trigger=((12931, 1),), - archipelago_id=LOCATION_OFFSET + 52, + archipelago_id=LOCATION_OFFSET + 57, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -583,7 +619,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.IN_MAGIC_WE_TRUST: ZorkGrandInquisitorLocationData( game_state_trigger=((13062, 1),), - archipelago_id=LOCATION_OFFSET + 53, + archipelago_id=LOCATION_OFFSET + 58, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -594,15 +630,54 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.ITS_ALMOST_AS_IF_IT_WERE_INFINITE: ZorkGrandInquisitorLocationData( + game_state_trigger=((11005, 15),), + archipelago_id=LOCATION_OFFSET + 59, + region=ZorkGrandInquisitorRegions.GUE_TECH, + tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), + ), ZorkGrandInquisitorLocations.ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe3j"),), - archipelago_id=LOCATION_OFFSET + 54, + archipelago_id=LOCATION_OFFSET + 60, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), + ZorkGrandInquisitorLocations.ITS_PLAYING_A_LITTLE_HARD_TO_GET: ZorkGrandInquisitorLocationData( + game_state_trigger=((3816, 1006),), + archipelago_id=LOCATION_OFFSET + 61, + region=ZorkGrandInquisitorRegions.DM_LAIR, + tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), + requirements=( + ZorkGrandInquisitorItems.SPELL_OBIDIL, + ( + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), + ), + ), + ZorkGrandInquisitorLocations.IT_DOESNT_APPEAR_TO_BE_FOOLED: ZorkGrandInquisitorLocationData( + game_state_trigger=((3816, 1009),), + archipelago_id=LOCATION_OFFSET + 62, + region=ZorkGrandInquisitorRegions.DM_LAIR, + tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), + requirements=( + ZorkGrandInquisitorItems.SPELL_BEBURTT, + ( + ZorkGrandInquisitorItems.HOTSPOT_SNAPDRAGON, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_DM_LAIR, + ), + ), + ), + ZorkGrandInquisitorLocations.I_AM_NOT_IMPRESSED: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "hp4f"), (8419, 1)), + archipelago_id=LOCATION_OFFSET + 63, + region=ZorkGrandInquisitorRegions.HADES, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=(ZorkGrandInquisitorItems.SPELL_SNAVIG,), + ), ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY: ZorkGrandInquisitorLocationData( game_state_trigger=((3816, 1008),), - archipelago_id=LOCATION_OFFSET + 55, + archipelago_id=LOCATION_OFFSET + 64, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -615,13 +690,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.I_DONT_WANT_NO_TROUBLE: ZorkGrandInquisitorLocationData( game_state_trigger=((10694, 1),), - archipelago_id=LOCATION_OFFSET + 56, + archipelago_id=LOCATION_OFFSET + 65, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.I_LIKE_YOUR_STYLE: ZorkGrandInquisitorLocationData( game_state_trigger=((16374, 1),), - archipelago_id=LOCATION_OFFSET + 57, + archipelago_id=LOCATION_OFFSET + 66, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -640,21 +715,21 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.I_SPIT_ON_YOUR_FILTHY_COINAGE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tp1e"), (9, 87), (1011, 1)), - archipelago_id=LOCATION_OFFSET + 58, + archipelago_id=LOCATION_OFFSET + 67, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.POUCH_OF_ZORKMIDS,), ), ZorkGrandInquisitorLocations.LIT_SUNFLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((4129, 1),), - archipelago_id=LOCATION_OFFSET + 59, + archipelago_id=LOCATION_OFFSET + 68, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_THROCK,), ), ZorkGrandInquisitorLocations.LOOK_AN_ICE_CREAM_BAR: ZorkGrandInquisitorLocationData( game_state_trigger=((12517, 1),), - archipelago_id=LOCATION_OFFSET + 60, + archipelago_id=LOCATION_OFFSET + 69, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -671,7 +746,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.MAILED_IT_TO_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((2498, (1, 2)),), - archipelago_id=LOCATION_OFFSET + 61, + archipelago_id=LOCATION_OFFSET + 70, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -688,7 +763,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.MAKE_LOVE_NOT_WAR: ZorkGrandInquisitorLocationData( game_state_trigger=(((8623, 8734), 21),), - archipelago_id=LOCATION_OFFSET + 62, + archipelago_id=LOCATION_OFFSET + 71, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -698,7 +773,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.MEAD_LIGHT: ZorkGrandInquisitorLocationData( game_state_trigger=((10485, 1),), - archipelago_id=LOCATION_OFFSET + 63, + archipelago_id=LOCATION_OFFSET + 72, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -709,15 +784,21 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.ME_I_AM_THE_BOSS_OF_YOU: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "px1k"),), + archipelago_id=LOCATION_OFFSET + 73, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.MIKES_PANTS: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tr2p"),), - archipelago_id=LOCATION_OFFSET + 64, + archipelago_id=LOCATION_OFFSET + 74, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData( game_state_trigger=((4217, 1),), - archipelago_id=LOCATION_OFFSET + 65, + archipelago_id=LOCATION_OFFSET + 75, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -730,7 +811,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NATIONAL_TREASURE: ZorkGrandInquisitorLocationData( game_state_trigger=((14318, 1),), - archipelago_id=LOCATION_OFFSET + 66, + archipelago_id=LOCATION_OFFSET + 76, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -747,13 +828,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NATURAL_AND_SUPERNATURAL_CREATURES_OF_QUENDOR: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1p"),), - archipelago_id=LOCATION_OFFSET + 67, + archipelago_id=LOCATION_OFFSET + 77, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.NOOOOOOOOOOOOO: ZorkGrandInquisitorLocationData( game_state_trigger=((12706, 1),), - archipelago_id=LOCATION_OFFSET + 68, + archipelago_id=LOCATION_OFFSET + 78, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -770,7 +851,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NOTHIN_LIKE_A_GOOD_STOGIE: ZorkGrandInquisitorLocationData( game_state_trigger=((4237, 1),), - archipelago_id=LOCATION_OFFSET + 69, + archipelago_id=LOCATION_OFFSET + 79, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -783,14 +864,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT: ZorkGrandInquisitorLocationData( game_state_trigger=((8935, 1),), - archipelago_id=LOCATION_OFFSET + 70, + archipelago_id=LOCATION_OFFSET + 80, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_SNAVIG,), ), ZorkGrandInquisitorLocations.NO_AUTOGRAPHS: ZorkGrandInquisitorLocationData( game_state_trigger=((10476, 1),), - archipelago_id=LOCATION_OFFSET + 71, + archipelago_id=LOCATION_OFFSET + 81, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -802,7 +883,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.NO_BONDAGE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe2e"), (10262, 2), (15150, 83)), - archipelago_id=LOCATION_OFFSET + 72, + archipelago_id=LOCATION_OFFSET + 82, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -813,9 +894,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.NO_ONE_RETURNS_FROM_HADES: ZorkGrandInquisitorLocationData( + game_state_trigger=((15204, 1),), + archipelago_id=LOCATION_OFFSET + 83, + region=ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.OBIDIL_DRIED_UP: ZorkGrandInquisitorLocationData( game_state_trigger=((12164, 1),), - archipelago_id=LOCATION_OFFSET + 73, + archipelago_id=LOCATION_OFFSET + 84, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -828,7 +915,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OH_DEAR_GOD_ITS_A_DRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=((1300, 1),), - archipelago_id=LOCATION_OFFSET + 74, + archipelago_id=LOCATION_OFFSET + 85, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -843,7 +930,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OH_VERY_FUNNY_GUYS: ZorkGrandInquisitorLocationData( game_state_trigger=((2448, 1),), - archipelago_id=LOCATION_OFFSET + 75, + archipelago_id=LOCATION_OFFSET + 86, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -856,7 +943,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OH_WOW_TALK_ABOUT_DEJA_VU: ZorkGrandInquisitorLocationData( game_state_trigger=((4869, 1),), - archipelago_id=LOCATION_OFFSET + 76, + archipelago_id=LOCATION_OFFSET + 87, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -866,20 +953,20 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OLD_SCRATCH_WINNER: ZorkGrandInquisitorLocationData( game_state_trigger=((4512, 32),), - archipelago_id=LOCATION_OFFSET + 77, + archipelago_id=LOCATION_OFFSET + 88, region=ZorkGrandInquisitorRegions.ANYWHERE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.OLD_SCRATCH_CARD,), ), ZorkGrandInquisitorLocations.ONLY_YOU_CAN_PREVENT_FOOZLE_FIRES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pe5n"),), - archipelago_id=LOCATION_OFFSET + 78, + archipelago_id=LOCATION_OFFSET + 89, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.OPEN_THE_GATES_OF_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((8730, 1),), - archipelago_id=LOCATION_OFFSET + 79, + archipelago_id=LOCATION_OFFSET + 90, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -889,7 +976,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.OUTSMART_THE_QUELBEES: ZorkGrandInquisitorLocationData( game_state_trigger=((4241, 1),), - archipelago_id=LOCATION_OFFSET + 80, + archipelago_id=LOCATION_OFFSET + 91, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -903,25 +990,25 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.PERMASEAL: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "mt1g"),), - archipelago_id=LOCATION_OFFSET + 81, + archipelago_id=LOCATION_OFFSET + 92, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PLANETFALL: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "pp1j"),), - archipelago_id=LOCATION_OFFSET + 82, + archipelago_id=LOCATION_OFFSET + 93, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_JACKS_SHOP, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PLEASE_DONT_THROCK_THE_GRASS: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "te1g"),), - archipelago_id=LOCATION_OFFSET + 83, + archipelago_id=LOCATION_OFFSET + 94, region=ZorkGrandInquisitorRegions.GUE_TECH_ENTRANCE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.PORT_FOOZLE_TIME_TUNNEL: ZorkGrandInquisitorLocationData( game_state_trigger=((9404, 1),), - archipelago_id=LOCATION_OFFSET + 84, + archipelago_id=LOCATION_OFFSET + 95, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -939,7 +1026,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.PROZORKED: ZorkGrandInquisitorLocationData( game_state_trigger=((4115, 1),), - archipelago_id=LOCATION_OFFSET + 85, + archipelago_id=LOCATION_OFFSET + 96, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -950,9 +1037,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.PURPLE_BEAST_ALARM_SYSTEM: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "tp1f"),), + archipelago_id=LOCATION_OFFSET + 97, + region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.REASSEMBLE_SNAVIG: ZorkGrandInquisitorLocationData( game_state_trigger=((4512, 98),), - archipelago_id=LOCATION_OFFSET + 86, + archipelago_id=LOCATION_OFFSET + 98, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -964,15 +1057,27 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.RESTOCKED_ON_GRUESDAY: ZorkGrandInquisitorLocationData( + game_state_trigger=(("location", "tr2h"),), + archipelago_id=LOCATION_OFFSET + 99, + region=ZorkGrandInquisitorRegions.GUE_TECH, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE: ZorkGrandInquisitorLocationData( game_state_trigger=((4698, 3),), - archipelago_id=LOCATION_OFFSET + 87, + archipelago_id=LOCATION_OFFSET + 100, + region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, + tags=(ZorkGrandInquisitorTags.CORE,), + ), + ZorkGrandInquisitorLocations.RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE: ZorkGrandInquisitorLocationData( + game_state_trigger=((4698, 4),), + archipelago_id=LOCATION_OFFSET + 101, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.SNAVIG_REPAIRED: ZorkGrandInquisitorLocationData( game_state_trigger=((12161, 1),), - archipelago_id=LOCATION_OFFSET + 88, + archipelago_id=LOCATION_OFFSET + 102, region=ZorkGrandInquisitorRegions.SPELL_LAB, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -985,7 +1090,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.SOUVENIR: ZorkGrandInquisitorLocationData( game_state_trigger=((13408, 1),), - archipelago_id=LOCATION_OFFSET + 89, + archipelago_id=LOCATION_OFFSET + 103, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -996,9 +1101,25 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.SPELL_CHECK_COMPLETE: ZorkGrandInquisitorLocationData( + game_state_trigger=((12168, 1),), + archipelago_id=LOCATION_OFFSET + 104, + region=ZorkGrandInquisitorRegions.SPELL_LAB, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=( + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), + ( + ZorkGrandInquisitorItems.HOTSPOT_BLANK_SCROLL_BOX, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), + ) + ), ZorkGrandInquisitorLocations.STRAIGHT_TO_HELL: ZorkGrandInquisitorLocationData( game_state_trigger=((9719, 1),), - archipelago_id=LOCATION_OFFSET + 90, + archipelago_id=LOCATION_OFFSET + 105, region=ZorkGrandInquisitorRegions.MONASTERY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1015,7 +1136,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.STRIP_GRUE_FIRE_WATER: ZorkGrandInquisitorLocationData( game_state_trigger=((14511, 1), (14524, 5)), - archipelago_id=LOCATION_OFFSET + 91, + archipelago_id=LOCATION_OFFSET + 106, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1035,7 +1156,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.SUCKING_ROCKS: ZorkGrandInquisitorLocationData( game_state_trigger=((12859, 1),), - archipelago_id=LOCATION_OFFSET + 92, + archipelago_id=LOCATION_OFFSET + 107, region=ZorkGrandInquisitorRegions.GUE_TECH, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1057,7 +1178,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.TALK_TO_ME_GRAND_INQUISITOR: ZorkGrandInquisitorLocationData( game_state_trigger=((10299, 1),), - archipelago_id=LOCATION_OFFSET + 93, + archipelago_id=LOCATION_OFFSET + 108, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1069,13 +1190,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.TAMING_YOUR_SNAPDRAGON: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dv1h"),), - archipelago_id=LOCATION_OFFSET + 94, + archipelago_id=LOCATION_OFFSET + 109, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THAR_SHE_BLOWS: ZorkGrandInquisitorLocationData( game_state_trigger=((1311, 1), (1312, 1)), - archipelago_id=LOCATION_OFFSET + 95, + archipelago_id=LOCATION_OFFSET + 110, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO_DRAGON, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1091,7 +1212,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THATS_A_ROPE: ZorkGrandInquisitorLocationData( game_state_trigger=((10486, 1),), - archipelago_id=LOCATION_OFFSET + 96, + archipelago_id=LOCATION_OFFSET + 111, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1104,20 +1225,20 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THATS_IT_JUST_KEEP_HITTING_THOSE_BUTTONS: ZorkGrandInquisitorLocationData( game_state_trigger=((13805, 1),), - archipelago_id=LOCATION_OFFSET + 97, + archipelago_id=LOCATION_OFFSET + 112, region=ZorkGrandInquisitorRegions.SUBWAY_CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), ), ZorkGrandInquisitorLocations.THATS_STILL_A_ROPE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "tp1e"), (9, 83), (1011, 1)), - archipelago_id=LOCATION_OFFSET + 98, + archipelago_id=LOCATION_OFFSET + 113, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorEvents.ROPE_GLORFABLE,), ), ZorkGrandInquisitorLocations.THATS_THE_SPIRIT: ZorkGrandInquisitorLocationData( game_state_trigger=((10341, 95),), - archipelago_id=LOCATION_OFFSET + 99, + archipelago_id=LOCATION_OFFSET + 114, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1129,25 +1250,25 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THE_ALCHEMICAL_DEBACLE: ZorkGrandInquisitorLocationData( game_state_trigger=((9459, 1),), - archipelago_id=LOCATION_OFFSET + 100, + archipelago_id=LOCATION_OFFSET + 115, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_ENDLESS_FIRE: ZorkGrandInquisitorLocationData( game_state_trigger=((9473, 1),), - archipelago_id=LOCATION_OFFSET + 101, + archipelago_id=LOCATION_OFFSET + 116, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_FLATHEADIAN_FUDGE_FIASCO: ZorkGrandInquisitorLocationData( game_state_trigger=((9520, 1),), - archipelago_id=LOCATION_OFFSET + 102, + archipelago_id=LOCATION_OFFSET + 117, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_ONLY_WAY_TO_WIN_IS_NOT_TO_PLAY: ZorkGrandInquisitorLocationData( game_state_trigger=((16286, 1),), - archipelago_id=LOCATION_OFFSET + 103, + archipelago_id=LOCATION_OFFSET + 118, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1157,13 +1278,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THE_PERILS_OF_MAGIC: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "me1j"),), - archipelago_id=LOCATION_OFFSET + 104, + archipelago_id=LOCATION_OFFSET + 119, region=ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.THE_UNDERGROUND_UNDERGROUND: ZorkGrandInquisitorLocationData( game_state_trigger=((13167, 1),), - archipelago_id=LOCATION_OFFSET + 105, + archipelago_id=LOCATION_OFFSET + 120, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1176,14 +1297,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.THIS_DOESNT_LOOK_ANYTHING_LIKE_THE_BROCHURE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "cd60"), (1524, 1)), - archipelago_id=LOCATION_OFFSET + 106, + archipelago_id=LOCATION_OFFSET + 121, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.TOTEM_LUCY,), ), ZorkGrandInquisitorLocations.THROCKED_MUSHROOM_HAMMERED: ZorkGrandInquisitorLocationData( game_state_trigger=((4219, 1),), - archipelago_id=LOCATION_OFFSET + 107, + archipelago_id=LOCATION_OFFSET + 122, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1197,34 +1318,40 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.TIME_TRAVEL_FOR_DUMMIES: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "th3z"),), - archipelago_id=LOCATION_OFFSET + 108, + archipelago_id=LOCATION_OFFSET + 123, region=ZorkGrandInquisitorRegions.GUE_TECH_HALLWAY, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorEvents.DUNCE_LOCKER_OPENABLE,), ), ZorkGrandInquisitorLocations.TOTEMIZED_DAILY_BILLBOARD: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "px1h"),), - archipelago_id=LOCATION_OFFSET + 109, + archipelago_id=LOCATION_OFFSET + 124, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.UH_OH_BROG_CANT_SWIM: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "cd60"), (1520, 1)), - archipelago_id=LOCATION_OFFSET + 110, + archipelago_id=LOCATION_OFFSET + 125, region=ZorkGrandInquisitorRegions.DRAGON_ARCHIPELAGO, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.TOTEM_BROG,), ), + ZorkGrandInquisitorLocations.UM_AH_UM_AH_UM_AH: ZorkGrandInquisitorLocationData( + game_state_trigger=((16997, 4),), + archipelago_id=LOCATION_OFFSET + 126, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.UMBRELLA_FLOWERS: ZorkGrandInquisitorLocationData( game_state_trigger=((12926, 1),), - archipelago_id=LOCATION_OFFSET + 111, + archipelago_id=LOCATION_OFFSET + 127, region=ZorkGrandInquisitorRegions.CROSSROADS, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_BEBURTT,), ), ZorkGrandInquisitorLocations.UP: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 5200),), - archipelago_id=LOCATION_OFFSET + 112, + archipelago_id=LOCATION_OFFSET + 128, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1237,14 +1364,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.USELESS_BUT_FUN: ZorkGrandInquisitorLocationData( game_state_trigger=((14321, 1),), - archipelago_id=LOCATION_OFFSET + 113, + archipelago_id=LOCATION_OFFSET + 129, region=ZorkGrandInquisitorRegions.SUBWAY_FLOOD_CONTROL_DAM, tags=(ZorkGrandInquisitorTags.CORE,), requirements=(ZorkGrandInquisitorItems.SPELL_GOLGATEM,), ), ZorkGrandInquisitorLocations.UUUUUP: ZorkGrandInquisitorLocationData( game_state_trigger=((3619, 3500),), - archipelago_id=LOCATION_OFFSET + 114, + archipelago_id=LOCATION_OFFSET + 130, region=ZorkGrandInquisitorRegions.WHITE_HOUSE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1257,13 +1384,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.VOYAGE_OF_CAPTAIN_ZAHAB: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "uh1h"),), - archipelago_id=LOCATION_OFFSET + 115, + archipelago_id=LOCATION_OFFSET + 131, region=ZorkGrandInquisitorRegions.HADES_SHORE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO: ZorkGrandInquisitorLocationData( game_state_trigger=((4034, 1),), - archipelago_id=LOCATION_OFFSET + 116, + archipelago_id=LOCATION_OFFSET + 132, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1276,9 +1403,15 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO_PAST: ZorkGrandInquisitorLocationData( + game_state_trigger=((17006, 5001),), + archipelago_id=LOCATION_OFFSET + 133, + region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, + tags=(ZorkGrandInquisitorTags.CORE,), + ), ZorkGrandInquisitorLocations.WE_DONT_SERVE_YOUR_KIND_HERE: ZorkGrandInquisitorLocationData( game_state_trigger=((2461, 1),), - archipelago_id=LOCATION_OFFSET + 117, + archipelago_id=LOCATION_OFFSET + 134, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1291,7 +1424,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WE_GOT_A_HIGH_ROLLER: ZorkGrandInquisitorLocationData( game_state_trigger=((15472, 1),), - archipelago_id=LOCATION_OFFSET + 118, + archipelago_id=LOCATION_OFFSET + 135, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1311,7 +1444,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WHAT_ARE_YOU_STUPID: ZorkGrandInquisitorLocationData( game_state_trigger=((10484, 1),), - archipelago_id=LOCATION_OFFSET + 119, + archipelago_id=LOCATION_OFFSET + 136, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1324,7 +1457,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.WHITE_HOUSE_TIME_TUNNEL: ZorkGrandInquisitorLocationData( game_state_trigger=((4983, 1),), - archipelago_id=LOCATION_OFFSET + 120, + archipelago_id=LOCATION_OFFSET + 137, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1335,15 +1468,22 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ZorkGrandInquisitorItems.SPELL_NARWILE, ), ), + ZorkGrandInquisitorLocations.WHOOPS: ZorkGrandInquisitorLocationData( + game_state_trigger=((15959, (1, 2)),), + archipelago_id=LOCATION_OFFSET + 138, + region=ZorkGrandInquisitorRegions.WHITE_HOUSE_INTERIOR, + tags=(ZorkGrandInquisitorTags.CORE,), + requirements=(ZorkGrandInquisitorItems.BROGS_GRUE_EGG,) + ), ZorkGrandInquisitorLocations.WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dc10"), (1596, 1)), - archipelago_id=LOCATION_OFFSET + 121, + archipelago_id=LOCATION_OFFSET + 139, region=ZorkGrandInquisitorRegions.WALKING_CASTLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.YAD_GOHDNUORGREDNU_3_YRAUBORF: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dm2g"),), - archipelago_id=LOCATION_OFFSET + 122, + archipelago_id=LOCATION_OFFSET + 140, region=ZorkGrandInquisitorRegions.DM_LAIR_INTERIOR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1355,7 +1495,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "dg4e"), (4266, 1), (9, 21), (4035, 1)), - archipelago_id=LOCATION_OFFSET + 123, + archipelago_id=LOCATION_OFFSET + 141, region=ZorkGrandInquisitorRegions.DM_LAIR, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1368,14 +1508,14 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: ZorkGrandInquisitorLocationData( game_state_trigger=((16405, 1),), - archipelago_id=LOCATION_OFFSET + 124, + archipelago_id=LOCATION_OFFSET + 142, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=(ZorkGrandInquisitorItems.SPELL_REZROV,), ), ZorkGrandInquisitorLocations.YOU_GAINED_86_EXPERIENCE_POINTS: ZorkGrandInquisitorLocationData( game_state_trigger=((16342, 1),), - archipelago_id=LOCATION_OFFSET + 125, + archipelago_id=LOCATION_OFFSET + 143, region=ZorkGrandInquisitorRegions.SPELL_LAB_BRIDGE, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1388,7 +1528,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "qs1e"), (14511, 1), (14524, 5)), - archipelago_id=LOCATION_OFFSET + 126, + archipelago_id=LOCATION_OFFSET + 144, region=ZorkGrandInquisitorRegions.PORT_FOOZLE_PAST_TAVERN, tags=(ZorkGrandInquisitorTags.CORE,), requirements=( @@ -1408,13 +1548,13 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ZorkGrandInquisitorLocations.YOU_ONE_OF_THEM_AGITATORS_AINT_YA: ZorkGrandInquisitorLocationData( game_state_trigger=((10586, 1),), - archipelago_id=LOCATION_OFFSET + 127, + archipelago_id=LOCATION_OFFSET + 145, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE,), ), ZorkGrandInquisitorLocations.YOU_WANT_A_PIECE_OF_ME_DOCK_BOY: ZorkGrandInquisitorLocationData( game_state_trigger=((15151, 1),), - archipelago_id=LOCATION_OFFSET + 128, + archipelago_id=LOCATION_OFFSET + 146, region=ZorkGrandInquisitorRegions.PORT_FOOZLE, tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), requirements=( @@ -1424,6 +1564,19 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ), + ZorkGrandInquisitorLocations.ZIMDOR_IS_UNDAMAGED: ZorkGrandInquisitorLocationData( + game_state_trigger=((12167, 1),), + archipelago_id=LOCATION_OFFSET + 147, + region=ZorkGrandInquisitorRegions.SPELL_LAB, + tags=(ZorkGrandInquisitorTags.CORE, ZorkGrandInquisitorTags.MISSABLE), + requirements=( + ZorkGrandInquisitorItems.ZIMDOR_SCROLL, + ( + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ZorkGrandInquisitorItems.HOTSPOT_REGIONAL_SPELL_LAB, + ), + ) + ), # Deathsanity ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK: ZorkGrandInquisitorLocationData( game_state_trigger=(("location", "gjde"), (2201, 1)), diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 3b10ffb2711a..a5f2a14c7fee 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -1,8 +1,12 @@ -from typing import Dict, Optional, Tuple +from typing import Dict, Optional, Tuple, Union from ..enums import ( + ZorkGrandInquisitorCraftableSpellBehaviors, + ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, + ZorkGrandInquisitorHotspots, ZorkGrandInquisitorItems, + ZorkGrandInquisitorLandmarksanity, ZorkGrandInquisitorRegions, ZorkGrandInquisitorStartingLocations, ) @@ -35,8 +39,8 @@ ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: ZorkGrandInquisitorRegions.WALKING_CASTLE, ZorkGrandInquisitorGoals.SPELL_HEIST: ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorGoals.ZORK_TOUR: ZorkGrandInquisitorRegions.PORT_FOOZLE, - ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: ( - ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, + ZorkGrandInquisitorGoals.GRIM_JOURNEY: ( + ZorkGrandInquisitorRegions.HADES_BEYOND_GATES ), } @@ -304,6 +308,44 @@ ), } +labels_for_enum_items: Dict[ + Union[ + ZorkGrandInquisitorCraftableSpellBehaviors, + ZorkGrandInquisitorDeathsanity, + ZorkGrandInquisitorGoals, + ZorkGrandInquisitorHotspots, + ZorkGrandInquisitorLandmarksanity, + ZorkGrandInquisitorStartingLocations, + ], + str +] = { + ZorkGrandInquisitorCraftableSpellBehaviors.VANILLA: "Vanilla", + ZorkGrandInquisitorCraftableSpellBehaviors.ANY_SPELL: "Any Spell", + ZorkGrandInquisitorCraftableSpellBehaviors.ANYTHING: "Anything", + ZorkGrandInquisitorDeathsanity.OFF: "Off", + ZorkGrandInquisitorDeathsanity.ON: "On", + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: "Three Artifacts", + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: "Artifact of Magic Hunt", + ZorkGrandInquisitorGoals.SPELL_HEIST: "Spell Heist", + ZorkGrandInquisitorGoals.ZORK_TOUR: "Zork Tour", + ZorkGrandInquisitorGoals.GRIM_JOURNEY: "Grim Journey", + ZorkGrandInquisitorHotspots.ENABLED: "Enabled", + ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_REGION: "Require Item Per Region", + ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_HOTSPOT: "Require Item Per Hotspot", + ZorkGrandInquisitorLandmarksanity.OFF: "Off", + ZorkGrandInquisitorLandmarksanity.ON: "On", + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: "Port Foozle", + ZorkGrandInquisitorStartingLocations.CROSSROADS: "Crossroads", + ZorkGrandInquisitorStartingLocations.DM_LAIR: "Dungeon Master's Lair", + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: "Dungeon Master's House", + ZorkGrandInquisitorStartingLocations.GUE_TECH: "GUE Tech", + ZorkGrandInquisitorStartingLocations.SPELL_LAB: "Spell Lab", + ZorkGrandInquisitorStartingLocations.HADES_SHORE: "Hades Shore", + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: "Flood Control Dam #3", + ZorkGrandInquisitorStartingLocations.MONASTERY: "Monastery Totemizer", + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: "Monastery Exhibit", +} + starter_kits_for_starting_location: Dict[ ZorkGrandInquisitorStartingLocations, Optional[Tuple[Tuple[ZorkGrandInquisitorItems, ...], ...]] ] = { diff --git a/worlds/zork_grand_inquisitor/data/missable_location_data.py b/worlds/zork_grand_inquisitor/data/missable_location_data.py index 67271e7c28d8..97f439333b74 100644 --- a/worlds/zork_grand_inquisitor/data/missable_location_data.py +++ b/worlds/zork_grand_inquisitor/data/missable_location_data.py @@ -117,6 +117,27 @@ class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple): item_conditions=(ZorkGrandInquisitorItems.SPELL_IGRAM,), ) , + ZorkGrandInquisitorLocations.ITS_ALMOST_AS_IF_IT_WERE_INFINITE: + ZorkGrandInquisitorMissableLocationGrantConditionsData( + game_location_condition="th10", + location_condition=(ZorkGrandInquisitorLocations.A_SMALLWAY,), + item_conditions=None, + ) + , + ZorkGrandInquisitorLocations.ITS_PLAYING_A_LITTLE_HARD_TO_GET: + ZorkGrandInquisitorMissableLocationGrantConditionsData( + game_location_condition="dg2f", + location_condition=(ZorkGrandInquisitorLocations.PROZORKED,), + item_conditions=(ZorkGrandInquisitorItems.SPELL_OBIDIL,), + ) + , + ZorkGrandInquisitorLocations.IT_DOESNT_APPEAR_TO_BE_FOOLED: + ZorkGrandInquisitorMissableLocationGrantConditionsData( + game_location_condition="dg2f", + location_condition=(ZorkGrandInquisitorLocations.PROZORKED,), + item_conditions=(ZorkGrandInquisitorItems.SPELL_BEBURTT,), + ) + , ZorkGrandInquisitorLocations.I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY: ZorkGrandInquisitorMissableLocationGrantConditionsData( game_location_condition="dg2f", @@ -220,7 +241,10 @@ class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple): ZorkGrandInquisitorMissableLocationGrantConditionsData( game_location_condition="dv10", location_condition=(ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO,), - item_conditions=(ZorkGrandInquisitorItems.SWORD, ZorkGrandInquisitorItems.HOTSPOT_HARRY), + item_conditions=( + ZorkGrandInquisitorItems.SWORD, + ZorkGrandInquisitorItems.HOTSPOT_HARRY + ), ) , ZorkGrandInquisitorLocations.YOU_DONT_GO_MESSING_WITH_A_MANS_ZIPPER: @@ -237,4 +261,14 @@ class ZorkGrandInquisitorMissableLocationGrantConditionsData(NamedTuple): item_conditions=None, ) , + ZorkGrandInquisitorLocations.ZIMDOR_IS_UNDAMAGED: + ZorkGrandInquisitorMissableLocationGrantConditionsData( + game_location_condition="tp4g", + location_condition=(ZorkGrandInquisitorLocations.WANT_SOME_RYE_COURSE_YA_DO,), + item_conditions=( + ZorkGrandInquisitorItems.ZIMDOR_SCROLL, + ZorkGrandInquisitorItems.HOTSPOT_SPELL_CHECKER, + ), + ) + , } diff --git a/worlds/zork_grand_inquisitor/data/transform_data.py b/worlds/zork_grand_inquisitor/data/transform_data.py index 1390a7f1bebc..20945d0a77c9 100644 --- a/worlds/zork_grand_inquisitor/data/transform_data.py +++ b/worlds/zork_grand_inquisitor/data/transform_data.py @@ -78,7 +78,7 @@ ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: None, ZorkGrandInquisitorGoals.SPELL_HEIST: None, ZorkGrandInquisitorGoals.ZORK_TOUR: None, - ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: None, + ZorkGrandInquisitorGoals.GRIM_JOURNEY: None, ZorkGrandInquisitorDeathsanity.OFF: { ZorkGrandInquisitorItemTransforms.MAKE_FILLER: ( ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ, @@ -104,29 +104,41 @@ ZorkGrandInquisitorStartingLocations.CROSSROADS: None, ZorkGrandInquisitorStartingLocations.DM_LAIR: None, ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: None, - ZorkGrandInquisitorStartingLocations.GUE_TECH: { + ZorkGrandInquisitorStartingLocations.GUE_TECH: None, + ZorkGrandInquisitorStartingLocations.SPELL_LAB: None, + ZorkGrandInquisitorStartingLocations.HADES_SHORE: None, + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: None, + ZorkGrandInquisitorStartingLocations.MONASTERY: None, + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: None, + ZorkGrandInquisitorGoals.THREE_ARTIFACTS: None, + ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: { ZorkGrandInquisitorLocationTransforms.REMOVE: ( - ZorkGrandInquisitorLocations.LANDMARK_GUE_TECH_FOUNTAIN_INSIDE, - ) + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT, + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG, + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP, + ), }, - ZorkGrandInquisitorStartingLocations.SPELL_LAB: None, - ZorkGrandInquisitorStartingLocations.HADES_SHORE: { + ZorkGrandInquisitorGoals.SPELL_HEIST: { ZorkGrandInquisitorLocationTransforms.REMOVE: ( - ZorkGrandInquisitorLocations.LANDMARK_HADES_SHORE, - ) + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT, + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG, + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP, + ), }, - ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: None, - ZorkGrandInquisitorStartingLocations.MONASTERY: { + ZorkGrandInquisitorGoals.ZORK_TOUR: { ZorkGrandInquisitorLocationTransforms.REMOVE: ( - ZorkGrandInquisitorLocations.LANDMARK_TOTEMIZER, - ) + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT, + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG, + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP, + ), + }, + ZorkGrandInquisitorGoals.GRIM_JOURNEY: { + ZorkGrandInquisitorLocationTransforms.REMOVE: ( + ZorkGrandInquisitorLocations.COME_TO_PAPA_YOU_NUT, + ZorkGrandInquisitorLocations.GOOD_PUZZLE_SMART_BROG, + ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP, + ), }, - ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: None, - ZorkGrandInquisitorGoals.THREE_ARTIFACTS: None, - ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: None, # TODO: Remember to remove 3 locations of artifacts - ZorkGrandInquisitorGoals.SPELL_HEIST: None, - ZorkGrandInquisitorGoals.ZORK_TOUR: None, - ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: None, ZorkGrandInquisitorDeathsanity.OFF: { ZorkGrandInquisitorLocationTransforms.REMOVE: ( ZorkGrandInquisitorLocations.DEATH_ARRESTED_WITH_JACK, diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index be3d1bf8c99a..d50b5df5ab2d 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -218,12 +218,6 @@ def prepare_location_data( Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents], ZorkGrandInquisitorLocationData ] = dict() - # Force certain options depending on goal - if goal == ZorkGrandInquisitorGoals.NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE: - deathsanity = ZorkGrandInquisitorDeathsanity.ON - elif goal == ZorkGrandInquisitorGoals.ZORK_TOUR: - landmarksanity = ZorkGrandInquisitorLandmarksanity.ON - # Filter locations location: Union[ZorkGrandInquisitorLocations, ZorkGrandInquisitorEvents] data: ZorkGrandInquisitorLocationData @@ -246,7 +240,8 @@ def prepare_location_data( location: ZorkGrandInquisitorLocations for location in locations_to_remove: - del transformed_location_data[location] + if location in transformed_location_data: + del transformed_location_data[location] return transformed_location_data @@ -373,6 +368,8 @@ def entrance_access_rule_for( lambda_string += f"state.has(\"{requirement.value}\", {player})" elif requirement_type == ZorkGrandInquisitorRegions: lambda_string += f"state.can_reach(\"{requirement.value}\", \"Region\", {player})" + elif isinstance(requirement, list): + lambda_string += f"state.has(\"{requirement[0].value}\", {player}, {requirement[1]})" elif isinstance(requirement, tuple): lambda_string += "(" @@ -401,10 +398,42 @@ def goal_access_rule_for( region: ZorkGrandInquisitorRegions, goal: ZorkGrandInquisitorGoals, player: int, + artifacts_of_magic_required: int, ) -> str: + dataset: Dict[ + Tuple[ + ZorkGrandInquisitorRegions, + ZorkGrandInquisitorRegions, + ], + Union[ + Tuple[ + Tuple[ + Union[ + ZorkGrandInquisitorEvents, + ZorkGrandInquisitorItems, + ZorkGrandInquisitorRegions, + List[Union[ZorkGrandInquisitorItems, int]], + ], + ..., + ], + ..., + ], + None, + ], + ] = endgame_entrance_data_by_goal[goal] + + # Replace placeholder with actual number of artifacts of magic required + if goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + dataset[ + ( + ZorkGrandInquisitorRegions.WALKING_CASTLE, + ZorkGrandInquisitorRegions.ENDGAME + ) + ][0][0][1] = artifacts_of_magic_required + return entrance_access_rule_for( region, ZorkGrandInquisitorRegions.ENDGAME, player, - dataset=endgame_entrance_data_by_goal[goal], + dataset=dataset, ) diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 30b0a1b8609e..8b17ee92eadc 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -33,7 +33,7 @@ class ZorkGrandInquisitorGoals(enum.Enum): ARTIFACT_OF_MAGIC_HUNT = 1 SPELL_HEIST = 2 ZORK_TOUR = 3 - NECROMANCER_OF_THE_GREAT_UNDERGROUND_EMPIRE = 4 + GRIM_JOURNEY = 4 class ZorkGrandInquisitorHotspots(enum.Enum): @@ -43,6 +43,7 @@ class ZorkGrandInquisitorHotspots(enum.Enum): class ZorkGrandInquisitorItems(enum.Enum): + ARTIFACT_OF_MAGIC = "Artifact of Magic" BROGS_BICKERING_TORCH = "Brog's Bickering Torch" BROGS_FLICKERING_TORCH = "Brog's Flickering Torch" BROGS_GRUE_EGG = "Brog's Grue Egg" @@ -51,11 +52,79 @@ class ZorkGrandInquisitorItems(enum.Enum): COCOA_INGREDIENTS = "Cocoa Ingredients" COCONUT_OF_QUENDOR = "Coconut of Quendor" CUBE_OF_FOUNDATION = "Cube of Foundation" - FILLER_FROBOZZ_ELECTRIC_GADGET = "Frobozz Electric Gadget" - FILLER_INQUISITION_PROPAGANDA_FLYER = "Inquisition Propaganda Flyer" - FILLER_MAGIC_CONTRABAND = "Magic Contraband" - FILLER_NONSENSICAL_INQUISITION_PAPERWORK = "Nonsensical Inquisition Paperwork" - FILLER_UNREADABLE_SPELL_SCROLL = "Unreadable Spell Scroll" + DEATH = "Grim Journey Death" + FILLER_AIMFIZ_SCROLL = "AIMFIZ Scroll: Transport Caster to Someone Else's Location" + FILLER_BAYALA_SCROLL = "BAYALA Scroll: Deform Body" + FILLER_BITTYJOO_SCROLL = "BITTYJOO Scroll: Make Lies Undetectable" + FILLER_BLORB_SCROLL = "BLORB Scroll: Safely Protext a Small Object" + FILLER_BLORPLE_SCROLL = "BLORPLE Scroll: Explore an Object's Mystic Connections" + FILLER_BOOZNIK_SCROLL = "BOOZNIK Scroll: Reverse Spells in Spellbook" + FILLER_BORCH_SCROLL = "BORCH Scroll: Put Insects to Sleep" + FILLER_CASKLY_SCROLL = "CASKLY Scroll: Cause Perfection" + FILLER_CLEESH_SCROLL = "CLEESH Scroll: Change a Creature Into a Small Amphibian" + FILLER_CONBAK_SCROLL = "CONBAK Scroll: Build Strong Bodies 12 Different Ways" + FILLER_DABHHU_SCROLL = "DABHHU Scroll: Ensure Complete Obedience" + FILLER_DRILBO_SCROLL = "DRILBO Scroll: Strip a Floor of Yellowed Wax" + FILLER_ESPNIS_SCROLL = "ESPNIS Scroll: Sleep" + FILLER_EXEX_SCROLL = "EXEX Scroll: Make Things Move With Greater Speed" + FILLER_FAIFT_SCROLL = "FAIFT Scroll: Change Appearance to Look Younger" + FILLER_FILFRE_SCROLL = "FILFRE Scroll: Create Gratuitous Fireworks" + FILLER_FIZMO_SCROLL = "FIZMO Scroll: Cause Stopped-Up Pipes to Unclog" + FILLER_FOBLUB_SCROLL = "FOBLUB Scroll: Glue Audience to Seats" + FILLER_FRIPPLE_SCROLL = "FRIPPLE Scroll: Erect a Magical Barrier" + FILLER_FROTZ_SCROLL = "FROTZ Scroll: Cause Something to Give Off Light" + FILLER_FWEEP_SCROLL = "FWEEP Scroll: Turn Caster Into a Bat" + FILLER_GASPAR_SCROLL = "GASPAR Scroll: Provide for Your Own Resurrection" + FILLER_GILCH_SCROLL = "GILCH Scroll: Astral Travel" + FILLER_GIRGOL_SCROLL = "GIRGOL Scroll: Stop Time" + FILLER_GHELOOH_SCROLL = "GHEL-OOH Scroll: Suspend Subject in a Gelatinous Substance" + FILLER_GIZGUM_SCROLL = "GIZGUM Scroll: Predict Visits From Relatives" + FILLER_GLOTH_SCROLL = "GLOTH Scroll: Fold Dough 83 Times" + FILLER_GNUSTO_SCROLL = "GNUSTO Scroll: Write a Magic Spell Into a Spellbook" + FILLER_GOLMAC_SCROLL = "GOLMAC Scroll: Travel Temporally" + FILLER_GONDAR_SCROLL = "GONDAR Scroll: Extinguish Fire" + FILLER_GORCH_SCROLL = "GORCH Scroll: Create Ladder" + FILLER_GUNCHO_SCROLL = "GUNCHO Scroll: Banish Victim to Another Plane of Existence" + FILLER_IMALI_SCROLL = "IMALI Scroll: Worsen Eyesight" + FILLER_IZYUK_SCROLL = "IZYUK Scroll: Fly Like a Bird" + FILLER_JINDAK_SCROLL = "JINDAK Scroll: Detect Magic" + FILLER_KEPMKOMN_SCROLL = "KEPMKOMN Scroll: Cause Massive Destruction to Edifices" + FILLER_KOAASST_SCROLL = "KOAASST Scroll: Play Soothing Ambient Music" + FILLER_KRAK_SCROLL = "KRAK Scroll: Slow Time Drastically" + FILLER_KREBF_SCROLL = "KREBF Scroll: Repair Willful Damage" + FILLER_KULCAD_SCROLL = "KULCAD Scroll: Dispel a Magic Spell" + FILLER_LESOCH_SCROLL = "LESOCH Scroll: Gust of Wind" + FILLER_LEXDOM_SCROLL = "LEXDOM Scroll: Create Lock and Key" + FILLER_LIDIBO_SCROLL = "LIDIBO Scroll: Make a Creature Think You Are Really Ugly" + FILLER_LISKON_SCROLL = "LISKON Scroll: Shrink a Living Thing" + FILLER_LOBAL_SCROLL = "LOBAL Scroll: Sharpen Hearing" + FILLER_LOKTAR_SCROLL = "LOKTAR Scroll: Cause Temporal Distension" + FILLER_MALYON_SCROLL = "MALYON Scroll: Animate Inanimate Objects" + FILLER_MEEF_SCROLL = "MEEF Scroll: Cause Plants to Wilt" + FILLER_MELBOR_SCROLL = "MELBOR Scroll: Protect From Harm by Evil Beings" + FILLER_MUSDEX_SCROLL = "MUSDEX Scroll: Improve Muscle Tone" + FILLER_NERZO_SCROLL = "NERZO Scroll: Balance Checkbook" + FILLER_NIKMO_SCROLL = "NIKMO Scroll: Cause Urge to Initiate a Temporary Relationship" + FILLER_NITFOL_SCROLL = "NITFOL Scroll: Converse With Beasts in Their Own Tongue" + FILLER_OTSUNG_SCROLL = "OTSUNG Scroll: Erase Spell Written in Spellbook" + FILLER_OZMOO_SCROLL = "OZMOO Scroll: Survive Unnatural Death" + FILLER_PAXTEN_SCROLL = "PAX-TEN Scroll: Slow Productivity Through Confusion" + FILLER_PULVER_SCROLL = "PULVER Scroll: Dry Liquids" + FILLER_QUELBO_SCROLL = "QUELBO Scroll: Transmute Coconuts Into Gold" + FILLER_STEGAW_SCROLL = "STEGAW Scroll: Turn Eggs Into Ripe Guano" + FILLER_SWANZO_SCROLL = "SWANZO Scroll: Exorcise an Inhabiting Presence" + FILLER_TINSOT_SCROLL = "TINSOT Scroll: Freeze Into Place" + FILLER_TOSSIO_SCROLL = "TOSSIO Scroll: Turn Granite Into Fettuccini" + FILLER_UMBOZ_SCROLL = "UMBOZ Scroll: Obviate Need for Dusting" + FILLER_VARDIK_SCROLL = "VARDIK Scroll: Shield a Mind From an Evil Spirit" + FILLER_VAXUM_SCROLL = "VAXUM Scroll: Make a Hostile Creature Your Friend" + FILLER_VEZZA_SCROLL = "VEZZA Scroll: View the Future" + FILLER_YOMIN_SCROLL = "YOMIN Scroll: Mind Probe" + FILLER_YONK_SCROLL = "YONK Scroll: Augment the Power of Certain Spells" + FILLER_YOZOZZO_SCROLL = "YOZOZZO Scroll: Turn Person Into a Mallard" + FILLER_ZIMBOR_SCROLL = "ZIMBOR Scroll: Turn One Really Big City Into Lots of Tiny, Little Ashes" + FILLER_ZOOKA_SCROLL = "ZOOKA Scroll: Turn Eggs Into Overripe Cabbage" + FILLER_ZUGTHUG_SCROLL = "ZUGTHUG Scroll: Automatically Correct Speling Errors" GRIFFS_AIR_PUMP = "Griff's Air Pump" GRIFFS_DRAGON_TOOTH = "Griff's Dragon Tooth" GRIFFS_INFLATABLE_RAFT = "Griff's Inflatable Raft" @@ -132,6 +201,7 @@ class ZorkGrandInquisitorItems(enum.Enum): HOTSPOT_TOTEMIZER_SWITCH = "Hotspot: Totemizer Switch" HOTSPOT_TOTEMIZER_WHEELS = "Hotspot: Totemizer Wheels" HUNGUS_LARD = "Hungus Lard" + LANDMARK = "Zork Tour Landmark" LARGE_TELEGRAPH_HAMMER = "Large Telegraph Hammer" LUCYS_PLAYING_CARD_1 = "Lucy's Playing Card: 1 Pip" LUCYS_PLAYING_CARD_2 = "Lucy's Playing Card: 2 Pips" @@ -199,6 +269,7 @@ class ZorkGrandInquisitorLandmarksanity(enum.Enum): class ZorkGrandInquisitorLocations(enum.Enum): ALARM_SYSTEM_IS_DOWN = "Alarm System is Down" + AN_EXCELLENT_POPPING_UTENSIL = "An Excellent Popping Utensil" ARREST_THE_VANDAL = "Arrest the Vandal!" ARTIFACTS_EXPLAINED = "Artifacts, Explained" A_BIG_FAT_SASSY_2_HEADED_MONSTER = "A Big, Fat, SASSY 2-Headed Monster" @@ -243,7 +314,9 @@ class ZorkGrandInquisitorLocations(enum.Enum): DEATH_YOURE_NOT_CHARON = "Death: Not Charon" DEATH_ZORK_ROCKS_EXPLODED = "Death: Pretty Painless" DENIED_BY_THE_LAKE_MONSTER = "Denied by the Lake Monster" + DINGWHACKER_DELUXE = "Dingwhacker Deluxe" DONT_EVEN_START_WITH_US_SPARKY = "Don't Even Start With Us, Sparky" + DONT_GO_SPENDING_IT_ALL_IN_ONE_PLACE = "Don't Go Spending it All in One Place" DOOOOOOWN = "Doooooown" DOWN = "Down" DRAGON_ARCHIPELAGO_TIME_TUNNEL = "Dragon Archipelago Time Tunnel" @@ -258,23 +331,29 @@ class ZorkGrandInquisitorLocations(enum.Enum): FLYING_SNAPDRAGON = "Flying Snapdragon" FROBUARY_3_UNDERGROUNDHOG_DAY = "Frobruary 3 - Undergroundhog Day" GETTING_SOME_CHANGE = "Getting Some Change" - GO_AWAY = "GO AWAY!" GOOD_PUZZLE_SMART_BROG = "Good Puzzle. Smart Brog" + GO_AWAY = "GO AWAY!" GUE_TECH_ENTRANCE_EXAM = "GUE Tech Entrance Exam" HAVE_A_HELL_OF_A_DAY = "Have a Hell of a Day!" HELLO_THIS_IS_SHONA_FROM_GURTH_PUBLISHING = "Hello, This is Shona from Gurth Publishing" HELP_ME_CANT_BREATHE = "Help... Me. Can't... Breathe" HEY_FREE_DIRT = "Hey, Free Dirt!" + HMMM_BIG_TOOTHPICK = "Hmmm. Big Toothpick" HMMM_INFORMATIVE_YET_DEEPLY_DISTURBING = "Hmmm. Informative. Yet Deeply Disturbing" HOW_TO_HYPNOTIZE_YOURSELF = "How to Hypnotize Yourself" HOW_TO_WIN_AT_DOUBLE_FANUCCI = "How to Win at Double Fanucci" IMBUE_BEBURTT = "Imbue BEBURTT" IM_COMPLETELY_NUDE = "I'm Completely Nude" + INFLATUS_THE_ETERNAL = "Inflatus the Eternal" INTO_THE_FOLIAGE = "Into the Foliage" INVISIBLE_FLOWERS = "Invisible Flowers" IN_CASE_OF_ADVENTURE = "In Case of Adventure, Break Glass!" IN_MAGIC_WE_TRUST = "In Magic We Trust" + ITS_ALMOST_AS_IF_IT_WERE_INFINITE = "It's Almost as if it Were... Infinite" ITS_ONE_OF_THOSE_ADVENTURERS_AGAIN = "It's One of Those Adventurers Again..." + ITS_PLAYING_A_LITTLE_HARD_TO_GET = "It's Playing a Little Hard to Get" + IT_DOESNT_APPEAR_TO_BE_FOOLED = "It Doesn't Appear to be Fooled" + I_AM_NOT_IMPRESSED = "I Am Not Impressed" I_DONT_THINK_YOU_WOULDVE_WANTED_THAT_TO_WORK_ANYWAY = "I Don't Think You Would've Wanted That to Work Anyway" I_DONT_WANT_NO_TROUBLE = "I Don't Want No Trouble!" I_LIKE_YOUR_STYLE = "I Like Your Style!" @@ -304,6 +383,7 @@ class ZorkGrandInquisitorLocations(enum.Enum): MAILED_IT_TO_HELL = "Mailed it to Hell" MAKE_LOVE_NOT_WAR = "Make Love, Not War" MEAD_LIGHT = "Mead Light?" + ME_I_AM_THE_BOSS_OF_YOU = "Me! I am the Boss of You!" MIKES_PANTS = "Mike's Pants" MUSHROOM_HAMMERED = "Mushroom, Hammered" NATIONAL_TREASURE = "300 Year Old National Treasure" @@ -313,6 +393,7 @@ class ZorkGrandInquisitorLocations(enum.Enum): NOW_YOU_LOOK_LIKE_US_WHICH_IS_AN_IMPROVEMENT = "Now You Look Like Us, Which is an Improvement" NO_AUTOGRAPHS = "No Autographs" NO_BONDAGE = "No Bondage" + NO_ONE_RETURNS_FROM_HADES = "NO ONE Returns from Hades" OBIDIL_DRIED_UP = "OBIDIL, Dried Up" OH_DEAR_GOD_ITS_A_DRAGON = "Oh Dear God, It's a Dragon!" OH_VERY_FUNNY_GUYS = "Oh, Very Funny Guys" @@ -326,10 +407,14 @@ class ZorkGrandInquisitorLocations(enum.Enum): PLEASE_DONT_THROCK_THE_GRASS = "Please Don't THROCK the Grass" PORT_FOOZLE_TIME_TUNNEL = "Port Foozle Time Tunnel" PROZORKED = "Prozorked" + PURPLE_BEAST_ALARM_SYSTEM = "Purple Beast Alarm System" REASSEMBLE_SNAVIG = "Reassemble SNAVIG" + RESTOCKED_ON_GRUESDAY = "Restocked on Gruesday" RIGHT_HELLO_YES_UH_THIS_IS_SNEFFLE = "Right. Hello. Yes. Uh, This is Sneffle" + RIGHT_UH_SORRY_ITS_ME_AGAIN_SNEFFLE = "Right. Uh, Sorry. It's Me Again. Sneffle" SNAVIG_REPAIRED = "SNAVIG, Repaired" SOUVENIR = "Souvenir" + SPELL_CHECK_COMPLETE = "Spell Check Complete" STRAIGHT_TO_HELL = "Straight to Hell" STRIP_GRUE_FIRE_WATER = "Strip Grue, Fire, Water" SUCKING_ROCKS = "Sucking Rocks" @@ -352,15 +437,18 @@ class ZorkGrandInquisitorLocations(enum.Enum): TOTEMIZED_DAILY_BILLBOARD = "Totemized Daily Billboard Functioning Correctly" UH_OH_BROG_CANT_SWIM = "Uh-Oh. Brog Can't Swim" UMBRELLA_FLOWERS = "Umbrella Flowers" + UM_AH_UM_AH_UM_AH = "Um. Ah. Um. Ah. Um. Ah." UP = "Up" USELESS_BUT_FUN = "Useless, But Fun" UUUUUP = "Uuuuup" VOYAGE_OF_CAPTAIN_ZAHAB = "Voyage of Captain Zahab" WANT_SOME_RYE_COURSE_YA_DO = "Want Some Rye? Course Ya Do!" + WANT_SOME_RYE_COURSE_YA_DO_PAST = "Want Some Rye? ... Course Ya Do!" WE_DONT_SERVE_YOUR_KIND_HERE = "We Don't Serve Your Kind Here" WE_GOT_A_HIGH_ROLLER = "We Got a High Roller!" WHAT_ARE_YOU_STUPID = "What Are You, Stupid?" WHITE_HOUSE_TIME_TUNNEL = "White House Time Tunnel" + WHOOPS = "Whoops!" WOW_IVE_NEVER_GONE_INSIDE_HIM_BEFORE = "Wow! I've Never Gone Inside Him Before!" YAD_GOHDNUORGREDNU_3_YRAUBORF = "yaD gohdnuorgrednU - 3 yrauborF" YOUR_PUNY_WEAPONS_DONT_PHASE_ME_BABY = "Your Puny Weapons Don't Phase Me, Baby!" @@ -369,6 +457,7 @@ class ZorkGrandInquisitorLocations(enum.Enum): YOU_LOSE_MUFFET_ANTE_UP = "You Lose, Muffet. Ante Up" YOU_ONE_OF_THEM_AGITATORS_AINT_YA = "You One of Them Agitators, Ain't Ya?" YOU_WANT_A_PIECE_OF_ME_DOCK_BOY = "You Want a Piece of Me, Dock Boy? or Girl" + ZIMDOR_IS_UNDAMAGED = "ZIMDOR is Undamaged" class ZorkGrandInquisitorLocationTransforms(enum.Enum): @@ -424,7 +513,11 @@ class ZorkGrandInquisitorTags(enum.Enum): CORE = "Core" DEATHSANITY = "Deathsanity" FILLER = "Filler" + GOAL_ARTIFACT_OF_MAGIC_HUNT = "Goal: Artifact of Magic Hunt" + GOAL_GRIM_JOURNEY = "Goal: Grim Journey" + GOAL_SPELL_HEIST = "Goal: Spell Heist" GOAL_THREE_ARTIFACTS = "Goal: Three Artifacts" + GOAL_ZORK_TOUR = "Goal: Zork Tour" HOTSPOT = "Hotspot" HOTSPOT_REGIONAL = "Regional Hotspot" INVENTORY_ITEM = "Inventory Item" diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 58489e6a8ef8..389cb3311c24 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -8,7 +8,7 @@ from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.mapping_data import hotspots_for_regional_hotspot +from .data.mapping_data import hotspots_for_regional_hotspot, labels_for_enum_items from .data.missable_location_data import ( missable_location_grant_conditions_data, @@ -18,6 +18,7 @@ from .data_funcs import game_id_to_items, items_with_tag, locations_with_tag from .enums import ( + ZorkGrandInquisitorCraftableSpellBehaviors, ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, ZorkGrandInquisitorHotspots, @@ -42,7 +43,9 @@ class GameController: completed_locations_queue: collections.deque received_items_queue: collections.deque + all_spell_items: Set[ZorkGrandInquisitorItems] all_hotspot_items: Set[ZorkGrandInquisitorItems] + all_goal_items: Set[ZorkGrandInquisitorItems] game_id_to_items: Dict[int, ZorkGrandInquisitorItems] @@ -50,11 +53,15 @@ class GameController: available_inventory_slots: Set[int] + goal_item_count: int goal_completed: bool option_goal: Optional[ZorkGrandInquisitorGoals] + option_artifacts_of_magic_required: Optional[int] + option_artifacts_of_magic_total: Optional[int] option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] option_hotspots: Optional[ZorkGrandInquisitorHotspots] + option_craftable_spells: Optional[ZorkGrandInquisitorCraftableSpellBehaviors] option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] @@ -72,12 +79,20 @@ def __init__(self, logger=None) -> None: self.completed_locations_queue = collections.deque() self.received_items_queue = collections.deque() + self.all_spell_items = items_with_tag(ZorkGrandInquisitorTags.SPELL) + self.all_hotspot_items = ( items_with_tag(ZorkGrandInquisitorTags.HOTSPOT) | items_with_tag(ZorkGrandInquisitorTags.SUBWAY_DESTINATION) | items_with_tag(ZorkGrandInquisitorTags.TOTEMIZER_DESTINATION) ) + self.all_goal_items = { + ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC, + ZorkGrandInquisitorItems.LANDMARK, + ZorkGrandInquisitorItems.DEATH, + } + self.game_id_to_items = game_id_to_items() self.possible_inventory_items = ( @@ -88,11 +103,15 @@ def __init__(self, logger=None) -> None: self.available_inventory_slots = set() + self.goal_item_count = 0 self.goal_completed = False self.option_goal = None + self.option_artifacts_of_magic_required = None + self.option_artifacts_of_magic_total = None self.option_starting_location = None self.option_hotspots = None + self.option_craftable_spells = None self.option_deathsanity = None self.option_landmarksanity = None self.option_grant_missable_location_checks = None @@ -155,6 +174,52 @@ def close_process_handle(self) -> bool: def is_process_running(self) -> bool: return self.game_state_manager.is_process_running + def output_seed_information(self) -> None: + if self.option_goal is not None: + self.log("Seed Information:") + self.log(f" Goal: {labels_for_enum_items[self.option_goal]}") + + if self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + self.log(f" Artifacts of Magic Required: {self.option_artifacts_of_magic_required}") + self.log(f" Artifacts of Magic Total: {self.option_artifacts_of_magic_total}") + + self.log(f" Starting Location: {labels_for_enum_items[self.option_starting_location]}") + self.log(f" Hotspots: {labels_for_enum_items[self.option_hotspots]}") + self.log(f" Craftable Spells: {labels_for_enum_items[self.option_craftable_spells]}") + self.log(f" Deathsanity: {labels_for_enum_items[self.option_deathsanity]}") + self.log(f" Landmarksanity: {labels_for_enum_items[self.option_landmarksanity]}") + + if self.option_grant_missable_location_checks: + self.log(f" Grant Missable Location Checks: On") + else: + self.log(f" Grant Missable Location Checks: Off") + + def output_goal_item_update(self) -> None: + if self.goal_completed: + return + + if self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + self.log( + f"Received Artifact of Magic {self.goal_item_count} of {self.option_artifacts_of_magic_required}" + ) + + if self.goal_item_count >= self.option_artifacts_of_magic_required: + self.log("All needed Artifacts of Magic have been found! Get to the Walking Castle") + elif self.option_goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + self.log( + f"Visited {self.goal_item_count} of 20 Landmarks" + ) + + if self.goal_item_count == 20: + self.log("All Landmarks have been visited! Get to the Port Foozle signpost") + elif self.option_goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: + self.log( + f"Experienced {self.goal_item_count} of 22 Deaths" + ) + + if self.goal_item_count == 22: + self.log("All Deaths have been experienced! Go beyond the gates of hell") + def list_received_brog_items(self) -> None: self.log("Received Brog Items:") @@ -1091,6 +1156,22 @@ def _check_for_victory(self) -> None: skull_is_placed = self._read_game_state_value_for(2321) == 1 self.goal_completed = coconut_is_placed and cube_is_placed and skull_is_placed + elif self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + if self.goal_item_count >= self.option_artifacts_of_magic_required: + if self._player_is_at("dc10") and self._player_is_afgncaap(): + self.goal_completed = True + elif self.option_goal == ZorkGrandInquisitorGoals.SPELL_HEIST: + if not len(self.all_spell_items - self.received_items): + if self._player_is_at("ps1e"): + self.goal_completed = True + elif self.option_goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + if self.goal_item_count == 20: + if self._player_is_at("ps1e"): + self.goal_completed = True + elif self.option_goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: + if self.goal_item_count == 22: + if self._player_is_at("hp60"): + self.goal_completed = True def _determine_game_state_inventory(self) -> Set[ZorkGrandInquisitorItems]: game_state_inventory: Set[ZorkGrandInquisitorItems] = set() @@ -1103,7 +1184,16 @@ def _determine_game_state_inventory(self) -> Set[ZorkGrandInquisitorItems]: game_state_inventory.add(self.game_id_to_items[item_on_cursor]) # Item in Inspector - item_in_inspector: int = self._read_game_state_value_for(4512) + item_in_inspector: int = 0 + + if self._player_is_afgncaap(): + item_in_inspector = self._read_game_state_value_for(4512) + elif self._player_is_brog(): + item_in_inspector = self._read_game_state_value_for(2194) + elif self._player_is_griff(): + item_in_inspector = self._read_game_state_value_for(2196) + elif self._player_is_lucy(): + item_in_inspector = self._read_game_state_value_for(2198) if item_in_inspector != 0: if item_in_inspector in self.game_id_to_items: @@ -1306,6 +1396,8 @@ def _filter_received_inventory_items( to_filter_inventory_items.add(item) elif self._read_game_state_value_for(4034) == 1: to_filter_inventory_items.add(item) + elif self._read_game_state_value_for(12167) == 1: + to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.ZORK_ROCKS: if self._read_game_state_value_for(12486) == 1: to_filter_inventory_items.add(item) diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index f3cf1743024e..40c04f9912a2 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -1,21 +1,59 @@ from dataclasses import dataclass -from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Toggle +from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Range, Toggle class Goal(Choice): """ Determines the victory condition - Three Artifacts: Retrieve the three artifacts of magic and place them in the walking castle + Three Artifacts: Retrieve the Coconut of Quendor, the Cube of Foundation and the Skull of Yoruk + Artifact of Magic Hunt: Retrieve X artifacts of magic and bring them to the walking castle + Spell Heist: Acquire all spells and travel to the Port Foozle signpost + Zork Tour: Visit all 20 landmarks and travel to the Port Foozle signpost + Grim Journey: Experience all 22 player deaths and go beyond the gates of Hades """ display_name: str = "Goal" option_three_artifacts: int = 0 + option_artifact_of_magic_hunt: int = 1 + option_spell_heist: int = 2 + option_zork_tour: int = 3 + option_grim_journey: int = 4 default = 0 +class ArtifactsOfMagicTotal(Range): + """ + Determines how many Artifacts of Magic are in the item pool + + Only relevant if the selected goal is Artifact of Magic Hunt + """ + + display_name = "Artifacts of Magic Total" + + range_start = 5 + range_end = 25 + + default = 15 + + +class ArtifactsOfMagicRequired(Range): + """ + Determines how many Artifacts of Magic are required to win + + Only relevant if the selected goal is Artifact of Magic Hunt + """ + + display_name = "Artifacts of Magic Required" + + range_start = 5 + range_end = 25 + + default = 10 + + class StartingLocation(Choice): """ Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to @@ -77,21 +115,23 @@ class CraftableSpells(Choice): class Deathsanity(Toggle): - """If true, adds 22 unique player death locations to the world""" # TODO: Add note about it being forced in Necro goal + """ + If true, adds 22 unique player death locations to the world + + This option will be forced on if your goal is Grim Journey + """ display_name: str = "Deathsanity" class Landmarksanity(DefaultOnToggle): - """If true, adds 20 landmark locations to the world""" # TODO: Add note about it being forced in Zork Tour goal - - display_name: str = "Landmarksanity" - + """ + If true, adds 20 landmark locations to the world -class PlaceEarlyItemsLocally(Toggle): - """If true, items to be placed early in the multiworld (when applicable) will be placed locally""" + This option will be forced on if your goal is Zork Tour + """ - display_name: str = "Place Early Items Locally" + display_name: str = "Landmarksanity" class GrantMissableLocationChecks(Toggle): @@ -110,10 +150,11 @@ class GrantMissableLocationChecks(Toggle): @dataclass class ZorkGrandInquisitorOptions(PerGameCommonOptions): goal: Goal + artifacts_of_magic_total: ArtifactsOfMagicTotal + artifacts_of_magic_required: ArtifactsOfMagicRequired starting_location: StartingLocation hotspots: Hotspots craftable_spells: CraftableSpells deathsanity: Deathsanity landmarksanity: Landmarksanity - place_early_items_locally: PlaceEarlyItemsLocally grant_missable_location_checks: GrantMissableLocationChecks diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index a5c7e9c5e971..5baf7766727f 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -30,6 +30,7 @@ items_with_tag, location_groups, locations_by_region_for_world, + locations_with_tag, prepare_item_data, prepare_location_data, location_access_rule_for, @@ -101,6 +102,8 @@ class ZorkGrandInquisitorWorld(World): web = ZorkGrandInquisitorWebWorld() + artifacts_of_magic_required: int + artifacts_of_magic_total: int craftable_spells: ZorkGrandInquisitorCraftableSpellBehaviors deathsanity: ZorkGrandInquisitorDeathsanity early_items: Tuple[ZorkGrandInquisitorItems, ...] @@ -118,12 +121,18 @@ class ZorkGrandInquisitorWorld(World): ] locked_items: Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems] - place_early_items_locally: bool starter_kit: Tuple[ZorkGrandInquisitorItems, ...] starting_location: ZorkGrandInquisitorStartingLocations def generate_early(self) -> None: self.goal = id_to_goals()[self.options.goal.value] + + self.artifacts_of_magic_required = self.options.artifacts_of_magic_required.value + self.artifacts_of_magic_total = self.options.artifacts_of_magic_total.value + + if self.artifacts_of_magic_required > self.artifacts_of_magic_total: + self.artifacts_of_magic_total = self.artifacts_of_magic_required + self.starting_location = id_to_starting_locations()[self.options.starting_location.value] self.starter_kit = tuple() @@ -144,9 +153,19 @@ def generate_early(self) -> None: self.hotspots = id_to_hotspots()[self.options.hotspots] self.deathsanity = id_to_deathsanity()[self.options.deathsanity] + + if self.goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY and ( + self.deathsanity == ZorkGrandInquisitorDeathsanity.OFF + ): + self.deathsanity = ZorkGrandInquisitorDeathsanity.ON + self.landmarksanity = id_to_landmarksanity()[self.options.landmarksanity] - self.place_early_items_locally = bool(self.options.place_early_items_locally) + if self.goal == ZorkGrandInquisitorGoals.ZORK_TOUR and ( + self.landmarksanity == ZorkGrandInquisitorLandmarksanity.OFF + ): + self.landmarksanity = ZorkGrandInquisitorLandmarksanity.ON + self.grant_missable_location_checks = bool(self.options.grant_missable_location_checks) self.item_data = prepare_item_data( @@ -228,7 +247,13 @@ def create_regions(self) -> None: region.connect(region_mapping[region_exit], rule=eval(entrance_access_rule)) if region_enum_item == region_connecting_endgame: - goal_access_rule: str = goal_access_rule_for(region_enum_item, self.goal, self.player) + goal_access_rule: str = goal_access_rule_for( + region_enum_item, + self.goal, + self.player, + self.artifacts_of_magic_required, + ) + region.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) self.multiworld.regions.append(region) @@ -241,7 +266,13 @@ def create_regions(self) -> None: region_menu.connect(region_mapping[region_starting_location]) if region_connecting_endgame == ZorkGrandInquisitorRegions.MENU: - goal_access_rule: str = goal_access_rule_for(ZorkGrandInquisitorRegions.MENU, self.goal, self.player) + goal_access_rule: str = goal_access_rule_for( + ZorkGrandInquisitorRegions.MENU, + self.goal, + self.player, + self.artifacts_of_magic_required, + ) + region_menu.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) self.multiworld.regions.append(region_menu) @@ -259,6 +290,12 @@ def create_items(self) -> None: for item in items_with_tag(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS): items_to_ignore.add(item) + if self.goal != ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + items_to_ignore.add(ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC) + + items_to_ignore.add(ZorkGrandInquisitorItems.LANDMARK) + items_to_ignore.add(ZorkGrandInquisitorItems.DEATH) + for item in self.locked_items.values(): items_to_ignore.add(item) @@ -302,7 +339,11 @@ def create_items(self) -> None: if item in items_to_ignore or item in items_to_precollect: continue - item_pool.append(self.create_item(item.value)) + if item == ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC: + for _ in range(self.artifacts_of_magic_total): + item_pool.append(self.create_item(item.value)) + else: + item_pool.append(self.create_item(item.value)) total_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))] @@ -313,14 +354,10 @@ def create_items(self) -> None: for item in items_to_precollect: self.multiworld.push_precollected(self.create_item(item.value)) - # Set Early Items - # TODO: Does this even work? Needs testing + # Early Items if len(items_to_place_early): - early: Dict[int, Dict[str, int]] - early = self.multiworld.local_early_items if self.place_early_items_locally else self.multiworld.early_items - for item in items_to_place_early: - early[self.player][item.value] = 1 + self.multiworld.early_items[self.player][item.value] = 1 def create_item(self, name: str) -> ZorkGrandInquisitorItem: data: ZorkGrandInquisitorItemData = self.item_data[self.item_name_to_item[name]] @@ -338,6 +375,8 @@ def generate_basic(self) -> None: def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = self.options.as_dict( "goal", + "artifacts_of_magic_required", + "artifacts_of_magic_total", "starting_location", "hotspots", "craftable_spells", @@ -371,6 +410,22 @@ def _prepare_locked_items( locked_items[ ZorkGrandInquisitorLocations.YOU_LOSE_MUFFET_ANTE_UP ] = ZorkGrandInquisitorItems.CUBE_OF_FOUNDATION + elif self.goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + landmarksanity_locations: Set[ZorkGrandInquisitorLocations] = locations_with_tag( + ZorkGrandInquisitorTags.LANDMARKSANITY + ) + + location: ZorkGrandInquisitorLocations + for location in landmarksanity_locations: + locked_items[location] = ZorkGrandInquisitorItems.LANDMARK + elif self.goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: + deathsanity_locations: Set[ZorkGrandInquisitorLocations] = locations_with_tag( + ZorkGrandInquisitorTags.DEATHSANITY + ) + + location: ZorkGrandInquisitorLocations + for location in deathsanity_locations: + locked_items[location] = ZorkGrandInquisitorItems.DEATH # Craftable Spells if self.craftable_spells == ZorkGrandInquisitorCraftableSpellBehaviors.VANILLA: From b9e06c20463306f76d1cb83a8626576fa2acd665 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 22 Nov 2024 14:37:52 -0500 Subject: [PATCH 11/51] typo --- worlds/zork_grand_inquisitor/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 8b17ee92eadc..f7d9a9b037d5 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -56,7 +56,7 @@ class ZorkGrandInquisitorItems(enum.Enum): FILLER_AIMFIZ_SCROLL = "AIMFIZ Scroll: Transport Caster to Someone Else's Location" FILLER_BAYALA_SCROLL = "BAYALA Scroll: Deform Body" FILLER_BITTYJOO_SCROLL = "BITTYJOO Scroll: Make Lies Undetectable" - FILLER_BLORB_SCROLL = "BLORB Scroll: Safely Protext a Small Object" + FILLER_BLORB_SCROLL = "BLORB Scroll: Safely Protect a Small Object" FILLER_BLORPLE_SCROLL = "BLORPLE Scroll: Explore an Object's Mystic Connections" FILLER_BOOZNIK_SCROLL = "BOOZNIK Scroll: Reverse Spells in Spellbook" FILLER_BORCH_SCROLL = "BORCH Scroll: Put Insects to Sleep" From 4417e5906ba998ef731979037d7c330be0d046ce Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 22 Nov 2024 15:48:02 -0500 Subject: [PATCH 12/51] output starter in client on /zork --- worlds/zork_grand_inquisitor/client.py | 4 ++++ .../zork_grand_inquisitor/game_controller.py | 24 +++++++++++++++++++ worlds/zork_grand_inquisitor/world.py | 1 + 3 files changed, 29 insertions(+) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 6b859d4bc839..1f49d70e1196 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -34,6 +34,7 @@ def _cmd_zork(self) -> None: self.output("Successfully attached to Zork Grand Inquisitor process.") self.ctx.game_controller.output_seed_information() + self.ctx.game_controller.output_starter_kit() else: self.output("Failed to attach to Zork Grand Inquisitor process.") @@ -140,6 +141,9 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["grant_missable_location_checks"] == 1 ) + # Starter Kit + self.game_controller.starter_kit = _args["slot_data"]["starter_kit"] + # Initial Totemizer Destination self.game_controller.initial_totemizer_destination = item_names_to_item()[ _args["slot_data"]["initial_totemizer_destination"] diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 389cb3311c24..44c25dbdeafe 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -66,6 +66,7 @@ class GameController: option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] + starter_kit: Optional[List[str]] initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] def __init__(self, logger=None) -> None: @@ -116,6 +117,7 @@ def __init__(self, logger=None) -> None: self.option_landmarksanity = None self.option_grant_missable_location_checks = None + self.starter_kit = None self.initial_totemizer_destination = None @functools.cached_property @@ -194,6 +196,28 @@ def output_seed_information(self) -> None: else: self.log(f" Grant Missable Location Checks: Off") + def output_starter_kit(self) -> None: + if self.starter_kit is None: + return + + self.log("Starter Kit:") + + if len(self.starter_kit): + item: str + for item in self.starter_kit: + if self.option_hotspots == ZorkGrandInquisitorHotspots.ENABLED: + if item.startswith("Hotspot"): + continue + elif item in ( + "Hotspot: Dungeon Master's Lair Entrance", + "Hotspot: Spell Lab Bridge Exit", + ): + continue + + self.log(f" {item}") + else: + self.log(" Nothing") + def output_goal_item_update(self) -> None: if self.goal_completed: return diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 5baf7766727f..9861fd8b8ba2 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -385,6 +385,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "grant_missable_location_checks", ) + slot_data["starter_kit"] = sorted([item.value for item in self.starter_kit]) slot_data["initial_totemizer_destination"] = self.initial_totemizer_destination.value return slot_data From 110ab68e4c6b4d4aa21b9ac7cd2b0a32c72475d5 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 22 Nov 2024 16:34:07 -0500 Subject: [PATCH 13/51] add option to control how much information about the seed is revealed on /zork --- worlds/zork_grand_inquisitor/client.py | 5 +++++ .../data/mapping_data.py | 4 ++++ worlds/zork_grand_inquisitor/data_funcs.py | 5 +++++ worlds/zork_grand_inquisitor/enums.py | 6 ++++++ .../zork_grand_inquisitor/game_controller.py | 11 +++++++++++ worlds/zork_grand_inquisitor/options.py | 19 +++++++++++++++++++ worlds/zork_grand_inquisitor/world.py | 1 + 7 files changed, 51 insertions(+) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 1f49d70e1196..f13ab47a4147 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -10,6 +10,7 @@ item_names_to_id, item_names_to_item, location_names_to_id, + id_to_client_seed_information, id_to_craftable_spell_behaviors, id_to_deathsanity, id_to_hotspots, @@ -141,6 +142,10 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["grant_missable_location_checks"] == 1 ) + self.game_controller.option_client_seed_information = ( + id_to_client_seed_information()[_args["slot_data"]["client_seed_information"]] + ) + # Starter Kit self.game_controller.starter_kit = _args["slot_data"]["starter_kit"] diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index a5f2a14c7fee..9338ee5e2911 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -1,6 +1,7 @@ from typing import Dict, Optional, Tuple, Union from ..enums import ( + ZorkGrandInquisitorClientSeedInformation, ZorkGrandInquisitorCraftableSpellBehaviors, ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, @@ -319,6 +320,9 @@ ], str ] = { + ZorkGrandInquisitorClientSeedInformation.REVEAL_NOTHING: "Reveal Nothing", + ZorkGrandInquisitorClientSeedInformation.REVEAL_GOAL: "Reveal Goal", + ZorkGrandInquisitorClientSeedInformation.REVEAL_GOAL_AND_OPTIONS: "Reveal Goal and Options", ZorkGrandInquisitorCraftableSpellBehaviors.VANILLA: "Vanilla", ZorkGrandInquisitorCraftableSpellBehaviors.ANY_SPELL: "Any Spell", ZorkGrandInquisitorCraftableSpellBehaviors.ANYTHING: "Anything", diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index d50b5df5ab2d..56f50828e815 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -8,6 +8,7 @@ from .data.transform_data import item_data_transforms, location_data_transforms from .enums import ( + ZorkGrandInquisitorClientSeedInformation, ZorkGrandInquisitorCraftableSpellBehaviors, ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorEvents, @@ -48,6 +49,10 @@ def location_names_to_location() -> Dict[Any, ZorkGrandInquisitorLocations]: } +def id_to_client_seed_information() -> Dict[int, ZorkGrandInquisitorClientSeedInformation]: + return {info.value: info for info in ZorkGrandInquisitorClientSeedInformation} + + def id_to_craftable_spell_behaviors() -> Dict[int, ZorkGrandInquisitorCraftableSpellBehaviors]: return {behavior.value: behavior for behavior in ZorkGrandInquisitorCraftableSpellBehaviors} diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index f7d9a9b037d5..0e58f11bbf62 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -1,6 +1,12 @@ import enum +class ZorkGrandInquisitorClientSeedInformation(enum.Enum): + REVEAL_NOTHING = 0 + REVEAL_GOAL = 1 + REVEAL_GOAL_AND_OPTIONS = 2 + + class ZorkGrandInquisitorCraftableSpellBehaviors(enum.Enum): VANILLA = 0 ANY_SPELL = 1 diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 44c25dbdeafe..dfa12cc71f70 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -18,6 +18,7 @@ from .data_funcs import game_id_to_items, items_with_tag, locations_with_tag from .enums import ( + ZorkGrandInquisitorClientSeedInformation, ZorkGrandInquisitorCraftableSpellBehaviors, ZorkGrandInquisitorDeathsanity, ZorkGrandInquisitorGoals, @@ -65,6 +66,7 @@ class GameController: option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] + option_client_seed_information: Optional[ZorkGrandInquisitorClientSeedInformation] starter_kit: Optional[List[str]] initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] @@ -116,6 +118,7 @@ def __init__(self, logger=None) -> None: self.option_deathsanity = None self.option_landmarksanity = None self.option_grant_missable_location_checks = None + self.option_client_seed_information = None self.starter_kit = None self.initial_totemizer_destination = None @@ -179,8 +182,16 @@ def is_process_running(self) -> bool: def output_seed_information(self) -> None: if self.option_goal is not None: self.log("Seed Information:") + + if self.option_client_seed_information == ZorkGrandInquisitorClientSeedInformation.REVEAL_NOTHING: + self.log(" REDACTED by the Inquisition") + return + self.log(f" Goal: {labels_for_enum_items[self.option_goal]}") + if self.option_client_seed_information == ZorkGrandInquisitorClientSeedInformation.REVEAL_GOAL: + return + if self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: self.log(f" Artifacts of Magic Required: {self.option_artifacts_of_magic_required}") self.log(f" Artifacts of Magic Total: {self.option_artifacts_of_magic_total}") diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index 40c04f9912a2..c5f64d660d87 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -147,6 +147,24 @@ class GrantMissableLocationChecks(Toggle): display_name: str = "Grant Missable Checks" +class ClientSeedInformation(Choice): + """ + Determines what information about the seed the client will reveal after using the /zork command + + Reveal Nothing: No information about the seed is displayed + Reveal Goal: Only the goal of the seed is displayed + Reveal Goal and Options: Both the goal and the options of the seed are displayed + """ + + display_name: str = "Client Seed Information" + + option_reveal_nothing: int = 0 + option_reveal_goal: int = 1 + option_reveal_goal_and_options: int = 2 + + default = 2 + + @dataclass class ZorkGrandInquisitorOptions(PerGameCommonOptions): goal: Goal @@ -158,3 +176,4 @@ class ZorkGrandInquisitorOptions(PerGameCommonOptions): deathsanity: Deathsanity landmarksanity: Landmarksanity grant_missable_location_checks: GrantMissableLocationChecks + client_seed_information: ClientSeedInformation diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 9861fd8b8ba2..096f93e31f9f 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -383,6 +383,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "deathsanity", "landmarksanity", "grant_missable_location_checks", + "client_seed_information", ) slot_data["starter_kit"] = sorted([item.value for item in self.starter_kit]) From 6750b353b4f38b380c3c1c77111a495d1f2ae330 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 22 Nov 2024 20:01:22 -0500 Subject: [PATCH 14/51] implement wild VOXAM option --- worlds/zork_grand_inquisitor/client.py | 3 + .../data/mapping_data.py | 104 ++++++++++++++++++ .../zork_grand_inquisitor/game_controller.py | 53 ++++++++- worlds/zork_grand_inquisitor/options.py | 54 ++++++--- worlds/zork_grand_inquisitor/world.py | 2 + 5 files changed, 197 insertions(+), 19 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index f13ab47a4147..9d5daee0b6fd 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -130,6 +130,9 @@ def on_package(self, cmd: str, _args: Any) -> None: id_to_craftable_spell_behaviors()[_args["slot_data"]["craftable_spells"]] ) + self.game_controller.option_wild_voxam = _args["slot_data"]["wild_voxam"] == 1 + self.game_controller.option_wild_voxam_chance = _args["slot_data"]["wild_voxam_chance"] + self.game_controller.option_deathsanity = ( id_to_deathsanity()[_args["slot_data"]["deathsanity"]] ) diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 9338ee5e2911..43d8b9d8ef07 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -730,3 +730,107 @@ ZorkGrandInquisitorStartingLocations.MONASTERY: ZorkGrandInquisitorRegions.MONASTERY, ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, } + +voxam_cast_game_locations: Dict[ + ZorkGrandInquisitorStartingLocations, + Tuple[Tuple[str, int], ...] +] = { + ZorkGrandInquisitorStartingLocations.PORT_FOOZLE: ( + ("px1j", 0), + ("ps20", 1), + ("pe20", 1), + ("pe2j", 0), + ("pe30", 1), + ("pe40", 1), + ("pe50", 1), + ("pe5e", 0), + ("pe5f", 0), + ("pe6e", 0), + ), + ZorkGrandInquisitorStartingLocations.CROSSROADS: ( + ("uc10", 1), + ("uc20", 1), + ("uc30", 1), + ("uc3e", 0), + ("uc40", 1), + ("uc4e", 0), + ("uc50", 1), + ("uc6e", 0), + ), + ZorkGrandInquisitorStartingLocations.DM_LAIR: ( + ("dg10", 1), + ("dg20", 1), + ("dg4f", 0), + ("dg30", 1), + ("dg3e", 0), + ), + ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: ( + ("dv10", 1), + ("dv1j", 0), + ("dw10", 1), + ("dw1g", 0), + ), + ZorkGrandInquisitorStartingLocations.GUE_TECH: ( + ("tr20", 1), + ("tr1k", 0), + ("tr1g", 0), + ("tr2g", 0), + ("tr50", 1), + ("tr5e", 0), + ("tr5f", 0), + ("tr5g", 0), + ("tr4g", 0), + ("tr4f", 0), + ("th30", 1), + ("th50", 1), + ("th60", 1), + ("th40", 1), + ), + ZorkGrandInquisitorStartingLocations.SPELL_LAB: ( + ("tp20", 1), + ("tp50", 1), + ("tp10", 1), + ("tp2f", 0), + ("tp2g", 0), + ("tp2e", 0), + ("tp30", 1), + ("tp3f", 0), + ("tp3e", 0), + ("tp4f", 0), + ("tp4e", 0), + ), + ZorkGrandInquisitorStartingLocations.HADES_SHORE: ( + ("uh10", 1), + ("uh20", 1), + ("uh2f", 0), + ("uh2e", 0), + ), + ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: ( + ("ue10", 1), + ("ue20", 1), + ("ue2g", 0), + ("ue2e", 0), + ("ue2j", 0), + ("ue2k", 0), + ("ue2f", 0), + ), + ZorkGrandInquisitorStartingLocations.MONASTERY: ( + ("mt20", 1), + ("mt1e", 0), + ("mt1f", 0), + ("mt2g", 0), + ("mt2e", 0), + ("mt30", 1), + ), + ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ( + ("me10", 1), + ("me1f", 0), + ("me1h", 0), + ("me1g", 0), + ("me20", 1), + ("me2h", 0), + ("me2j", 0), + ("me5f", 0), + ("me2m", 0), + ), +} diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index dfa12cc71f70..bd07bc7d4d67 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1,6 +1,7 @@ import collections import functools import logging +import random import traceback # TODO: Only in dev import time @@ -8,7 +9,12 @@ from .data.item_data import item_data, ZorkGrandInquisitorItemData from .data.location_data import location_data, ZorkGrandInquisitorLocationData -from .data.mapping_data import hotspots_for_regional_hotspot, labels_for_enum_items + +from .data.mapping_data import ( + hotspots_for_regional_hotspot, + labels_for_enum_items, + voxam_cast_game_locations, +) from .data.missable_location_data import ( missable_location_grant_conditions_data, @@ -63,6 +69,8 @@ class GameController: option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] option_hotspots: Optional[ZorkGrandInquisitorHotspots] option_craftable_spells: Optional[ZorkGrandInquisitorCraftableSpellBehaviors] + option_wild_voxam: Optional[bool] + option_wild_voxam_chance: Optional[int] option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] @@ -115,6 +123,8 @@ def __init__(self, logger=None) -> None: self.option_starting_location = None self.option_hotspots = None self.option_craftable_spells = None + self.option_wild_voxam = None + self.option_wild_voxam_chance = None self.option_deathsanity = None self.option_landmarksanity = None self.option_grant_missable_location_checks = None @@ -199,6 +209,12 @@ def output_seed_information(self) -> None: self.log(f" Starting Location: {labels_for_enum_items[self.option_starting_location]}") self.log(f" Hotspots: {labels_for_enum_items[self.option_hotspots]}") self.log(f" Craftable Spells: {labels_for_enum_items[self.option_craftable_spells]}") + + if self.option_wild_voxam: + self.log(f" Wild VOXAM: On ({self.option_wild_voxam_chance}% chance)") + else: + self.log(f" Wild VOXAM: Off") + self.log(f" Deathsanity: {labels_for_enum_items[self.option_deathsanity]}") self.log(f" Landmarksanity: {labels_for_enum_items[self.option_landmarksanity]}") @@ -362,15 +378,15 @@ def _apply_starting_location(self, force: bool = False) -> None: elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: self.game_state_manager.set_game_location("dv10", 1673) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.GUE_TECH: - self.game_state_manager.set_game_location("tr10", 150) + self.game_state_manager.set_game_location("tr20", 150) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SPELL_LAB: self.game_state_manager.set_game_location("tp20", 1244) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.HADES_SHORE: - self.game_state_manager.set_game_location("hp10", 534) + self.game_state_manager.set_game_location("uh10", 950) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.SUBWAY_FLOOD_CONTROL_DAM: self.game_state_manager.set_game_location("ue10", 1578) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY: - self.game_state_manager.set_game_location("mt10", 1483) + self.game_state_manager.set_game_location("mt20", 0) elif self.option_starting_location == ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: self.game_state_manager.set_game_location("me10", 1023) @@ -1179,10 +1195,37 @@ def _apply_conditional_teleports(self) -> None: zork_rocks_inert = self._read_game_state_value_for(11767) == 0 if self._read_game_state_value_for(9) == 224: + time.sleep(0.1) self._write_game_state_value_for(9, 0) if zork_rocks_inert: - self._apply_starting_location(force=True) + self._cast_voxam() + + def _cast_voxam(self) -> None: + if not self.option_wild_voxam: + self._apply_starting_location(force=True) + + voxam_roll: int = random.randint(1, 100) + + if voxam_roll <= self.option_wild_voxam_chance: + starting_location: ZorkGrandInquisitorStartingLocations = ( + random.choice(tuple(voxam_cast_game_locations.keys())) + ) + + game_location: Tuple[Tuple[str, int], ...] = ( + random.choice(voxam_cast_game_locations[starting_location]) + ) + + game_location_offset: int = 0 + + if game_location[1] == 1: + game_location_offset = random.randint(0, 1800) + + self.game_state_manager.set_game_location( + game_location[0], game_location_offset + ) + else: + self._apply_starting_location(force=True) def _check_for_victory(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS: diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index c5f64d660d87..e1db799a1836 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -5,7 +5,7 @@ class Goal(Choice): """ - Determines the victory condition + Determines the victory condition. Three Artifacts: Retrieve the Coconut of Quendor, the Cube of Foundation and the Skull of Yoruk Artifact of Magic Hunt: Retrieve X artifacts of magic and bring them to the walking castle @@ -26,9 +26,9 @@ class Goal(Choice): class ArtifactsOfMagicTotal(Range): """ - Determines how many Artifacts of Magic are in the item pool + Determines how many Artifacts of Magic are in the item pool. - Only relevant if the selected goal is Artifact of Magic Hunt + Only relevant if the selected goal is Artifact of Magic Hunt. """ display_name = "Artifacts of Magic Total" @@ -41,9 +41,9 @@ class ArtifactsOfMagicTotal(Range): class ArtifactsOfMagicRequired(Range): """ - Determines how many Artifacts of Magic are required to win + Determines how many Artifacts of Magic are required to win. - Only relevant if the selected goal is Artifact of Magic Hunt + Only relevant if the selected goal is Artifact of Magic Hunt. """ display_name = "Artifacts of Magic Required" @@ -58,7 +58,7 @@ class StartingLocation(Choice): """ Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to teleport back to the starting location at any time. Depending on the starting location, the player may also be given - a starter kit of items to help them get going + a starter kit of items to help them get going. """ display_name: str = "Starting Location" @@ -98,7 +98,7 @@ class Hotspots(Choice): class CraftableSpells(Choice): """ Determines the behavior when craftable spells (BEBURTT, OBIDIL, SNAVIG, YASTARD) are obtained. - Spells in a starting location's starter kit always have precedence over this option + Spells in a starting location's starter kit always have precedence over this option. Vanilla: After crafting a spell, the player will be given that exact spell Any Spell: After crafting a spell, the player will be given a random spell @@ -114,11 +114,35 @@ class CraftableSpells(Choice): default = 2 +class WildVoxam(Toggle): + """ + If true, casting VOXAM will have a small chance to teleport the player to a different location. + + This option can enable small stretches of out-of-logic gameplay in the early game, with strong diminishing returns + as the game progresses. + """ + + display_name: str = "Wild VOXAM" + + +class WildVoxamChance(Range): + """ + Determines the percentage chance that a VOXAM cast will be wild. + """ + + display_name = "Wild VOXAM Chance %" + + range_start = 1 + range_end = 10 + + default = 5 + + class Deathsanity(Toggle): """ - If true, adds 22 unique player death locations to the world + If true, adds 22 unique player death locations to the world. - This option will be forced on if your goal is Grim Journey + This option will be forced on if your goal is Grim Journey. """ display_name: str = "Deathsanity" @@ -126,9 +150,9 @@ class Deathsanity(Toggle): class Landmarksanity(DefaultOnToggle): """ - If true, adds 20 landmark locations to the world + If true, adds 20 landmark locations to the world. - This option will be forced on if your goal is Zork Tour + This option will be forced on if your goal is Zork Tour. """ display_name: str = "Landmarksanity" @@ -137,11 +161,11 @@ class Landmarksanity(DefaultOnToggle): class GrantMissableLocationChecks(Toggle): """ If true, performing an irreversible action will grant the locations checks that would have become unobtainable as a - result of that action when you meet the item requirements + result of that action when you meet the item requirements. Otherwise, the player is expected to potentially have to use the save system to reach those location checks. If you don't like the idea of rarely having to reload an earlier save to get a location check, make sure this option is - enabled + enabled. """ display_name: str = "Grant Missable Checks" @@ -149,7 +173,7 @@ class GrantMissableLocationChecks(Toggle): class ClientSeedInformation(Choice): """ - Determines what information about the seed the client will reveal after using the /zork command + Determines what information about the seed the client will reveal after using the /zork command. Reveal Nothing: No information about the seed is displayed Reveal Goal: Only the goal of the seed is displayed @@ -173,6 +197,8 @@ class ZorkGrandInquisitorOptions(PerGameCommonOptions): starting_location: StartingLocation hotspots: Hotspots craftable_spells: CraftableSpells + wild_voxam: WildVoxam + wild_voxam_chance: WildVoxamChance deathsanity: Deathsanity landmarksanity: Landmarksanity grant_missable_location_checks: GrantMissableLocationChecks diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 096f93e31f9f..7d5c89685a36 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -380,6 +380,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "starting_location", "hotspots", "craftable_spells", + "wild_voxam", + "wild_voxam_chance", "deathsanity", "landmarksanity", "grant_missable_location_checks", From d189799ade77976bddabdb942ad458f4f006862a Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 22 Nov 2024 20:39:13 -0500 Subject: [PATCH 15/51] start inventory from pool --- worlds/zork_grand_inquisitor/options.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index e1db799a1836..464f159f9099 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -1,6 +1,13 @@ from dataclasses import dataclass -from Options import Choice, DefaultOnToggle, PerGameCommonOptions, Range, Toggle +from Options import ( + Choice, + DefaultOnToggle, + PerGameCommonOptions, + Range, + StartInventoryPool, + Toggle, +) class Goal(Choice): @@ -191,6 +198,7 @@ class ClientSeedInformation(Choice): @dataclass class ZorkGrandInquisitorOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool goal: Goal artifacts_of_magic_total: ArtifactsOfMagicTotal artifacts_of_magic_required: ArtifactsOfMagicRequired From caf0d45a83ab067a3b7ed148824529d206dbbed2 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 11:31:59 -0500 Subject: [PATCH 16/51] death link support --- worlds/zork_grand_inquisitor/client.py | 36 +++++++++++++++ .../data/mapping_data.py | 26 +++++++++++ .../zork_grand_inquisitor/game_controller.py | 44 +++++++++++++++++++ worlds/zork_grand_inquisitor/options.py | 3 +- worlds/zork_grand_inquisitor/world.py | 1 + 5 files changed, 109 insertions(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 9d5daee0b6fd..cffa4b2d6e8a 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -55,6 +55,13 @@ def _cmd_hotspots(self) -> None: """List received Hotspots.""" self.ctx.game_controller.list_received_hotspots() + def _cmd_deathlink(self) -> None: + """Toggle deathlink status.""" + if not self.ctx.game_controller.option_death_link: + return + + self.ctx.death_link_status = not self.ctx.death_link_status + class ZorkGrandInquisitorContext(CommonClient.CommonContext): tags: Set[str] = {"AP"} @@ -70,6 +77,7 @@ class ZorkGrandInquisitorContext(CommonClient.CommonContext): id_to_locations: Dict[int, ZorkGrandInquisitorLocations] = id_to_locations() game_controller: GameController + death_link_status: bool = False controller_task: Optional[asyncio.Task] @@ -149,6 +157,11 @@ def on_package(self, cmd: str, _args: Any) -> None: id_to_client_seed_information()[_args["slot_data"]["client_seed_information"]] ) + is_death_link = _args["slot_data"]["death_link"] == 1 + + self.game_controller.option_death_link = is_death_link # Represents the option; will never change + self.death_link_status = is_death_link # Represents the toggleable status + # Starter Kit self.game_controller.starter_kit = _args["slot_data"]["starter_kit"] @@ -157,6 +170,10 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["initial_totemizer_destination"] ] + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.last_death_link = max(data["time"], self.last_death_link) + self.game_controller.pending_death_link = (True, data.get("source"), data.get("cause")) + async def controller(self): while not self.exit_event.is_set(): await asyncio.sleep(0.1) @@ -226,6 +243,25 @@ async def controller(self): } ]) + # Handle Death Link + await self.update_death_link(self.death_link_status) + + if self.game_controller.outgoing_death_link[0]: + if self.death_link_status: + death_cause: Optional[str] = self.game_controller.outgoing_death_link[1] + + if death_cause: + death_cause = death_cause.replace( + "PLAYER", + self.player_names[self.slot] + ) + else: + death_cause = "" + + await self.send_death(death_cause) + + self.game_controller.outgoing_death_link = (False, None) + def main() -> None: Utils.init_logging("ZorkGrandInquisitorClient", exception_logger="Client") diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 43d8b9d8ef07..4219d291fdc4 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -12,6 +12,32 @@ ZorkGrandInquisitorStartingLocations, ) + +death_cause_labels: Dict[int, str] = { + 1: "PLAYER got their noggin smitten in twain", + 3: "PLAYER decided to jump into a bottomless pit", + 4: "PLAYER decided to step into the infinite", + 5: "PLAYER became an evil spawn's plaything", + 6: "PLAYER started a career as a talking manhole cover", + 7: "PLAYER got sucked into a starship's tractor beam", + 8: "PLAYER became a paperweight", + 9: "PLAYER rolled into the airless expanse of the cosmos", + 10: "PLAYER got their head bitten off", + 11: "PLAYER was swallowed whole by a dragon", + 13: "PLAYER decided to spend an eternity staring at scenic vistas", + 18: "PLAYER was eaten by a grue", + 19: "PLAYER was vaporized by Zork Rocks", + 20: "PLAYER got stung by a thousand quelbees", + 21: "PLAYER broke curfew", + 22: "PLAYER lost their soul to a scratch-and-win card", + 29: "PLAYER was outsmarted by bees", + 30: "PLAYER got pureed by a six-armed invisible guard", + 32: "PLAYER's head exploded", + 33: "PLAYER died of arteriosclerosis", + 34: "PLAYER decided to ignore the sign and THROCK the grass", + 37: "PLAYER lost a game of strip grue, fire, water", +} + # Avoid spells in early items to prevent clash with craftable spells early_items_for_starting_location: Dict[ ZorkGrandInquisitorStartingLocations, Optional[Tuple[Tuple[ZorkGrandInquisitorItems, ...], ...]] diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index bd07bc7d4d67..80ea925a2db6 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -11,6 +11,7 @@ from .data.location_data import location_data, ZorkGrandInquisitorLocationData from .data.mapping_data import ( + death_cause_labels, hotspots_for_regional_hotspot, labels_for_enum_items, voxam_cast_game_locations, @@ -75,10 +76,15 @@ class GameController: option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] option_grant_missable_location_checks: Optional[bool] option_client_seed_information: Optional[ZorkGrandInquisitorClientSeedInformation] + option_death_link: Optional[bool] starter_kit: Optional[List[str]] initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] + pending_death_link: Tuple[bool, Optional[str], Optional[str]] + outgoing_death_link: Tuple[bool, Optional[str]] + pause_death_monitoring: bool + def __init__(self, logger=None) -> None: self.logger = logger @@ -129,10 +135,15 @@ def __init__(self, logger=None) -> None: self.option_landmarksanity = None self.option_grant_missable_location_checks = None self.option_client_seed_information = None + self.option_death_link = None self.starter_kit = None self.initial_totemizer_destination = None + self.pending_death_link = (False, None, None) + self.outgoing_death_link = (False, None) + self.pause_death_monitoring = False + @functools.cached_property def brog_items(self) -> Set[ZorkGrandInquisitorItems]: return { @@ -359,6 +370,9 @@ def update(self) -> None: self._apply_conditional_teleports() + if self.option_death_link: + self._handle_death_link() + self._check_for_victory() except Exception as e: self.log_debug(e) @@ -1227,6 +1241,36 @@ def _cast_voxam(self) -> None: else: self._apply_starting_location(force=True) + def _handle_death_link(self) -> None: + # Pause Monitoring Flag + if self.pause_death_monitoring and not self._player_is_at("gjde"): + self.pause_death_monitoring = False + + # Incoming Death Link + if not self._player_is_at("gjde") and self.pending_death_link[0]: + self._write_game_state_value_for(2201, 35) + self.game_state_manager.set_game_location("gjde", 0) + + if self.pending_death_link[2]: + self.log(f"Death Link: {self.pending_death_link[2]}") + else: + self.log(f"Death Link: Triggered by {self.pending_death_link[1]}") + + self.pending_death_link = (False, None, None) + + # Outgoing Death Link + if not self.pause_death_monitoring: + death_cause_id: int = self._read_game_state_value_for(2201) + + if self._player_is_at("gjde") and death_cause_id != 35: + death_cause: str = death_cause_labels.get( + death_cause_id, + "PLAYER died of unknown causes" + ) + + self.outgoing_death_link = (True, death_cause) + self.pause_death_monitoring = True + def _check_for_victory(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.THREE_ARTIFACTS: coconut_is_placed = self._read_game_state_value_for(2200) == 1 diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index 464f159f9099..b06e90ba09e1 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -2,6 +2,7 @@ from Options import ( Choice, + DeathLinkMixin, DefaultOnToggle, PerGameCommonOptions, Range, @@ -197,7 +198,7 @@ class ClientSeedInformation(Choice): @dataclass -class ZorkGrandInquisitorOptions(PerGameCommonOptions): +class ZorkGrandInquisitorOptions(PerGameCommonOptions, DeathLinkMixin): start_inventory_from_pool: StartInventoryPool goal: Goal artifacts_of_magic_total: ArtifactsOfMagicTotal diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 7d5c89685a36..32bf1479d671 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -386,6 +386,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "landmarksanity", "grant_missable_location_checks", "client_seed_information", + "death_link", ) slot_data["starter_kit"] = sorted([item.value for item in self.starter_kit]) From 06e2062ceec5bfdc9de17796a0fd23799bf40bf7 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 11:32:55 -0500 Subject: [PATCH 17/51] output death link option in seed information --- worlds/zork_grand_inquisitor/game_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 80ea925a2db6..ff4403be6d0e 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -234,6 +234,11 @@ def output_seed_information(self) -> None: else: self.log(f" Grant Missable Location Checks: Off") + if self.option_death_link: + self.log(f" Death Link: On") + else: + self.log(f" Death Link: Off") + def output_starter_kit(self) -> None: if self.starter_kit is None: return From 4735276b3d70db755214cac124361ec579056e99 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 14:12:51 -0500 Subject: [PATCH 18/51] wait until the student id card is back in inventory before filtering it --- worlds/zork_grand_inquisitor/game_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index ff4403be6d0e..87f1482f5921 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1505,7 +1505,8 @@ def _filter_received_inventory_items( to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.STUDENT_ID: if self._read_game_state_value_for(11838) == 1: - to_filter_inventory_items.add(item) + if self._read_game_state_value_for(9) != 39: + to_filter_inventory_items.add(item) elif item == ZorkGrandInquisitorItems.SUBWAY_TOKEN: if self._read_game_state_value_for(13167) == 1: to_filter_inventory_items.add(item) From 38ad9a6da5f2f85cb6ccddc4a7e4af0c40edaba3 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 15:32:48 -0500 Subject: [PATCH 19/51] make it so the well rope cannot be picked up --- worlds/zork_grand_inquisitor/game_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 87f1482f5921..542df3288a46 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -584,6 +584,7 @@ def _apply_permanent_game_flags(self) -> None: self._write_game_flags_value_for(4875, 2) # Cocoa Ingredient - Mug self._write_game_flags_value_for(4873, 2) # Cocoa Ingredient - Quelbee Honeycomb self._write_game_flags_value_for(10809, 2) # Back of Jack's Shop + self._write_game_flags_value_for(10314, 2) # Well Rope def _check_for_completed_locations(self) -> None: location: ZorkGrandInquisitorLocations From cdbce91441b2c4e1133b8a39391717dbaff92bea Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 15:42:21 -0500 Subject: [PATCH 20/51] allow i'm not impressed to be triggered shore-side, while keeping the logic expectation of being across the river --- worlds/zork_grand_inquisitor/data/location_data.py | 3 ++- worlds/zork_grand_inquisitor/game_controller.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/worlds/zork_grand_inquisitor/data/location_data.py b/worlds/zork_grand_inquisitor/data/location_data.py index 1dda0568d35a..004fe8eda24e 100644 --- a/worlds/zork_grand_inquisitor/data/location_data.py +++ b/worlds/zork_grand_inquisitor/data/location_data.py @@ -17,6 +17,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): Tuple[int, int], Tuple[int, Tuple[int, ...]], Tuple[Tuple[int, ...], int], + Tuple[str, Tuple[str, ...]], ], ..., ] @@ -669,7 +670,7 @@ class ZorkGrandInquisitorLocationData(NamedTuple): ), ), ZorkGrandInquisitorLocations.I_AM_NOT_IMPRESSED: ZorkGrandInquisitorLocationData( - game_state_trigger=(("location", "hp4f"), (8419, 1)), + game_state_trigger=(("location", ("hp4f", "hp1g")), (8419, 1)), archipelago_id=LOCATION_OFFSET + 63, region=ZorkGrandInquisitorRegions.HADES, tags=(ZorkGrandInquisitorTags.CORE,), diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 542df3288a46..21c729a75cf7 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -598,12 +598,17 @@ def _check_for_completed_locations(self) -> None: is_location_completed: bool = True trigger: Union[str, int, Tuple[int, ...]] - value: Union[str, int, Tuple[int, ...]] + value: Union[str, int, Tuple[int, ...], Tuple[str, ...]] for trigger, value in data.game_state_trigger: if trigger == "location": - if not self._player_is_at(value): - is_location_completed = False - break + if isinstance(value, str): + if not self._player_is_at(value): + is_location_completed = False + break + elif isinstance(value, tuple): + if not any(self._player_is_at(key) for key in value): + is_location_completed = False + break elif isinstance(trigger, int): if isinstance(value, int): if self._read_game_state_value_for(trigger) != value: From b769fcc2d800cec940d2a640780f00ce212e395c Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 16:00:54 -0500 Subject: [PATCH 21/51] fix cards being visible on first render of the card game --- worlds/zork_grand_inquisitor/game_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 21c729a75cf7..d9d11f883529 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -470,6 +470,7 @@ def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(1341, 1) # Griff's Inflatable Raft Taken self._write_game_state_value_for(1477, 1) # Griff's Air Pump Taken self._write_game_state_value_for(1814, 1) # Griff's Dragon Tooth Taken + self._write_game_state_value_for(15424, 1) # Initial State of Card Game self._write_game_state_value_for(15403, 0) # Lucy's Cards Taken self._write_game_state_value_for(15404, 1) # Lucy's Cards Taken self._write_game_state_value_for(15405, 4) # Lucy's Cards Taken From 092f6348bc8cee8ec4d3bae7f22004a7b2404477 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 23 Nov 2024 17:34:55 -0500 Subject: [PATCH 22/51] add save IDs; better robustness around AP connection states and game saves --- worlds/zork_grand_inquisitor/client.py | 19 +++++ .../zork_grand_inquisitor/game_controller.py | 78 +++++++++++++++---- .../game_state_manager.py | 7 -- worlds/zork_grand_inquisitor/world.py | 6 ++ 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index cffa4b2d6e8a..83f0f86e038a 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -28,6 +28,10 @@ class ZorkGrandInquisitorCommandProcessor(CommonClient.ClientCommandProcessor): def _cmd_zork(self) -> None: """Attach to an open Zork Grand Inquisitor process.""" + if not self.ctx.server or not self.ctx.slot: + self.output("You must be connected to an Archipelago server before using /zork.") + return + result: bool = self.ctx.game_controller.open_process_handle() if result: @@ -111,6 +115,18 @@ async def server_auth(self, password_requested: bool = False): await self.get_username() await self.send_connect() + async def disconnect(self, allow_autoreconnect: bool = False): + try: + # Close process handle if possible, ensuring that the player will have to /zork again upon reconnect + self.game_controller.close_process_handle() + + self.game_controller.valid_save_message_shown = False + self.game_controller.invalid_save_message_shown = False + except Exception: + pass + + await super().disconnect(allow_autoreconnect) + def on_package(self, cmd: str, _args: Any) -> None: if cmd == "Connected": self.game = self.slot_info[self.slot].game @@ -170,6 +186,9 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["initial_totemizer_destination"] ] + # Save IDs + self.game_controller.save_ids = tuple(_args["slot_data"]["save_ids"]) + def on_deathlink(self, data: Dict[str, Any]) -> None: self.last_death_link = max(data["time"], self.last_death_link) self.game_controller.pending_death_link = (True, data.get("source"), data.get("cause")) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index d9d11f883529..a413fcdba4ca 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -85,6 +85,11 @@ class GameController: outgoing_death_link: Tuple[bool, Optional[str]] pause_death_monitoring: bool + save_ids: Optional[Tuple[int, int, int]] + + valid_save_message_shown: bool + invalid_save_message_shown: bool + def __init__(self, logger=None) -> None: self.logger = logger @@ -144,6 +149,11 @@ def __init__(self, logger=None) -> None: self.outgoing_death_link = (False, None) self.pause_death_monitoring = False + self.save_ids = None + + self.valid_save_message_shown = False + self.invalid_save_message_shown = False + @functools.cached_property def brog_items(self) -> Set[ZorkGrandInquisitorItems]: return { @@ -355,6 +365,9 @@ def update(self) -> None: try: self.game_state_manager.refresh_game_location() + if not self._check_for_valid_save(): + return + self._apply_initial_totemizer_destination() self._apply_starting_location() @@ -383,6 +396,55 @@ def update(self) -> None: self.log_debug(e) traceback.print_exc() + def _check_for_valid_save(self) -> bool: + if self._player_is_at("gary"): + return False + + save_ids: Tuple[int, int, int] = ( + self._read_game_state_value_for(19997), + self._read_game_state_value_for(19998), + self._read_game_state_value_for(19999), + ) + + if save_ids == (0, 0, 0): + self._write_game_state_value_for(19997, self.save_ids[0]) + self._write_game_state_value_for(19998, self.save_ids[1]) + self._write_game_state_value_for(19999, self.save_ids[2]) + elif save_ids != self.save_ids: + if not self.invalid_save_message_shown: + self.log( + "Unexpected save file for this seed. Please load a valid save file or start a new game." + ) + + self.invalid_save_message_shown = True + self.valid_save_message_shown = False + + return False + + if not self.valid_save_message_shown: + self.log("Valid save file detected. Have fun!") + + self.valid_save_message_shown = True + self.invalid_save_message_shown = False + + return True + + def _apply_initial_totemizer_destination(self) -> None: + if self.initial_totemizer_destination is None: + return None + + if self._read_game_state_value_for(19986) == 0: + mapping: Dict[ZorkGrandInquisitorItems, int] = { + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION: 0, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: 1, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY: 2, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: 3, + ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: 4, + } + + self._write_game_state_value_for(9617, mapping[self.initial_totemizer_destination]) + self._write_game_state_value_for(19986, 1) + def _apply_starting_location(self, force: bool = False) -> None: if self.option_starting_location is None: return None @@ -412,22 +474,6 @@ def _apply_starting_location(self, force: bool = False) -> None: self._write_game_state_value_for(19985, 1) time.sleep(0.1) - def _apply_initial_totemizer_destination(self) -> None: - if self.initial_totemizer_destination is None: - return None - - if self._read_game_state_value_for(19986) == 0: - mapping: Dict[ZorkGrandInquisitorItems, int] = { - ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_HALL_OF_INQUISITION: 0, - ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_SURFACE_OF_MERZ: 1, - ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY: 2, - ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY: 3, - ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL: 4, - } - - self._write_game_state_value_for(9617, mapping[self.initial_totemizer_destination]) - self._write_game_state_value_for(19986, 1) - def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(10934, 1) # Rope Taken self._write_game_state_value_for(10418, 1) # Mead Light Taken diff --git a/worlds/zork_grand_inquisitor/game_state_manager.py b/worlds/zork_grand_inquisitor/game_state_manager.py index f38d1097e5f1..25b35969bf5e 100644 --- a/worlds/zork_grand_inquisitor/game_state_manager.py +++ b/worlds/zork_grand_inquisitor/game_state_manager.py @@ -93,13 +93,6 @@ def open_process_handle(self) -> bool: self.script_manager_struct_address = self._resolve_address(0x5276600, (0xC8, 0x0)) self.render_manager_struct_address = self._resolve_address(0x5276600, (0xD0, 0x120)) - # 0xD8 Cursor Manager - # 0xE0 String Manager - # 0xE8 Search Manager - # 0xF0 Text Renderer - # 0xF8 Midi Manager - # 0x100 Save Manager - # 0x108 Menu Handler except Exception: return False diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 32bf1479d671..e0df48601d60 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -392,6 +392,12 @@ def fill_slot_data(self) -> Dict[str, Any]: slot_data["starter_kit"] = sorted([item.value for item in self.starter_kit]) slot_data["initial_totemizer_destination"] = self.initial_totemizer_destination.value + slot_data["save_ids"] = ( + self.random.randint(1, 65365), + self.random.randint(1, 65365), + self.random.randint(1, 65365), + ) + return slot_data def get_filler_item_name(self) -> str: From f915d4e646bcad2ab73d20c2dee5ca985c443063 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sun, 24 Nov 2024 10:59:07 -0500 Subject: [PATCH 23/51] allow client to stay open and switching server and/or slot without weird side effects --- worlds/zork_grand_inquisitor/client.py | 83 ++++++++++--------- .../zork_grand_inquisitor/game_controller.py | 38 +++++++++ 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 83f0f86e038a..203ad7a65c57 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -117,14 +117,17 @@ async def server_auth(self, password_requested: bool = False): async def disconnect(self, allow_autoreconnect: bool = False): try: - # Close process handle if possible, ensuring that the player will have to /zork again upon reconnect self.game_controller.close_process_handle() - - self.game_controller.valid_save_message_shown = False - self.game_controller.invalid_save_message_shown = False except Exception: pass + self.game_controller.reset() + + self.items_received = [] + self.locations_info = {} + + self.can_display_process_message = True + await super().disconnect(allow_autoreconnect) def on_package(self, cmd: str, _args: Any) -> None: @@ -237,49 +240,51 @@ async def controller(self): CommonClient.logger.info(process_message) self.can_display_process_message = False - # Send Checked Locations - checked_location_ids: List[int] = list() + # Network Operations + if self.server and self.slot: + # Send Checked Locations + checked_location_ids: List[int] = list() - while len(self.game_controller.completed_locations_queue) > 0: - location: ZorkGrandInquisitorLocations = self.game_controller.completed_locations_queue.popleft() - location_id: int = self.location_name_to_id[location.value] + while len(self.game_controller.completed_locations_queue) > 0: + location: ZorkGrandInquisitorLocations = self.game_controller.completed_locations_queue.popleft() + location_id: int = self.location_name_to_id[location.value] - checked_location_ids.append(location_id) + checked_location_ids.append(location_id) - await self.send_msgs([ - { - "cmd": "LocationChecks", - "locations": checked_location_ids - } - ]) - - # Check for Goal Completion - if self.game_controller.goal_completed: await self.send_msgs([ { - "cmd": "StatusUpdate", - "status": CommonClient.ClientStatus.CLIENT_GOAL + "cmd": "LocationChecks", + "locations": checked_location_ids } ]) - # Handle Death Link - await self.update_death_link(self.death_link_status) - - if self.game_controller.outgoing_death_link[0]: - if self.death_link_status: - death_cause: Optional[str] = self.game_controller.outgoing_death_link[1] - - if death_cause: - death_cause = death_cause.replace( - "PLAYER", - self.player_names[self.slot] - ) - else: - death_cause = "" - - await self.send_death(death_cause) - - self.game_controller.outgoing_death_link = (False, None) + # Check for Goal Completion + if self.game_controller.goal_completed: + await self.send_msgs([ + { + "cmd": "StatusUpdate", + "status": CommonClient.ClientStatus.CLIENT_GOAL + } + ]) + + # Handle Death Link + await self.update_death_link(self.death_link_status) + + if self.game_controller.outgoing_death_link[0]: + if self.death_link_status: + death_cause: Optional[str] = self.game_controller.outgoing_death_link[1] + + if death_cause: + death_cause = death_cause.replace( + "PLAYER", + self.player_names[self.slot] + ) + else: + death_cause = "" + + await self.send_death(death_cause) + + self.game_controller.outgoing_death_link = (False, None) def main() -> None: diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index a413fcdba4ca..556eda14dbb7 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -396,6 +396,44 @@ def update(self) -> None: self.log_debug(e) traceback.print_exc() + def reset(self) -> None: + self.received_items = set() + self.completed_locations = set() + + self.completed_locations_queue = collections.deque() + self.received_items_queue = collections.deque() + + self.available_inventory_slots = set() + + self.goal_item_count = 0 + self.goal_completed = False + + self.option_goal = None + self.option_artifacts_of_magic_required = None + self.option_artifacts_of_magic_total = None + self.option_starting_location = None + self.option_hotspots = None + self.option_craftable_spells = None + self.option_wild_voxam = None + self.option_wild_voxam_chance = None + self.option_deathsanity = None + self.option_landmarksanity = None + self.option_grant_missable_location_checks = None + self.option_client_seed_information = None + self.option_death_link = None + + self.starter_kit = None + self.initial_totemizer_destination = None + + self.pending_death_link = (False, None, None) + self.outgoing_death_link = (False, None) + self.pause_death_monitoring = False + + self.save_ids = None + + self.valid_save_message_shown = False + self.invalid_save_message_shown = False + def _check_for_valid_save(self) -> bool: if self._player_is_at("gary"): return False From d1a03ea7f1b8006962ae89c2de9e1479deba4479 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sun, 24 Nov 2024 12:29:32 -0500 Subject: [PATCH 24/51] allow spell quickbar to be used on the surface (for VOXAM) + fix way for VOXAM to eventually stop working --- .../zork_grand_inquisitor/game_controller.py | 28 +++++++++++++++++++ worlds/zork_grand_inquisitor/options.py | 9 ++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 556eda14dbb7..227fd247599a 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -568,6 +568,7 @@ def _apply_permanent_game_state(self) -> None: self._write_game_state_value_for(8620, 1) # First Coin Paid to Charon self._write_game_state_value_for(8731, 1) # First Coin Paid to Charon self._write_game_state_value_for(191, 1) # VOXAM Learned + self._write_game_state_value_for(19243, 0) # Keep VOXAM Miscast Counter at 0 self._write_game_state_value_for(15384, 0) # Never Consider All Artifacts to be Placed def _apply_conditional_game_state(self): @@ -670,6 +671,33 @@ def _apply_permanent_game_flags(self) -> None: self._write_game_flags_value_for(4873, 2) # Cocoa Ingredient - Quelbee Honeycomb self._write_game_flags_value_for(10809, 2) # Back of Jack's Shop self._write_game_flags_value_for(10314, 2) # Well Rope + self._write_game_flags_value_for(10848, 2) # Keep Spellbar Enabled (ps10) + self._write_game_flags_value_for(10862, 2) # Keep Spellbar Enabled (ps1e) + self._write_game_flags_value_for(10868, 2) # Keep Spellbar Enabled (ps20) + self._write_game_flags_value_for(10302, 2) # Keep Spellbar Enabled (pc10) + self._write_game_flags_value_for(10311, 2) # Keep Spellbar Enabled (pc1e) + self._write_game_flags_value_for(10918, 2) # Keep Spellbar Enabled (px10) + self._write_game_flags_value_for(10967, 2) # Keep Spellbar Enabled (px1h) + self._write_game_flags_value_for(10984, 2) # Keep Spellbar Enabled (px1j) + self._write_game_flags_value_for(10993, 2) # Keep Spellbar Enabled (px1k) + self._write_game_flags_value_for(10414, 2) # Keep Spellbar Enabled (pe10) + self._write_game_flags_value_for(10492, 2) # Keep Spellbar Enabled (pe20) + self._write_game_flags_value_for(10516, 2) # Keep Spellbar Enabled (pe2e) + self._write_game_flags_value_for(10589, 2) # Keep Spellbar Enabled (pe30) + self._write_game_flags_value_for(10639, 2) # Keep Spellbar Enabled (pe3k) + self._write_game_flags_value_for(10659, 2) # Keep Spellbar Enabled (pe40) + self._write_game_flags_value_for(10677, 2) # Keep Spellbar Enabled (pe4g) + self._write_game_flags_value_for(10697, 2) # Keep Spellbar Enabled (pe50) + self._write_game_flags_value_for(10773, 2) # Keep Spellbar Enabled (pe5h) + self._write_game_flags_value_for(10756, 2) # Keep Spellbar Enabled (pe5f) + self._write_game_flags_value_for(10786, 2) # Keep Spellbar Enabled (pe6e) + self._write_game_flags_value_for(10722, 2) # Keep Spellbar Enabled (pe5e) + self._write_game_flags_value_for(19603, 2) # Keep Spellbar Enabled (pe5n) + self._write_game_flags_value_for(10620, 2) # Keep Spellbar Enabled (pe3j) + self._write_game_flags_value_for(10439, 2) # Keep Spellbar Enabled (pe1e) + self._write_game_flags_value_for(10805, 2) # Keep Spellbar Enabled (pp10) + self._write_game_flags_value_for(10805, 2) # Keep Spellbar Enabled (pp10) + self._write_game_flags_value_for(10838, 2) # Keep Spellbar Enabled (pp1j) def _check_for_completed_locations(self) -> None: location: ZorkGrandInquisitorLocations diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index b06e90ba09e1..3dcb796e05e7 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -64,9 +64,12 @@ class ArtifactsOfMagicRequired(Range): class StartingLocation(Choice): """ - Determines the in-game location the player will start at. The player always starts with VOXAM, which can be used to - teleport back to the starting location at any time. Depending on the starting location, the player may also be given - a starter kit of items to help them get going. + Determines the in-game location the player will start at. + + The player always starts with VOXAM, which can be used to teleport back to the starting location at any time. + Depending on the starting location, the player may also be given a starter kit of items to help them get going. + + Note: VOXAM will only work from the spell quickbar. It will have no effect from the spellbook. """ display_name: str = "Starting Location" From 31fa87b5ee890260a918dae5fd6e2bd257f64339 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sun, 24 Nov 2024 13:16:53 -0500 Subject: [PATCH 25/51] can no longer cache is_deathsanity property if we allow switching slots with the same client --- worlds/zork_grand_inquisitor/game_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 227fd247599a..4719f0c721e7 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -189,7 +189,7 @@ def totem_items(self) -> Set[ZorkGrandInquisitorItems]: def missable_locations(self) -> Set[ZorkGrandInquisitorLocations]: return locations_with_tag(ZorkGrandInquisitorTags.MISSABLE) - @functools.cached_property + @property def is_deathsanity(self) -> bool: return self.option_deathsanity == ZorkGrandInquisitorDeathsanity.ON From c33652950a316dad12906310669c3ee48c3d7826 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Mon, 25 Nov 2024 17:26:09 -0500 Subject: [PATCH 26/51] make the amount of needed landmarks and deaths configurable for zork tour and grim journey --- worlds/zork_grand_inquisitor/client.py | 8 +++++ .../data/entrance_rule_data.py | 4 +-- worlds/zork_grand_inquisitor/data_funcs.py | 18 ++++++++++- .../zork_grand_inquisitor/game_controller.py | 30 +++++++++++------ worlds/zork_grand_inquisitor/options.py | 32 +++++++++++++++++++ worlds/zork_grand_inquisitor/world.py | 11 +++++++ 6 files changed, 90 insertions(+), 13 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 203ad7a65c57..49ccf6cd5b85 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -145,6 +145,14 @@ def on_package(self, cmd: str, _args: Any) -> None: _args["slot_data"]["artifacts_of_magic_total"] ) + self.game_controller.option_landmarks_required = ( + _args["slot_data"]["landmarks_required"] + ) + + self.game_controller.option_deaths_required = ( + _args["slot_data"]["deaths_required"] + ) + self.game_controller.option_starting_location = ( id_to_starting_locations()[_args["slot_data"]["starting_location"]] ) diff --git a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py index 7ebbaeec6b30..775adcab3d2f 100644 --- a/worlds/zork_grand_inquisitor/data/entrance_rule_data.py +++ b/worlds/zork_grand_inquisitor/data/entrance_rule_data.py @@ -546,14 +546,14 @@ ZorkGrandInquisitorGoals.ZORK_TOUR: { (ZorkGrandInquisitorRegions.PORT_FOOZLE, ZorkGrandInquisitorRegions.ENDGAME): ( ( - [ZorkGrandInquisitorItems.LANDMARK, 20], + [ZorkGrandInquisitorItems.LANDMARK, 999], # Will get replaced with the actual number ), ), }, ZorkGrandInquisitorGoals.GRIM_JOURNEY: { (ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, ZorkGrandInquisitorRegions.ENDGAME): ( ( - [ZorkGrandInquisitorItems.DEATH, 22], + [ZorkGrandInquisitorItems.DEATH, 999], # Will get replaced with the actual number ), ), }, diff --git a/worlds/zork_grand_inquisitor/data_funcs.py b/worlds/zork_grand_inquisitor/data_funcs.py index 56f50828e815..8fc95a72a93a 100644 --- a/worlds/zork_grand_inquisitor/data_funcs.py +++ b/worlds/zork_grand_inquisitor/data_funcs.py @@ -404,6 +404,8 @@ def goal_access_rule_for( goal: ZorkGrandInquisitorGoals, player: int, artifacts_of_magic_required: int, + landmarks_required: int, + deaths_required: int, ) -> str: dataset: Dict[ Tuple[ @@ -427,7 +429,7 @@ def goal_access_rule_for( ], ] = endgame_entrance_data_by_goal[goal] - # Replace placeholder with actual number of artifacts of magic required + # Replace placeholder with actual number of goal items required if goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: dataset[ ( @@ -435,6 +437,20 @@ def goal_access_rule_for( ZorkGrandInquisitorRegions.ENDGAME ) ][0][0][1] = artifacts_of_magic_required + elif goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + dataset[ + ( + ZorkGrandInquisitorRegions.PORT_FOOZLE, + ZorkGrandInquisitorRegions.ENDGAME + ) + ][0][0][1] = landmarks_required + elif goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: + dataset[ + ( + ZorkGrandInquisitorRegions.HADES_BEYOND_GATES, + ZorkGrandInquisitorRegions.ENDGAME + ) + ][0][0][1] = deaths_required return entrance_access_rule_for( region, diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 4719f0c721e7..4edec9f865a0 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -67,6 +67,8 @@ class GameController: option_goal: Optional[ZorkGrandInquisitorGoals] option_artifacts_of_magic_required: Optional[int] option_artifacts_of_magic_total: Optional[int] + option_landmarks_required: Optional[int] + option_deaths_required: Optional[int] option_starting_location: Optional[ZorkGrandInquisitorStartingLocations] option_hotspots: Optional[ZorkGrandInquisitorHotspots] option_craftable_spells: Optional[ZorkGrandInquisitorCraftableSpellBehaviors] @@ -131,6 +133,8 @@ def __init__(self, logger=None) -> None: self.option_goal = None self.option_artifacts_of_magic_required = None self.option_artifacts_of_magic_total = None + self.option_landmarks_required = None + self.option_deaths_required = None self.option_starting_location = None self.option_hotspots = None self.option_craftable_spells = None @@ -226,6 +230,10 @@ def output_seed_information(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: self.log(f" Artifacts of Magic Required: {self.option_artifacts_of_magic_required}") self.log(f" Artifacts of Magic Total: {self.option_artifacts_of_magic_total}") + elif self.option_goal == ZorkGrandInquisitorGoals.ZORK_TOUR: + self.log(f" Landmarks Required: {self.option_landmarks_required}") + elif self.option_goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: + self.log(f" Deaths Required: {self.option_deaths_required}") self.log(f" Starting Location: {labels_for_enum_items[self.option_starting_location]}") self.log(f" Hotspots: {labels_for_enum_items[self.option_hotspots]}") @@ -277,25 +285,25 @@ def output_goal_item_update(self) -> None: if self.option_goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: self.log( - f"Received Artifact of Magic {self.goal_item_count} of {self.option_artifacts_of_magic_required}" + f"Received {self.goal_item_count} of {self.option_artifacts_of_magic_required} required Artifacts of Magic" ) if self.goal_item_count >= self.option_artifacts_of_magic_required: - self.log("All needed Artifacts of Magic have been found! Get to the Walking Castle") + self.log("All needed Artifacts of Magic have been found! Get to the Walking Castle to win") elif self.option_goal == ZorkGrandInquisitorGoals.ZORK_TOUR: self.log( - f"Visited {self.goal_item_count} of 20 Landmarks" + f"Visited {self.goal_item_count} of {self.option_landmarks_required} required Landmarks" ) - if self.goal_item_count == 20: - self.log("All Landmarks have been visited! Get to the Port Foozle signpost") + if self.goal_item_count >= self.option_landmarks_required: + self.log("All needed Landmarks have been visited! Get to the Port Foozle signpost to win") elif self.option_goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: self.log( - f"Experienced {self.goal_item_count} of 22 Deaths" + f"Experienced {self.goal_item_count} of {self.option_deaths_required} required Deaths" ) - if self.goal_item_count == 22: - self.log("All Deaths have been experienced! Go beyond the gates of hell") + if self.goal_item_count >= self.option_deaths_required: + self.log("All needed Deaths have been experienced! Go beyond the gates of hell to win") def list_received_brog_items(self) -> None: self.log("Received Brog Items:") @@ -411,6 +419,8 @@ def reset(self) -> None: self.option_goal = None self.option_artifacts_of_magic_required = None self.option_artifacts_of_magic_total = None + self.option_landmarks_required = None + self.option_deaths_required = None self.option_starting_location = None self.option_hotspots = None self.option_craftable_spells = None @@ -1411,11 +1421,11 @@ def _check_for_victory(self) -> None: if self._player_is_at("ps1e"): self.goal_completed = True elif self.option_goal == ZorkGrandInquisitorGoals.ZORK_TOUR: - if self.goal_item_count == 20: + if self.goal_item_count >= self.option_landmarks_required: if self._player_is_at("ps1e"): self.goal_completed = True elif self.option_goal == ZorkGrandInquisitorGoals.GRIM_JOURNEY: - if self.goal_item_count == 22: + if self.goal_item_count >= self.option_deaths_required: if self._player_is_at("hp60"): self.goal_completed = True diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index 3dcb796e05e7..493c780401a3 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -62,6 +62,36 @@ class ArtifactsOfMagicRequired(Range): default = 10 +class LandmarksRequired(Range): + """ + Determines how many Landmarks are required to win. + + Only relevant if the selected goal is Zork Tour. + """ + + display_name = "Landmarks Required" + + range_start = 10 + range_end = 20 + + default = 20 + + +class DeathsRequired(Range): + """ + Determines how many Deaths are required to win. + + Only relevant if the selected goal is Grim Journey. + """ + + display_name = "Deaths Required" + + range_start = 10 + range_end = 22 + + default = 22 + + class StartingLocation(Choice): """ Determines the in-game location the player will start at. @@ -206,6 +236,8 @@ class ZorkGrandInquisitorOptions(PerGameCommonOptions, DeathLinkMixin): goal: Goal artifacts_of_magic_total: ArtifactsOfMagicTotal artifacts_of_magic_required: ArtifactsOfMagicRequired + landmarks_required: LandmarksRequired + deaths_required: DeathsRequired starting_location: StartingLocation hotspots: Hotspots craftable_spells: CraftableSpells diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index e0df48601d60..176b3dc45e50 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -105,6 +105,7 @@ class ZorkGrandInquisitorWorld(World): artifacts_of_magic_required: int artifacts_of_magic_total: int craftable_spells: ZorkGrandInquisitorCraftableSpellBehaviors + deaths_required: int deathsanity: ZorkGrandInquisitorDeathsanity early_items: Tuple[ZorkGrandInquisitorItems, ...] filler_item_names: List[str] = item_groups()["Filler"] @@ -114,6 +115,7 @@ class ZorkGrandInquisitorWorld(World): initial_totemizer_destination: ZorkGrandInquisitorItems item_data: Dict[ZorkGrandInquisitorItems, ZorkGrandInquisitorItemData] item_name_to_item: Dict[str, ZorkGrandInquisitorItems] = item_names_to_item() + landmarks_required: int landmarksanity: ZorkGrandInquisitorLandmarksanity location_data: Dict[ @@ -133,6 +135,9 @@ def generate_early(self) -> None: if self.artifacts_of_magic_required > self.artifacts_of_magic_total: self.artifacts_of_magic_total = self.artifacts_of_magic_required + self.landmarks_required = self.options.landmarks_required.value + self.deaths_required = self.options.deaths_required.value + self.starting_location = id_to_starting_locations()[self.options.starting_location.value] self.starter_kit = tuple() @@ -252,6 +257,8 @@ def create_regions(self) -> None: self.goal, self.player, self.artifacts_of_magic_required, + self.landmarks_required, + self.deaths_required, ) region.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) @@ -271,6 +278,8 @@ def create_regions(self) -> None: self.goal, self.player, self.artifacts_of_magic_required, + self.landmarks_required, + self.deaths_required, ) region_menu.connect(region_mapping[ZorkGrandInquisitorRegions.ENDGAME], rule=eval(goal_access_rule)) @@ -377,6 +386,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "goal", "artifacts_of_magic_required", "artifacts_of_magic_total", + "landmarks_required", + "deaths_required", "starting_location", "hotspots", "craftable_spells", From ddbbf973d60206d42b4acabb4052a34570524931 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 10:44:13 -0500 Subject: [PATCH 27/51] traps! (and option groups) --- worlds/zork_grand_inquisitor/client.py | 14 +- .../zork_grand_inquisitor/data/item_data.py | 25 ++++ .../data/mapping_data.py | 7 + worlds/zork_grand_inquisitor/enums.py | 5 + .../zork_grand_inquisitor/game_controller.py | 117 +++++++++++++++- .../game_state_manager.py | 24 ++++ worlds/zork_grand_inquisitor/options.py | 132 ++++++++++++++++++ worlds/zork_grand_inquisitor/world.py | 117 +++++++++++----- 8 files changed, 399 insertions(+), 42 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index 49ccf6cd5b85..a9fa7a380b11 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -176,6 +176,8 @@ def on_package(self, cmd: str, _args: Any) -> None: id_to_landmarksanity()[_args["slot_data"]["landmarksanity"]] ) + self.game_controller.option_trap_percentage = _args["slot_data"]["trap_percentage"] + self.game_controller.option_grant_missable_location_checks = ( _args["slot_data"]["grant_missable_location_checks"] == 1 ) @@ -211,6 +213,10 @@ async def controller(self): # Enqueue Received Item Delta goal_item_count: int = 0 + trap_item_counts: Dict[ZorkGrandInquisitorItems, int] = { + item: 0 for item in self.game_controller.all_trap_items + } + network_item: NetUtils.NetworkItem for network_item in self.items_received: item: ZorkGrandInquisitorItems = self.id_to_items[network_item.item] @@ -218,8 +224,10 @@ async def controller(self): if item in self.game_controller.all_goal_items: goal_item_count += 1 continue - - if item not in self.game_controller.received_items: + elif item in self.game_controller.all_trap_items: + trap_item_counts[item] += 1 + continue + elif item not in self.game_controller.received_items: if item not in self.game_controller.received_items_queue: self.game_controller.received_items_queue.append(item) @@ -227,6 +235,8 @@ async def controller(self): self.game_controller.goal_item_count = goal_item_count self.game_controller.output_goal_item_update() + self.game_controller.trap_counters = trap_item_counts + # Game Controller Update if self.game_controller.is_process_running(): self.game_controller.update() diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index a91e4d82aa70..e7503d561008 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -1397,4 +1397,29 @@ class ZorkGrandInquisitorItemData(NamedTuple): classification=ItemClassification.progression, tags=(ZorkGrandInquisitorTags.GOAL_GRIM_JOURNEY,), ), + # Trap Items + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 0, + classification=ItemClassification.trap | ItemClassification.useful, + tags=(ZorkGrandInquisitorTags.TRAP,), + ), + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 1, + classification=ItemClassification.trap, + tags=(ZorkGrandInquisitorTags.TRAP,), + ), + ZorkGrandInquisitorItems.TRAP_TELEPORT: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 2, + classification=ItemClassification.trap | ItemClassification.useful, + tags=(ZorkGrandInquisitorTags.TRAP,), + ), + ZorkGrandInquisitorItems.TRAP_ZVISION: ZorkGrandInquisitorItemData( + statemap_keys=None, + archipelago_id=ITEM_OFFSET + 900 + 3, + classification=ItemClassification.trap, + tags=(ZorkGrandInquisitorTags.TRAP,), + ), } diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 4219d291fdc4..9eae2a87579b 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -757,6 +757,13 @@ ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, } +traps_to_game_state_key: Dict[ZorkGrandInquisitorItems, int] = { + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 19990, + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 19991, + ZorkGrandInquisitorItems.TRAP_TELEPORT: 19992, + ZorkGrandInquisitorItems.TRAP_ZVISION: 19993, +} + voxam_cast_game_locations: Dict[ ZorkGrandInquisitorStartingLocations, Tuple[Tuple[str, int], ...] diff --git a/worlds/zork_grand_inquisitor/enums.py b/worlds/zork_grand_inquisitor/enums.py index 0e58f11bbf62..7b762e2768f6 100644 --- a/worlds/zork_grand_inquisitor/enums.py +++ b/worlds/zork_grand_inquisitor/enums.py @@ -259,6 +259,10 @@ class ZorkGrandInquisitorItems(enum.Enum): TOTEMIZER_DESTINATION_NEWARK_NEW_JERSEY = "Totemizer Destination: Newark, New Jersey" TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL = "Totemizer Destination: Straight to Hell" TOTEMIZER_DESTINATION_SURFACE_OF_MERZ = "Totemizer Destination: Surface of Merz" + TRAP_INFINITE_CORRIDOR = "Infinite Corridor Trap" + TRAP_REVERSE_CONTROLS = "Reverse Controls Trap" + TRAP_TELEPORT = "Teleport Trap" + TRAP_ZVISION = "ZVision Trap" WELL_ROPE = "Well Rope" ZIMDOR_SCROLL = "ZIMDOR Scroll" ZORK_ROCKS = "Zork Rocks" @@ -534,3 +538,4 @@ class ZorkGrandInquisitorTags(enum.Enum): TELEPORTER_DESTINATION = "Teleporter Destination" TOTEMIZER_DESTINATION = "Totemizer Destination" TOTEM = "Totem" + TRAP = "Trap" diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 4edec9f865a0..a435094f9196 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1,4 +1,5 @@ import collections +import datetime import functools import logging import random @@ -14,6 +15,7 @@ death_cause_labels, hotspots_for_regional_hotspot, labels_for_enum_items, + traps_to_game_state_key, voxam_cast_game_locations, ) @@ -54,6 +56,7 @@ class GameController: all_spell_items: Set[ZorkGrandInquisitorItems] all_hotspot_items: Set[ZorkGrandInquisitorItems] all_goal_items: Set[ZorkGrandInquisitorItems] + all_trap_items: Set[ZorkGrandInquisitorItems] game_id_to_items: Dict[int, ZorkGrandInquisitorItems] @@ -76,6 +79,7 @@ class GameController: option_wild_voxam_chance: Optional[int] option_deathsanity: Optional[ZorkGrandInquisitorDeathsanity] option_landmarksanity: Optional[ZorkGrandInquisitorLandmarksanity] + option_trap_percentage: Optional[int] option_grant_missable_location_checks: Optional[bool] option_client_seed_information: Optional[ZorkGrandInquisitorClientSeedInformation] option_death_link: Optional[bool] @@ -83,6 +87,11 @@ class GameController: starter_kit: Optional[List[str]] initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] + trap_counters: Dict[ZorkGrandInquisitorItems, int] + + active_trap: Optional[ZorkGrandInquisitorItems] + active_trap_until: Optional[datetime.datetime] + pending_death_link: Tuple[bool, Optional[str], Optional[str]] outgoing_death_link: Tuple[bool, Optional[str]] pause_death_monitoring: bool @@ -117,6 +126,8 @@ def __init__(self, logger=None) -> None: ZorkGrandInquisitorItems.DEATH, } + self.all_trap_items = items_with_tag(ZorkGrandInquisitorTags.TRAP) + self.game_id_to_items = game_id_to_items() self.possible_inventory_items = ( @@ -142,6 +153,7 @@ def __init__(self, logger=None) -> None: self.option_wild_voxam_chance = None self.option_deathsanity = None self.option_landmarksanity = None + self.option_trap_percentage = None self.option_grant_missable_location_checks = None self.option_client_seed_information = None self.option_death_link = None @@ -149,6 +161,16 @@ def __init__(self, logger=None) -> None: self.starter_kit = None self.initial_totemizer_destination = None + self.trap_counters = { + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 0, + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 0, + ZorkGrandInquisitorItems.TRAP_TELEPORT: 0, + ZorkGrandInquisitorItems.TRAP_ZVISION: 0, + } + + self.active_trap = None + self.active_trap_until = None + self.pending_death_link = (False, None, None) self.outgoing_death_link = (False, None) self.pause_death_monitoring = False @@ -246,6 +268,7 @@ def output_seed_information(self) -> None: self.log(f" Deathsanity: {labels_for_enum_items[self.option_deathsanity]}") self.log(f" Landmarksanity: {labels_for_enum_items[self.option_landmarksanity]}") + self.log(f" Trap Percentage: {self.option_trap_percentage}%") if self.option_grant_missable_location_checks: self.log(f" Grant Missable Location Checks: On") @@ -396,6 +419,9 @@ def update(self) -> None: self._apply_conditional_teleports() + if self.option_trap_percentage: + self._manage_traps() + if self.option_death_link: self._handle_death_link() @@ -428,6 +454,7 @@ def reset(self) -> None: self.option_wild_voxam_chance = None self.option_deathsanity = None self.option_landmarksanity = None + self.option_trap_percentage = None self.option_grant_missable_location_checks = None self.option_client_seed_information = None self.option_death_link = None @@ -435,6 +462,16 @@ def reset(self) -> None: self.starter_kit = None self.initial_totemizer_destination = None + self.trap_counters = { + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 0, + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 0, + ZorkGrandInquisitorItems.TRAP_TELEPORT: 0, + ZorkGrandInquisitorItems.TRAP_ZVISION: 0, + } + + self.active_trap = None + self.active_trap_until = None + self.pending_death_link = (False, None, None) self.outgoing_death_link = (False, None) self.pause_death_monitoring = False @@ -1349,13 +1386,13 @@ def _apply_conditional_teleports(self) -> None: if zork_rocks_inert: self._cast_voxam() - def _cast_voxam(self) -> None: - if not self.option_wild_voxam: + def _cast_voxam(self, force_wild: bool = False) -> None: + if not self.option_wild_voxam and not force_wild: self._apply_starting_location(force=True) voxam_roll: int = random.randint(1, 100) - if voxam_roll <= self.option_wild_voxam_chance: + if voxam_roll <= self.option_wild_voxam_chance or force_wild: starting_location: ZorkGrandInquisitorStartingLocations = ( random.choice(tuple(voxam_cast_game_locations.keys())) ) @@ -1375,6 +1412,80 @@ def _cast_voxam(self) -> None: else: self._apply_starting_location(force=True) + def _manage_traps(self) -> None: + if not self._player_is_afgncaap() or self._read_game_state_value_for(19985) == 0: + return None + + if self.active_trap_until: + if datetime.datetime.now() > self.active_trap_until: + if self.active_trap == ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: + self._deactivate_trap_reverse_controls() + elif self.active_trap == ZorkGrandInquisitorItems.TRAP_ZVISION: + self._deactivate_trap_zvision() + + self.active_trap = None + self.active_trap_until = None + + if self.active_trap is not None: + if self.active_trap == ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: + self._activate_trap_reverse_controls() + elif self.active_trap == ZorkGrandInquisitorItems.TRAP_ZVISION: + self._activate_trap_zvision() + + return None + + trap: ZorkGrandInquisitorItems + count: int + for trap, count in self.trap_counters.items(): + game_count: int = self._read_game_state_value_for(traps_to_game_state_key[trap]) + + if game_count < count: + if trap == ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: + self._activate_trap_infinite_corridor() + elif trap == ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: + self.active_trap = ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS + self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) + + self._activate_trap_reverse_controls() + elif trap == ZorkGrandInquisitorItems.TRAP_TELEPORT: + self._activate_trap_teleport() + elif trap == ZorkGrandInquisitorItems.TRAP_ZVISION: + self.active_trap = ZorkGrandInquisitorItems.TRAP_ZVISION + self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) + + self._activate_trap_zvision() + + self._write_game_state_value_for(traps_to_game_state_key[trap], count) + self.trap_counters[trap] = game_count + + break + + def _activate_trap_infinite_corridor(self) -> None: + depth = random.randint(10, 20) + + self._write_game_state_value_for(11005, depth) + self.game_state_manager.set_game_location("th20", random.randint(0, 1800)) + + time.sleep(0.1) + + self._write_game_state_value_for(11005, depth) + + def _activate_trap_reverse_controls(self) -> None: + self.game_state_manager.set_panorama_reversed(True) + + def _deactivate_trap_reverse_controls(self) -> None: + self.game_state_manager.set_panorama_reversed(False) + + def _activate_trap_teleport(self) -> None: + self._cast_voxam(force_wild=True) + time.sleep(0.1) + + def _activate_trap_zvision(self) -> None: + self.game_state_manager.set_zvision(True) + + def _deactivate_trap_zvision(self) -> None: + self.game_state_manager.set_zvision(False) + def _handle_death_link(self) -> None: # Pause Monitoring Flag if self.pause_death_monitoring and not self._player_is_at("gjde"): diff --git a/worlds/zork_grand_inquisitor/game_state_manager.py b/worlds/zork_grand_inquisitor/game_state_manager.py index 25b35969bf5e..31a168404be7 100644 --- a/worlds/zork_grand_inquisitor/game_state_manager.py +++ b/worlds/zork_grand_inquisitor/game_state_manager.py @@ -82,6 +82,14 @@ def next_location_address(self) -> int: def next_location_offset_address(self) -> int: return self.script_manager_struct_address + 0x40C + @property + def zvision_address(self) -> int: + return self.render_manager_struct_address + 0x0 + + @property + def render_type_address(self) -> int: + return self.render_manager_struct_address + 0x10 + @property def panorama_reversed_address(self) -> int: return self.render_manager_struct_address + 0x1C @@ -250,6 +258,22 @@ def set_game_location(self, game_location: str, offset: int) -> Optional[bool]: return None + def set_zvision(self, is_zvision: bool) -> Optional[bool]: + if self.is_process_running: + self.process.write_int(self.zvision_address, 320 if is_zvision else 640) + + return True + + return None + + def set_render_type(self, render_type: int) -> Optional[bool]: + if self.is_process_running: + self.process.write_int(self.render_type_address, render_type) + + return True + + return None + def set_panorama_reversed(self, is_reversed: bool) -> Optional[bool]: if self.is_process_running: self.process.write_int(self.panorama_reversed_address, 1 if is_reversed else 0) diff --git a/worlds/zork_grand_inquisitor/options.py b/worlds/zork_grand_inquisitor/options.py index 493c780401a3..69f956224291 100644 --- a/worlds/zork_grand_inquisitor/options.py +++ b/worlds/zork_grand_inquisitor/options.py @@ -1,9 +1,12 @@ +from typing import List + from dataclasses import dataclass from Options import ( Choice, DeathLinkMixin, DefaultOnToggle, + OptionGroup, PerGameCommonOptions, Range, StartInventoryPool, @@ -199,6 +202,85 @@ class Landmarksanity(DefaultOnToggle): display_name: str = "Landmarksanity" +class TrapPercentage(Range): + """ + Determines the percentage chance that a trap will replace a filler item. + + Possible traps are: + - Infinite Corridor Trap: The player is teleported to a random depth in the Infinite Corridor + - Reverse Controls Trap: The player's panorama controls are reversed for 30 seconds + - Teleport Trap: The player is teleported to a random location + - ZVision Trap: The player's vision is obscured for 30 seconds + """ + + display_name = "Trap Percentage" + + range_start = 0 + range_end = 100 + + default = 0 + + +class InfiniteCorridorTrapWeight(Range): + """ + Determines the weight of the Infinite Corridor Trap. + + The higher the weight, the more likely this trap will be chosen when a trap is rolled. + """ + + display_name = "Infinite Corridor Trap Weight" + + range_start = 0 + range_end = 100 + + default = 1 + + +class ReverseControlsTrapWeight(Range): + """ + Determines the weight of the Reverse Controls Trap. + + The higher the weight, the more likely this trap will be chosen when a trap is rolled. + """ + + display_name = "Reverse Controls Trap Weight" + + range_start = 0 + range_end = 100 + + default = 1 + + +class TeleportTrapWeight(Range): + """ + Determines the weight of the Teleport Trap. + + The higher the weight, the more likely this trap will be chosen when a trap is rolled. + """ + + display_name = "Teleport Trap Weight" + + range_start = 0 + range_end = 100 + + default = 1 + + +class ZVisionTrapWeight(Range): + """ + Determines the weight of the ZVision Trap. + + The higher the weight, the more likely this trap will be chosen when a trap is rolled. + """ + + display_name = "ZVision Trap Weight" + + range_start = 0 + range_end = 100 + + default = 1 + + class GrantMissableLocationChecks(Toggle): """ If true, performing an irreversible action will grant the locations checks that would have become unobtainable as a @@ -245,5 +327,55 @@ class ZorkGrandInquisitorOptions(PerGameCommonOptions, DeathLinkMixin): wild_voxam_chance: WildVoxamChance deathsanity: Deathsanity landmarksanity: Landmarksanity + trap_percentage: TrapPercentage + infinite_corridor_trap_weight: InfiniteCorridorTrapWeight + reverse_controls_trap_weight: ReverseControlsTrapWeight + teleport_trap_weight: TeleportTrapWeight + zvision_trap_weight: ZVisionTrapWeight grant_missable_location_checks: GrantMissableLocationChecks client_seed_information: ClientSeedInformation + + +# Option presets here... + +option_groups: List[OptionGroup] = [ + OptionGroup( + "Goal Options", + [ + Goal, + ArtifactsOfMagicTotal, + ArtifactsOfMagicRequired, + LandmarksRequired, + DeathsRequired, + ], + ), + OptionGroup( + "Gameplay Options", + [ + StartingLocation, + Hotspots, + CraftableSpells, + WildVoxam, + WildVoxamChance, + Deathsanity, + Landmarksanity, + ], + ), + OptionGroup( + "Trap Options", + [ + TrapPercentage, + InfiniteCorridorTrapWeight, + ReverseControlsTrapWeight, + TeleportTrapWeight, + ZVisionTrapWeight, + ], + ), + OptionGroup( + "Client Options", + [ + GrantMissableLocationChecks, + ClientSeedInformation, + ], + ), +] diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index 176b3dc45e50..fd44ec79f656 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -1,6 +1,9 @@ +import logging + from typing import Any, Dict, List, Set, Tuple, Union from BaseClasses import Item, ItemClassification, Location, Region, Tutorial +from Options import OptionError from worlds.AutoWorld import WebWorld, World @@ -52,7 +55,7 @@ ZorkGrandInquisitorTags, ) -from .options import ZorkGrandInquisitorOptions +from .options import ZorkGrandInquisitorOptions, option_groups class ZorkGrandInquisitorItem(Item): @@ -77,6 +80,9 @@ class ZorkGrandInquisitorWebWorld(WebWorld): ) ] + # Option presets here... + option_groups = option_groups + class ZorkGrandInquisitorWorld(World): """ @@ -125,6 +131,8 @@ class ZorkGrandInquisitorWorld(World): locked_items: Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems] starter_kit: Tuple[ZorkGrandInquisitorItems, ...] starting_location: ZorkGrandInquisitorStartingLocations + trap_percentage: int + trap_weights: Tuple[int, ...] def generate_early(self) -> None: self.goal = id_to_goals()[self.options.goal.value] @@ -135,6 +143,12 @@ def generate_early(self) -> None: if self.artifacts_of_magic_required > self.artifacts_of_magic_total: self.artifacts_of_magic_total = self.artifacts_of_magic_required + if self.goal == ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: + logging.warning( + f"Zork Grand Inquisitor: {self.player_name} has more required artifacts than " + "total artifacts. Using required artifacts as total artifacts..." + ) + self.landmarks_required = self.options.landmarks_required.value self.deaths_required = self.options.deaths_required.value @@ -191,6 +205,20 @@ def generate_early(self) -> None: self.initial_totemizer_destination = self._select_initial_totemizer_destination() + self.trap_percentage = self.options.trap_percentage.value / 100 + + self.trap_weights = ( + self.options.infinite_corridor_trap_weight.value, + self.options.reverse_controls_trap_weight.value, + self.options.teleport_trap_weight.value, + self.options.zvision_trap_weight.value, + ) + + if self.trap_percentage and not any(self.trap_weights): + raise OptionError( + f"Zork Grand Inquisitor: {self.player_name} has traps enabled but all traps are weighted at 0." + ) + def create_regions(self) -> None: region_mapping: Dict[ZorkGrandInquisitorRegions, Region] = dict() @@ -287,29 +315,23 @@ def create_regions(self) -> None: self.multiworld.regions.append(region_menu) def create_items(self) -> None: - items_to_ignore: Set[ZorkGrandInquisitorItems] = set() - items_to_precollect: Set[ZorkGrandInquisitorItems] = set() - items_to_place_early: Set[ZorkGrandInquisitorItems] - - item: ZorkGrandInquisitorItems - - for item in items_with_tag(ZorkGrandInquisitorTags.FILLER): - items_to_ignore.add(item) - - for item in items_with_tag(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS): - items_to_ignore.add(item) + # Populate Items to Ignore and Precollect + items_to_ignore: Set[ZorkGrandInquisitorItems] = ( + items_with_tag(ZorkGrandInquisitorTags.FILLER) + | items_with_tag(ZorkGrandInquisitorTags.TRAP) + | items_with_tag(ZorkGrandInquisitorTags.GOAL_THREE_ARTIFACTS) + | items_with_tag(ZorkGrandInquisitorTags.GOAL_ZORK_TOUR) + | items_with_tag(ZorkGrandInquisitorTags.GOAL_GRIM_JOURNEY) + | set(self.locked_items.values()) + ) if self.goal != ZorkGrandInquisitorGoals.ARTIFACT_OF_MAGIC_HUNT: - items_to_ignore.add(ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC) + items_to_ignore |= items_with_tag(ZorkGrandInquisitorTags.GOAL_ARTIFACT_OF_MAGIC_HUNT) - items_to_ignore.add(ZorkGrandInquisitorItems.LANDMARK) - items_to_ignore.add(ZorkGrandInquisitorItems.DEATH) - - for item in self.locked_items.values(): - items_to_ignore.add(item) - - for item in self.starter_kit: - items_to_precollect.add(item) + items_to_precollect: Set[ZorkGrandInquisitorItems] = ( + set(self.starter_kit) + | {self.initial_totemizer_destination} + ) hotspot_items: Set[ZorkGrandInquisitorItems] = items_with_tag(ZorkGrandInquisitorTags.HOTSPOT) @@ -318,19 +340,12 @@ def create_items(self) -> None: ) if self.hotspots == ZorkGrandInquisitorHotspots.ENABLED: - for item in hotspot_items: - items_to_ignore.add(item) - - for item in hotspot_regional_items: - items_to_precollect.add(item) + items_to_ignore |= hotspot_items + items_to_precollect |= hotspot_regional_items elif self.hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_REGION: - for item in hotspot_items: - items_to_ignore.add(item) + items_to_ignore |= hotspot_items elif self.hotspots == ZorkGrandInquisitorHotspots.REQUIRE_ITEM_PER_HOTSPOT: - for item in hotspot_regional_items: - items_to_ignore.add(item) - - items_to_precollect.add(self.initial_totemizer_destination) + items_to_ignore |= hotspot_regional_items if self.starting_location != ZorkGrandInquisitorStartingLocations.DM_LAIR_INTERIOR: items_to_precollect.add(ZorkGrandInquisitorItems.HOTSPOT_DUNGEON_MASTERS_HOUSE_EXIT) @@ -338,14 +353,14 @@ def create_items(self) -> None: if self.starting_location != ZorkGrandInquisitorStartingLocations.SPELL_LAB: items_to_precollect.add(ZorkGrandInquisitorItems.HOTSPOT_SPELL_LAB_BRIDGE_EXIT) - items_to_place_early = set(self.early_items) - items_to_precollect + items_to_ignore |= items_to_precollect # Create Item Pool item_pool: List[ZorkGrandInquisitorItem] = list() data: ZorkGrandInquisitorItemData for item, data in self.item_data.items(): - if item in items_to_ignore or item in items_to_precollect: + if item in items_to_ignore: continue if item == ZorkGrandInquisitorItems.ARTIFACT_OF_MAGIC: @@ -354,8 +369,19 @@ def create_items(self) -> None: else: item_pool.append(self.create_item(item.value)) - total_locations: int = len(self.multiworld.get_unfilled_locations(self.player)) - item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))] + total_location_count: int = len(self.multiworld.get_unfilled_locations(self.player)) + to_fill_location_count: int = total_location_count - len(item_pool) + + trap_count: int = int(round(to_fill_location_count * self.trap_percentage)) + + if trap_count: + item_pool += [ + self.create_item(trap.value) for trap in self._sample_trap_items(trap_count) + ] + + item_pool += [ + self.create_filler() for _ in range(to_fill_location_count - trap_count) + ] self.multiworld.itempool += item_pool @@ -363,7 +389,11 @@ def create_items(self) -> None: for item in items_to_precollect: self.multiworld.push_precollected(self.create_item(item.value)) - # Early Items + # Define Early Items + items_to_place_early: Set[ZorkGrandInquisitorItems] = ( + set(self.early_items) - items_to_ignore + ) + if len(items_to_place_early): for item in items_to_place_early: self.multiworld.early_items[self.player][item.value] = 1 @@ -395,6 +425,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "wild_voxam_chance", "deathsanity", "landmarksanity", + "trap_percentage", "grant_missable_location_checks", "client_seed_information", "death_link", @@ -526,3 +557,15 @@ def _select_initial_totemizer_destination(self) -> ZorkGrandInquisitorItems: ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_INFINITY, ZorkGrandInquisitorItems.TOTEMIZER_DESTINATION_STRAIGHT_TO_HELL, )) + + def _sample_trap_items(self, count: int) -> List[ZorkGrandInquisitorItems]: + return self.random.choices( + ( + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR, + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS, + ZorkGrandInquisitorItems.TRAP_TELEPORT, + ZorkGrandInquisitorItems.TRAP_ZVISION, + ), + weights=self.trap_weights, + k=count, + ) From d2762eff34f8b68fef603a9ca72059a26f53a138 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 13:47:01 -0500 Subject: [PATCH 28/51] remove dual classification for teleport traps --- worlds/zork_grand_inquisitor/data/item_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/zork_grand_inquisitor/data/item_data.py b/worlds/zork_grand_inquisitor/data/item_data.py index e7503d561008..d4ed5e1e51a4 100644 --- a/worlds/zork_grand_inquisitor/data/item_data.py +++ b/worlds/zork_grand_inquisitor/data/item_data.py @@ -1401,7 +1401,7 @@ class ZorkGrandInquisitorItemData(NamedTuple): ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 0, - classification=ItemClassification.trap | ItemClassification.useful, + classification=ItemClassification.trap, tags=(ZorkGrandInquisitorTags.TRAP,), ), ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: ZorkGrandInquisitorItemData( @@ -1413,7 +1413,7 @@ class ZorkGrandInquisitorItemData(NamedTuple): ZorkGrandInquisitorItems.TRAP_TELEPORT: ZorkGrandInquisitorItemData( statemap_keys=None, archipelago_id=ITEM_OFFSET + 900 + 2, - classification=ItemClassification.trap | ItemClassification.useful, + classification=ItemClassification.trap, tags=(ZorkGrandInquisitorTags.TRAP,), ), ZorkGrandInquisitorItems.TRAP_ZVISION: ZorkGrandInquisitorItemData( From d991132f9e18bdc9f2b4fce74d94f01184453bff Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 13:48:16 -0500 Subject: [PATCH 29/51] add missing spell bar override for pe2j --- worlds/zork_grand_inquisitor/game_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index a435094f9196..eae1a00fbf33 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -730,6 +730,7 @@ def _apply_permanent_game_flags(self) -> None: self._write_game_flags_value_for(10414, 2) # Keep Spellbar Enabled (pe10) self._write_game_flags_value_for(10492, 2) # Keep Spellbar Enabled (pe20) self._write_game_flags_value_for(10516, 2) # Keep Spellbar Enabled (pe2e) + self._write_game_flags_value_for(10575, 2) # Keep Spellbar Enabled (pe2j) self._write_game_flags_value_for(10589, 2) # Keep Spellbar Enabled (pe30) self._write_game_flags_value_for(10639, 2) # Keep Spellbar Enabled (pe3k) self._write_game_flags_value_for(10659, 2) # Keep Spellbar Enabled (pe40) From 9be327ff2325ab0a32d20b1c1f187bc3f2982511 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 13:52:11 -0500 Subject: [PATCH 30/51] fix operator precedence issue with voxam casts --- worlds/zork_grand_inquisitor/game_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index eae1a00fbf33..126ffff82f3c 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1393,7 +1393,7 @@ def _cast_voxam(self, force_wild: bool = False) -> None: voxam_roll: int = random.randint(1, 100) - if voxam_roll <= self.option_wild_voxam_chance or force_wild: + if (voxam_roll <= self.option_wild_voxam_chance) or force_wild: starting_location: ZorkGrandInquisitorStartingLocations = ( random.choice(tuple(voxam_cast_game_locations.keys())) ) From baf60cfeb62e0a1972a7f8faaefec1869330af06 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 14:17:18 -0500 Subject: [PATCH 31/51] only increment save file trap counts by 1 when managing traps --- worlds/zork_grand_inquisitor/game_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 126ffff82f3c..3a38792bc87e 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -1456,9 +1456,7 @@ def _manage_traps(self) -> None: self._activate_trap_zvision() - self._write_game_state_value_for(traps_to_game_state_key[trap], count) - self.trap_counters[trap] = game_count - + self._write_game_state_value_for(traps_to_game_state_key[trap], game_count + 1) break def _activate_trap_infinite_corridor(self) -> None: From 620bfbcbd7bde4da689c5bbbdd4ecb2b417b34c0 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 15:20:42 -0500 Subject: [PATCH 32/51] add one-way energy link by hitting the mushroom with the hammer --- worlds/zork_grand_inquisitor/client.py | 18 ++++++++ .../zork_grand_inquisitor/game_controller.py | 42 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index a9fa7a380b11..acb7720ab78a 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -285,6 +285,24 @@ async def controller(self): } ]) + # Handle Energy Link + while len(self.game_controller.energy_link_queue) > 0: + energy_to_add: int = self.game_controller.energy_link_queue.popleft() + + await self.send_msgs([ + { + "cmd": "Set", + "key": f"EnergyLink{self.team}", + "slot": self.slot, + "operations": + [ + {"operation": "add", "value": energy_to_add}, + ], + }, + ]) + + CommonClient.logger.info(f"Added {energy_to_add} J to the Energy Link pool") + # Handle Death Link await self.update_death_link(self.death_link_status) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 3a38792bc87e..56c5accdea45 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -92,6 +92,9 @@ class GameController: active_trap: Optional[ZorkGrandInquisitorItems] active_trap_until: Optional[datetime.datetime] + energy_link_queue: collections.deque + pause_energy_link_monitoring: bool + pending_death_link: Tuple[bool, Optional[str], Optional[str]] outgoing_death_link: Tuple[bool, Optional[str]] pause_death_monitoring: bool @@ -171,6 +174,9 @@ def __init__(self, logger=None) -> None: self.active_trap = None self.active_trap_until = None + self.energy_link_queue = collections.deque() + self.pause_energy_link_monitoring = False + self.pending_death_link = (False, None, None) self.outgoing_death_link = (False, None) self.pause_death_monitoring = False @@ -422,6 +428,9 @@ def update(self) -> None: if self.option_trap_percentage: self._manage_traps() + if self._player_is_at("dg3e"): + self._manage_energy_link() + if self.option_death_link: self._handle_death_link() @@ -472,6 +481,9 @@ def reset(self) -> None: self.active_trap = None self.active_trap_until = None + self.energy_link_queue = collections.deque() + self.pause_energy_link_monitoring = False + self.pending_death_link = (False, None, None) self.outgoing_death_link = (False, None) self.pause_death_monitoring = False @@ -1485,6 +1497,36 @@ def _activate_trap_zvision(self) -> None: def _deactivate_trap_zvision(self) -> None: self.game_state_manager.set_zvision(False) + def _manage_energy_link(self) -> None: + mushroom_hammered: bool = self._read_game_state_value_for(4217) == 1 + mushroom_hammered_throck: bool = self._read_game_state_value_for(4219) == 1 + mushroom_hammered_snapdragon: bool = self._read_game_state_value_for(4220) == 1 + mushroom_hammered_snapdragon_throck: bool = self._read_game_state_value_for(4222) == 1 + + any_mushroom_hammered: bool = ( + mushroom_hammered + or mushroom_hammered_throck + or mushroom_hammered_snapdragon + or mushroom_hammered_snapdragon_throck + ) + + # Pause Monitoring Flag + if self.pause_energy_link_monitoring and not any_mushroom_hammered: + self.pause_energy_link_monitoring = False + + if not self.pause_energy_link_monitoring and any_mushroom_hammered: + # Contribute Energy + if mushroom_hammered: + self.energy_link_queue.append(750) + elif mushroom_hammered_throck: + self.energy_link_queue.append(1500) + elif mushroom_hammered_snapdragon: + self.energy_link_queue.append(750) + elif mushroom_hammered_snapdragon_throck: + self.energy_link_queue.append(1500) + + self.pause_energy_link_monitoring = True + def _handle_death_link(self) -> None: # Pause Monitoring Flag if self.pause_death_monitoring and not self._player_is_at("gjde"): From 26aaf894e24ef55ba8dfedf821d8cd91ed5344bf Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 19:00:36 -0500 Subject: [PATCH 33/51] rework client process messages --- worlds/zork_grand_inquisitor/client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index acb7720ab78a..ec4c6f03452c 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -126,8 +126,6 @@ async def disconnect(self, allow_autoreconnect: bool = False): self.items_received = [] self.locations_info = {} - self.can_display_process_message = True - await super().disconnect(allow_autoreconnect) def on_package(self, cmd: str, _args: Any) -> None: @@ -246,12 +244,13 @@ async def controller(self): if self.process_attached_at_least_once: process_message = ( - "Lost connection to Zork Grand Inquisitor process. Please restart the game and use the /zork " - "command to reattach." + "Connection to the Zork Grand Inquisitor process was lost. Ensure you are connected " + "to an Archipelago server and the game is running, then use the /zork command to reconnect." ) else: process_message = ( - "Please use the /zork command to attach to a running Zork Grand Inquisitor process." + "To start playing, connect to an Archipelago server and use the /zork command to " + "link to an active Zork Grand Inquisitor process." ) if self.can_display_process_message: From eb295ed2a0de0f14f7aa7d604f806575bc4201ed Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 19:54:26 -0500 Subject: [PATCH 34/51] make sure traps are activated in received order + trap client messages --- worlds/zork_grand_inquisitor/client.py | 8 +- .../data/mapping_data.py | 7 -- .../zork_grand_inquisitor/game_controller.py | 88 +++++++++++-------- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/worlds/zork_grand_inquisitor/client.py b/worlds/zork_grand_inquisitor/client.py index ec4c6f03452c..40ba8a6a483f 100644 --- a/worlds/zork_grand_inquisitor/client.py +++ b/worlds/zork_grand_inquisitor/client.py @@ -211,9 +211,7 @@ async def controller(self): # Enqueue Received Item Delta goal_item_count: int = 0 - trap_item_counts: Dict[ZorkGrandInquisitorItems, int] = { - item: 0 for item in self.game_controller.all_trap_items - } + received_traps: List[ZorkGrandInquisitorItems] = list() network_item: NetUtils.NetworkItem for network_item in self.items_received: @@ -223,7 +221,7 @@ async def controller(self): goal_item_count += 1 continue elif item in self.game_controller.all_trap_items: - trap_item_counts[item] += 1 + received_traps.append(item) continue elif item not in self.game_controller.received_items: if item not in self.game_controller.received_items_queue: @@ -233,7 +231,7 @@ async def controller(self): self.game_controller.goal_item_count = goal_item_count self.game_controller.output_goal_item_update() - self.game_controller.trap_counters = trap_item_counts + self.game_controller.received_traps = received_traps # Game Controller Update if self.game_controller.is_process_running(): diff --git a/worlds/zork_grand_inquisitor/data/mapping_data.py b/worlds/zork_grand_inquisitor/data/mapping_data.py index 9eae2a87579b..4219d291fdc4 100644 --- a/worlds/zork_grand_inquisitor/data/mapping_data.py +++ b/worlds/zork_grand_inquisitor/data/mapping_data.py @@ -757,13 +757,6 @@ ZorkGrandInquisitorStartingLocations.MONASTERY_EXHIBIT: ZorkGrandInquisitorRegions.MONASTERY_EXHIBIT, } -traps_to_game_state_key: Dict[ZorkGrandInquisitorItems, int] = { - ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 19990, - ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 19991, - ZorkGrandInquisitorItems.TRAP_TELEPORT: 19992, - ZorkGrandInquisitorItems.TRAP_ZVISION: 19993, -} - voxam_cast_game_locations: Dict[ ZorkGrandInquisitorStartingLocations, Tuple[Tuple[str, int], ...] diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 56c5accdea45..47f32b3fcccf 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -15,7 +15,6 @@ death_cause_labels, hotspots_for_regional_hotspot, labels_for_enum_items, - traps_to_game_state_key, voxam_cast_game_locations, ) @@ -87,7 +86,7 @@ class GameController: starter_kit: Optional[List[str]] initial_totemizer_destination: Optional[ZorkGrandInquisitorItems] - trap_counters: Dict[ZorkGrandInquisitorItems, int] + received_traps: List[ZorkGrandInquisitorItems] active_trap: Optional[ZorkGrandInquisitorItems] active_trap_until: Optional[datetime.datetime] @@ -164,12 +163,7 @@ def __init__(self, logger=None) -> None: self.starter_kit = None self.initial_totemizer_destination = None - self.trap_counters = { - ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 0, - ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 0, - ZorkGrandInquisitorItems.TRAP_TELEPORT: 0, - ZorkGrandInquisitorItems.TRAP_ZVISION: 0, - } + self.received_traps = list() self.active_trap = None self.active_trap_until = None @@ -471,12 +465,7 @@ def reset(self) -> None: self.starter_kit = None self.initial_totemizer_destination = None - self.trap_counters = { - ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: 0, - ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: 0, - ZorkGrandInquisitorItems.TRAP_TELEPORT: 0, - ZorkGrandInquisitorItems.TRAP_ZVISION: 0, - } + self.received_traps = list() self.active_trap = None self.active_trap_until = None @@ -1447,29 +1436,56 @@ def _manage_traps(self) -> None: return None + processed_trap_counters: Dict[ZorkGrandInquisitorItems, int] = { + ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: self._read_game_state_value_for(19990), + ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: self._read_game_state_value_for(19991), + ZorkGrandInquisitorItems.TRAP_TELEPORT: self._read_game_state_value_for(19992), + ZorkGrandInquisitorItems.TRAP_ZVISION: self._read_game_state_value_for(19993), + } + + traps_remaining: int = len(self.received_traps) - sum(processed_trap_counters.values()) - 1 + traps_remaining_message: str = f"Traps remaining: {traps_remaining}" if traps_remaining else "" + trap: ZorkGrandInquisitorItems - count: int - for trap, count in self.trap_counters.items(): - game_count: int = self._read_game_state_value_for(traps_to_game_state_key[trap]) - - if game_count < count: - if trap == ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: - self._activate_trap_infinite_corridor() - elif trap == ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: - self.active_trap = ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS - self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) - - self._activate_trap_reverse_controls() - elif trap == ZorkGrandInquisitorItems.TRAP_TELEPORT: - self._activate_trap_teleport() - elif trap == ZorkGrandInquisitorItems.TRAP_ZVISION: - self.active_trap = ZorkGrandInquisitorItems.TRAP_ZVISION - self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) - - self._activate_trap_zvision() - - self._write_game_state_value_for(traps_to_game_state_key[trap], game_count + 1) - break + for trap in self.received_traps: + if processed_trap_counters[trap]: + processed_trap_counters[trap] -= 1 + continue + + game_state_key: int = -1 + if trap == ZorkGrandInquisitorItems.TRAP_INFINITE_CORRIDOR: + game_state_key = 19990 + self._activate_trap_infinite_corridor() + + self.log(f"Infinite Corridor Trap! {traps_remaining_message}") + elif trap == ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS: + game_state_key = 19991 + + self.active_trap = ZorkGrandInquisitorItems.TRAP_REVERSE_CONTROLS + self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) + + self._activate_trap_reverse_controls() + + self.log(f"Reverse Controls Trap for 30 seconds! {traps_remaining_message}") + elif trap == ZorkGrandInquisitorItems.TRAP_TELEPORT: + game_state_key = 19992 + self._activate_trap_teleport() + + self.log(f"Teleport Trap! {traps_remaining_message}") + elif trap == ZorkGrandInquisitorItems.TRAP_ZVISION: + game_state_key = 19993 + + self.active_trap = ZorkGrandInquisitorItems.TRAP_ZVISION + self.active_trap_until = datetime.datetime.now() + datetime.timedelta(seconds=30) + + self._activate_trap_zvision() + + self.log(f"ZVision Trap for 30 seconds! {traps_remaining_message}") + + current_count: int = self._read_game_state_value_for(game_state_key) + self._write_game_state_value_for(game_state_key, current_count + 1) + + break def _activate_trap_infinite_corridor(self) -> None: depth = random.randint(10, 20) From 6af0dae069daa8ebad52a5bdd7f3b0cb6feafe77 Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Fri, 29 Nov 2024 21:19:12 -0500 Subject: [PATCH 35/51] universal tracker support --- worlds/zork_grand_inquisitor/world.py | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/worlds/zork_grand_inquisitor/world.py b/worlds/zork_grand_inquisitor/world.py index fd44ec79f656..f284c5a00ebd 100644 --- a/worlds/zork_grand_inquisitor/world.py +++ b/worlds/zork_grand_inquisitor/world.py @@ -219,6 +219,10 @@ def generate_early(self) -> None: f"Zork Grand Inquisitor: {self.player_name} has traps enabled but all traps are weighted at 0." ) + # Universal Tracker Support + if hasattr(self.multiworld, "re_gen_passthrough"): + self._apply_ut_passthrough() + def create_regions(self) -> None: region_mapping: Dict[ZorkGrandInquisitorRegions, Region] = dict() @@ -434,17 +438,81 @@ def fill_slot_data(self) -> Dict[str, Any]: slot_data["starter_kit"] = sorted([item.value for item in self.starter_kit]) slot_data["initial_totemizer_destination"] = self.initial_totemizer_destination.value + slot_data["trap_weights"] = self.trap_weights + slot_data["save_ids"] = ( self.random.randint(1, 65365), self.random.randint(1, 65365), self.random.randint(1, 65365), ) + # Relay generate_early Overrides + if slot_data["artifacts_of_magic_total"] != self.artifacts_of_magic_total: + slot_data["artifacts_of_magic_total"] = self.artifacts_of_magic_total + + if slot_data["deathsanity"] != self.deathsanity.value: + slot_data["deathsanity"] = self.deathsanity.value + + if slot_data["landmarksanity"] != self.landmarksanity.value: + slot_data["landmarksanity"] = self.landmarksanity.value + return slot_data def get_filler_item_name(self) -> str: return self.random.choice(self.filler_item_names) + # Universal Tracker Support + @staticmethod + def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: + slot_data["goal"] = id_to_goals()[slot_data["goal"]] + slot_data["starting_location"] = id_to_starting_locations()[slot_data["starting_location"]] + slot_data["hotspots"] = id_to_hotspots()[slot_data["hotspots"]] + slot_data["craftable_spells"] = id_to_craftable_spell_behaviors()[slot_data["craftable_spells"]] + slot_data["deathsanity"] = id_to_deathsanity()[slot_data["deathsanity"]] + slot_data["landmarksanity"] = id_to_landmarksanity()[slot_data["landmarksanity"]] + slot_data["starter_kit"] = tuple([ZorkGrandInquisitorItems(item) for item in slot_data["starter_kit"]]) + + slot_data["initial_totemizer_destination"] = ZorkGrandInquisitorItems( + slot_data["initial_totemizer_destination"] + ) + + return slot_data + + def _apply_ut_passthrough(self) -> None: + if "Zork Grand Inquisitor" in self.multiworld.re_gen_passthrough: + passthrough: Dict[str, Any] = self.multiworld.re_gen_passthrough["Zork Grand Inquisitor"] + + self.goal = passthrough["goal"] + self.artifacts_of_magic_required = passthrough["artifacts_of_magic_required"] + self.artifacts_of_magic_total = passthrough["artifacts_of_magic_total"] + self.landmarks_required = passthrough["landmarks_required"] + self.deaths_required = passthrough["deaths_required"] + self.starting_location = passthrough["starting_location"] + self.starter_kit = passthrough["starter_kit"] + self.craftable_spells = passthrough["craftable_spells"] + self.hotspots = passthrough["hotspots"] + self.deathsanity = passthrough["deathsanity"] + self.landmarksanity = passthrough["landmarksanity"] + + self.item_data = prepare_item_data( + self.starting_location, + self.goal, + self.deathsanity, + self.landmarksanity, + ) + + self.location_data = prepare_location_data( + self.starting_location, + self.goal, + self.deathsanity, + self.landmarksanity, + ) + + self.locked_items = self._prepare_locked_items() + self.initial_totemizer_destination = passthrough["initial_totemizer_destination"] + self.trap_percentage = passthrough["trap_percentage"] / 100 + self.trap_weights = passthrough["trap_weights"] + def _prepare_locked_items( self, ) -> Dict[ZorkGrandInquisitorLocations, ZorkGrandInquisitorItems]: From d6b73c355f1eac3bcb5503a37bdf75ec8d5b034f Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Sat, 30 Nov 2024 08:07:13 -0500 Subject: [PATCH 36/51] require 13->5 losses to get lucy's strip grue, fire, water death --- worlds/zork_grand_inquisitor/game_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/zork_grand_inquisitor/game_controller.py b/worlds/zork_grand_inquisitor/game_controller.py index 47f32b3fcccf..079fa0d6b2ed 100644 --- a/worlds/zork_grand_inquisitor/game_controller.py +++ b/worlds/zork_grand_inquisitor/game_controller.py @@ -698,6 +698,10 @@ def _apply_conditional_game_state(self): else: self._write_game_state_value_for(10998, 0) + # Lucy Strip Grue, Fire, Water Losses + if self._read_game_state_value_for(14568) < 8: + self._write_game_state_value_for(14568, 8) + def _apply_permanent_game_flags(self) -> None: self._write_game_flags_value_for(13597, 2) # Monastery Vent self._write_game_flags_value_for(9437, 2) # Monastery Exhibit Door to Outside From 9a708180a716d6a4bb331a83d3073bd7566f2d0a Mon Sep 17 00:00:00 2001 From: Nicholas Brochu Date: Thu, 27 Feb 2025 17:52:41 -0500 Subject: [PATCH 37/51] ZGI V2 Entrance Randomizer --- .gitattributes | 1 + .github/pyright-config.json | 18 +- .github/workflows/analyze-modified-files.yml | 2 +- .github/workflows/build.yml | 10 +- .github/workflows/ctest.yml | 4 +- .github/workflows/release.yml | 5 +- .github/workflows/scan-build.yml | 6 +- .github/workflows/strict-type-check.yml | 2 +- .github/workflows/unittests.yml | 4 +- BaseClasses.py | 231 +- CommonClient.py | 78 +- Fill.py | 108 +- Generate.py | 37 +- LICENSE | 2 +- Launcher.py | 79 +- LinksAwakeningClient.py | 6 +- LttPAdjuster.py | 5 + Main.py | 89 +- ModuleUpdate.py | 11 +- MultiServer.py | 277 +- NetUtils.py | 63 +- OoTAdjuster.py | 2 - Options.py | 109 +- README.md | 4 + SNIClient.py | 3 + Utils.py | 61 +- WebHost.py | 7 +- WebHostLib/__init__.py | 7 +- WebHostLib/api/__init__.py | 4 +- WebHostLib/api/user.py | 2 +- WebHostLib/autolauncher.py | 21 +- WebHostLib/check.py | 5 +- WebHostLib/customserver.py | 5 +- WebHostLib/generate.py | 8 +- WebHostLib/misc.py | 7 - WebHostLib/requirements.txt | 4 +- WebHostLib/session.py | 31 + WebHostLib/static/assets/faq/en.md | 2 +- WebHostLib/templates/gameInfo.html | 2 +- WebHostLib/templates/genericTracker.html | 4 + WebHostLib/templates/hostRoom.html | 11 +- WebHostLib/templates/islandFooter.html | 2 +- WebHostLib/templates/macros.html | 2 +- .../templates/multitrackerHintTable.html | 16 +- .../playerOptions/playerOptions.html | 2 +- WebHostLib/templates/session.html | 30 + WebHostLib/templates/siteMap.html | 1 + WebHostLib/templates/templates.html | 3 - WebHostLib/templates/tutorial.html | 2 +- .../templates/weightedOptions/macros.html | 2 +- WebHostLib/tracker.py | 6 +- _speedups.pyx | 43 +- _speedups.pyxbld | 18 +- data/client.kv | 13 +- data/lua/connector_bizhawk_generic.lua | 29 +- data/lua/connector_oot.lua | 4 +- data/options.yaml | 6 +- docs/CODEOWNERS | 28 +- docs/apworld_dev_faq.md | 23 + docs/contributing.md | 2 +- docs/entrance randomization.md | 424 + docs/network protocol.md | 42 +- docs/options api.md | 2 +- docs/running from source.md | 10 +- docs/webhost configuration sample.yaml | 10 +- docs/world api.md | 140 +- entrance_rando.py | 448 + inno_setup.iss | 5 + kvui.py | 215 +- pytest.ini | 3 + requirements.txt | 2 +- settings.py | 36 +- setup.py | 4 +- test/benchmark/locations.py | 10 +- test/cpp/CMakeLists.txt | 6 +- test/general/__init__.py | 10 +- test/general/test_entrance_rando.py | 418 + test/general/test_entrances.py | 63 + test/general/test_helpers.py | 11 +- test/general/test_implemented.py | 76 +- test/general/test_items.py | 20 +- test/general/test_locations.py | 6 + test/general/test_memory.py | 11 +- test/general/test_names.py | 4 +- test/general/test_options.py | 2 +- test/general/test_reachability.py | 4 +- test/general/test_settings.py | 16 + test/general/test_state.py | 29 + test/netutils/test_location_store.py | 41 + test/options/test_generate_templates.py | 55 + test/webhost/test_docs.py | 10 +- test/webhost/test_option_presets.py | 5 + worlds/AutoSNIClient.py | 8 +- worlds/AutoWorld.py | 23 +- worlds/LauncherComponents.py | 51 +- worlds/__init__.py | 19 +- worlds/_bizhawk/README.md | 10 +- worlds/_bizhawk/__init__.py | 38 +- worlds/_bizhawk/client.py | 16 +- worlds/_bizhawk/context.py | 25 +- worlds/adventure/Locations.py | 2 - worlds/adventure/Options.py | 5 +- worlds/adventure/Regions.py | 8 +- worlds/adventure/Rom.py | 10 +- worlds/adventure/__init__.py | 25 +- worlds/ahit/DeathWishRules.py | 18 +- worlds/ahit/Locations.py | 7 +- worlds/ahit/Options.py | 2 +- worlds/ahit/Regions.py | 15 +- worlds/ahit/Rules.py | 14 +- worlds/ahit/__init__.py | 4 +- worlds/ahit/docs/setup_en.md | 4 +- worlds/alttp/Bosses.py | 4 +- worlds/alttp/Client.py | 2 +- worlds/alttp/ItemPool.py | 5 +- worlds/alttp/Items.py | 8 +- worlds/alttp/Options.py | 166 +- worlds/alttp/Rom.py | 9 +- worlds/alttp/Rules.py | 6 +- worlds/alttp/Shops.py | 3 +- worlds/alttp/StateHelpers.py | 47 +- worlds/alttp/__init__.py | 35 +- worlds/alttp/docs/fr_A Link to the Past.md | 32 + worlds/alttp/docs/multiworld_es.md | 251 +- worlds/alttp/docs/multiworld_fr.md | 204 +- .../docs/retroarch-network-commands-fr.png | Bin 0 -> 20461 bytes worlds/alttp/test/dungeons/TestGanonsTower.py | 14 +- worlds/alttp/test/dungeons/TestMiseryMire.py | 2 +- worlds/aquaria/Items.py | 440 +- worlds/aquaria/Locations.py | 800 +- worlds/aquaria/Options.py | 52 +- worlds/aquaria/Regions.py | 1190 ++- worlds/aquaria/__init__.py | 110 +- worlds/aquaria/docs/en_Aquaria.md | 2 +- worlds/aquaria/docs/setup_en.md | 4 +- worlds/aquaria/docs/setup_fr.md | 5 +- worlds/aquaria/test/__init__.py | 405 +- worlds/aquaria/test/test_beast_form_access.py | 24 +- ...test_beast_form_or_arnassi_armor_access.py | 46 +- worlds/aquaria/test/test_bind_song_access.py | 33 +- .../test/test_bind_song_option_access.py | 40 +- .../aquaria/test/test_confined_home_water.py | 9 +- worlds/aquaria/test/test_dual_song_access.py | 17 +- .../aquaria/test/test_energy_form_access.py | 29 +- .../test_energy_form_or_dual_form_access.py | 132 +- worlds/aquaria/test/test_fish_form_access.py | 39 +- worlds/aquaria/test/test_li_song_access.py | 55 +- worlds/aquaria/test/test_light_access.py | 99 +- .../aquaria/test/test_nature_form_access.py | 79 +- ...st_no_progression_hard_hidden_locations.py | 53 +- .../test_progression_hard_hidden_locations.py | 51 +- .../aquaria/test/test_spirit_form_access.py | 38 +- worlds/aquaria/test/test_sun_form_access.py | 24 +- .../test_unconfine_home_water_via_both.py | 9 +- ...st_unconfine_home_water_via_energy_door.py | 9 +- ...st_unconfine_home_water_via_transturtle.py | 9 +- worlds/blasphemous/Options.py | 12 +- worlds/blasphemous/__init__.py | 15 +- worlds/celeste64/docs/guide_en.md | 8 +- worlds/cv64/client.py | 17 +- worlds/cv64/data/patches.py | 17 + worlds/cv64/rom.py | 83 +- worlds/cvcotm/LICENSES.txt | 248 + worlds/cvcotm/NOTICE.txt | 4 + worlds/cvcotm/__init__.py | 221 + worlds/cvcotm/aesthetics.py | 761 ++ worlds/cvcotm/client.py | 563 ++ worlds/cvcotm/cvcotm_text.py | 178 + worlds/cvcotm/data/iname.py | 36 + worlds/cvcotm/data/ips/AllowAlwaysDrop.ips | Bin 0 -> 67 bytes worlds/cvcotm/data/ips/AllowSpeedDash.ips | Bin 0 -> 66 bytes worlds/cvcotm/data/ips/BrokenMaidens.ips | Bin 0 -> 54 bytes worlds/cvcotm/data/ips/BuffFamiliars.ips | Bin 0 -> 44 bytes worlds/cvcotm/data/ips/BuffSubweapons.ips | Bin 0 -> 68 bytes worlds/cvcotm/data/ips/CandleFix.ips | Bin 0 -> 20 bytes worlds/cvcotm/data/ips/CardCombosRevealed.ips | Bin 0 -> 17 bytes worlds/cvcotm/data/ips/CardUp_v3_Custom2.ips | Bin 0 -> 348 bytes worlds/cvcotm/data/ips/Countdown.ips | Bin 0 -> 240 bytes worlds/cvcotm/data/ips/DSSGlitchFix.ips | Bin 0 -> 95 bytes worlds/cvcotm/data/ips/DSSRunSpeed.ips | Bin 0 -> 18 bytes worlds/cvcotm/data/ips/DemoForceFirst.ips | Bin 0 -> 15 bytes .../data/ips/DropReworkMultiEdition.ips | Bin 0 -> 783 bytes worlds/cvcotm/data/ips/GameClearBypass.ips | Bin 0 -> 29 bytes worlds/cvcotm/data/ips/MPComboFix.ips | Bin 0 -> 15 bytes worlds/cvcotm/data/ips/MapEdits.ips | Bin 0 -> 32 bytes worlds/cvcotm/data/ips/MultiLastKey.ips | Bin 0 -> 191 bytes worlds/cvcotm/data/ips/NoDSSDrops.ips | Bin 0 -> 232 bytes worlds/cvcotm/data/ips/NoMPDrain.ips | Bin 0 -> 17 bytes worlds/cvcotm/data/ips/PermanentDash.ips | Bin 0 -> 33 bytes .../cvcotm/data/ips/SeedDisplay20Digits.ips | Bin 0 -> 110 bytes worlds/cvcotm/data/ips/ShooterStrength.ips | Bin 0 -> 16 bytes worlds/cvcotm/data/ips/SoftlockBlockFix.ips | Bin 0 -> 15 bytes worlds/cvcotm/data/lname.py | 128 + worlds/cvcotm/data/patches.py | 431 + .../en_Castlevania - Circle of the Moon.md | 169 + worlds/cvcotm/docs/setup_en.md | 72 + worlds/cvcotm/items.py | 211 + worlds/cvcotm/locations.py | 265 + worlds/cvcotm/lz10.py | 265 + worlds/cvcotm/options.py | 282 + worlds/cvcotm/presets.py | 190 + worlds/cvcotm/regions.py | 189 + worlds/cvcotm/rom.py | 600 ++ worlds/cvcotm/rules.py | 203 + worlds/cvcotm/test/__init__.py | 5 + worlds/cvcotm/test/test_access.py | 811 ++ worlds/dark_souls_3/Bosses.py | 8 +- worlds/dark_souls_3/Locations.py | 28 +- worlds/dark_souls_3/__init__.py | 224 +- .../detailed_location_descriptions.py | 6 +- worlds/dark_souls_3/docs/locations_en.md | 48 +- worlds/dark_souls_3/docs/setup_en.md | 68 +- worlds/dkc3/Client.py | 3 +- worlds/dkc3/Items.py | 2 +- worlds/dkc3/Options.py | 3 +- worlds/dkc3/Regions.py | 5 +- worlds/dkc3/Rom.py | 3 +- worlds/dkc3/Rules.py | 4 +- worlds/dkc3/__init__.py | 8 +- worlds/dlcquest/__init__.py | 14 +- worlds/doom_1993/Options.py | 13 +- worlds/doom_1993/docs/setup_en.md | 20 +- worlds/doom_ii/Options.py | 13 +- worlds/doom_ii/docs/setup_en.md | 20 +- worlds/factorio/Client.py | 5 +- worlds/factorio/Mod.py | 4 +- worlds/factorio/Options.py | 79 +- worlds/factorio/Technologies.py | 24 +- worlds/factorio/__init__.py | 32 +- worlds/factorio/data/mod/lib.lua | 125 +- worlds/factorio/data/mod_template/control.lua | 124 +- .../data/mod_template/data-final-fixes.lua | 2 + worlds/factorio/data/techs.json | 2 +- worlds/factorio/test_file_validation.py | 39 + worlds/faxanadu/Items.py | 58 + worlds/faxanadu/Locations.py | 199 + worlds/faxanadu/Options.py | 107 + worlds/faxanadu/Regions.py | 66 + worlds/faxanadu/Rules.py | 79 + worlds/faxanadu/__init__.py | 189 + worlds/faxanadu/docs/en_Faxanadu.md | 27 + worlds/faxanadu/docs/setup_en.md | 32 + worlds/ffmq/Client.py | 17 +- worlds/ffmq/Items.py | 3 +- worlds/ffmq/Options.py | 3 +- worlds/ffmq/Regions.py | 16 +- worlds/ffmq/__init__.py | 11 +- .../docs/en_Final Fantasy Mystic Quest.md | 2 +- worlds/generic/Rules.py | 8 +- worlds/generic/docs/advanced_settings_en.md | 4 +- worlds/generic/docs/commands_en.md | 2 + worlds/generic/docs/mac_en.md | 4 +- worlds/heretic/Options.py | 17 +- worlds/heretic/docs/setup_en.md | 19 +- worlds/hk/Extractor.py | 6 +- worlds/hk/Items.py | 1 + worlds/hk/Options.py | 46 +- worlds/hk/__init__.py | 96 +- worlds/hk/requirements.txt | 1 - worlds/hk/test/__init__.py | 1 - worlds/hk/test/test_grub_count.py | 3 +- worlds/inscryption/Items.py | 158 + worlds/inscryption/Locations.py | 127 + worlds/inscryption/Options.py | 137 + worlds/inscryption/Regions.py | 14 + worlds/inscryption/Rules.py | 181 + worlds/inscryption/__init__.py | 144 + worlds/inscryption/docs/en_Inscryption.md | 22 + worlds/inscryption/docs/setup_en.md | 65 + worlds/inscryption/docs/setup_fr.md | 67 + worlds/inscryption/test/TestAccess.py | 221 + worlds/inscryption/test/TestGoal.py | 108 + worlds/inscryption/test/__init__.py | 7 + worlds/kdl3/regions.py | 2 +- worlds/kdl3/rom.py | 8 +- worlds/kdl3/rules.py | 34 +- worlds/kh1/Regions.py | 3 + worlds/kh1/Rules.py | 22 +- worlds/kh1/__init__.py | 9 +- worlds/kh2/Client.py | 216 +- worlds/kh2/Regions.py | 178 +- worlds/kh2/Rules.py | 24 +- worlds/kh2/__init__.py | 13 +- worlds/kh2/docs/setup_en.md | 58 +- worlds/ladx/ItemIconGuessing.py | 531 ++ worlds/ladx/Items.py | 144 +- worlds/ladx/LADXR/generator.py | 72 +- worlds/ladx/LADXR/itempool.py | 6 +- worlds/ladx/LADXR/locations/birdKey.py | 17 - worlds/ladx/LADXR/locations/boomerangGuy.py | 5 - worlds/ladx/LADXR/locations/constants.py | 6 +- worlds/ladx/LADXR/locations/itemInfo.py | 9 + worlds/ladx/LADXR/locations/items.py | 5 +- worlds/ladx/LADXR/logic/dungeon1.py | 15 +- worlds/ladx/LADXR/logic/dungeon2.py | 14 +- worlds/ladx/LADXR/logic/dungeon3.py | 38 +- worlds/ladx/LADXR/logic/dungeon4.py | 44 +- worlds/ladx/LADXR/logic/dungeon5.py | 49 +- worlds/ladx/LADXR/logic/dungeon6.py | 36 +- worlds/ladx/LADXR/logic/dungeon7.py | 35 +- worlds/ladx/LADXR/logic/dungeon8.py | 50 +- worlds/ladx/LADXR/logic/dungeonColor.py | 16 +- worlds/ladx/LADXR/logic/overworld.py | 322 +- worlds/ladx/LADXR/logic/requirements.py | 74 +- worlds/ladx/LADXR/patches/bank34.py | 2 +- .../ladx/LADXR/patches/bank3e.asm/chest.asm | 4 +- .../LADXR/patches/bank3e.asm/itemnames.asm | 8 +- worlds/ladx/LADXR/patches/bank3e.py | 4 +- worlds/ladx/LADXR/patches/core.py | 14 +- worlds/ladx/LADXR/patches/droppedKey.py | 8 +- worlds/ladx/LADXR/patches/maptweaks.py | 22 + worlds/ladx/LADXR/patches/songs.py | 4 + worlds/ladx/LADXR/patches/tradeSequence.py | 68 +- worlds/ladx/LADXR/settings.py | 4 +- worlds/ladx/Locations.py | 48 +- worlds/ladx/Options.py | 192 +- worlds/ladx/__init__.py | 220 +- worlds/ladx/test/TestDungeonLogic.py | 26 +- worlds/landstalker/Constants.py | 28 + worlds/landstalker/Hints.py | 2 +- worlds/landstalker/Items.py | 3 +- worlds/landstalker/Locations.py | 18 +- worlds/landstalker/Rules.py | 3 +- worlds/landstalker/__init__.py | 20 +- worlds/landstalker/data/world_node.py | 36 + worlds/landstalker/data/world_path.py | 25 + worlds/landstalker/data/world_region.py | 13 +- .../landstalker/data/world_teleport_tree.py | 10 +- worlds/lingo/__init__.py | 12 +- worlds/lingo/data/generated.dat | Bin 149230 -> 149504 bytes worlds/lingo/data/ids.yaml | 1 + worlds/lingo/items.py | 1 + worlds/lingo/options.py | 10 + worlds/lingo/player_logic.py | 4 +- worlds/lingo/static_logic.py | 2 +- worlds/lingo/test/TestDatafile.py | 7 +- worlds/lingo/test/TestOptions.py | 9 +- worlds/lingo/utils/assign_ids.rb | 3 + worlds/lingo/utils/pickle_static_data.py | 17 +- worlds/lufia2ac/__init__.py | 2 +- worlds/lufia2ac/test/TestCustomItemPool.py | 4 +- worlds/meritous/__init__.py | 13 +- worlds/messenger/__init__.py | 32 +- worlds/messenger/client_setup.py | 5 +- worlds/messenger/connections.py | 8 +- worlds/messenger/constants.py | 18 +- worlds/messenger/options.py | 3 +- worlds/messenger/portals.py | 12 +- worlds/messenger/regions.py | 13 +- worlds/messenger/rules.py | 8 +- worlds/messenger/shop.py | 18 +- worlds/messenger/subclasses.py | 10 +- worlds/messenger/test/test_shop.py | 2 +- worlds/minecraft/Constants.py | 2 +- worlds/mm2/__init__.py | 6 +- worlds/mm2/client.py | 11 +- worlds/mm2/options.py | 2 +- worlds/mm2/rom.py | 2 +- worlds/mm2/rules.py | 84 +- worlds/mm2/text.py | 2 +- worlds/mmbn3/Items.py | 29 +- worlds/mmbn3/Locations.py | 37 +- worlds/mmbn3/Names/ItemName.py | 2 + worlds/mmbn3/Names/LocationName.py | 2 + worlds/mmbn3/Options.py | 12 +- worlds/mmbn3/Regions.py | 4 +- worlds/mmbn3/__init__.py | 178 +- worlds/mmbn3/data/bn3-ap-patch.bsdiff | Bin 59914 -> 61276 bytes worlds/musedash/Items.py | 1 + worlds/musedash/MuseDashCollection.py | 98 +- worlds/musedash/MuseDashData.py | 615 ++ worlds/musedash/MuseDashData.txt | 597 -- worlds/musedash/Options.py | 31 +- worlds/musedash/__init__.py | 11 +- worlds/musedash/test/TestDifficultyRanges.py | 12 +- worlds/noita/items.py | 5 +- worlds/noita/options.py | 4 + worlds/oot/Options.py | 4 +- worlds/oot/Patches.py | 2 +- worlds/oot/__init__.py | 3 +- worlds/osrs/LogicCSV/locations_generated.py | 6 +- worlds/osrs/Names.py | 2 +- worlds/osrs/Rules.py | 337 + worlds/osrs/__init__.py | 503 +- worlds/pokemon_emerald/CHANGELOG.md | 13 + worlds/pokemon_emerald/__init__.py | 145 +- worlds/pokemon_emerald/client.py | 7 +- worlds/pokemon_emerald/data.py | 53 +- worlds/pokemon_emerald/data/items.json | 102 +- worlds/pokemon_emerald/data/locations.json | 4014 ++++++--- worlds/pokemon_emerald/groups.py | 721 ++ worlds/pokemon_emerald/items.py | 26 +- worlds/pokemon_emerald/locations.py | 100 +- worlds/pokemon_emerald/options.py | 61 +- worlds/pokemon_emerald/rules.py | 24 +- worlds/pokemon_emerald/sanity_check.py | 19 +- worlds/pokemon_rb/__init__.py | 4 +- worlds/pokemon_rb/docs/setup_es.md | 49 +- worlds/pokemon_rb/encounters.py | 9 +- worlds/pokemon_rb/locations.py | 8 +- worlds/pokemon_rb/regions.py | 2 +- worlds/pokemon_rb/rules.py | 3 + worlds/raft/__init__.py | 68 +- worlds/sa2b/docs/setup_en.md | 36 +- worlds/saving_princess/Client.py | 258 + worlds/saving_princess/Constants.py | 97 + worlds/saving_princess/Items.py | 98 + worlds/saving_princess/Locations.py | 82 + worlds/saving_princess/Options.py | 183 + worlds/saving_princess/Regions.py | 110 + worlds/saving_princess/Rules.py | 132 + worlds/saving_princess/__init__.py | 174 + .../docs/en_Saving Princess.md | 55 + worlds/saving_princess/docs/setup_en.md | 148 + worlds/sc2/Locations.py | 2 +- worlds/sc2/MissionTables.py | 3 + worlds/sc2/Regions.py | 2 +- worlds/shivers/Constants.py | 22 +- worlds/shivers/Items.py | 306 +- worlds/shivers/Options.py | 96 +- worlds/shivers/Rules.py | 315 +- worlds/shivers/__init__.py | 276 +- worlds/shivers/data/excluded_locations.json | 2 +- worlds/shivers/data/locations.json | 144 +- worlds/shivers/data/regions.json | 88 +- worlds/shivers/docs/en_Shivers.md | 5 +- worlds/shivers/docs/setup_en.md | 17 +- worlds/sm/Rom.py | 45 +- worlds/sm/__init__.py | 36 +- worlds/sm/variaRandomizer/randomizer.py | 26 +- worlds/sm/variaRandomizer/rom/ips.py | 19 +- worlds/sm/variaRandomizer/rom/rom.py | 62 +- worlds/sm/variaRandomizer/rom/rompatcher.py | 9 +- worlds/sm64ex/Options.py | 45 +- worlds/sm64ex/__init__.py | 54 +- worlds/sm64ex/docs/setup_en.md | 22 +- worlds/smz3/Options.py | 4 +- worlds/smz3/Rom.py | 24 +- .../TotalSMZ3/Regions/Zelda/GanonsTower.py | 3 +- worlds/smz3/__init__.py | 60 +- worlds/soe/options.py | 2 +- worlds/soe/requirements.txt | 73 +- worlds/soe/test/test_oob.py | 45 +- worlds/stardew_valley/__init__.py | 163 +- worlds/stardew_valley/bundles/bundle_item.py | 24 +- worlds/stardew_valley/bundles/bundle_room.py | 2 +- worlds/stardew_valley/content/__init__.py | 21 +- .../content/feature/__init__.py | 1 + .../content/feature/skill_progression.py | 46 + worlds/stardew_valley/content/game_content.py | 3 +- worlds/stardew_valley/content/mods/sve.py | 19 +- worlds/stardew_valley/content/unpacking.py | 6 +- worlds/stardew_valley/content/vanilla/base.py | 2 +- .../content/vanilla/qi_board.py | 1 - worlds/stardew_valley/data/artisan.py | 4 +- worlds/stardew_valley/data/bundle_data.py | 4 +- worlds/stardew_valley/data/craftable_data.py | 21 +- worlds/stardew_valley/data/game_item.py | 18 +- worlds/stardew_valley/data/harvest.py | 17 +- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/data/locations.csv | 6 +- worlds/stardew_valley/data/recipe_data.py | 42 +- worlds/stardew_valley/data/recipe_source.py | 2 +- worlds/stardew_valley/data/shop.py | 13 +- worlds/stardew_valley/data/skill.py | 18 +- .../stardew_valley/docs/en_Stardew Valley.md | 2 +- worlds/stardew_valley/docs/setup_en.md | 2 +- worlds/stardew_valley/early_items.py | 11 +- worlds/stardew_valley/items.py | 88 +- worlds/stardew_valley/locations.py | 95 +- worlds/stardew_valley/logic/ability_logic.py | 10 +- worlds/stardew_valley/logic/action_logic.py | 1 - worlds/stardew_valley/logic/building_logic.py | 11 +- worlds/stardew_valley/logic/crafting_logic.py | 5 +- worlds/stardew_valley/logic/farming_logic.py | 15 +- worlds/stardew_valley/logic/grind_logic.py | 3 +- worlds/stardew_valley/logic/logic.py | 2 +- worlds/stardew_valley/logic/mine_logic.py | 25 +- worlds/stardew_valley/logic/money_logic.py | 13 +- worlds/stardew_valley/logic/shipping_logic.py | 5 +- worlds/stardew_valley/logic/skill_logic.py | 35 +- .../logic/special_order_logic.py | 7 +- worlds/stardew_valley/logic/walnut_logic.py | 22 +- .../mods/logic/deepwoods_logic.py | 7 +- .../stardew_valley/mods/logic/item_logic.py | 7 +- .../stardew_valley/mods/logic/quests_logic.py | 5 +- .../stardew_valley/mods/logic/skills_logic.py | 11 +- worlds/stardew_valley/option_groups.py | 76 - worlds/stardew_valley/options/__init__.py | 6 + .../stardew_valley/options/forced_options.py | 60 + .../stardew_valley/options/option_groups.py | 68 + .../stardew_valley/{ => options}/options.py | 82 +- worlds/stardew_valley/options/presets.py | 371 + worlds/stardew_valley/presets.py | 378 - worlds/stardew_valley/regions.py | 645 +- worlds/stardew_valley/requirements.txt | 2 - worlds/stardew_valley/rules.py | 117 +- worlds/stardew_valley/scripts/update_data.py | 8 +- worlds/stardew_valley/stardew_rule/base.py | 6 +- .../stardew_rule/rule_explain.py | 20 +- worlds/stardew_valley/stardew_rule/state.py | 47 +- .../strings/ap_names/ap_option_names.py | 35 +- .../strings/ap_names/event_names.py | 10 +- .../strings/ap_names/mods/__init__.py | 0 .../stardew_valley/strings/craftable_names.py | 4 + worlds/stardew_valley/test/TestBooksanity.py | 9 +- worlds/stardew_valley/test/TestCrops.py | 8 +- worlds/stardew_valley/test/TestGeneration.py | 4 +- .../test/TestMultiplePlayers.py | 2 - .../test/TestNumberLocations.py | 9 +- worlds/stardew_valley/test/TestOptions.py | 15 +- .../stardew_valley/test/TestOptionsPairs.py | 19 +- worlds/stardew_valley/test/TestRegions.py | 19 +- .../stardew_valley/test/TestWalnutsanity.py | 28 +- worlds/stardew_valley/test/__init__.py | 97 +- .../test/assertion/rule_assert.py | 41 +- .../test/assertion/world_assert.py | 2 +- .../stardew_valley/test/content/__init__.py | 3 +- worlds/stardew_valley/test/mods/TestMods.py | 95 +- .../stardew_valley/test/mods/TestModsFill.py | 28 + .../test/options/TestForcedOptions.py | 115 + .../test/{ => options}/TestPresets.py | 10 +- .../stardew_valley/test/options/__init__.py | 0 worlds/stardew_valley/test/options/utils.py | 68 + .../stardew_valley/test/rules/TestArcades.py | 52 +- worlds/stardew_valley/test/rules/TestBooks.py | 8 +- .../test/rules/TestBuildings.py | 17 +- .../test/rules/TestCookingRecipes.py | 32 +- .../test/rules/TestCraftingRecipes.py | 30 +- .../test/rules/TestDonations.py | 6 +- .../stardew_valley/test/rules/TestFishing.py | 9 +- .../test/rules/TestFriendship.py | 34 +- .../stardew_valley/test/rules/TestShipping.py | 11 +- .../stardew_valley/test/rules/TestSkills.py | 10 +- .../test/rules/TestStateRules.py | 28 +- worlds/stardew_valley/test/rules/TestTools.py | 45 +- .../stardew_valley/test/rules/TestWeapons.py | 48 +- .../test/stability/TestStability.py | 9 +- .../test/stability/TestUniversalTracker.py | 4 +- worlds/subnautica/__init__.py | 9 +- worlds/subnautica/options.py | 3 +- worlds/timespinner/Items.py | 8 +- worlds/timespinner/Locations.py | 4 +- worlds/timespinner/LogicExtensions.py | 3 + worlds/timespinner/Options.py | 26 +- worlds/timespinner/__init__.py | 94 +- worlds/tloz/Locations.py | 6 +- worlds/tloz/Rules.py | 102 +- worlds/tloz/docs/multiworld_en.md | 2 +- worlds/tunic/__init__.py | 277 +- worlds/tunic/combat_logic.py | 440 + worlds/tunic/er_data.py | 315 +- worlds/tunic/er_rules.py | 628 +- worlds/tunic/er_scripts.py | 94 +- worlds/tunic/grass.py | 7946 +++++++++++++++++ worlds/tunic/items.py | 74 +- worlds/tunic/ladder_storage_data.py | 15 +- worlds/tunic/locations.py | 131 +- worlds/tunic/options.py | 69 +- worlds/tunic/regions.py | 47 +- worlds/tunic/rules.py | 31 +- worlds/tunic/test/test_access.py | 6 +- worlds/tunic/test/test_combat.py | 119 + worlds/witness/__init__.py | 11 +- worlds/witness/data/WitnessItems.txt | 12 +- worlds/witness/data/WitnessLogic.txt | 4 +- worlds/witness/data/WitnessLogicExpert.txt | 4 +- worlds/witness/data/WitnessLogicVanilla.txt | 4 +- worlds/witness/data/WitnessLogicVariety.txt | 4 +- .../Door_Shuffle/Complex_Door_Panels.txt | 5 + .../Door_Shuffle/Elevators_Come_To_You.txt | 11 - .../settings/Door_Shuffle/Simple_Panels.txt | 4 +- worlds/witness/data/settings/Early_Caves.txt | 6 +- .../data/settings/Early_Caves_Start.txt | 6 +- worlds/witness/data/static_items.py | 16 +- worlds/witness/data/static_logic.py | 2 + worlds/witness/data/utils.py | 4 - worlds/witness/entity_hunt.py | 57 +- worlds/witness/hints.py | 34 +- worlds/witness/options.py | 85 +- worlds/witness/player_items.py | 68 +- worlds/witness/player_logic.py | 92 +- worlds/witness/presets.py | 9 +- worlds/witness/regions.py | 29 +- worlds/witness/rules.py | 23 +- worlds/witness/test/test_auto_elevators.py | 68 +- .../test/test_disable_non_randomized.py | 2 + worlds/witness/test/test_door_shuffle.py | 59 +- .../witness/test/test_roll_other_options.py | 10 +- worlds/yugioh06/__init__.py | 28 +- worlds/yugioh06/items.py | 29 + worlds/zillion/__init__.py | 146 +- worlds/zillion/client.py | 69 +- worlds/zillion/gen_data.py | 3 +- worlds/zillion/id_maps.py | 29 +- worlds/zillion/item.py | 28 + worlds/zillion/logic.py | 13 +- worlds/zillion/options.py | 27 +- worlds/zillion/patch.py | 24 +- worlds/zillion/region.py | 10 +- worlds/zillion/requirements.txt | 2 +- worlds/zillion/test/TestOptions.py | 17 +- worlds/zillion/test/TestReproducibleRandom.py | 7 +- worlds/zillion/test/__init__.py | 8 +- worlds/zork_grand_inquisitor/__init__.py | 2 +- worlds/zork_grand_inquisitor/client.py | 12 + ...entrance_rule_data.py => entrance_data.py} | 333 +- .../data/entrance_randomizer_data.py | 188 + .../data/location_data.py | 37 +- .../data/mapping_data.py | 349 +- .../zork_grand_inquisitor/data/region_data.py | 184 - worlds/zork_grand_inquisitor/data_funcs.py | 76 +- worlds/zork_grand_inquisitor/enums.py | 19 +- .../zork_grand_inquisitor/game_controller.py | 348 +- worlds/zork_grand_inquisitor/options.py | 39 +- .../{test_access.py => skip_test_access.py} | 0 ..._data_funcs.py => skip_test_data_funcs.py} | 0 ...st_locations.py => skip_test_locations.py} | 0 worlds/zork_grand_inquisitor/world.py | 284 +- 619 files changed, 38402 insertions(+), 10697 deletions(-) create mode 100644 WebHostLib/session.py create mode 100644 WebHostLib/templates/session.html create mode 100644 docs/entrance randomization.md create mode 100644 entrance_rando.py create mode 100644 test/general/test_entrance_rando.py create mode 100644 test/general/test_entrances.py create mode 100644 test/general/test_settings.py create mode 100644 test/general/test_state.py create mode 100644 test/options/test_generate_templates.py create mode 100644 worlds/alttp/docs/fr_A Link to the Past.md create mode 100644 worlds/alttp/docs/retroarch-network-commands-fr.png create mode 100644 worlds/cvcotm/LICENSES.txt create mode 100644 worlds/cvcotm/NOTICE.txt create mode 100644 worlds/cvcotm/__init__.py create mode 100644 worlds/cvcotm/aesthetics.py create mode 100644 worlds/cvcotm/client.py create mode 100644 worlds/cvcotm/cvcotm_text.py create mode 100644 worlds/cvcotm/data/iname.py create mode 100644 worlds/cvcotm/data/ips/AllowAlwaysDrop.ips create mode 100644 worlds/cvcotm/data/ips/AllowSpeedDash.ips create mode 100644 worlds/cvcotm/data/ips/BrokenMaidens.ips create mode 100644 worlds/cvcotm/data/ips/BuffFamiliars.ips create mode 100644 worlds/cvcotm/data/ips/BuffSubweapons.ips create mode 100644 worlds/cvcotm/data/ips/CandleFix.ips create mode 100644 worlds/cvcotm/data/ips/CardCombosRevealed.ips create mode 100644 worlds/cvcotm/data/ips/CardUp_v3_Custom2.ips create mode 100644 worlds/cvcotm/data/ips/Countdown.ips create mode 100644 worlds/cvcotm/data/ips/DSSGlitchFix.ips create mode 100644 worlds/cvcotm/data/ips/DSSRunSpeed.ips create mode 100644 worlds/cvcotm/data/ips/DemoForceFirst.ips create mode 100644 worlds/cvcotm/data/ips/DropReworkMultiEdition.ips create mode 100644 worlds/cvcotm/data/ips/GameClearBypass.ips create mode 100644 worlds/cvcotm/data/ips/MPComboFix.ips create mode 100644 worlds/cvcotm/data/ips/MapEdits.ips create mode 100644 worlds/cvcotm/data/ips/MultiLastKey.ips create mode 100644 worlds/cvcotm/data/ips/NoDSSDrops.ips create mode 100644 worlds/cvcotm/data/ips/NoMPDrain.ips create mode 100644 worlds/cvcotm/data/ips/PermanentDash.ips create mode 100644 worlds/cvcotm/data/ips/SeedDisplay20Digits.ips create mode 100644 worlds/cvcotm/data/ips/ShooterStrength.ips create mode 100644 worlds/cvcotm/data/ips/SoftlockBlockFix.ips create mode 100644 worlds/cvcotm/data/lname.py create mode 100644 worlds/cvcotm/data/patches.py create mode 100644 worlds/cvcotm/docs/en_Castlevania - Circle of the Moon.md create mode 100644 worlds/cvcotm/docs/setup_en.md create mode 100644 worlds/cvcotm/items.py create mode 100644 worlds/cvcotm/locations.py create mode 100644 worlds/cvcotm/lz10.py create mode 100644 worlds/cvcotm/options.py create mode 100644 worlds/cvcotm/presets.py create mode 100644 worlds/cvcotm/regions.py create mode 100644 worlds/cvcotm/rom.py create mode 100644 worlds/cvcotm/rules.py create mode 100644 worlds/cvcotm/test/__init__.py create mode 100644 worlds/cvcotm/test/test_access.py create mode 100644 worlds/factorio/test_file_validation.py create mode 100644 worlds/faxanadu/Items.py create mode 100644 worlds/faxanadu/Locations.py create mode 100644 worlds/faxanadu/Options.py create mode 100644 worlds/faxanadu/Regions.py create mode 100644 worlds/faxanadu/Rules.py create mode 100644 worlds/faxanadu/__init__.py create mode 100644 worlds/faxanadu/docs/en_Faxanadu.md create mode 100644 worlds/faxanadu/docs/setup_en.md delete mode 100644 worlds/hk/requirements.txt create mode 100644 worlds/inscryption/Items.py create mode 100644 worlds/inscryption/Locations.py create mode 100644 worlds/inscryption/Options.py create mode 100644 worlds/inscryption/Regions.py create mode 100644 worlds/inscryption/Rules.py create mode 100644 worlds/inscryption/__init__.py create mode 100644 worlds/inscryption/docs/en_Inscryption.md create mode 100644 worlds/inscryption/docs/setup_en.md create mode 100644 worlds/inscryption/docs/setup_fr.md create mode 100644 worlds/inscryption/test/TestAccess.py create mode 100644 worlds/inscryption/test/TestGoal.py create mode 100644 worlds/inscryption/test/__init__.py create mode 100644 worlds/ladx/ItemIconGuessing.py create mode 100644 worlds/landstalker/Constants.py create mode 100644 worlds/musedash/MuseDashData.py delete mode 100644 worlds/musedash/MuseDashData.txt create mode 100644 worlds/osrs/Rules.py create mode 100644 worlds/pokemon_emerald/groups.py create mode 100644 worlds/saving_princess/Client.py create mode 100644 worlds/saving_princess/Constants.py create mode 100644 worlds/saving_princess/Items.py create mode 100644 worlds/saving_princess/Locations.py create mode 100644 worlds/saving_princess/Options.py create mode 100644 worlds/saving_princess/Regions.py create mode 100644 worlds/saving_princess/Rules.py create mode 100644 worlds/saving_princess/__init__.py create mode 100644 worlds/saving_princess/docs/en_Saving Princess.md create mode 100644 worlds/saving_princess/docs/setup_en.md create mode 100644 worlds/stardew_valley/content/feature/skill_progression.py delete mode 100644 worlds/stardew_valley/option_groups.py create mode 100644 worlds/stardew_valley/options/__init__.py create mode 100644 worlds/stardew_valley/options/forced_options.py create mode 100644 worlds/stardew_valley/options/option_groups.py rename worlds/stardew_valley/{ => options}/options.py (90%) create mode 100644 worlds/stardew_valley/options/presets.py delete mode 100644 worlds/stardew_valley/presets.py delete mode 100644 worlds/stardew_valley/requirements.txt create mode 100644 worlds/stardew_valley/strings/ap_names/mods/__init__.py create mode 100644 worlds/stardew_valley/test/mods/TestModsFill.py create mode 100644 worlds/stardew_valley/test/options/TestForcedOptions.py rename worlds/stardew_valley/test/{ => options}/TestPresets.py (86%) create mode 100644 worlds/stardew_valley/test/options/__init__.py create mode 100644 worlds/stardew_valley/test/options/utils.py create mode 100644 worlds/tunic/combat_logic.py create mode 100644 worlds/tunic/grass.py create mode 100644 worlds/tunic/test/test_combat.py delete mode 100644 worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt rename worlds/zork_grand_inquisitor/data/{entrance_rule_data.py => entrance_data.py} (74%) create mode 100644 worlds/zork_grand_inquisitor/data/entrance_randomizer_data.py delete mode 100644 worlds/zork_grand_inquisitor/data/region_data.py rename worlds/zork_grand_inquisitor/test/{test_access.py => skip_test_access.py} (100%) rename worlds/zork_grand_inquisitor/test/{test_data_funcs.py => skip_test_data_funcs.py} (100%) rename worlds/zork_grand_inquisitor/test/{test_locations.py => skip_test_locations.py} (100%) diff --git a/.gitattributes b/.gitattributes index 537a05f68b67..5ab537933405 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ worlds/blasphemous/region_data.py linguist-generated=true +worlds/yachtdice/YachtWeights.py linguist-generated=true diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f19b5..de7758a71566 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -1,8 +1,20 @@ { "include": [ - "type_check.py", + "../BizHawkClient.py", + "../Patch.py", + "../test/general/test_groups.py", + "../test/general/test_helpers.py", + "../test/general/test_memory.py", + "../test/general/test_names.py", + "../test/multiworld/__init__.py", + "../test/multiworld/test_multiworlds.py", + "../test/netutils/__init__.py", + "../test/programs/__init__.py", + "../test/programs/test_multi_server.py", + "../test/utils/__init__.py", + "../test/webhost/test_descriptions.py", "../worlds/AutoSNIClient.py", - "../Patch.py" + "type_check.py" ], "exclude": [ @@ -16,7 +28,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2d043..b59336fafe9b 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-python@v5 if: env.diff != '' with: - python-version: 3.8 + python-version: '3.10' - name: "Install dependencies" if: env.diff != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23c463fb947a..27ca76e41f8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,14 +24,15 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py38: # RCs will still be built and signed by hand + build-win: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '~3.12.7' + check-latest: true - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip @@ -111,10 +112,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract diff --git a/.github/workflows/ctest.yml b/.github/workflows/ctest.yml index 9492c83c9e53..a0ae2cb25206 100644 --- a/.github/workflows/ctest.yml +++ b/.github/workflows/ctest.yml @@ -11,7 +11,7 @@ on: - '**.hh?' - '**.hpp' - '**.hxx' - - '**.CMakeLists' + - '**/CMakeLists.txt' - '.github/workflows/ctest.yml' pull_request: paths: @@ -21,7 +21,7 @@ on: - '**.hh?' - '**.hpp' - '**.hxx' - - '**.CMakeLists' + - '**/CMakeLists.txt' - '.github/workflows/ctest.yml' jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f8651d408e7..aec4f90998cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,10 +44,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index 5234d862b4d3..ac842070625f 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -40,10 +40,10 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 17 + sudo ./llvm.sh 19 - name: Install scan-build command run: | - sudo apt install clang-tools-17 + sudo apt install clang-tools-19 - name: Get a recent python uses: actions/setup-python@v5 with: @@ -56,7 +56,7 @@ jobs: - name: scan-build run: | source venv/bin/activate - scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y + scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y - name: Store report if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/strict-type-check.yml b/.github/workflows/strict-type-check.yml index bafd572a26ae..2ccdad8d11af 100644 --- a/.github/workflows/strict-type-check.yml +++ b/.github/workflows/strict-type-check.yml @@ -26,7 +26,7 @@ jobs: - name: "Install dependencies" run: | - python -m pip install --upgrade pip pyright==1.1.358 + python -m pip install --upgrade pip pyright==1.1.392.post0 python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes - name: "pyright: strict check on specific files" diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a38fef8fda08..88b5d12987ad 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -33,13 +33,11 @@ jobs: matrix: os: [ubuntu-latest] python: - - {version: '3.8'} - - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} - {version: '3.12'} include: - - python: {version: '3.8'} # win7 compat + - python: {version: '3.10'} # old compat os: windows-latest - python: {version: '3.12'} # current os: windows-latest diff --git a/BaseClasses.py b/BaseClasses.py index 46edeb5ea059..3d0004806cc5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,18 +1,16 @@ from __future__ import annotations import collections -import itertools import functools import logging import random import secrets -import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, - Optional, Protocol, Set, Tuple, Union, Type) + Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) from typing_extensions import NotRequired, TypedDict @@ -20,7 +18,8 @@ import Options import Utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: + from entrance_rando import ERPlacementState from worlds import AutoWorld @@ -231,7 +230,7 @@ def set_options(self, args: Namespace) -> None: for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass + options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -428,12 +427,12 @@ def get_entrance(self, entrance_name: str, player: int) -> Entrance: def get_location(self, location_name: str, player: int) -> Location: return self.regions.location_cache[player][location_name] - def get_all_state(self, use_cache: bool) -> CollectionState: + def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False) -> CollectionState: cached = getattr(self, "_all_state", None) if use_cache and cached: return cached.copy() - ret = CollectionState(self) + ret = CollectionState(self, allow_partial_entrances) for item in self.itempool: self.worlds[item.player].collect(ret, item) @@ -606,6 +605,49 @@ def get_spheres(self) -> Iterator[Set[Location]]: state.collect(location.item, True, location) locations -= sphere + def get_sendable_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere + + If there are unreachable locations, the last sphere of reachable locations is followed by an empty set, + and then a set of all of the unreachable locations. + """ + state = CollectionState(self) + locations: Set[Location] = set() + events: Set[Location] = set() + for location in self.get_filled_locations(): + if type(location.item.code) is int: + locations.add(location) + else: + events.add(location) + + while locations: + sphere: Set[Location] = set() + + # cull events out + done_events: Set[Union[Location, None]] = {None} + while done_events: + done_events = set() + for event in events: + if event.can_reach(state): + state.collect(event.item, True, event) + done_events.add(event) + events -= done_events + + for location in locations: + if location.can_reach(state): + sphere.add(location) + + yield sphere + if not sphere: + if locations: + yield locations # unreachable locations + break + + for location in sphere: + state.collect(location.item, True, location) + locations -= sphere + def fulfills_accessibility(self, state: Optional[CollectionState] = None): """Check if accessibility rules are fulfilled with current or supplied state.""" if not state: @@ -676,10 +718,11 @@ class CollectionState(): path: Dict[Union[Region, Entrance], PathValue] locations_checked: Set[Location] stale: Dict[int, bool] + allow_partial_entrances: bool additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = [] additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] - def __init__(self, parent: MultiWorld): + def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False): self.prog_items = {player: Counter() for player in parent.get_all_ids()} self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} @@ -688,6 +731,7 @@ def __init__(self, parent: MultiWorld): self.path = {} self.locations_checked = set() self.stale = {player: True for player in parent.get_all_ids()} + self.allow_partial_entrances = allow_partial_entrances for function in self.additional_init_functions: function(self, parent) for items in parent.precollected_items.values(): @@ -722,6 +766,8 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu if new_region in reachable_regions: blocked_connections.remove(connection) elif connection.can_reach(self): + if self.allow_partial_entrances and not new_region: + continue assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) @@ -747,7 +793,9 @@ def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: if new_region in reachable_regions: blocked_connections.remove(connection) elif connection.can_reach(self): - assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" + if self.allow_partial_entrances and not new_region: + continue + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) blocked_connections.update(new_region.exits) @@ -767,6 +815,7 @@ def copy(self) -> CollectionState: ret.advancements = self.advancements.copy() ret.path = self.path.copy() ret.locations_checked = self.locations_checked.copy() + ret.allow_partial_entrances = self.allow_partial_entrances for function in self.additional_copy_functions: ret = function(self, ret) return ret @@ -820,21 +869,40 @@ def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) def has(self, item: str, player: int, count: int = 1) -> bool: return self.prog_items[player][item] >= count + # for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of + # creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the + # argument to all() would be a new generator instance, for example. def has_all(self, items: Iterable[str], player: int) -> bool: """Returns True if each item name of items is in state at least once.""" - return all(self.prog_items[player][item] for item in items) + player_prog_items = self.prog_items[player] + for item in items: + if not player_prog_items[item]: + return False + return True def has_any(self, items: Iterable[str], player: int) -> bool: """Returns True if at least one item name of items is in state at least once.""" - return any(self.prog_items[player][item] for item in items) + player_prog_items = self.prog_items[player] + for item in items: + if player_prog_items[item]: + return True + return False def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool: """Returns True if each item name is in the state at least as many times as specified.""" - return all(self.prog_items[player][item] >= count for item, count in item_counts.items()) + player_prog_items = self.prog_items[player] + for item, count in item_counts.items(): + if player_prog_items[item] < count: + return False + return True def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool: """Returns True if at least one item name is in the state at least as many times as specified.""" - return any(self.prog_items[player][item] >= count for item, count in item_counts.items()) + player_prog_items = self.prog_items[player] + for item, count in item_counts.items(): + if player_prog_items[item] >= count: + return True + return False def count(self, item: str, player: int) -> int: return self.prog_items[player][item] @@ -862,11 +930,20 @@ def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" - return sum(self.prog_items[player][item_name] for item_name in items) + player_prog_items = self.prog_items[player] + total = 0 + for item_name in items: + total += player_prog_items[item_name] + return total def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" - return sum(self.prog_items[player][item_name] > 0 for item_name in items) + player_prog_items = self.prog_items[player] + total = 0 + for item_name in items: + if player_prog_items[item_name] > 0: + total += 1 + return total # item name group related def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: @@ -931,6 +1008,11 @@ def remove(self, item: Item): self.stale[item.player] = True +class EntranceType(IntEnum): + ONE_WAY = 1 + TWO_WAY = 2 + + class Entrance: access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) hide_path: bool = False @@ -938,19 +1020,24 @@ class Entrance: name: str parent_region: Optional[Region] connected_region: Optional[Region] = None + randomization_group: int + randomization_type: EntranceType # LttP specific, TODO: should make a LttPEntrance addresses = None target = None - def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None: + def __init__(self, player: int, name: str = "", parent: Optional[Region] = None, + randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None: self.name = name self.parent_region = parent self.player = player + self.randomization_group = randomization_group + self.randomization_type = randomization_type def can_reach(self, state: CollectionState) -> bool: assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region" if self.parent_region.can_reach(state) and self.access_rule(state): - if not self.hide_path and not self in state.path: + if not self.hide_path and self not in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) return True @@ -962,6 +1049,32 @@ def connect(self, region: Region, addresses: Any = None, target: Any = None) -> self.addresses = addresses region.entrances.append(self) + def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool: + """ + Determines whether this is a valid source transition, that is, whether the entrance + randomizer is allowed to pair it to place any other regions. By default, this is the + same as a reachability check, but can be modified by Entrance implementations to add + other restrictions based on the placement state. + + :param er_state: The current (partial) state of the ongoing entrance randomization + """ + return self.can_reach(er_state.collection_state) + + def can_connect_to(self, other: Entrance, dead_end: bool, er_state: "ERPlacementState") -> bool: + """ + Determines whether a given Entrance is a valid target transition, that is, whether + the entrance randomizer is allowed to pair this Entrance to that Entrance. By default, + only allows connection between entrances of the same type (one ways only go to one ways, + two ways always go to two ways) and prevents connecting an exit to itself in coupled mode. + + :param other: The proposed Entrance to connect to + :param dead_end: Whether the other entrance considered a dead end by Entrance randomization + :param er_state: The current (partial) state of the ongoing entrance randomization + """ + # the implementation of coupled causes issues for self-loops since the reverse entrance will be the + # same as the forward entrance. In uncoupled they are ok. + return self.randomization_type == other.randomization_type and (not er_state.coupled or self.name != other.name) + def __repr__(self): multiworld = self.parent_region.multiworld if self.parent_region else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -975,7 +1088,7 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - entrance_type: ClassVar[Type[Entrance]] = Entrance + entrance_type: ClassVar[type[Entrance]] = Entrance class Register(MutableSequence): region_manager: MultiWorld.RegionManager @@ -1075,7 +1188,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) return entrance.parent_region.get_connecting_entrance(is_main_entrance) def add_locations(self, locations: Dict[str, Optional[int]], - location_type: Optional[Type[Location]] = None) -> None: + location_type: Optional[type[Location]] = None) -> None: """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. @@ -1111,8 +1224,18 @@ def create_exit(self, name: str) -> Entrance: self.exits.append(exit_) return exit_ + def create_er_target(self, name: str) -> Entrance: + """ + Creates and returns an Entrance object as an entrance to this region + + :param name: name of the Entrance being created + """ + entrance = self.entrance_type(self.player, name) + entrance.connect(self) + return entrance + def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], - rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None: + rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]: """ Connects current region to regions in exit dictionary. Passed region names must exist first. @@ -1122,10 +1245,14 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], """ if not isinstance(exits, Dict): exits = dict.fromkeys(exits) - for connecting_region, name in exits.items(): - self.connect(self.multiworld.get_region(connecting_region, self.player), - name, - rules[connecting_region] if rules and connecting_region in rules else None) + return [ + self.connect( + self.multiworld.get_region(connecting_region, self.player), + name, + rules[connecting_region] if rules and connecting_region in rules else None, + ) + for connecting_region, name in exits.items() + ] def __repr__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' @@ -1209,13 +1336,26 @@ def hint_text(self) -> str: class ItemClassification(IntFlag): - filler = 0b0000 # aka trash, as in filler items like ammo, currency etc, - progression = 0b0001 # Item that is logically relevant - useful = 0b0010 # Item that is generally quite useful, but not required for anything logical - trap = 0b0100 # detrimental item - skip_balancing = 0b1000 # should technically never occur on its own - # Item that is logically relevant, but progression balancing should not touch. - # Typically currency or other counted items. + filler = 0b0000 + """ aka trash, as in filler items like ammo, currency etc """ + + progression = 0b0001 + """ Item that is logically relevant. + Protects this item from being placed on excluded or unreachable locations. """ + + useful = 0b0010 + """ Item that is especially useful. + Protects this item from being placed on excluded or unreachable locations. + When combined with another flag like "progression", it means "an especially useful progression item". """ + + trap = 0b0100 + """ Item that is detrimental in some way. """ + + skip_balancing = 0b1000 + """ should technically never occur on its own + Item that is logically relevant, but progression balancing should not touch. + Typically currency or other counted items. """ + progression_skip_balancing = 0b1001 # only progression gets balanced def as_flag(self) -> int: @@ -1264,6 +1404,10 @@ def useful(self) -> bool: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def filler(self) -> bool: + return not (self.advancement or self.useful or self.trap) + @property def excludable(self) -> bool: return not (self.advancement or self.useful) @@ -1386,14 +1530,21 @@ def create_playthrough(self, create_paths: bool = True) -> None: # second phase, sphere 0 removed_precollected: List[Item] = [] - for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): - logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) - multiworld.precollected_items[item.player].remove(item) - multiworld.state.remove(item) - if not multiworld.can_beat_game(): - multiworld.push_precollected(item) - else: - removed_precollected.append(item) + + for precollected_items in multiworld.precollected_items.values(): + # The list of items is mutated by removing one item at a time to determine if each item is required to beat + # the game, and re-adding that item if it was required, so a copy needs to be made before iterating. + for item in precollected_items.copy(): + if not item.advancement: + continue + logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) + precollected_items.remove(item) + multiworld.state.remove(item) + if not multiworld.can_beat_game(): + # Add the item back into `precollected_items` and collect it into `multiworld.state`. + multiworld.push_precollected(item) + else: + removed_precollected.append(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others @@ -1532,7 +1683,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: [f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else [f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) if self.unreachables: - outfile.write('\n\nUnreachable Items:\n\n') + outfile.write('\n\nUnreachable Progression Items:\n\n') outfile.write( '\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) diff --git a/CommonClient.py b/CommonClient.py index 77ed85b5c652..33792f0ed28b 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,7 +23,7 @@ from MultiServer import CommandProcessor from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, - RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType) + RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType) from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os @@ -31,6 +31,7 @@ if typing.TYPE_CHECKING: import kvui + import argparse logger = logging.getLogger("Client") @@ -412,6 +413,7 @@ async def disconnect(self, allow_autoreconnect: bool = False): await self.server.socket.close() if self.server_task is not None: await self.server_task + self.ui.update_hints() async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: """ `msgs` JSON serializable """ @@ -458,6 +460,13 @@ async def send_connect(self, **kwargs: typing.Any) -> None: await self.send_msgs([payload]) await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) + async def check_locations(self, locations: typing.Collection[int]) -> set[int]: + """Send new location checks to the server. Returns the set of actually new locations that were sent.""" + locations = set(locations) & self.missing_locations + if locations: + await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}]) + return locations + async def console_input(self) -> str: if self.ui: self.ui.focus_textinput() @@ -551,7 +560,14 @@ async def shutdown(self): await self.ui_task if self.input_task: self.input_task.cancel() - + + # Hints + def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None: + msg = {"cmd": "UpdateHint", "location": location, "player": finding_player} + if status is not None: + msg["status"] = status + async_start(self.send_msgs([msg]), name="update_hint") + # DataPackage async def prepare_data_package(self, relevant_games: typing.Set[str], remote_date_package_versions: typing.Dict[str, int], @@ -693,8 +709,16 @@ def handle_connection_loss(self, msg: str) -> None: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> typing.Type["kvui.GameManager"]: - """To return the Kivy App class needed for run_gui so it can be overridden before being built""" + def make_gui(self) -> "type[kvui.GameManager]": + """ + To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built + + Common changes are changing `base_title` to update the window title of the client and + updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger. + + ex. `logging_pairs.append(("Foo", "Bar"))` + will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")` + """ from kvui import GameManager class TextManager(GameManager): @@ -710,6 +734,11 @@ def run_gui(self): def run_cli(self): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") @@ -878,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.disconnected_intentionally = True ctx.event_invalid_game() elif 'IncompatibleVersion' in errors: + ctx.disconnected_intentionally = True raise Exception('Server reported your client version as incompatible. ' 'This probably means you have to update.') elif 'InvalidItemsHandling' in errors: @@ -1028,6 +1058,32 @@ def get_base_parser(description: typing.Optional[str] = None): return parser +def handle_url_arg(args: "argparse.Namespace", + parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace": + """ + Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient + If alternate data is required the urlparse response is saved back to args.url if valid + """ + if not args.url: + return args + + url = urllib.parse.urlparse(args.url) + if url.scheme != "archipelago": + if not parser: + parser = get_base_parser() + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + return args + + args.url = url + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + + return args + + def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry @@ -1040,7 +1096,7 @@ async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(TextContext, self).server_auth(password_requested) await self.get_username() - await self.send_connect() + await self.send_connect(game="") def on_package(self, cmd: str, args: dict): if cmd == "Connected": @@ -1069,17 +1125,7 @@ async def main(args): parser.add_argument("url", nargs="?", help="Archipelago connection url") args = parser.parse_args(args) - # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost - if args.url: - url = urllib.parse.urlparse(args.url) - if url.scheme == "archipelago": - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) - else: - parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + args = handle_url_arg(args, parser=parser) # use colorama to display colored text highlighting on windows colorama.init() diff --git a/Fill.py b/Fill.py index 706cca657457..d1773c82139b 100644 --- a/Fill.py +++ b/Fill.py @@ -36,7 +36,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location], item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True, + name: str = "Unknown") -> None: """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. @@ -63,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati placed = 0 while any(reachable_items.values()) and locations: - # grab one item per player - items_to_place = [items.pop() - for items in reachable_items.values() if items] + if one_item_per_player: + # grab one item per player + items_to_place = [items.pop() + for items in reachable_items.values() if items] + else: + next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items]) + items_to_place = [] + if item_pool: + items_to_place.append(reachable_items[next_player].pop()) + for item in items_to_place: for p, pool_item in enumerate(item_pool): if pool_item is item: item_pool.pop(p) break + maximum_exploration_state = sweep_from_pool( base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player) if single_player_placement else None) @@ -226,18 +235,30 @@ def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item], name: str = "Remaining", - move_unplaceable_to_start_inventory: bool = False) -> None: + move_unplaceable_to_start_inventory: bool = False, + check_location_can_fill: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() total = min(len(itempool), len(locations)) placed = 0 + + # Optimisation: Decide whether to do full location.can_fill check (respect excluded), or only check the item rule + if check_location_can_fill: + state = CollectionState(multiworld) + + def location_can_fill_item(location_to_fill: Location, item_to_fill: Item): + return location_to_fill.can_fill(state, item_to_fill, check_access=False) + else: + def location_can_fill_item(location_to_fill: Location, item_to_fill: Item): + return location_to_fill.item_rule(item_to_fill) + while locations and itempool: item_to_place = itempool.pop() spot_to_fill: typing.Optional[Location] = None for i, location in enumerate(locations): - if location.item_rule(item_to_place): + if location_can_fill_item(location, item_to_place): # popping by index is faster than removing by content, spot_to_fill = locations.pop(i) # skipping a scan for the element @@ -258,7 +279,7 @@ def remaining_fill(multiworld: MultiWorld, location.item = None placed_item.location = None - if location.item_rule(item_to_place): + if location_can_fill_item(location, item_to_place): # Add this item to the existing placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) @@ -480,7 +501,14 @@ def mark_for_locking(location: Location): if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority", one_item_per_player=True, allow_partial=True) + + if prioritylocations: + # retry with one_item_per_player off because some priority fills can fail to fill with that optimization + fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry", one_item_per_player=False) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations @@ -509,7 +537,8 @@ def mark_for_locking(location: Location): if progitempool: raise FillError( f"Not enough locations for progression items. " - f"There are {len(progitempool)} more progression items than there are available locations.", + f"There are {len(progitempool)} more progression items than there are available locations.\n" + f"Unfilled locations:\n{multiworld.get_unfilled_locations()}.", multiworld=multiworld, ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) @@ -527,7 +556,7 @@ def mark_for_locking(location: Location): if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " - f"There are {len(excludedlocations)} more excluded locations than filler or trap items.", + f"There are {len(excludedlocations)} more excluded locations than excludable items.", multiworld=multiworld, ) @@ -548,6 +577,26 @@ def mark_for_locking(location: Location): print_data = {"items": items_counter, "locations": locations_counter} logging.info(f"Per-Player counts: {print_data})") + more_locations = locations_counter - items_counter + more_items = items_counter - locations_counter + for player in multiworld.player_ids: + if more_locations[player]: + logging.error( + f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.") + elif more_items[player]: + logging.warning( + f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.") + if unfilled: + raise FillError( + f"Unable to fill all locations.\n" + + f"Unfilled locations({len(unfilled)}): {unfilled}" + ) + else: + logging.warning( + f"Unable to place all items.\n" + + f"Unplaced items({len(unplaced)}): {unplaced}" + ) + def flood_items(multiworld: MultiWorld) -> None: # get items to distribute @@ -978,15 +1027,32 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: multiworld.random.shuffle(items) count = 0 err: typing.List[str] = [] - successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] + successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = [] + claimed_indices: typing.Set[typing.Optional[int]] = set() for item_name in items: - item = multiworld.worlds[player].create_item(item_name) + index_to_delete: typing.Optional[int] = None + if from_pool: + try: + # If from_pool, try to find an existing item with this name & player in the itempool and use it + index_to_delete, item = next( + (i, item) for i, item in enumerate(multiworld.itempool) + if item.player == player and item.name == item_name and i not in claimed_indices + ) + except StopIteration: + warn( + f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.", + placement['force']) + item = multiworld.worlds[player].create_item(item_name) + else: + item = multiworld.worlds[player].create_item(item_name) + for location in reversed(candidates): if (location.address is None) == (item.code is None): # either both None or both not None if not location.item: if location.item_rule(item): if location.can_fill(multiworld.state, item, False): - successful_pairs.append((item, location)) + successful_pairs.append((index_to_delete, item, location)) + claimed_indices.add(index_to_delete) candidates.remove(location) count = count + 1 break @@ -998,6 +1064,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: err.append(f"Cannot place {item_name} into already filled location {location}.") else: err.append(f"Mismatch between {item_name} and {location}, only one is an event.") + if count == maxcount: break if count < placement['count']['min']: @@ -1005,17 +1072,16 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: failed( f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", placement['force']) - for (item, location) in successful_pairs: + + # Sort indices in reverse so we can remove them one by one + successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True) + + for (index, item, location) in successful_pairs: multiworld.push_item(location, item, collect=False) location.locked = True logging.debug(f"Plando placed {item} at {location}") - if from_pool: - try: - multiworld.itempool.remove(item) - except ValueError: - warn( - f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", - placement['force']) + if index is not None: # If this item is from_pool and was found in the pool, remove it. + multiworld.itempool.pop(index) except Exception as e: raise Exception( diff --git a/Generate.py b/Generate.py index bc359a203da7..b057db25a311 100644 --- a/Generate.py +++ b/Generate.py @@ -42,7 +42,9 @@ def mystery_argparse(): help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) - parser.add_argument('--log_level', default='info', help='Sets log level') + parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level') + parser.add_argument('--log_time', help="Add timestamps to STDOUT", + default=defaults.logtime, action='store_true') parser.add_argument("--csv_output", action="store_true", help="Output rolled player options to csv (made for async multiworld).") parser.add_argument("--plando", default=defaults.plando_options, @@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: seed = get_seed(args.seed) - Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time) random.seed(seed) seed_name = get_seed_name(random) @@ -114,7 +116,14 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: - weights_cache[fname] = read_weights_yamls(path) + weights_for_file = [] + for doc_idx, yaml in enumerate(read_weights_yamls(path)): + if yaml is None: + logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}") + else: + weights_for_file.append(yaml) + weights_cache[fname] = tuple(weights_for_file) + except Exception as e: raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e @@ -431,7 +440,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if "linked_options" in weights: weights = roll_linked_options(weights) - valid_keys = set() + valid_keys = {"triggers"} if "triggers" in weights: weights = roll_triggers(weights, weights["triggers"], valid_keys) @@ -453,6 +462,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] @@ -486,15 +499,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in world_type.options_dataclass.type_hints.items(): handle_option(ret, game_weights, option_key, option, plando_options) valid_keys.add(option_key) - for option_key in game_weights: - if option_key in {"triggers", *valid_keys}: - continue - logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") + + # TODO remove plando_items after moving it to the options system + valid_keys.add("plando_items") if PlandoOptions.items in plando_options: ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) if ret.game == "A Link to the Past": + # TODO there are still more LTTP options not on the options system + valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"} roll_alttp_settings(ret, game_weights) + # log a warning for options within a game section that aren't determined as valid + for option_key in game_weights: + if option_key in valid_keys: + continue + logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers " + f"for player {ret.name}.") + return ret diff --git a/LICENSE b/LICENSE index 40716cff4275..60d31b7b7de8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2017 LLCoolDave -Copyright (c) 2022 Berserker66 +Copyright (c) 2025 Berserker66 Copyright (c) 2022 CaitSith2 Copyright (c) 2021 LegendaryLinux diff --git a/Launcher.py b/Launcher.py index 2620f786a54b..22c0944ab1a4 100644 --- a/Launcher.py +++ b/Launcher.py @@ -22,16 +22,15 @@ from shutil import which from typing import Callable, Optional, Sequence, Tuple, Union -import Utils -import settings -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths - if __name__ == "__main__": import ModuleUpdate ModuleUpdate.update() -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ - is_windows, is_macos, is_linux +import settings +import Utils +from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename, + user_path) +from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type def open_host_yaml(): @@ -127,12 +126,13 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: elif component.display_name == "Text Client": text_client_component = component - from kvui import App, Button, BoxLayout, Label, Clock, Window + if client_component is None: + run_component(text_client_component, *launch_args) + return - class Popup(App): - timer_label: Label - remaining_time: Optional[int] + from kvui import App, Button, BoxLayout, Label, Window + class Popup(App): def __init__(self): self.title = "Connect to Multiworld" self.icon = r"data/icon.png" @@ -140,47 +140,29 @@ def __init__(self): def build(self): layout = BoxLayout(orientation="vertical") + layout.add_widget(Label(text="Select client to open and connect with.")) + button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) - if client_component is None: - self.remaining_time = 7 - label_text = (f"A game client able to parse URIs was not detected for {game}.\n" - f"Launching Text Client in 7 seconds...") - self.timer_label = Label(text=label_text) - layout.add_widget(self.timer_label) - Clock.schedule_interval(self.update_label, 1) - else: - layout.add_widget(Label(text="Select client to open and connect with.")) - button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) - - text_client_button = Button( - text=text_client_component.display_name, - on_release=lambda *args: run_component(text_client_component, *launch_args) - ) - button_row.add_widget(text_client_button) + text_client_button = Button( + text=text_client_component.display_name, + on_release=lambda *args: run_component(text_client_component, *launch_args) + ) + button_row.add_widget(text_client_button) - game_client_button = Button( - text=client_component.display_name, - on_release=lambda *args: run_component(client_component, *launch_args) - ) - button_row.add_widget(game_client_button) + game_client_button = Button( + text=client_component.display_name, + on_release=lambda *args: run_component(client_component, *launch_args) + ) + button_row.add_widget(game_client_button) - layout.add_widget(button_row) + layout.add_widget(button_row) return layout - def update_label(self, dt): - if self.remaining_time > 1: - # countdown the timer and string replace the number - self.remaining_time -= 1 - self.timer_label.text = self.timer_label.text.replace( - str(self.remaining_time + 1), str(self.remaining_time) - ) - else: - # our timer is finished so launch text client and close down - run_component(text_client_component, *launch_args) - Clock.unschedule(self.update_label) - App.get_running_app().stop() - Window.close() + def _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) Popup().run() @@ -242,9 +224,8 @@ def launch(exe, in_terminal=False): def run_gui(): - from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget + from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kivy.core.window import Window - from kivy.uix.image import AsyncImage from kivy.uix.relativelayout import RelativeLayout class Launcher(App): @@ -277,8 +258,8 @@ def build_button(component: Component) -> Widget: button.component = component button.bind(on_release=self.component_action) if component.icon != "icon": - image = AsyncImage(source=icon_paths[component.icon], - size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + image = ApAsyncImage(source=icon_paths[component.icon], + size=(38, 38), size_hint=(None, 1), pos=(5, 0)) box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout.add_widget(button) box_layout.add_widget(image) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index 298788098d9e..e2e16922fa95 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -235,7 +235,7 @@ async def async_read_memory_safe(self, address, size=1): def check_command_response(self, command: str, response: bytes): if command == "VERSION": - ok = re.match("\d+\.\d+\.\d+", response.decode('ascii')) is not None + ok = re.match(r"\d+\.\d+\.\d+", response.decode('ascii')) is not None else: ok = response.startswith(command.encode()) if not ok: @@ -560,6 +560,10 @@ async def server_auth(self, password_requested: bool = False): while self.client.auth == None: await asyncio.sleep(0.1) + + # Just return if we're closing + if self.exit_event.is_set(): + return self.auth = self.client.auth await self.send_connect() diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 7e33a3d5efe8..963557e8da81 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -33,10 +33,15 @@ WINDOW_MIN_WIDTH = 425 class AdjusterWorld(object): + class AdjusterSubWorld(object): + def __init__(self, random): + self.random = random + def __init__(self, sprite_pool): import random self.sprite_pool = {1: sprite_pool} self.per_slot_randoms = {1: random} + self.worlds = {1: self.AdjusterSubWorld(random)} class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): diff --git a/Main.py b/Main.py index 4008ca5e9017..d0e7a7f8793d 100644 --- a/Main.py +++ b/Main.py @@ -148,50 +148,44 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: multiworld.worlds[1].options.non_local_items.value = set() multiworld.worlds[1].options.local_items.value = set() - + + AutoWorld.call_all(multiworld, "connect_entrances") AutoWorld.call_all(multiworld, "generate_basic") # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. - if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): - new_items: List[Item] = [] - old_items: List[Item] = [] - depletion_pool: Dict[int, Dict[str, int]] = { - player: getattr(multiworld.worlds[player].options, - "start_inventory_from_pool", - StartInventoryPool({})).value.copy() - for player in multiworld.player_ids - } - for player, items in depletion_pool.items(): - player_world: AutoWorld.World = multiworld.worlds[player] - for count in items.values(): - for _ in range(count): - new_items.append(player_world.create_filler()) - target: int = sum(sum(items.values()) for items in depletion_pool.values()) - for i, item in enumerate(multiworld.itempool): + fallback_inventory = StartInventoryPool({}) + depletion_pool: Dict[int, Dict[str, int]] = { + player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy() + for player in multiworld.player_ids + } + target_per_player = { + player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items + } + + if target_per_player: + new_itempool: List[Item] = [] + + # Make new itempool with start_inventory_from_pool items removed + for item in multiworld.itempool: if depletion_pool[item.player].get(item.name, 0): - target -= 1 depletion_pool[item.player][item.name] -= 1 - # quick abort if we have found all items - if not target: - old_items.extend(multiworld.itempool[i+1:]) - break else: - old_items.append(item) - - # leftovers? - if target: - for player, remaining_items in depletion_pool.items(): - remaining_items = {name: count for name, count in remaining_items.items() if count} - if remaining_items: - logger.warning(f"{multiworld.get_player_name(player)}" - f" is trying to remove items from their pool that don't exist: {remaining_items}") - # find all filler we generated for the current player and remove until it matches - removables = [item for item in new_items if item.player == player] - for _ in range(sum(remaining_items.values())): - new_items.remove(removables.pop()) - assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items + old_items + new_itempool.append(item) + + # Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool + for player, target in target_per_player.items(): + unfound_items = {item: count for item, count in depletion_pool[player].items() if count} + + if unfound_items: + player_name = multiworld.get_player_name(player) + logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}") + + needed_items = target_per_player[player] - sum(unfound_items.values()) + new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)] + + assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change." + multiworld.itempool[:] = new_itempool multiworld.link_items() @@ -249,6 +243,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No def write_multidata(): import NetUtils + from NetUtils import HintStatus slot_data = {} client_versions = {} games = {} @@ -273,10 +268,10 @@ def write_multidata(): for slot in multiworld.player_ids: slot_data[slot] = multiworld.worlds[slot].fill_slot_data() - def precollect_hint(location): + def precollect_hint(location: Location, auto_status: HintStatus): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, entrance, location.item.flags) + location.item.code, False, entrance, location.item.flags, auto_status) precollected_hints[location.player].add(hint) if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) @@ -289,19 +284,22 @@ def precollect_hint(location): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ "location.address should then also be None. Location: " \ - f" {location}" + f" {location}, Item: {location.item}" assert location.address not in locations_data[location.player], ( f"Locations with duplicate address. {location} and " f"{locations_data[location.player][location.address]}") locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags + auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY if location.name in multiworld.worlds[location.player].options.start_location_hints: - precollect_hint(location) + if not location.item.trap: # Unspecified status for location hints, except traps + auto_status = HintStatus.HINT_UNSPECIFIED + precollect_hint(location, auto_status) elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: - precollect_hint(location) + precollect_hint(location, auto_status) elif any([location.item.name in multiworld.worlds[player].options.start_hints for player in multiworld.groups.get(location.item.player, {}).get("players", [])]): - precollect_hint(location) + precollect_hint(location, auto_status) # embedded data package data_package = { @@ -313,11 +311,10 @@ def precollect_hint(location): # get spheres -> filter address==None -> skip empty spheres: List[Dict[int, Set[int]]] = [] - for sphere in multiworld.get_spheres(): + for sphere in multiworld.get_sendable_spheres(): current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) for sphere_location in sphere: - if type(sphere_location.address) is int: - current_sphere[sphere_location.player].add(sphere_location.address) + current_sphere[sphere_location.player].add(sphere_location.address) if current_sphere: spheres.append(dict(current_sphere)) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index f49182bb7863..04cf25ea5594 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,15 @@ import warnings -if sys.version_info < (3, 8, 6): - raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") +if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11): + # Official micro version updates. This should match the number in docs/running from source.md. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.") +elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15): + # There are known security issues, but no easy way to install fixed versions on Windows for testing. + warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.") +elif sys.version_info < (3, 10, 1): + # Other platforms may get security backports instead of micro updates, so the number is unreliable. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) diff --git a/MultiServer.py b/MultiServer.py index 764b56362ecc..a310808b3aec 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -28,9 +28,11 @@ if typing.TYPE_CHECKING: import ssl + from NetUtils import ServerConnection -import websockets import colorama +import websockets +from websockets.extensions.permessage_deflate import PerMessageDeflate try: # ponyorm is a requirement for webhost, not default server, so may not be importable from pony.orm.dbapiprovider import OperationalError @@ -41,7 +43,8 @@ import Utils from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ - SlotType, LocationStore + SlotType, LocationStore, Hint, HintStatus +from BaseClasses import ItemClassification min_client_version = Version(0, 1, 6) colorama.init() @@ -118,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int: class Client(Endpoint): version = Version(0, 0, 0) - tags: typing.List[str] = [] + tags: typing.List[str] remote_items: bool remote_start_inventory: bool no_items: bool no_locations: bool + no_text: bool - def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context): + def __init__(self, socket: "ServerConnection", ctx: Context) -> None: super().__init__(socket) self.auth = False self.team = None @@ -174,6 +178,7 @@ class Context: "compatibility": int} # team -> slot id -> list of clients authenticated to slot. clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] + endpoints: list[Client] locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]] hints_used: typing.Dict[typing.Tuple[int, int], int] @@ -228,7 +233,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.hint_cost = hint_cost self.location_check_points = location_check_points self.hints_used = collections.defaultdict(int) - self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) + self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set) self.release_mode: str = release_mode self.remaining_mode: str = remaining_mode self.collect_mode: str = collect_mode @@ -363,18 +368,28 @@ async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint] return True def broadcast_all(self, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in self.endpoints + if endpoint.auth and not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast_text_all(self, text: str, additional_arguments: dict = {}): self.logger.info("Notice (all): %s" % text) self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) def broadcast_team(self, team: int, msgs: typing.List[dict]): - msgs = self.dumper(msgs) - endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values())) - async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) + msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs) + data = self.dumper(msgs) + endpoints = ( + endpoint + for endpoint in itertools.chain.from_iterable(self.clients[team].values()) + if not (msg_is_text and endpoint.no_text) + ) + async_start(self.broadcast_send_encoded_msgs(endpoints, data)) def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]): msgs = self.dumper(msgs) @@ -388,13 +403,13 @@ async def disconnect(self, endpoint: Client): await on_client_disconnected(self, endpoint) def notify_client(self, client: Client, text: str, additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}])) def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}): - if not client.auth: + if not client.auth or client.no_text: return async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} @@ -443,7 +458,7 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A self.slot_info = decoded_obj["slot_info"] self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} - self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items() + self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items() if slot_info.type == SlotType.group} self.clients = {0: {}} @@ -656,13 +671,29 @@ def get_hint_cost(self, slot): return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 - def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): + def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None, + changed: typing.Optional[typing.Set[team_slot]] = None) -> None: + """Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot + will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot) + pair that has at least one hint modified will be added to the set. + """ for hint_team, hint_slot in self.hints: - if (team is None or team == hint_team) and (slot is None or slot == hint_slot): - self.hints[hint_team, hint_slot] = { - hint.re_check(self, hint_team) for hint in - self.hints[hint_team, hint_slot] - } + if team != hint_team and team is not None: + continue # Check specified team only, all if team is None + if slot != hint_slot and slot is not None: + continue # Check specified slot only, all if slot is None + new_hints: typing.Set[Hint] = set() + for hint in self.hints[hint_team, hint_slot]: + new_hint = hint.re_check(self, hint_team) + new_hints.add(new_hint) + if hint == new_hint: + continue + for player in self.slot_set(hint.receiving_player) | {hint.finding_player}: + if changed is not None: + changed.add((hint_team,player)) + if slot is not None and slot != player: + self.replace_hint(hint_team, player, hint, new_hint) + self.hints[hint_team, hint_slot] = new_hints def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) @@ -711,7 +742,7 @@ def get_aliased_name(self, team: int, slot: int): else: return self.player_names[team, slot] - def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, + def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False, recipients: typing.Sequence[int] = None): """Send and remember hints.""" if only_new: @@ -726,7 +757,8 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b concerns[player].append(data) if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) - # remember hints in all cases + + # only remember hints that were not already found at the time of creation if not hint.found: # since hints are bidirectional, finding player and receiving player, # we can check once if hint already exists @@ -742,13 +774,24 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b self.on_new_hint(team, slot) for slot, hint_data in concerns.items(): if recipients is None or slot in recipients: - clients = self.clients[team].get(slot) + clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, [])) if not clients: continue client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)] for client in clients: async_start(self.send_msgs(client, client_hints)) + def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]: + for hint in self.hints[team, finding_player]: + if hint.location == seeked_location and hint.finding_player == finding_player: + return hint + return None + + def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None: + if old_hint in self.hints[team, slot]: + self.hints[team, slot].remove(old_hint) + self.hints[team, slot].add(new_hint) + # "events" def on_goal_achieved(self, client: Client): @@ -790,7 +833,7 @@ def update_aliases(ctx: Context, team: int): async_start(ctx.send_encoded_msgs(client, cmd)) -async def server(websocket, path: str = "/", ctx: Context = None): +async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None: client = Client(websocket, ctx) ctx.endpoints.append(client) @@ -881,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client): "If your client supports it, " "you may have additional local commands you can list with /help.", {"type": "Tutorial"}) + if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions): + ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! " + "It may stop working in the future. If you are a player, please report this to the " + "client's developer.") ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) @@ -947,9 +994,13 @@ def get_status_string(ctx: Context, team: int, tag: str): tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" - goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." + status_text = ( + " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else + " and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else + "." + ) text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{tag_text}{goal_text} {completion_text}" + f"{tag_text}{status_text} {completion_text}" return text @@ -1027,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], count_activity: bool = True): + slot_locations = ctx.locations[slot] new_locations = set(locations) - ctx.location_checks[team, slot] - new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata + new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata if new_locations: if count_activity: ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) + + sortable: list[tuple[int, int, int, int]] = [] for location in new_locations: - item_id, target_player, flags = ctx.locations[slot][location] + # extract all fields to avoid runtime overhead in LocationStore + item_id, target_player, flags = slot_locations[location] + # sort/group by receiver and item + sortable.append((target_player, item_id, location, flags)) + + info_texts: list[dict[str, typing.Any]] = [] + for target_player, item_id, location, flags in sorted(sortable): new_item = NetworkItem(item_id, location, slot, flags) send_items_to(ctx, team, target_player, new_item) ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) - info_text = json_format_send_event(new_item, target_player) - ctx.broadcast_team(team, [info_text]) + if len(info_texts) >= 140: + # split into chunks that are close to compression window of 64K but not too big on the wire + # (roughly 1300-2600 bytes after compression depending on repetitiveness) + ctx.broadcast_team(team, info_texts) + info_texts.clear() + info_texts.append(json_format_send_event(new_item, target_player)) + ctx.broadcast_team(team, info_texts) + del info_texts + del sortable ctx.location_checks[team, slot] |= new_locations send_new_items(ctx) @@ -1050,14 +1117,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi "hint_points": get_slot_points(ctx, team, slot), "checked_locations": new_locations, # send back new checks only }]) - old_hints = ctx.hints[team, slot].copy() - ctx.recheck_hints(team, slot) - if old_hints != ctx.hints[team, slot]: - ctx.on_changed_hints(team, slot) + updated_slots: typing.Set[tuple[int, int]] = set() + ctx.recheck_hints(team, slot, updated_slots) + for hint_team, hint_slot in updated_slots: + ctx.on_changed_hints(hint_team, hint_slot) ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \ + -> typing.List[Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): @@ -1067,31 +1135,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] for finding_player, location_id, item_id, receiving_player, item_flags \ in ctx.locations.find_item(slots, seeked_item_id): - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance, - item_flags)) + prev_hint = ctx.get_hint(team, finding_player, location_id) + if prev_hint: + hints.append(prev_hint) + else: + found = location_id in ctx.location_checks[team, finding_player] + entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance, + item_flags, new_status)) return hints -def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: +def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \ + -> typing.List[Hint]: seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] - return collect_hint_location_id(ctx, team, slot, seeked_location) + return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status) -def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]: +def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \ + -> typing.List[Hint]: + prev_hint = ctx.get_hint(team, slot, seeked_location) + if prev_hint: + return [prev_hint] result = ctx.locations[slot].get(seeked_location, (None, None, None)) if any(result): item_id, receiving_player, item_flags = result found = seeked_location in ctx.location_checks[team, slot] entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") - return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)] + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, + new_status)] return [] -def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +def format_hint(ctx: Context, team: int, hint: Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ @@ -1099,7 +1194,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: if hint.entrance: text += f" at {hint.entrance}" - return text + (". (found)" if hint.found else ".") + + return text + ". " + status_names.get(hint.status, "(unknown)") def json_format_send_event(net_item: NetworkItem, receiving_player: int): @@ -1503,7 +1599,7 @@ def _cmd_getitem(self, item_name: str) -> bool: def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) cost = self.ctx.get_hint_cost(self.client.slot) - + auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -1529,9 +1625,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] elif not for_location: - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: - hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: game = self.ctx.games[self.client.slot] @@ -1551,16 +1647,16 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: hints = [] for item_name in self.ctx.item_name_groups[game][hint_name]: if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) elif hint_name in self.ctx.location_name_groups[game]: # location group name hints = [] for loc_name in self.ctx.location_name_groups[game][hint_name]: if loc_name in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status)) else: # location name - hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) else: self.output(response) @@ -1725,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): ctx.clients[team][slot].append(client) client.version = args['version'] client.tags = args['tags'] - client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags + # set NoText for old PopTracker clients that predate the tag to save traffic + client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1)) connected_packet = { "cmd": "Connected", "team": client.team, "slot": client.slot, @@ -1798,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): client.tags = args["tags"] if set(old_tags) != set(client.tags): client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags + client.no_text = "NoText" in client.tags or ( + "PopTracker" in client.tags and client.version < (0, 5, 1) + ) ctx.broadcast_text_all( f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " f"from {old_tags} to {client.tags}.", @@ -1826,19 +1927,63 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): for location in args["locations"]: if type(location) is not int: await ctx.send_msgs(client, - [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'Locations has to be a list of integers', "original_cmd": cmd}]) return target_item, target_player, flags = ctx.locations[client.slot][location] if create_as_hint: - hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) + hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location, + HintStatus.HINT_UNSPECIFIED)) locs.append(NetworkItem(target_item, location, target_player, flags)) ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) if locs and create_as_hint: ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) - + + elif cmd == 'UpdateHint': + location = args["location"] + player = args["player"] + status = args["status"] + if not isinstance(player, int) or not isinstance(location, int) \ + or (status is not None and not isinstance(status, int)): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint', + "original_cmd": cmd}]) + return + hint = ctx.get_hint(client.team, player, location) + if not hint: + return # Ignored safely + if client.slot not in ctx.slot_set(hint.receiving_player): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission', + "original_cmd": cmd}]) + return + new_hint = hint + if status is None: + return + try: + status = HintStatus(status) + except ValueError: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Invalid Status', "original_cmd": cmd}]) + return + if status == HintStatus.HINT_FOUND: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}]) + return + new_hint = new_hint.re_prioritize(ctx, status) + if hint == new_hint: + return + ctx.replace_hint(client.team, hint.finding_player, hint, new_hint) + ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint) + ctx.save() + ctx.on_changed_hints(client.team, hint.finding_player) + ctx.on_changed_hints(client.team, hint.receiving_player) + elif cmd == 'StatusUpdate': update_client_status(ctx, client, args["status"]) @@ -1886,6 +2031,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): args["cmd"] = "SetReply" value = ctx.stored_data.get(args["key"], args.get("default", 0)) args["original_value"] = copy.copy(value) + args["slot"] = client.slot for operation in args["operations"]: func = modify_functions[operation["operation"]] value = func(value, operation["value"]) @@ -2143,9 +2289,9 @@ def _cmd_hint(self, player_name: str, *item_name: str) -> bool: hints = [] for item_name_from_group in self.ctx.item_name_groups[game][item]: if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY)) else: # item name or id - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY) if hints: self.ctx.notify_hints(team, hints) @@ -2179,14 +2325,17 @@ def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: if usable: if isinstance(location, int): - hints = collect_hint_location_id(self.ctx, team, slot, location) + hints = collect_hint_location_id(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: hints = [] for loc_name_from_group in self.ctx.location_name_groups[game][location]: if loc_name_from_group in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) + hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group, + HintStatus.HINT_UNSPECIFIED)) else: - hints = collect_hint_location_name(self.ctx, team, slot, location) + hints = collect_hint_location_name(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) if hints: self.ctx.notify_hints(team, hints) else: @@ -2276,6 +2425,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--cert_key', help="Path to SSL Certificate Key file") parser.add_argument('--loglevel', default=defaults["loglevel"], choices=['debug', 'info', 'warning', 'error', 'critical']) + parser.add_argument('--logtime', help="Add timestamps to STDOUT", + default=defaults["logtime"], action='store_true') parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') @@ -2356,7 +2507,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte async def main(args: argparse.Namespace): - Utils.init_logging("Server", loglevel=args.loglevel.lower()) + Utils.init_logging(name="Server", + loglevel=args.loglevel.lower(), + add_timestamp=args.logtime) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, diff --git a/NetUtils.py b/NetUtils.py index 4776b228db17..f2ae2a63a056 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -5,11 +5,20 @@ import warnings from json import JSONEncoder, JSONDecoder -import websockets +if typing.TYPE_CHECKING: + from websockets import WebSocketServerProtocol as ServerConnection from Utils import ByValue, Version +class HintStatus(ByValue, enum.IntEnum): + HINT_UNSPECIFIED = 0 + HINT_NO_PRIORITY = 10 + HINT_AVOID = 20 + HINT_PRIORITY = 30 + HINT_FOUND = 40 + + class JSONMessagePart(typing.TypedDict, total=False): text: str # optional @@ -19,6 +28,8 @@ class JSONMessagePart(typing.TypedDict, total=False): player: int # if type == item indicates item flags flags: int + # if type == hint_status + hint_status: HintStatus class ClientStatus(ByValue, enum.IntEnum): @@ -141,7 +152,7 @@ def _object_hook(o: typing.Any) -> typing.Any: class Endpoint: - socket: websockets.WebSocketServerProtocol + socket: "ServerConnection" def __init__(self, socket): self.socket = socket @@ -184,6 +195,7 @@ class JSONTypes(str, enum.Enum): location_name = "location_name" location_id = "location_id" entrance_name = "entrance_name" + hint_status = "hint_status" class JSONtoTextParser(metaclass=HandlerMeta): @@ -224,7 +236,7 @@ def _handle_text(self, node: JSONMessagePart): def _handle_player_id(self, node: JSONMessagePart): player = int(node["text"]) - node["color"] = 'magenta' if player == self.ctx.slot else 'yellow' + node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow' node["text"] = self.ctx.player_names[player] return self._handle_color(node) @@ -265,6 +277,10 @@ def _handle_entrance_name(self, node: JSONMessagePart): node["color"] = 'blue' return self._handle_color(node) + def _handle_hint_status(self, node: JSONMessagePart): + node["color"] = status_colors.get(node["hint_status"], "red") + return self._handle_color(node) + class RawJSONtoTextParser(JSONtoTextParser): def _handle_color(self, node: JSONMessagePart): @@ -297,6 +313,27 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +status_colors: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "green", + HintStatus.HINT_UNSPECIFIED: "white", + HintStatus.HINT_NO_PRIORITY: "slateblue", + HintStatus.HINT_AVOID: "salmon", + HintStatus.HINT_PRIORITY: "plum", +} + + +def add_json_hint_status(parts: list, hint_status: HintStatus, text: typing.Optional[str] = None, **kwargs): + parts.append({"text": text if text != None else status_names.get(hint_status, "(unknown)"), + "hint_status": hint_status, "type": JSONTypes.hint_status, **kwargs}) + + class Hint(typing.NamedTuple): receiving_player: int finding_player: int @@ -305,14 +342,21 @@ class Hint(typing.NamedTuple): found: bool entrance: str = "" item_flags: int = 0 + status: HintStatus = HintStatus.HINT_UNSPECIFIED def re_check(self, ctx, team) -> Hint: - if self.found: + if self.found and self.status == HintStatus.HINT_FOUND: return self found = self.location in ctx.location_checks[team, self.finding_player] if found: - return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance, - self.item_flags) + return self._replace(found=found, status=HintStatus.HINT_FOUND) + return self + + def re_prioritize(self, ctx, status: HintStatus) -> Hint: + if self.found and status != HintStatus.HINT_FOUND: + status = HintStatus.HINT_FOUND + if status != self.status: + return self._replace(status=status) return self def __hash__(self): @@ -334,10 +378,7 @@ def as_network_message(self) -> dict: else: add_json_text(parts, "'s World") add_json_text(parts, ". ") - if self.found: - add_json_text(parts, "(found)", type="color", color="green") - else: - add_json_text(parts, "(not found)", type="color", color="red") + add_json_hint_status(parts, self.status) return {"cmd": "PrintJSON", "data": parts, "type": "Hint", "receiving": self.receiving_player, @@ -383,6 +424,8 @@ def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int] checked = state[team, slot] if not checked: # This optimizes the case where everyone connects to a fresh game at the same time. + if slot not in self: + raise KeyError(slot) return [] return [location_id for location_id in self[slot] if diff --git a/OoTAdjuster.py b/OoTAdjuster.py index 9519b191e704..1581d6539825 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -1,7 +1,6 @@ import tkinter as tk import argparse import logging -import random import os import zipfile from itertools import chain @@ -197,7 +196,6 @@ def set_icon(window): def adjust(args): # Create a fake multiworld and OOTWorld to use as a base multiworld = MultiWorld(1) - multiworld.per_slot_randoms = {1: random} ootworld = OOTWorld(multiworld, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): diff --git a/Options.py b/Options.py index aa6f175fa58d..49e82069ee8d 100644 --- a/Options.py +++ b/Options.py @@ -15,7 +15,7 @@ from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str, output_path +from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: from BaseClasses import MultiWorld, PlandoOptions @@ -137,7 +137,7 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): If this is False, the docstring is instead interpreted as plain text, and displayed as-is on the WebHost with whitespace preserved. - If this is None, it inherits the value of `World.rich_text_options_doc`. For + If this is None, it inherits the value of `WebWorld.rich_text_options_doc`. For backwards compatibility, this defaults to False, but worlds are encouraged to set it to True and use reStructuredText for their Option documentation. @@ -496,7 +496,7 @@ class TextChoice(Choice): def __init__(self, value: typing.Union[str, int]): assert isinstance(value, str) or isinstance(value, int), \ - f"{value} is not a valid option for {self.__class__.__name__}" + f"'{value}' is not a valid option for '{self.__class__.__name__}'" self.value = value @property @@ -617,17 +617,17 @@ def validate_plando_bosses(cls, options: typing.List[str]) -> None: used_locations.append(location) used_bosses.append(boss) if not cls.valid_boss_name(boss): - raise ValueError(f"{boss.title()} is not a valid boss name.") + raise ValueError(f"'{boss.title()}' is not a valid boss name.") if not cls.valid_location_name(location): - raise ValueError(f"{location.title()} is not a valid boss location name.") + raise ValueError(f"'{location.title()}' is not a valid boss location name.") if not cls.can_place_boss(boss, location): - raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.") + raise ValueError(f"'{location.title()}' is not a valid location for {boss.title()} to be placed.") else: if cls.duplicate_bosses: if not cls.valid_boss_name(option): - raise ValueError(f"{option} is not a valid boss name.") + raise ValueError(f"'{option}' is not a valid boss name.") else: - raise ValueError(f"{option.title()} is not formatted correctly.") + raise ValueError(f"'{option.title()}' is not formatted correctly.") @classmethod def can_place_boss(cls, boss: str, location: str) -> bool: @@ -689,9 +689,9 @@ def from_text(cls, text: str) -> Range: @classmethod def weighted_range(cls, text) -> Range: if text == "random-low": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start)) + return cls(cls.triangular(cls.range_start, cls.range_end, 0.0)) elif text == "random-high": - return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end)) + return cls(cls.triangular(cls.range_start, cls.range_end, 1.0)) elif text == "random-middle": return cls(cls.triangular(cls.range_start, cls.range_end)) elif text.startswith("random-range-"): @@ -717,11 +717,11 @@ def custom_range(cls, text) -> Range: f"{random_range[0]}-{random_range[1]} is outside allowed range " f"{cls.range_start}-{cls.range_end} for option {cls.__name__}") if text.startswith("random-range-low"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[0])) + return cls(cls.triangular(random_range[0], random_range[1], 0.0)) elif text.startswith("random-range-middle"): return cls(cls.triangular(random_range[0], random_range[1])) elif text.startswith("random-range-high"): - return cls(cls.triangular(random_range[0], random_range[1], random_range[1])) + return cls(cls.triangular(random_range[0], random_range[1], 1.0)) else: return cls(random.randint(random_range[0], random_range[1])) @@ -739,8 +739,16 @@ def __str__(self) -> str: return str(self.value) @staticmethod - def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int: - return int(round(random.triangular(lower, end, tri), 0)) + def triangular(lower: int, end: int, tri: float = 0.5) -> int: + """ + Integer triangular distribution for `lower` inclusive to `end` inclusive. + + Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined. + """ + # Use the continuous range [lower, end + 1) to produce an integer result in [lower, end]. + # random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even + # when a != b, so ensure the result is never more than `end`. + return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower)) class NamedRange(Range): @@ -754,7 +762,7 @@ def __init__(self, value: int) -> None: elif value > self.range_end and value not in self.special_range_names.values(): raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + f"and is also not one of the supported named special values: {self.special_range_names}") - + # See docstring for key in self.special_range_names: if key != key.lower(): @@ -817,18 +825,21 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P for item_name in self.value: if item_name not in world.item_names: picks = get_fuzzy_results(item_name, world.item_names, limit=1) - raise Exception(f"Item {item_name} from option {self} " - f"is not a valid item name from {world.game}. " + raise Exception(f"Item '{item_name}' from option '{self}' " + f"is not a valid item name from '{world.game}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") elif self.verify_location_name: for location_name in self.value: if location_name not in world.location_names: picks = get_fuzzy_results(location_name, world.location_names, limit=1) - raise Exception(f"Location {location_name} from option {self} " - f"is not a valid location name from {world.game}. " + raise Exception(f"Location '{location_name}' from option '{self}' " + f"is not a valid location name from '{world.game}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + def __iter__(self) -> typing.Iterator[typing.Any]: + return self.value.__iter__() + class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): default = {} supports_weighting = False @@ -860,6 +871,8 @@ class ItemDict(OptionDict): verify_item_name = True def __init__(self, value: typing.Dict[str, int]): + if any(item_count is None for item_count in value.values()): + raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") if any(item_count < 1 for item_count in value.values()): raise Exception("Cannot have non-positive item counts.") super(ItemDict, self).__init__(value) @@ -1106,11 +1119,11 @@ def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnecti used_entrances.append(entrance) used_exits.append(exit) if not cls.validate_entrance_name(entrance): - raise ValueError(f"{entrance.title()} is not a valid entrance.") + raise ValueError(f"'{entrance.title()}' is not a valid entrance.") if not cls.validate_exit_name(exit): - raise ValueError(f"{exit.title()} is not a valid exit.") + raise ValueError(f"'{exit.title()}' is not a valid exit.") if not cls.can_connect(entrance, exit): - raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + raise ValueError(f"Connection between '{entrance.title()}' and '{exit.title()}' is invalid.") @classmethod def from_any(cls, data: PlandoConFromAnyType) -> Self: @@ -1175,7 +1188,7 @@ def __len__(self) -> int: class Accessibility(Choice): """ Set rules for reachability of your items/locations. - + **Full:** ensure everything can be reached and acquired. **Minimal:** ensure what is needed to reach your goal can be acquired. @@ -1193,7 +1206,7 @@ class Accessibility(Choice): class ItemsAccessibility(Accessibility): """ Set rules for reachability of your items/locations. - + **Full:** ensure everything can be reached and acquired. **Minimal:** ensure what is needed to reach your goal can be acquired. @@ -1244,12 +1257,16 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: + def as_dict(self, + *option_names: str, + casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake", + toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]: """ Returns a dictionary of [str, Option.value] :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` + :param toggles_as_bools: whether toggle options should be output as bools instead of strings """ assert option_names, "options.as_dict() was used without any option names." option_results = {} @@ -1271,6 +1288,8 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, value = getattr(self, option_name).value if isinstance(value, set): value = sorted(value) + elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle): + value = bool(value) option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") @@ -1368,8 +1387,8 @@ def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, picks_group = get_fuzzy_results(item_name, world.item_name_groups.keys(), limit=1) picks_group = f" or '{picks_group[0][0]}' ({picks_group[0][1]}% sure)" if allow_item_groups else "" - raise Exception(f"Item {item_name} from item link {item_link} " - f"is not a valid item from {world.game} for {pool_name}. " + raise Exception(f"Item '{item_name}' from item link '{item_link}' " + f"is not a valid item from '{world.game}' for '{pool_name}'. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure){picks_group}") if allow_item_groups: pool |= world.item_name_groups.get(item_name, {item_name}) @@ -1460,22 +1479,26 @@ class OptionGroup(typing.NamedTuple): def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: """Generates and returns a dictionary for the option groups of a specified world.""" - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - # add a default option group for uncategorized options to get thrown into - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if visibility_level & option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()} - # if the world doesn't have any ungrouped options, this group will be empty so just remove it - if not grouped_options["Game Options"]: - del grouped_options["Game Options"] + ordered_groups = {group.name: group.options for group in world.web.option_groups} - return grouped_options + # add a default option group for uncategorized options to get thrown into + if "Game Options" not in ordered_groups: + grouped_options = set(option for group in ordered_groups.values() for option in group) + ungrouped_options = [option for option in option_to_name if option not in grouped_options] + # only add the game options group if we have ungrouped options + if ungrouped_options: + ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups} + + return { + group: { + option_to_name[option]: option + for option in group_options + if (visibility_level in option.visibility and option in option_to_name) + } + for group, group_options in ordered_groups.items() + } def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: @@ -1531,7 +1554,7 @@ def yaml_dump_scalar(scalar) -> str: del file_data - with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: + with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) @@ -1559,7 +1582,7 @@ def dump_player_options(multiworld: MultiWorld) -> None: } output.append(player_output) for option_key, option in world.options_dataclass.type_hints.items(): - if issubclass(Removed, option): + if option.visibility == Visibility.none: continue display_name = getattr(option, "display_name", option_key) player_output[display_name] = getattr(world.options, option_key).current_option_name diff --git a/README.md b/README.md index 0e57bce53b51..d60f1b96651f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,10 @@ Currently, the following games are supported: * Kingdom Hearts 1 * Mega Man 2 * Yacht Dice +* Faxanadu +* Saving Princess +* Castlevania: Circle of the Moon +* Inscryption For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 19440e1dc5be..9140c73c14e2 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -243,6 +243,9 @@ def on_package(self, cmd: str, args: typing.Dict[str, typing.Any]) -> None: # Once the games handled by SNIClient gets made to be remote items, # this will no longer be needed. async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}])) + + if self.client_handler is not None: + self.client_handler.on_package(self, cmd, args) def run_gui(self) -> None: from kvui import GameManager diff --git a/Utils.py b/Utils.py index 412011200f8a..0aa81af1502e 100644 --- a/Utils.py +++ b/Utils.py @@ -18,8 +18,8 @@ from argparse import Namespace from settings import Settings, get_settings -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union -from typing_extensions import TypeGuard +from time import sleep +from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump try: @@ -47,7 +47,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.1" +__version__ = "0.6.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -152,8 +152,15 @@ def home_path(*path: str) -> str: if hasattr(home_path, 'cached_path'): pass elif sys.platform.startswith('linux'): - home_path.cached_path = os.path.expanduser('~/Archipelago') - os.makedirs(home_path.cached_path, 0o700, exist_ok=True) + xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) + home_path.cached_path = xdg_data_home + '/Archipelago' + if not os.path.isdir(home_path.cached_path): + legacy_home_path = os.path.expanduser('~/Archipelago') + if os.path.isdir(legacy_home_path): + os.renames(legacy_home_path, home_path.cached_path) + os.symlink(home_path.cached_path, legacy_home_path) + else: + os.makedirs(home_path.cached_path, 0o700, exist_ok=True) else: # not implemented home_path.cached_path = local_path() # this will generate the same exceptions we got previously @@ -421,7 +428,8 @@ def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata - if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: + if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", + "SlotType", "NetworkSlot", "HintStatus"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate if module == "worlds.generic" and name == "PlandoItem": @@ -484,9 +492,9 @@ def get_text_after(text: str, start: str) -> str: loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} -def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w", - log_format: str = "[%(name)s at %(asctime)s]: %(message)s", - exception_logger: typing.Optional[str] = None): +def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, + write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s", + add_timestamp: bool = False, exception_logger: typing.Optional[str] = None): import datetime loglevel: int = loglevel_mapping.get(loglevel, loglevel) log_folder = user_path("logs") @@ -513,11 +521,15 @@ def __init__(self, filter_name: str, condition: typing.Callable[[logging.LogReco def filter(self, record: logging.LogRecord) -> bool: return self.condition(record) - file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage())) root_logger.addHandler(file_handler) if sys.stdout: + formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') stream_handler = logging.StreamHandler(sys.stdout) stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False))) + if add_timestamp: + stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) # Relay unhandled exceptions to logger. @@ -529,7 +541,8 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logging.getLogger(exception_logger).exception("Uncaught exception", - exc_info=(exc_type, exc_value, exc_traceback)) + exc_info=(exc_type, exc_value, exc_traceback), + extra={"NoStream": exception_logger is None}) return orig_hook(exc_type, exc_value, exc_traceback) handle_exception._wrapped = True @@ -552,7 +565,7 @@ def _cleanup(): import platform logging.info( f"Archipelago ({__version__}) logging initialized" - f" on {platform.platform()}" + f" on {platform.platform()} process {os.getpid()}" f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" f"{' (frozen)' if is_frozen() else ''}" ) @@ -568,6 +581,8 @@ def queuer(): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) @@ -852,11 +867,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non task.add_done_callback(_faf_tasks.discard) -def deprecate(message: str): +def deprecate(message: str, add_stacklevels: int = 0): if __debug__: raise Exception(message) - import warnings - warnings.warn(message) + warnings.warn(message, stacklevel=2 + add_stacklevels) class DeprecateDict(dict): @@ -870,10 +884,9 @@ def __init__(self, message: str, error: bool = False) -> None: def __getitem__(self, item: Any) -> Any: if self.should_error: - deprecate(self.log_message) + deprecate(self.log_message, add_stacklevels=1) elif __debug__: - import warnings - warnings.warn(self.log_message) + warnings.warn(self.log_message, stacklevel=2) return super().__getitem__(item) @@ -927,7 +940,7 @@ def freeze_support() -> None: def visualize_regions(root_region: Region, file_name: str, *, show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True, - linetype_ortho: bool = True) -> None: + linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None: """Visualize the layout of a world as a PlantUML diagram. :param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.) @@ -943,16 +956,22 @@ def visualize_regions(root_region: Region, file_name: str, *, Items without ID will be shown in italics. :param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown. :param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines. + :param regions_to_highlight: Regions that will be highlighted in green if they are reachable. Example usage in World code: from Utils import visualize_regions - visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") + state = self.multiworld.get_all_state(False) + state.update_reachable_regions(self.player) + visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True, + regions_to_highlight=state.reachable_regions[self.player]) Example usage in Main code: from Utils import visualize_regions for player in multiworld.player_ids: visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml") """ + if regions_to_highlight is None: + regions_to_highlight = set() assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region from collections import deque @@ -1005,7 +1024,7 @@ def visualize_locations(region: Region) -> None: uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}") def visualize_region(region: Region) -> None: - uml.append(f"class \"{fmt(region)}\"") + uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}") if show_locations: visualize_locations(region) visualize_exits(region) diff --git a/WebHost.py b/WebHost.py index e597de24763d..768eeb512289 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,11 +12,12 @@ # in case app gets imported by something like gunicorn import Utils import settings +from Utils import get_file_safe_name if typing.TYPE_CHECKING: from flask import Flask -Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 +Utils.local_path.cached_path = os.path.dirname(__file__) settings.no_gui = True configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home @@ -33,7 +34,7 @@ def get_app() -> "Flask": app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") # inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it. - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument('--config_override', default=None, help="Path to yaml config file that overrules config.yaml.") args = parser.parse_known_args()[0] @@ -71,7 +72,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] shutil.rmtree(base_target_path, ignore_errors=True) for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - target_path = os.path.join(base_target_path, game) + target_path = os.path.join(base_target_path, get_file_safe_name(game)) os.makedirs(target_path, exist_ok=True) if world.zip_path: diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index fdf3037fe015..9c713419c986 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,7 +9,7 @@ from pony.flask import Pony from werkzeug.routing import BaseConverter -from Utils import title_sorted +from Utils import title_sorted, get_file_safe_name UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -20,6 +20,7 @@ app.jinja_env.filters['any'] = any app.jinja_env.filters['all'] = all +app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["GENERATORS"] = 8 # maximum concurrent world gens @@ -38,6 +39,8 @@ app.config["JOB_THRESHOLD"] = 1 # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. app.config["JOB_TIME"] = 600 +# memory limit for generator processes in bytes +app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 app.config['SESSION_PERMANENT'] = True # waitress uses one thread for I/O, these are for processing of views that then get sent @@ -84,6 +87,6 @@ def register(): from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it - from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index cf05e87374ab..d0b9d05c16b8 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -3,13 +3,13 @@ from flask import Blueprint -from ..models import Seed +from ..models import Seed, Slot api_endpoints = Blueprint('api', __name__, url_prefix="/api") def get_players(seed: Seed) -> List[Tuple[str, str]]: - return [(slot.player_name, slot.game) for slot in seed.slots] + return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)] from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 116d3afa2288..0ddb6fe83ed8 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -30,4 +30,4 @@ def get_seeds(): "creation_time": seed.creation_time, "players": get_players(seed.slots), }) - return jsonify(response) \ No newline at end of file + return jsonify(response) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 08a1309ebc73..8ba093e014c5 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -6,6 +6,7 @@ import typing from datetime import timedelta, datetime from threading import Event, Thread +from typing import Any from uuid import UUID from pony.orm import db_session, select, commit @@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): generation.state = STATE_STARTED -def init_db(pony_config: dict): +def init_generator(config: dict[str, Any]) -> None: + try: + import resource + except ModuleNotFoundError: + pass # unix only module + else: + # set soft limit for memory to from config (default 4GiB) + soft_limit = config["GENERATOR_MEMORY_LIMIT"] + old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS) + if soft_limit != old_limit: + resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit)) + logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}") + del resource, soft_limit, hard_limit + + pony_config = config["PONY"] db.bind(**pony_config) db.generate_mapping() @@ -105,8 +120,8 @@ def keep_running(): try: with Locker("autogen"): - with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, - initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool: + with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, + initargs=(config,), maxtasksperchild=10) as generator_pool: with db_session: to_start = select(generation for generation in Generation if generation.state == STATE_STARTED) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 97cb797f7a56..4e0cf1178f4b 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]], plando_options=plando_options) else: for i, yaml_data in enumerate(yaml_datas): - rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, - plando_options=plando_options) + if yaml_data is not None: + rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, + plando_options=plando_options) except Exception as e: if e.__cause__: results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}" diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index a2eef108b0a1..76a2b8a4dc15 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -117,6 +117,7 @@ def load(self, room_id: int): self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})} self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})} + missing_checksum = False for game in list(multidata.get("datapackage", {})): game_data = multidata["datapackage"][game] @@ -132,11 +133,13 @@ def load(self, room_id: int): continue else: self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}") + else: + missing_checksum = True # Game rolled on old AP and will load data package from multidata self.gamespackage[game] = static_gamespackage.get(game, {}) self.item_name_groups[game] = static_item_name_groups.get(game, {}) self.location_name_groups[game] = static_location_name_groups.get(game, {}) - if not game_data_packages: + if not game_data_packages and not missing_checksum: # all static -> use the static dicts directly self.gamespackage = static_gamespackage self.item_name_groups = static_item_name_groups diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index b19f3d483515..0bd9f7e5e066 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -31,11 +31,11 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[s server_options = { "hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)), - "release_mode": options_source.get("release_mode", ServerOptions.release_mode), - "remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode), - "collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode), + "release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)), + "remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)), + "collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)), "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), - "server_password": options_source.get("server_password", None), + "server_password": str(options_source.get("server_password", None)), } generator_options = { "spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)), diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index c49b1ae17801..6be0e470b3b4 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -18,13 +18,6 @@ def get_world_theme(game_name: str): return 'grass' -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - @app.errorhandler(404) @app.errorhandler(jinja2.exceptions.TemplateNotFound) def page_not_found(err): diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 5c79415312d4..b7b14dea1e6f 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -5,9 +5,7 @@ waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 Flask-Limiter>=3.8.0 -bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.3; python_version == '3.9' -bokeh>=3.5.2; python_version >= '3.10' +bokeh>=3.5.2 markupsafe>=2.1.5 Markdown>=3.7 mdx-breakless-lists>=1.0.1 diff --git a/WebHostLib/session.py b/WebHostLib/session.py new file mode 100644 index 000000000000..d5dab7d6e6e6 --- /dev/null +++ b/WebHostLib/session.py @@ -0,0 +1,31 @@ +from uuid import uuid4, UUID + +from flask import session, render_template + +from WebHostLib import app + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.route('/session') +def show_session(): + return render_template( + "session.html", + ) + + +@app.route('/session/') +def set_session(_id: str): + new_id: UUID = UUID(_id, version=4) + old_id: UUID = session["_id"] + if old_id != new_id: + session["_id"] = new_id + return render_template( + "session.html", + old_id=old_id, + ) diff --git a/WebHostLib/static/assets/faq/en.md b/WebHostLib/static/assets/faq/en.md index e64535b42d03..96e526612be6 100644 --- a/WebHostLib/static/assets/faq/en.md +++ b/WebHostLib/static/assets/faq/en.md @@ -22,7 +22,7 @@ players to rely upon each other to complete their game. While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows players to randomize any of the supported games, and send items between them. This allows players of different -games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld. +games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds. Here is a list of our [Supported Games](https://archipelago.gg/games). ## Can I generate a single-player game with Archipelago? diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba82848d..3b908004b1be 100644 --- a/WebHostLib/templates/gameInfo.html +++ b/WebHostLib/templates/gameInfo.html @@ -11,7 +11,7 @@ {% block body %} {% include 'header/'+theme+'Header.html' %} -
+
{% endblock %} diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 947cf2837278..b92097ceea08 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,6 +98,8 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} + {% elif get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} {{ player_names_with_alias[(team, hint.finding_player)] }} @@ -107,6 +109,8 @@ {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% elif get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} {{ player_names_with_alias[(team, hint.receiving_player)] }} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 8e76dafc12fa..c5996d181ee0 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -178,8 +178,15 @@ }) .then(text => new DOMParser().parseFromString(text, 'text/html')) .then(newDocument => { - let el = newDocument.getElementById("host-room-info"); - document.getElementById("host-room-info").innerHTML = el.innerHTML; + ["host-room-info", "slots-table"].forEach(function(id) { + const newEl = newDocument.getElementById(id); + const oldEl = document.getElementById(id); + if (oldEl && newEl) { + oldEl.innerHTML = newEl.innerHTML; + } else if (newEl) { + console.warn(`Did not find element to replace for ${id}`) + } + }); }); } diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 08cf227990b8..7de14f0d827c 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -1,6 +1,6 @@ {% block footer %}