From 812dc413e5aa6eea767496246812a05a96c05719 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 26 Sep 2023 23:24:10 -0400 Subject: [PATCH 001/327] LTTP: Key Drop Shuffle (#282) Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Bondo <38083232+BadmoonzZ@users.noreply.github.com> Co-authored-by: Fabian Dill --- Fill.py | 6 - playerSettings.yaml | 5 +- test/TestBase.py | 14 +- worlds/alttp/Client.py | 2 +- worlds/alttp/Dungeons.py | 59 +- worlds/alttp/EntranceShuffle.py | 20 +- worlds/alttp/InvertedRegions.py | 256 ++++----- worlds/alttp/ItemPool.py | 82 ++- worlds/alttp/Options.py | 6 + worlds/alttp/Regions.py | 505 +++++++----------- worlds/alttp/Rom.py | 37 +- worlds/alttp/Rules.py | 270 +++++++--- worlds/alttp/UnderworldGlitchRules.py | 7 +- worlds/alttp/__init__.py | 16 +- .../alttp/test/dungeons/TestAgahnimsTower.py | 12 + .../alttp/test/dungeons/TestDesertPalace.py | 22 +- worlds/alttp/test/dungeons/TestDungeon.py | 5 +- .../alttp/test/dungeons/TestEasternPalace.py | 3 +- worlds/alttp/test/dungeons/TestGanonsTower.py | 58 +- worlds/alttp/test/dungeons/TestIcePalace.py | 9 +- worlds/alttp/test/dungeons/TestSkullWoods.py | 32 +- worlds/alttp/test/dungeons/TestThievesTown.py | 18 +- .../test/inverted/TestInvertedTurtleRock.py | 35 +- .../TestInvertedTurtleRock.py | 34 +- worlds/alttp/test/owg/TestDungeons.py | 1 + 25 files changed, 808 insertions(+), 706 deletions(-) diff --git a/Fill.py b/Fill.py index 21759eefe4..7c81aed7ba 100644 --- a/Fill.py +++ b/Fill.py @@ -753,8 +753,6 @@ def distribute_planned(world: MultiWorld) -> None: else: # not reachable with swept state non_early_locations[loc.player].append(loc.name) - # TODO: remove. Preferably by implementing key drop - from worlds.alttp.Regions import key_drop_data world_name_lookup = world.world_name_lookup block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] @@ -897,10 +895,6 @@ def distribute_planned(world: MultiWorld) -> None: for item_name in items: item = world.worlds[player].create_item(item_name) for location in reversed(candidates): - if location in key_drop_data: - warn( - f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.") - continue if not location.item: if location.item_rule(item): if location.can_fill(world.state, item, False): diff --git a/playerSettings.yaml b/playerSettings.yaml index e28963ddb3..f9585da246 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc game: # Pick a game to play A Link to the Past: 1 requires: - version: 0.3.3 # Version of Archipelago required for this yaml to work as expected. + version: 0.4.3 # Version of Archipelago required for this yaml to work as expected. A Link to the Past: progression_balancing: # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. @@ -114,6 +114,9 @@ A Link to the Past: different_world: 0 universal: 0 start_with: 0 + key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies + off: 50 + on: 0 compass_shuffle: # Compass Placement original_dungeon: 50 own_dungeons: 0 diff --git a/test/TestBase.py b/test/TestBase.py index dc79ad2855..1f0853ef14 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -25,8 +25,9 @@ class TestBase(unittest.TestCase): state = CollectionState(self.multiworld) for item in items: item.classification = ItemClassification.progression - state.collect(item) + state.collect(item, event=True) state.sweep_for_events() + state.update_reachable_regions(1) self._state_cache[self.multiworld, tuple(items)] = state return state @@ -53,7 +54,8 @@ class TestBase(unittest.TestCase): with self.subTest(msg="Reach Location", location=location, access=access, items=items, all_except=all_except, path=path, entry=i): - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access) + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, + f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") # check for partial solution if not all_except and access: # we are not supposed to be able to reach location with partial inventory @@ -61,7 +63,10 @@ class TestBase(unittest.TestCase): with self.subTest(msg="Location reachable without required item", location=location, items=item_pool[0], missing_item=missing_item, entry=i): state = self._get_items_partial(item_pool, missing_item) - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False) + + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False, + f"failed {self.multiworld.get_location(location, 1)}: succeeded with " + f"{missing_item} removed from: {item_pool}") def run_entrance_tests(self, access_pool): for i, (entrance, access, *item_pool) in enumerate(access_pool): @@ -80,7 +85,8 @@ class TestBase(unittest.TestCase): with self.subTest(msg="Entrance reachable without required item", entrance=entrance, items=item_pool[0], missing_item=missing_item, entry=i): state = self._get_items_partial(item_pool, missing_item) - self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False) + self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False, + f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}") def _get_items(self, item_pool, all_except): if all_except and len(all_except) > 0: diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 7ac24fde9f..22ef2a39a8 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -107,7 +107,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), "Hyrule Castle - Zelda's Chest": (0x80, 0x10), 'Hyrule Castle - Big Key Drop': (0x80, 0x400), 'Sewers - Dark Cross': (0x32, 0x10), - 'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400), + 'Sewers - Key Rat Key Drop': (0x21, 0x400), 'Sewers - Secret Room - Left': (0x11, 0x10), 'Sewers - Secret Room - Middle': (0x11, 0x20), 'Sewers - Secret Room - Right': (0x11, 0x40), diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index b789fd6db6..045969be53 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -8,7 +8,7 @@ from Fill import fill_restrictive from .Bosses import BossFactory, Boss from .Items import ItemFactory -from .Regions import lookup_boss_drops +from .Regions import lookup_boss_drops, key_drop_data from .Options import smallkey_shuffle if typing.TYPE_CHECKING: @@ -81,15 +81,17 @@ def create_dungeons(world: "ALTTPWorld"): return dungeon ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], - None, [ItemFactory('Small Key (Hyrule Castle)', player)], + ItemFactory('Big Key (Hyrule Castle)', player), + ItemFactory(['Small Key (Hyrule Castle)'] * 4, player), [ItemFactory('Map (Hyrule Castle)', player)]) EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'], - ItemFactory('Big Key (Eastern Palace)', player), [], + ItemFactory('Big Key (Eastern Palace)', player), + ItemFactory(['Small Key (Eastern Palace)'] * 2, player), ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player)) DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), - [ItemFactory('Small Key (Desert Palace)', player)], + ItemFactory(['Small Key (Desert Palace)'] * 4, player), ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player)) ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], @@ -105,7 +107,8 @@ def create_dungeons(world: "ALTTPWorld"): ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player)) TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], - ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], + ItemFactory('Big Key (Thieves Town)', player), + ItemFactory(['Small Key (Thieves Town)'] * 3, player), ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player)) SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', @@ -113,52 +116,54 @@ def create_dungeons(world: "ALTTPWorld"): 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), - ItemFactory(['Small Key (Skull Woods)'] * 3, player), + ItemFactory(['Small Key (Skull Woods)'] * 5, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player)) SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', - 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), - [ItemFactory('Small Key (Swamp Palace)', player)], + 'Swamp Palace (West)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], + ItemFactory('Big Key (Swamp Palace)', player), + ItemFactory(['Small Key (Swamp Palace)'] * 6, player), ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player)) IP = make_dungeon('Ice Palace', 'Kholdstare', - ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', - 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), - ItemFactory(['Small Key (Ice Palace)'] * 2, player), + ['Ice Palace (Entrance)', 'Ice Palace (Second Section)', 'Ice Palace (Main)', 'Ice Palace (East)', + 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), + ItemFactory(['Small Key (Ice Palace)'] * 6, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player)) MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), - ItemFactory(['Small Key (Misery Mire)'] * 3, player), + ItemFactory(['Small Key (Misery Mire)'] * 6, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player)) TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', + 'Turtle Rock (Pokey Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), - ItemFactory(['Small Key (Turtle Rock)'] * 4, player), + ItemFactory(['Small Key (Turtle Rock)'] * 6, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) if multiworld.mode[player] != 'inverted': AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, - ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) + ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), []) GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), - ItemFactory(['Small Key (Ganons Tower)'] * 4, player), + ItemFactory(['Small Key (Ganons Tower)'] * 8, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) else: AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, - ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) + ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), []) GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), - ItemFactory(['Small Key (Ganons Tower)'] * 4, player), + ItemFactory(['Small Key (Ganons Tower)'] * 8, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) GT.bosses['bottom'] = BossFactory('Armos Knights', player) @@ -195,10 +200,11 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): dungeon_specific: set = set() for subworld in multiworld.get_game_worlds("A Link to the Past"): player = subworld.player - localized |= {(player, item_name) for item_name in - subworld.dungeon_local_item_names} - dungeon_specific |= {(player, item_name) for item_name in - subworld.dungeon_specific_item_names} + if player not in multiworld.groups: + localized |= {(player, item_name) for item_name in + subworld.dungeon_local_item_names} + dungeon_specific |= {(player, item_name) for item_name in + subworld.dungeon_specific_item_names} if localized: in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized] @@ -249,7 +255,16 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if all_state_base.has("Triforce", player): all_state_base.remove(multiworld.worlds[player].create_item("Triforce")) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True) + for (player, key_drop_shuffle) in enumerate(multiworld.key_drop_shuffle.values(), start=1): + if not key_drop_shuffle and player not in multiworld.groups: + for key_loc in key_drop_data: + key_data = key_drop_data[key_loc] + all_state_base.remove(ItemFactory(key_data[3], player)) + loc = multiworld.get_location(key_loc, player) + + if loc in all_state_base.events: + all_state_base.events.remove(loc) + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index b7fe688431..07bb587eeb 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3134,6 +3134,7 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Swamp Palace Moat', 'Swamp Palace (First Room)'), ('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'), ('Swamp Palace (Center)', 'Swamp Palace (Center)'), + ('Swamp Palace (West)', 'Swamp Palace (West)'), ('Swamp Palace (North)', 'Swamp Palace (North)'), ('Thieves Town Big Key Door', 'Thieves Town (Deep)'), ('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'), @@ -3148,7 +3149,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Blind Fight', 'Blind Fight'), ('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'), ('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'), - ('Ice Palace Entrance Room', 'Ice Palace (Main)'), + ('Ice Palace (Main)', 'Ice Palace (Main)'), + ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'), ('Ice Palace (East)', 'Ice Palace (East)'), ('Ice Palace (East Top)', 'Ice Palace (East Top)'), ('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'), @@ -3158,9 +3160,11 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'), ('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'), ('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'), - ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'), + ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'), + ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'), + ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'), ('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'), - ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'), + ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'), ('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'), ('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'), ('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'), @@ -3285,6 +3289,7 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Swamp Palace Moat', 'Swamp Palace (First Room)'), ('Swamp Palace Small Key Door', 'Swamp Palace (Starting Area)'), ('Swamp Palace (Center)', 'Swamp Palace (Center)'), + ('Swamp Palace (West)', 'Swamp Palace (West)'), ('Swamp Palace (North)', 'Swamp Palace (North)'), ('Thieves Town Big Key Door', 'Thieves Town (Deep)'), ('Skull Woods Torch Room', 'Skull Woods Final Section (Mothula)'), @@ -3299,7 +3304,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Blind Fight', 'Blind Fight'), ('Desert Palace Pots (Outer)', 'Desert Palace Main (Inner)'), ('Desert Palace Pots (Inner)', 'Desert Palace Main (Outer)'), - ('Ice Palace Entrance Room', 'Ice Palace (Main)'), + ('Ice Palace (Main)', 'Ice Palace (Main)'), + ('Ice Palace (Second Section)', 'Ice Palace (Second Section)'), ('Ice Palace (East)', 'Ice Palace (East)'), ('Ice Palace (East Top)', 'Ice Palace (East Top)'), ('Ice Palace (Kholdstare)', 'Ice Palace (Kholdstare)'), @@ -3309,9 +3315,11 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'), ('Misery Mire (Vitreous)', 'Misery Mire (Vitreous)'), ('Turtle Rock Entrance Gap', 'Turtle Rock (First Section)'), ('Turtle Rock Entrance Gap Reverse', 'Turtle Rock (Entrance)'), - ('Turtle Rock Pokey Room', 'Turtle Rock (Chain Chomp Room)'), + ('Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room)'), + ('Turtle Rock (Pokey Room) (South)', 'Turtle Rock (First Section)'), + ('Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Chain Chomp Room)'), ('Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Second Section)'), - ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (First Section)'), + ('Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock (Pokey Room)'), ('Turtle Rock Chain Chomp Staircase', 'Turtle Rock (Chain Chomp Room)'), ('Turtle Rock (Big Chest) (North)', 'Turtle Rock (Second Section)'), ('Turtle Rock Big Key Door', 'Turtle Rock (Crystaroller Room)'), diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index acec73bf33..ffa23881d3 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -149,41 +149,37 @@ def create_inverted_regions(world, player): create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks', 'Desert Palace North Mirror Spot']), - create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', - ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], - ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', - 'Desert Palace East Wing']), - create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, - ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), - create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', - ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), + create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], + ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), + create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), + create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', - ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), + ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', + 'Desert Palace - Desert Tiles 2 Pot Key', + 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', - 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', - 'Eastern Palace - Prize'], ['Eastern Palace Exit']), + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', + 'Eastern Palace - Big Key Chest', + 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], + ['Eastern Palace Exit']), create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']), create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'), - create_lw_region(world, player, 'Hyrule Castle Ledge', None, - ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', - 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']), + create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']), create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', - 'Hyrule Castle - Zelda\'s Chest'], + 'Hyrule Castle - Zelda\'s Chest', + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', + 'Hyrule Castle - Big Key Drop'], ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks - create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'], - ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', - ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', - 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), + create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), + create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), - create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', - ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], - ['Agahnim 1', 'Inverted Agahnims Tower Exit']), + create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']), create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), @@ -253,14 +249,9 @@ def create_inverted_regions(world, player): 'Death Mountain (Top) Mirror Spot']), create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']), - create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', - ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], - ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), - create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', - ['Tower of Hera - Big Key Chest']), - create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', - ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', - 'Tower of Hera - Prize']), + create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']), + create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']), + create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']), create_dw_region(world, player, 'East Dark World', ['Pyramid'], ['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness', @@ -360,128 +351,82 @@ def create_inverted_regions(world, player): ['Floating Island Drop', 'Hookshot Cave Back Entrance']), create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), - create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, - ['Swamp Palace Moat', 'Swamp Palace Exit']), - create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], - ['Swamp Palace Small Key Door']), - create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', - ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', - ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', - 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']), - create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', - ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', - 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', - ['Thieves\' Town - Big Key Chest', - 'Thieves\' Town - Map Chest', - 'Thieves\' Town - Compass Chest', - 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), + create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), + create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), + create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', + 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), + create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']), + create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), + create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', + 'Swamp Palace - Boss', 'Swamp Palace - Prize']), + create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + 'Thieves\' Town - Map Chest', + 'Thieves\' Town - Compass Chest', + 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', - 'Thieves\' Town - Big Chest', - 'Thieves\' Town - Blind\'s Cell'], + 'Thieves\' Town - Big Chest', + 'Thieves\' Town - Hallway Pot Key', + 'Thieves\' Town - Spike Switch Pot Key', + 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), - create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', - ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), - create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], - ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', - 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', - ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', - ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], - ['Skull Woods First Section (Left) Door to Exit', - 'Skull Woods First Section (Left) Door to Right']), - create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', - ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), - create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, - ['Skull Woods Second Section (Drop)']), - create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', - ['Skull Woods - Big Key Chest'], - ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', - ['Skull Woods - Bridge Room'], - ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', - ['Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None, - ['Ice Palace Entrance Room', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', - ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', - 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], - ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], - ['Ice Palace (East Top)']), - create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', - ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']), - create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', - ['Ice Palace - Boss', 'Ice Palace - Prize']), - create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, - ['Misery Mire Entrance Gap', 'Misery Mire Exit']), - create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', - ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', - 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], - ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', - ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), - create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, - ['Misery Mire (Vitreous)']), - create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', - ['Misery Mire - Boss', 'Misery Mire - Prize']), - create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, - ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), - create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', - ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', - 'Turtle Rock - Roller Room - Right'], - ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', - ['Turtle Rock - Chain Chomps'], + create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), + create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), + create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), + create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), + create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), + create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), + create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']), + create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', + 'Ice Palace - Many Pots Pot Key', + 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), + create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), + create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), + create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), + create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), + create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', + 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest', + 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', + 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), + create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), + create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), + create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), + create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), + create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', + 'Turtle Rock - Roller Room - Right'], + ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), + create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', - ['Turtle Rock - Big Key Chest'], + ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']), - create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], - ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), - create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', - ['Turtle Rock - Crystaroller Room'], - ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, - ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', - ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', - 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], - ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', - 'Turtle Rock Isolated Ledge Exit']), - create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', - ['Turtle Rock - Boss', 'Turtle Rock - Prize']), - create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', - ['Palace of Darkness - Shooter Room'], - ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', - 'Palace of Darkness Exit']), - create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', - ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], - ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', - 'Palace of Darkness Big Key Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', - ['Palace of Darkness - Big Key Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', - ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], - ['Palace of Darkness Hammer Peg Drop']), - create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', - ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', - 'Palace of Darkness - Dark Basement - Right'], + create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), + create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), + create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', + 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], + ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), + create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), + create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), + create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], + ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), + create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), + create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), + create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', - ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', - 'Palace of Darkness - Big Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', - ['Palace of Darkness - Harmless Hellway']), - create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', - ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), + create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), + create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), + create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', - 'Ganons Tower - Hope Room - Right'], + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'], ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Inverted Ganons Tower Exit']), create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], @@ -489,10 +434,13 @@ def create_inverted_regions(world, player): create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', - 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']), + 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Conveyor Star Pits Pot Key'], + ['Ganons Tower (Bottom) (East)']), create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', - 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - Double Switch Pot Key'], ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', @@ -501,21 +449,21 @@ def create_inverted_regions(world, player): ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', - 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']), + 'Ganons Tower - Randomizer Room - Bottom Right'], + ['Ganons Tower (Bottom) (West)']), create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, - ['Ganons Tower Torch Rooms']), + create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', - 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']), - create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, - ['Ganons Tower Moldorm Gap']), - create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', - ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), + 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], + ['Ganons Tower Moldorm Door']), + create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), + + create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 56eb355837..f8fdd55ef6 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -12,6 +12,7 @@ from .EntranceShuffle import connect_entrance from .Items import ItemFactory, GetBeemizerItem from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses from .StateHelpers import has_triforce_pieces, has_melee_weapon +from .Regions import key_drop_data # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -80,7 +81,7 @@ difficulties = { basicglove=basicgloves, alwaysitems=alwaysitems, legacyinsanity=legacyinsanity, - universal_keys=['Small Key (Universal)'] * 28, + universal_keys=['Small Key (Universal)'] * 29, extras=[easyfirst15extra, easysecond15extra, easythird10extra, easyfourth5extra, easyfinal25extra], progressive_sword_limit=8, progressive_shield_limit=6, @@ -112,7 +113,7 @@ difficulties = { basicglove=basicgloves, alwaysitems=alwaysitems, legacyinsanity=legacyinsanity, - universal_keys=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, + universal_keys=['Small Key (Universal)'] * 19 + ['Rupees (20)'] * 10, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=4, progressive_shield_limit=3, @@ -144,7 +145,7 @@ difficulties = { basicglove=basicgloves, alwaysitems=alwaysitems, legacyinsanity=legacyinsanity, - universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, + universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=3, progressive_shield_limit=2, @@ -176,7 +177,7 @@ difficulties = { basicglove=basicgloves, alwaysitems=alwaysitems, legacyinsanity=legacyinsanity, - universal_keys=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16, + universal_keys=['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 16, extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit=2, progressive_shield_limit=1, @@ -212,7 +213,7 @@ for diff in {'easy', 'normal', 'hard', 'expert'}: basicglove=['Nothing'] * 2, alwaysitems=['Ice Rod'] + ['Nothing'] * 19, legacyinsanity=['Nothing'] * 2, - universal_keys=['Nothing'] * 28, + universal_keys=['Nothing'] * 29, extras=[['Nothing'] * 15, ['Nothing'] * 15, ['Nothing'] * 10, ['Nothing'] * 5, ['Nothing'] * 25], progressive_sword_limit=difficulties[diff].progressive_sword_limit, progressive_shield_limit=difficulties[diff].progressive_shield_limit, @@ -281,7 +282,6 @@ def generate_itempool(world): itempool.extend(['Arrows (10)'] * 7) if multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: itempool.extend(itemdiff.universal_keys) - itempool.append('Small Key (Universal)') for item in itempool: multiworld.push_precollected(ItemFactory(item, player)) @@ -374,11 +374,38 @@ def generate_itempool(world): dungeon_items = [item for item in get_dungeon_item_pool_player(world) if item.name not in multiworld.worlds[player].dungeon_local_item_names] - dungeon_item_replacements = difficulties[multiworld.difficulty[player]].extras[0]\ - + difficulties[multiworld.difficulty[player]].extras[1]\ - + difficulties[multiworld.difficulty[player]].extras[2]\ - + difficulties[multiworld.difficulty[player]].extras[3]\ - + difficulties[multiworld.difficulty[player]].extras[4] + + for key_loc in key_drop_data: + key_data = key_drop_data[key_loc] + drop_item = ItemFactory(key_data[3], player) + if multiworld.goal[player] == 'icerodhunt' or not multiworld.key_drop_shuffle[player]: + if drop_item in dungeon_items: + dungeon_items.remove(drop_item) + else: + dungeon = drop_item.name.split("(")[1].split(")")[0] + if multiworld.mode[player] == 'inverted': + if dungeon == "Agahnims Tower": + dungeon = "Inverted Agahnims Tower" + if dungeon == "Ganons Tower": + dungeon = "Inverted Ganons Tower" + if drop_item in world.dungeons[dungeon].small_keys: + world.dungeons[dungeon].small_keys.remove(drop_item) + elif world.dungeons[dungeon].big_key is not None and world.dungeons[dungeon].big_key == drop_item: + world.dungeons[dungeon].big_key = None + if not multiworld.key_drop_shuffle[player]: + # key drop item was removed from the pool because key drop shuffle is off + # and it will now place the removed key into its original location + loc = multiworld.get_location(key_loc, player) + loc.place_locked_item(drop_item) + loc.address = None + elif multiworld.goal[player] == 'icerodhunt': + # key drop item removed because of icerodhunt + multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) + multiworld.push_precollected(drop_item) + elif "Small" in key_data[3] and multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: + # key drop shuffle and universal keys are on. Add universal keys in place of key drop keys. + multiworld.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Small Key (Universal)'), player)) + dungeon_item_replacements = sum(difficulties[multiworld.difficulty[player]].extras, []) * 2 multiworld.random.shuffle(dungeon_item_replacements) if multiworld.goal[player] == 'icerodhunt': for item in dungeon_items: @@ -391,7 +418,7 @@ def generate_itempool(world): or (multiworld.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): - dungeon_items.remove(item) + dungeon_items.pop(x) multiworld.push_precollected(item) multiworld.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) multiworld.itempool.extend([item for item in dungeon_items]) @@ -639,14 +666,27 @@ def get_pool_core(world, player: int): pool = ['Rupees (5)' if item in replace else item for item in pool] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: pool.extend(diff.universal_keys) - item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing' if mode == 'standard': - key_location = world.random.choice( - ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', - 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) - place_item(key_location, item_to_place) - else: - pool.extend([item_to_place]) + if world.key_drop_shuffle[player] and world.goal[player] != 'icerodhunt': + key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop'] + key_location = world.random.choice(key_locations) + key_locations.remove(key_location) + place_item(key_location, "Small Key (Universal)") + key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest', + 'Hyrule Castle - Map Chest'] + key_location = world.random.choice(key_locations) + key_locations.remove(key_location) + place_item(key_location, "Small Key (Universal)") + key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'] + key_location = world.random.choice(key_locations) + key_locations.remove(key_location) + place_item(key_location, "Small Key (Universal)") + key_locations += ['Sewers - Key Rat Key Drop'] + key_location = world.random.choice(key_locations) + place_item(key_location, "Small Key (Universal)") + pool = pool[:-3] + if world.key_drop_shuffle[player]: + pass # pool.extend([item_to_place] * (len(key_drop_data) - 1)) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) @@ -799,7 +839,9 @@ def make_custom_item_pool(world, player): pool.extend(['Moon Pearl'] * customitemarray[28]) if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal: - itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal mode + itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode + if world.key_drop_shuffle[player]: + itemtotal = itemtotal - (len(key_drop_data) - 1) if itemtotal < total_items_to_place: pool.extend(['Nothing'] * (total_items_to_place - itemtotal)) logging.warning(f"Pool was filled up with {total_items_to_place - itemtotal} Nothing's for player {player}") diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b4b0958ac2..0f35be7459 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -101,6 +101,11 @@ class map_shuffle(DungeonItem): display_name = "Map Shuffle" +class key_drop_shuffle(Toggle): + """Shuffle keys found in pots and dropped from killed enemies.""" + display_name = "Key Drop Shuffle" + default = False + class Crystals(Range): range_start = 0 range_end = 7 @@ -432,6 +437,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "open_pyramid": OpenPyramid, "bigkey_shuffle": bigkey_shuffle, "smallkey_shuffle": smallkey_shuffle, + "key_drop_shuffle": key_drop_shuffle, "compass_shuffle": compass_shuffle, "map_shuffle": map_shuffle, "progressive": Progressive, diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 9badbd8774..8311bc3269 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -14,42 +14,26 @@ def create_regions(world, player): world.regions += [ create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']), create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', - 'Purple Chest', 'Flute Activation Spot'], - ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', - 'Kings Grave Outer Rocks', 'Dam', - 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', - 'Kakariko Well Drop', 'Kakariko Well Cave', - 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', - 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', - 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', - 'Lake Hylia Central Island Pier', - 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', - 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', - 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', - 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', - 'Kakariko Teleporter', - 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', - 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', - 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', - 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', - 'Hyrule Castle Main Gate', - 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', - 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', - 'Kakariko Gamble Game', 'Top of Pyramid']), - create_lw_region(world, player, 'Death Mountain Entrance', None, - ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), - create_lw_region(world, player, 'Lake Hylia Central Island', None, - ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), + 'Purple Chest', 'Flute Activation Spot'], + ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', + 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', + 'Blacksmiths Hut', 'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge', 'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', + 'Lumberjack Tree Tree', 'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier', + 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs', 'Eastern Palace', 'Master Sword Meadow', + 'Sanctuary', 'Sanctuary Grave', 'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter', 'South Hyrule Teleporter', 'Kakariko Teleporter', + 'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)', + 'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate', + 'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']), + create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']), + create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']), create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", - "Blind\'s Hideout - Left", - "Blind\'s Hideout - Right", - "Blind\'s Hideout - Far Left", - "Blind\'s Hideout - Far Right"]), - create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', - ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), + "Blind\'s Hideout - Left", + "Blind\'s Hideout - Right", + "Blind\'s Hideout - Far Left", + "Blind\'s Hideout - Far Right"]), + create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']), create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']), - create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', - ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), + create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']), create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']), create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), @@ -57,8 +41,7 @@ def create_regions(world, player): create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']), create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']), create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']), - create_cave_region(world, player, 'Elder House', 'a connector', None, - ['Elder House Exit (East)', 'Elder House Exit (West)']), + create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'), create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'), create_cave_region(world, player, 'Bush Covered House', 'the grass man'), @@ -79,12 +62,9 @@ def create_regions(world, player): create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']), create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']), - create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', - ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', - 'Sahasrahla']), - create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', - ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', - 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), + create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), + create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', + 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']), create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']), @@ -92,12 +72,9 @@ def create_regions(world, player): create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']), create_lw_region(world, player, 'Hobo Bridge', ['Hobo']), - create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], - ['Lost Woods Hideout (top to bottom)']), - create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, - ['Lost Woods Hideout Exit']), - create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], - ['Lumberjack Tree (top to bottom)']), + create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), + create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']), + create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']), create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']), create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']), create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']), @@ -105,9 +82,8 @@ def create_regions(world, player): create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']), create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']), create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'), - create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', - ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', - 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), + create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', + 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']), create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'), create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'), @@ -119,91 +95,56 @@ def create_regions(world, player): create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']), create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']), create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies'), - create_cave_region(world, player, 'Two Brothers House', 'a connector', None, - ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), + create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']), create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'), - create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], - ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']), + create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']), create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']), create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']), - create_lw_region(world, player, 'Desert Palace Lone Stairs', None, - ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), - create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, - ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']), - create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', - ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], - ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', - 'Desert Palace East Wing']), - create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, - ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), - create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', - ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), - create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', - ['Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), - create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', - ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', - 'Eastern Palace - Cannonball Chest', - 'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', - 'Eastern Palace - Prize'], ['Eastern Palace Exit']), + create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']), + create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']), + create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], + ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), + create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']), + create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']), + create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key', + 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), + create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', + 'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest', + 'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']), create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']), create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'), - create_lw_region(world, player, 'Hyrule Castle Courtyard', None, - ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']), - create_lw_region(world, player, 'Hyrule Castle Ledge', None, - ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', - 'Hyrule Castle Ledge Courtyard Drop']), - create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', - ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', - 'Hyrule Castle - Zelda\'s Chest'], - ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', - 'Throne Room']), + create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']), + create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']), + create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', + 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'], + ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks - create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross'], - ['Sewers Door']), - create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', - ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', - 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), + create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']), + create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', + 'Sewers - Secret Room - Right'], ['Sanctuary Push Door', 'Sewers Back Door']), create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']), - create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', - ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'], - ['Agahnim 1', 'Agahnims Tower Exit']), + create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']), create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None), - create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], - ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), - create_cave_region(world, player, 'Old Man House', 'a connector', None, - ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), - create_cave_region(world, player, 'Old Man House Back', 'a connector', None, - ['Old Man House Exit (Top)', 'Old Man House Back to Front']), - create_lw_region(world, player, 'Death Mountain', None, - ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', - 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', - 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']), - create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, - ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), - create_lw_region(world, player, 'Death Mountain Return Ledge', None, - ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), - create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], - ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), - create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, - ['Spectacle Rock Cave Exit']), - create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, - ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), - create_lw_region(world, player, 'East Death Mountain (Bottom)', None, - ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', - 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', - 'Spiral Cave (Bottom)']), + create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), + create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), + create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), + create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']), + create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), + create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']), + create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), + create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), + create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), + create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']), create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'), - create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, - ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', - 'Light World Death Mountain Shop']), + create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', - 'Paradox Cave Lower - Left', - 'Paradox Cave Lower - Right', - 'Paradox Cave Lower - Far Right', - 'Paradox Cave Lower - Middle', - 'Paradox Cave Upper - Left', - 'Paradox Cave Upper - Right'], + 'Paradox Cave Lower - Left', + 'Paradox Cave Lower - Right', + 'Paradox Cave Lower - Far Right', + 'Paradox Cave Lower - Middle', + 'Paradox Cave Upper - Left', + 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), create_cave_region(world, player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), @@ -342,162 +283,98 @@ def create_regions(world, player): create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']), create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), - create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, - ['Swamp Palace Moat', 'Swamp Palace Exit']), - create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], - ['Swamp Palace Small Key Door']), - create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', - ['Swamp Palace - Map Chest'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', - ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', - 'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest'], ['Swamp Palace (North)']), - create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', - ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', - 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', - ['Thieves\' Town - Big Key Chest', - 'Thieves\' Town - Map Chest', - 'Thieves\' Town - Compass Chest', - 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), + create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']), + create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']), + create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key', + 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), + create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + 'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']), + create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']), + create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', + 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', + 'Swamp Palace - Boss', 'Swamp Palace - Prize']), + create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + 'Thieves\' Town - Map Chest', + 'Thieves\' Town - Compass Chest', + 'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']), create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', - 'Thieves\' Town - Big Chest', - 'Thieves\' Town - Blind\'s Cell'], - ['Blind Fight']), - create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', - ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), - create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], - ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', - 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', - ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), - create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', - ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], - ['Skull Woods First Section (Left) Door to Exit', - 'Skull Woods First Section (Left) Door to Right']), - create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', - ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), - create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, - ['Skull Woods Second Section (Drop)']), - create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', - ['Skull Woods - Big Key Chest'], - ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', - ['Skull Woods - Bridge Room'], - ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', - ['Skull Woods - Boss', 'Skull Woods - Prize']), - create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', None, - ['Ice Palace Entrance Room', 'Ice Palace Exit']), - create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', - ['Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', - 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], - ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], - ['Ice Palace (East Top)']), - create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', - ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest']), - create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', - ['Ice Palace - Boss', 'Ice Palace - Prize']), - create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, - ['Misery Mire Entrance Gap', 'Misery Mire Exit']), - create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', - ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', - 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'], - ['Misery Mire (West)', 'Misery Mire Big Key Door']), - create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', - ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), - create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, - ['Misery Mire (Vitreous)']), - create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', - ['Misery Mire - Boss', 'Misery Mire - Prize']), - create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, - ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), - create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', - ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', - 'Turtle Rock - Roller Room - Right'], - ['Turtle Rock Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', - ['Turtle Rock - Chain Chomps'], - ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', - ['Turtle Rock - Big Key Chest'], - ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', - 'Turtle Rock Big Key Door']), - create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], - ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), - create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', - ['Turtle Rock - Crystaroller Room'], - ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), - create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, - ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), - create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', - ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', - 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], - ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', - 'Turtle Rock Isolated Ledge Exit']), - create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', - ['Turtle Rock - Boss', 'Turtle Rock - Prize']), - create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', - ['Palace of Darkness - Shooter Room'], - ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', - 'Palace of Darkness Exit']), - create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', - ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], - ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', - 'Palace of Darkness Big Key Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', - ['Palace of Darkness - Big Key Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', - ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], - ['Palace of Darkness Hammer Peg Drop']), - create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', - ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', - 'Palace of Darkness - Dark Basement - Right'], + 'Thieves\' Town - Big Chest', + 'Thieves\' Town - Hallway Pot Key', + 'Thieves\' Town - Spike Switch Pot Key', + 'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']), + create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']), + create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']), + create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']), + create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']), + create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), + create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), + create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), + create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Main)']), + create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', + 'Ice Palace - Many Pots Pot Key', + 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), + create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']), + create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']), + create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']), + create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']), + create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', + 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest', + 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key', + 'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']), + create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']), + create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']), + create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']), + create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']), + create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', + 'Turtle Rock - Roller Room - Right'], + ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), + create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']), + create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']), + create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']), + create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']), + create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', + 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], + ['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']), + create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']), + create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']), + create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'], + ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), + create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']), + create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']), + create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', - ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', - 'Palace of Darkness - Big Chest']), - create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', - ['Palace of Darkness - Harmless Hellway']), - create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', - ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), - create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', - ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', - 'Ganons Tower - Hope Room - Right'], - ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', - 'Ganons Tower Exit']), - create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], - ['Ganons Tower (Tile Room) Key Door']), - create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', - ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', - 'Ganons Tower - Compass Room - Bottom Left', - 'Ganons Tower - Compass Room - Bottom Right'], ['Ganons Tower (Bottom) (East)']), - create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', - ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', - 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'], + create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']), + create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']), + create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']), + create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', + 'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'], + ['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']), + create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']), + create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', + 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', + 'Ganons Tower - Conveyor Star Pits Pot Key'], + ['Ganons Tower (Bottom) (East)']), + create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right', + 'Ganons Tower - Double Switch Pot Key'], ['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']), create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', - ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), - create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', - ['Ganons Tower - Randomizer Room - Top Left', - 'Ganons Tower - Randomizer Room - Top Right', - 'Ganons Tower - Randomizer Room - Bottom Left', - 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']), - create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', - ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', - 'Ganons Tower - Big Key Room - Left', - 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, - ['Ganons Tower Torch Rooms']), - create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', - ['Ganons Tower - Mini Helmasaur Room - Left', - 'Ganons Tower - Mini Helmasaur Room - Right', - 'Ganons Tower - Pre-Moldorm Chest'], ['Ganons Tower Moldorm Door']), - create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, - ['Ganons Tower Moldorm Gap']), - create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', - ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), + create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']), + create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', + 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], + ['Ganons Tower (Bottom) (West)']), + create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', + 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), + create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']), + create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right', + 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']), + create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), + create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None), create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']), create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']), create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']), @@ -533,8 +410,12 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy ret.exits.append(Entrance(player, exit, ret)) if locations: for location in locations: - address, player_address, crystal, hint_text = location_table[location] - ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address)) + if location in key_drop_data: + ko_hint = key_drop_data[location][2] + ret.locations.append(ALttPLocation(player, location, key_drop_data[location][1], False, ko_hint, ret, key_drop_data[location][0])) + else: + address, player_address, crystal, hint_text = location_table[location] + ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address)) return ret @@ -587,39 +468,39 @@ old_location_address_to_new_location_address = { key_drop_data = { - 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037], - 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034], - 'Hyrule Castle - Key Rat Key Drop': [0x14000c, 0x14000d], - 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d], - 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b], - 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049], - 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031], - 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b], - 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028], - 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061], - 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052], - 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019], - 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016], - 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013], - 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010], - 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a], - 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e], - 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c], - 'Thieves\' Town - Hallway Pot Key': [0x14005d, 0x14005e], - 'Thieves\' Town - Spike Switch Pot Key': [0x14004e, 0x14004f], - 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004], - 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022], - 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025], - 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046], - 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055], - 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c], - 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064], - 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058], - 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007], - 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040], - 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043], - 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a], - 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f] + 'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'], + 'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034, 'in Hyrule Castle', 'Small Key (Hyrule Castle)'], + 'Sewers - Key Rat Key Drop': [0x14000c, 0x14000d, 'in the sewers', 'Small Key (Hyrule Castle)'], + 'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d, 'in Hyrule Castle', 'Big Key (Hyrule Castle)'], + 'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b, 'in Eastern Palace', 'Small Key (Eastern Palace)'], + 'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049, 'in Eastern Palace', 'Small Key (Eastern Palace)'], + 'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031, 'in Desert Palace', 'Small Key (Desert Palace)'], + 'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b, 'in Desert Palace', 'Small Key (Desert Palace)'], + 'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028, 'in Desert Palace', 'Small Key (Desert Palace)'], + 'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061, 'in Castle Tower', 'Small Key (Agahnims Tower)'], + 'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052, 'in Castle Tower', 'Small Key (Agahnims Tower)'], + 'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019, 'in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016, 'in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013, 'in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010, 'in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a, 'in Swamp Palace', 'Small Key (Swamp Palace)'], + 'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e, 'in Skull Woods', 'Small Key (Skull Woods)'], + 'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c, 'near Mothula', 'Small Key (Skull Woods)'], + "Thieves' Town - Hallway Pot Key": [0x14005d, 0x14005e, "in Thieves' Town", 'Small Key (Thieves Town)'], + "Thieves' Town - Spike Switch Pot Key": [0x14004e, 0x14004f, "in Thieves' Town", 'Small Key (Thieves Town)'], + 'Ice Palace - Jelly Key Drop': [0x140003, 0x140004, 'in Ice Palace', 'Small Key (Ice Palace)'], + 'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022, 'in Ice Palace', 'Small Key (Ice Palace)'], + 'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025, 'in Ice Palace', 'Small Key (Ice Palace)'], + 'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046, 'in Ice Palace', 'Small Key (Ice Palace)'], + 'Misery Mire - Spikes Pot Key': [0x140054, 0x140055 , 'in Misery Mire', 'Small Key (Misery Mire)'], + 'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c, 'in forgotten Mire', 'Small Key (Misery Mire)'], + 'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064 , 'in Misery Mire', 'Small Key (Misery Mire)'], + 'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058, 'in Turtle Rock', 'Small Key (Turtle Rock)'], + 'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007, 'in Turtle Rock', 'Small Key (Turtle Rock)'], + 'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040, "in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043, "in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a, "in Ganon's Tower", 'Small Key (Ganons Tower)'], + 'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f, "atop Ganon's Tower", 'Small Key (Ganons Tower)'] } # tuple contents: diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index ed222b5f5d..ef4f943575 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -25,7 +25,7 @@ from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to from .Shops import ShopType, ShopPriceType from .Dungeons import dungeon_music_addresses -from .Regions import old_location_address_to_new_location_address +from .Regions import old_location_address_to_new_location_address, key_drop_data from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \ Blind_texts, \ @@ -428,6 +428,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): rom.write_byte(0x04DE81, 6) rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. + # Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on. + # Replace them with a Slime enemy if they are placed. + if multiworld.key_drop_shuffle[player]: + key_drop_enemies = { + 0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201, + 0x4E20A, 0x4E326, 0x4E4F7, 0x4E686, 0x4E70C, 0x4E7C8, 0x4E7FA + } + for enemy in key_drop_enemies: + if rom.read_byte(enemy) == 0x12: + logging.debug(f"Moblin found and replaced at {enemy} in world {player}") + rom.write_byte(enemy, 0x8F) + for used in (randopatch_path, options_path): try: os.remove(used) @@ -897,6 +909,25 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): credits_total += 30 if 'w' in world.shop_shuffle[player] else 27 rom.write_byte(0x187010, credits_total) # dynamic credits + + if world.key_drop_shuffle[player]: + rom.write_byte(0x140000, 1) # enable key drop shuffle + credits_total += len(key_drop_data) + # update dungeon counters + rom.write_byte(0x187001, 12) # Hyrule Castle + rom.write_byte(0x187002, 8) # Eastern Palace + rom.write_byte(0x187003, 9) # Desert Palace + rom.write_byte(0x187004, 4) # Agahnims Tower + rom.write_byte(0x187005, 15) # Swamp Palace + rom.write_byte(0x187007, 11) # Misery Mire + rom.write_byte(0x187008, 10) # Skull Woods + rom.write_byte(0x187009, 12) # Ice Palace + rom.write_byte(0x18700B, 10) # Thieves Town + rom.write_byte(0x18700C, 14) # Turtle Rock + rom.write_byte(0x18700D, 31) # Ganons Tower + + + # collection rate address: 238C37 first_top, first_bot = credits_digit((credits_total / 100) % 10) mid_top, mid_bot = credits_digit((credits_total / 10) % 10) @@ -1824,10 +1855,10 @@ def apply_oof_sfx(rom, oof: str): # (We need to insert the second sigil at the end) rom.write_bytes(0x12803A, oof_bytes) rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB]) - + #Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT") rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08]) - + def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options, world=None, player=1, allow_random_on_event=False, reduceflashing=False, diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index ce4a941ead..1fddecd8f4 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -231,26 +231,41 @@ def global_rules(world, player): set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Sewers Door', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or ( + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or ( world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[ player] == 'standard')) # standard universal small keys cannot access the shop set_rule(world.get_entrance('Sewers Back Door', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)) set_rule(world.get_entrance('Agahnim 1', player), - lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 4)) set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8)) set_rule(world.get_location('Castle Tower - Dark Maze', player), lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', player)) - + set_rule(world.get_location('Castle Tower - Dark Archer Key Drop', player), + lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + player, 2)) + set_rule(world.get_location('Castle Tower - Circle of Pots Key Drop', player), + lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)', + player, 3)) + set_always_allow(world.get_location('Eastern Palace - Big Key Chest', player), + lambda state, item: item.name == 'Big Key (Eastern Palace)' and item.player == player) + set_rule(world.get_location('Eastern Palace - Big Key Chest', player), + lambda state: state._lttp_has_key('Small Key (Eastern Palace)', player, 2) or + ((location_item_name(state, 'Eastern Palace - Big Key Chest', player) == ('Big Key (Eastern Palace)', player) + and state.has('Small Key (Eastern Palace)', player)))) + set_rule(world.get_location('Eastern Palace - Dark Eyegore Key Drop', player), + lambda state: state.has('Big Key (Eastern Palace)', player)) set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player)) ep_boss = world.get_location('Eastern Palace - Boss', player) set_rule(ep_boss, lambda state: state.has('Big Key (Eastern Palace)', player) and + state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and ep_boss.parent_region.dungeon.boss.can_defeat(state)) ep_prize = world.get_location('Eastern Palace - Prize', player) set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and + state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and ep_prize.parent_region.dungeon.boss.can_defeat(state)) if not world.enemy_shuffle[player]: add_rule(ep_boss, lambda state: can_shoot_arrows(state, player)) @@ -258,9 +273,13 @@ def global_rules(world, player): set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player)) - set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) - set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) + + set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4)) + set_rule(world.get_location('Desert Palace - Big Key Chest', player), lambda state: can_kill_most_things(state, player)) + set_rule(world.get_location('Desert Palace - Beamos Hall Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 2) and can_kill_most_things(state, player)) + set_rule(world.get_location('Desert Palace - Desert Tiles 2 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 3) and can_kill_most_things(state, player)) + set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) + set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) # logic patch to prevent placing a crystal in Desert that's required to reach the required keys if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]): @@ -275,57 +294,98 @@ def global_rules(world, player): set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player)) - set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player)) + set_rule(world.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2)) + set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3)) + set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player)) + set_rule(world.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6) + if state.has('Hookshot', player) + else state._lttp_has_key('Small Key (Swamp Palace)', player, 4)) set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) if world.accessibility[player] != 'locations': allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') - set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player)) + set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']: forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) + set_rule(world.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) + set_rule(world.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) - set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) - set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) + + if world.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind": + set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) + + set_rule(world.get_location('Thieves\' Town - Big Chest', player), + lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player, 3)) and state.has('Hammer', player)) if world.accessibility[player] != 'locations': allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)') - set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) - set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) - set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player)) - set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section - set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) + set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3)) + set_rule(world.get_location('Thieves\' Town - Spike Switch Pot Key', player), + lambda state: state._lttp_has_key('Small Key (Thieves Town)', player)) + + # We need so many keys in the SW doors because they are all reachable as the last door (except for the door to mothula) + set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) + set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) + set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) + set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) if world.accessibility[player] != 'locations': allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain + set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain + add_rule(world.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) + add_rule(world.get_location('Skull Woods - Boss', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) - set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player)) + set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_melt_things(state, player)) + set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: can_melt_things(state, player) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: state._lttp_has_key('Small Key (Ice Palace)', player, 2)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) - set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( - item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) + set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 6) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 5)))) + # This is a complicated rule, so let's break it down. + # Hookshot always suffices to get to the right side. + # Also, once you get over there, you have to cross the spikes, so that's the last line. + # Alternatively, we could not have hookshot. Then we open the keydoor into right side in order to get there. + # This is conditional on whether we have the big key or not, as big key opens the ability to waste more keys. + # Specifically, if we have big key we can burn 2 extra keys near the boss and will need +2 keys. That's all of them as this could be the last door. + # Hence if big key is available then it's 6 keys, otherwise 4 keys. + # If key_drop is off, then we have 3 drop keys available, and can never satisfy the 6 key requirement because one key is on right side, + # so this reduces perfectly to original logic. + set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or + (state._lttp_has_key('Small Key (Ice Palace)', player, 4) + if item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), + ('Ice Palace - Hammer Block Key Drop', player), + ('Ice Palace - Big Key Chest', player), + ('Ice Palace - Map Chest', player)]) + else state._lttp_has_key('Small Key (Ice Palace)', player, 6))) and + (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ... + set_rule(world.get_location('Misery Mire - Fishbone Pot Key', player), lambda state: state.has('Big Key (Misery Mire)', player) or state._lttp_has_key('Small Key (Misery Mire)', player, 4)) + set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player)) - # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ... - # big key gives backdoor access to that from the teleporter in the north west - set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player)) - set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player)) + # How to access crystal switch: + # If have big key: then you will need 2 small keys to be able to hit switch and return to main area, as you can burn key in dark room + # If not big key: cannot burn key in dark room, hence need only 1 key. all doors immediately available lead to a crystal switch. + # The listed chests are those which can be reached if you can reach a crystal switch. + set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2)) + set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2)) # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet - set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if (( - location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or - ( - location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3)) + set_rule(world.get_location('Misery Mire - Conveyor Crystal Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 4) + if location_item_name(state, 'Misery Mire - Compass Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Big Key Chest', player) == ('Big Key (Misery Mire)', player) or location_item_name(state, 'Misery Mire - Conveyor Crystal Key Drop', player) == ('Big Key (Misery Mire)', player) + else state._lttp_has_key('Small Key (Misery Mire)', player, 5)) + set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 5) + if ((location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or (location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) + else state._lttp_has_key('Small Key (Misery Mire)', player, 6)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock Entrance Gap Reverse', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player)) # We could get here from the middle section without Cane as we don't cross the entrance gap! + set_rule(world.get_location('Turtle Rock - Compass Chest', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_location('Turtle Rock - Roller Room - Left', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) @@ -337,7 +397,7 @@ def global_rules(world, player): set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) - set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) if not world.enemy_shuffle[player]: set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player)) @@ -361,35 +421,46 @@ def global_rules(world, player): # these key rules are conservative, you might be able to get away with more lenient rules randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'] - compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'] + compass_room_chests = ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - Conveyor Star Pits Pot Key'] + back_chests = ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest'] + set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) - set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) - if world.accessibility[player] != 'locations': - set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) + if world.pot_shuffle[player]: + # Pot Shuffle can move this check into the hookshot room + set_rule(world.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) + set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or ( + location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))) - # It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements. - # However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos. - set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2)) - # It is possible to need more than 3 keys .... - set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3)) + # this seemed to be causing generation failure, disable for now + # if world.accessibility[player] != 'locations': + # set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) - #The actual requirements for these rooms to avoid key-lock - set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or (( - item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2))) + # It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements. + # However we need to leave these at the lower values to derive that with 7 keys it is always possible to reach Bob and Ice Armos. + set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 6)) + # It is possible to need more than 7 keys .... + set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests + back_chests, [player] * len(randomizer_room_chests + back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))) + + # The actual requirements for these rooms to avoid key-lock + set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or + ((item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))) for location in randomizer_room_chests: - set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) + set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or ( + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 6))) - # Once again it is possible to need more than 3 keys... - set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player)) + # Once again it is possible to need more than 7 keys... + set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))) + set_rule(world.get_entrance('Ganons Tower (Bottom) (East)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(back_chests, [player] * len(back_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5))) # Actual requirements for location in compass_room_chests: - set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))) + set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 7) or ( + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 5)))) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) @@ -408,9 +479,9 @@ def global_rules(world, player): set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), - lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3)) + lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7)) set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), - lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4)) + lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8)) set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player), lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) @@ -797,15 +868,21 @@ def add_conditional_lamps(world, player): if world.mode[player] != 'inverted': add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance') add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower') + add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower') + add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Agahnims Tower') else: add_conditional_lamp('Agahnim 1', 'Inverted Agahnims Tower', 'Entrance') add_conditional_lamp('Castle Tower - Dark Maze', 'Inverted Agahnims Tower') + add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Inverted Agahnims Tower') + add_conditional_lamp('Castle Tower - Circle of Pots Key Drop', 'Inverted Agahnims Tower') add_conditional_lamp('Old Man', 'Old Man Cave') add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance') add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance') add_conditional_lamp('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave', 'Entrance') add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance') add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance') + add_conditional_lamp('Eastern Palace - Dark Square Pot Key', 'Eastern Palace') + add_conditional_lamp('Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace', 'Location', True) add_conditional_lamp('Eastern Palace - Big Key Chest', 'Eastern Palace') add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True) add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True) @@ -817,17 +894,32 @@ def add_conditional_lamps(world, player): def open_rules(world, player): - # softlock protection as you can reach the sewers small key door with a guard drop key - set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) + def basement_key_rule(state): + if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player): + return state._lttp_has_key("Small Key (Hyrule Castle)", player, 2) + else: + return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3) + + set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), basement_key_rule) + set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), basement_key_rule) + + set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)) + + set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)) set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), - lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player)) + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and + state.has('Big Key (Hyrule Castle)', player)) def swordless_rules(world, player): set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain - set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace + + set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop if world.mode[player] != 'inverted': @@ -850,11 +942,27 @@ def add_connection(parent_name, target_name, entrance_name, world, player): def standard_rules(world, player): add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) world.get_entrance('Uncle S&Q', player).hide_path = True + set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player)) set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal: + set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)) + set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)) + + set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)) + set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and + state.has('Big Key (Hyrule Castle)', player)) + + set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)) + def toss_junk_item(world, player): items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', 'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap', @@ -869,7 +977,7 @@ def toss_junk_item(world, player): def set_trock_key_rules(world, player): # First set all relevant locked doors to impassible. - for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']: + for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: set_rule(world.get_entrance(entrance, player), lambda state: False) all_state = world.get_all_state(use_cache=False) @@ -892,6 +1000,7 @@ def set_trock_key_rules(world, player): if can_reach_middle and not can_reach_back and not can_reach_front: normal_regions = all_state.reachable_regions[player].copy() set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True) + set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True) all_state.update_reachable_regions(player) front_locked_regions = all_state.reachable_regions[player].difference(normal_regions) front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations) @@ -903,26 +1012,33 @@ def set_trock_key_rules(world, player): # otherwise crystaroller room might not be properly marked as reachable through the back. set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player)) - # No matter what, the key requirement for going from the middle to the bottom should be three keys. - set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) + # No matter what, the key requirement for going from the middle to the bottom should be five keys. + set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5)) # Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we - # might open all the locked doors in any order so we need maximally restrictive rules. + # might open all the locked doors in any order, so we need maximally restrictive rules. if can_reach_back: - set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) - set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) - # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided - set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) - set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)) - else: - # Middle to front requires 2 keys if the back is locked, otherwise 4 - set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2) - if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations) - else state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) + set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) + set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5)) + set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6)) - # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted) - set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)) - set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1)) + set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6)) + set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6)) + set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5)) + else: + # Middle to front requires 3 keys if the back is locked by this door, otherwise 5 + set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3) + if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)})) + else state._lttp_has_key('Small Key (Turtle Rock)', player, 5)) + # Middle to front requires 4 keys if the back is locked by this door, otherwise 6 + set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) + if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations) + else state._lttp_has_key('Small Key (Turtle Rock)', player, 6)) + + # Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted) + set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)) + set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)) + set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1)) set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state))) @@ -933,8 +1049,8 @@ def set_trock_key_rules(world, player): if item in [('Small Key (Turtle Rock)', player)]: return 0 if item in [('Big Key (Turtle Rock)', player)]: - return 2 - return 4 + return 4 + return 6 # If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential if not can_reach_front and not world.smallkey_shuffle[player]: @@ -943,10 +1059,12 @@ def set_trock_key_rules(world, player): if not can_reach_big_chest: # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) + forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player) if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt': if world.bigkey_shuffle[player] and can_reach_big_chest: # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', + 'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']: forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player) else: diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 11a95bf7cd..4b6bc54111 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -66,9 +66,12 @@ def underworld_glitches_rules(world, player): fix_fake_worlds = world.fix_fake_world[player] # Ice Palace Entrance Clip - # This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed. - add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') + # This is the easiest one since it's a simple internal clip. + # Need to also add melting to freezor chest since it's otherwise assumed. + # Also can pick up the first jelly key from behind. + add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player)) + add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or') # Kiki Skip diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8815fae092..65e36da3bd 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -15,7 +15,7 @@ from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem from .Options import alttp_options, smallkey_shuffle from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ - is_main_entrance + is_main_entrance, key_drop_data from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch @@ -303,6 +303,8 @@ class ALTTPWorld(World): world.local_items[player].value |= self.item_name_groups[option.item_name_group] elif option == "different_world": world.non_local_items[player].value |= self.item_name_groups[option.item_name_group] + if world.mode[player] == "standard": + world.non_local_items[player].value -= {"Small Key (Hyrule Castle)"} elif option.in_dungeon: self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group] if option == "original_dungeon": @@ -478,12 +480,17 @@ class ALTTPWorld(World): break else: raise FillError('Unable to place dungeon prizes') + if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \ + and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal and \ + world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons: + world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1 @classmethod def stage_pre_fill(cls, world): from .Dungeons import fill_dungeons_restrictive fill_dungeons_restrictive(world) + @classmethod def stage_post_fill(cls, world): ShopSlotFill(world) @@ -618,7 +625,6 @@ class ALTTPWorld(World): @classmethod def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): trash_counts = {} - for player in world.get_game_players("A Link to the Past"): if not world.ganonstower_vanilla[player] or \ world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: @@ -792,7 +798,7 @@ class ALTTPWorld(World): slot_options = ["crystals_needed_for_gt", "crystals_needed_for_ganon", "open_pyramid", "bigkey_shuffle", "smallkey_shuffle", "compass_shuffle", "map_shuffle", "progressive", "swordless", "retro_bow", "retro_caves", "shop_item_slots", - "boss_shuffle", "pot_shuffle", "enemy_shuffle"] + "boss_shuffle", "pot_shuffle", "enemy_shuffle", "key_drop_shuffle"] slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options} @@ -803,11 +809,11 @@ class ALTTPWorld(World): 'mm_medalion': self.multiworld.required_medallions[self.player][0], 'tr_medalion': self.multiworld.required_medallions[self.player][1], 'shop_shuffle': self.multiworld.shop_shuffle[self.player], - 'entrance_shuffle': self.multiworld.shuffle[self.player] + 'entrance_shuffle': self.multiworld.shuffle[self.player], } ) return slot_data - + def get_same_seed(world, seed_def: tuple) -> str: seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {}) diff --git a/worlds/alttp/test/dungeons/TestAgahnimsTower.py b/worlds/alttp/test/dungeons/TestAgahnimsTower.py index 6d0e1085f5..94e7854858 100644 --- a/worlds/alttp/test/dungeons/TestAgahnimsTower.py +++ b/worlds/alttp/test/dungeons/TestAgahnimsTower.py @@ -16,6 +16,18 @@ class TestAgahnimsTower(TestDungeon): ["Castle Tower - Dark Maze", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], ["Castle Tower - Dark Maze", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Lamp']], + ["Castle Tower - Dark Archer Key Drop", False, []], + ["Castle Tower - Dark Archer Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']], + ["Castle Tower - Dark Archer Key Drop", False, [], ['Lamp']], + ["Castle Tower - Dark Archer Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Dark Archer Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']], + + ["Castle Tower - Circle of Pots Key Drop", False, []], + ["Castle Tower - Circle of Pots Key Drop", False, ['Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)']], + ["Castle Tower - Circle of Pots Key Drop", False, [], ['Lamp']], + ["Castle Tower - Circle of Pots Key Drop", False, [], ['Progressive Sword', 'Hammer', 'Progressive Bow', 'Fire Rod', 'Ice Rod', 'Cane of Somaria', 'Cane of Byrna']], + ["Castle Tower - Circle of Pots Key Drop", True, ['Progressive Sword', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)', 'Lamp']], + ["Agahnim 1", False, []], ["Agahnim 1", False, ['Small Key (Agahnims Tower)'], ['Small Key (Agahnims Tower)']], ["Agahnim 1", False, [], ['Progressive Sword']], diff --git a/worlds/alttp/test/dungeons/TestDesertPalace.py b/worlds/alttp/test/dungeons/TestDesertPalace.py index 8423e681cf..2d19513911 100644 --- a/worlds/alttp/test/dungeons/TestDesertPalace.py +++ b/worlds/alttp/test/dungeons/TestDesertPalace.py @@ -18,12 +18,27 @@ class TestDesertPalace(TestDungeon): ["Desert Palace - Compass Chest", False, []], ["Desert Palace - Compass Chest", False, [], ['Small Key (Desert Palace)']], - ["Desert Palace - Compass Chest", True, ['Small Key (Desert Palace)']], + ["Desert Palace - Compass Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']], + ["Desert Palace - Compass Chest", False, ['Small Key (Desert Palace)']], + ["Desert Palace - Compass Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)']], - #@todo: Require a real weapon for enemizer? ["Desert Palace - Big Key Chest", False, []], ["Desert Palace - Big Key Chest", False, [], ['Small Key (Desert Palace)']], - ["Desert Palace - Big Key Chest", True, ['Small Key (Desert Palace)']], + ["Desert Palace - Big Key Chest", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']], + ["Desert Palace - Big Key Chest", False, ['Small Key (Desert Palace)']], + ["Desert Palace - Big Key Chest", True, ['Progressive Sword', 'Small Key (Desert Palace)']], + + ["Desert Palace - Desert Tiles 1 Pot Key", True, []], + + ["Desert Palace - Beamos Hall Pot Key", False, []], + ["Desert Palace - Beamos Hall Pot Key", False, [], ['Small Key (Desert Palace)']], + ["Desert Palace - Beamos Hall Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']], + ["Desert Palace - Beamos Hall Pot Key", True, ['Small Key (Desert Palace)', 'Progressive Sword']], + + ["Desert Palace - Desert Tiles 2 Pot Key", False, []], + ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Small Key (Desert Palace)']], + ["Desert Palace - Desert Tiles 2 Pot Key", False, ['Progressive Sword', 'Hammer', 'Fire Rod', 'Ice Rod', 'Progressive Bow', 'Cane of Somaria', 'Cane of Byrna']], + ["Desert Palace - Desert Tiles 2 Pot Key", True, ['Small Key (Desert Palace)', 'Progressive Sword']], ["Desert Palace - Boss", False, []], ["Desert Palace - Boss", False, [], ['Small Key (Desert Palace)']], @@ -33,7 +48,6 @@ class TestDesertPalace(TestDungeon): ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Fire Rod']], ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Progressive Sword']], ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Hammer']], - ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Ice Rod']], ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Somaria']], ["Desert Palace - Boss", True, ['Small Key (Desert Palace)', 'Big Key (Desert Palace)', 'Lamp', 'Cane of Byrna']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 73ece15417..81085ab10a 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -61,6 +61,7 @@ class TestDungeon(unittest.TestCase): for item in items: item.classification = ItemClassification.progression - state.collect(item) + state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up + state.sweep_for_events() # key drop keys repeatedly - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access) \ No newline at end of file + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestEasternPalace.py b/worlds/alttp/test/dungeons/TestEasternPalace.py index 0497a1132e..35c1b99283 100644 --- a/worlds/alttp/test/dungeons/TestEasternPalace.py +++ b/worlds/alttp/test/dungeons/TestEasternPalace.py @@ -18,7 +18,8 @@ class TestEasternPalace(TestDungeon): ["Eastern Palace - Big Key Chest", False, []], ["Eastern Palace - Big Key Chest", False, [], ['Lamp']], - ["Eastern Palace - Big Key Chest", True, ['Lamp']], + ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Small Key (Eastern Palace)', 'Small Key (Eastern Palace)']], + ["Eastern Palace - Big Key Chest", True, ['Lamp', 'Big Key (Eastern Palace)']], #@todo: Advanced? ["Eastern Palace - Boss", False, []], diff --git a/worlds/alttp/test/dungeons/TestGanonsTower.py b/worlds/alttp/test/dungeons/TestGanonsTower.py index f81509273f..d22dc92b36 100644 --- a/worlds/alttp/test/dungeons/TestGanonsTower.py +++ b/worlds/alttp/test/dungeons/TestGanonsTower.py @@ -33,46 +33,50 @@ class TestGanonsTower(TestDungeon): ["Ganons Tower - Randomizer Room - Top Left", False, []], ["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hammer']], ["Ganons Tower - Randomizer Room - Top Left", False, [], ['Hookshot']], - ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Randomizer Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Randomizer Room - Top Right", False, []], ["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hammer']], ["Ganons Tower - Randomizer Room - Top Right", False, [], ['Hookshot']], - ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Randomizer Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Randomizer Room - Bottom Left", False, []], ["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hammer']], ["Ganons Tower - Randomizer Room - Bottom Left", False, [], ['Hookshot']], - ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Randomizer Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Randomizer Room - Bottom Right", False, []], ["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hammer']], ["Ganons Tower - Randomizer Room - Bottom Right", False, [], ['Hookshot']], - ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Randomizer Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Firesnake Room", False, []], ["Ganons Tower - Firesnake Room", False, [], ['Hammer']], ["Ganons Tower - Firesnake Room", False, [], ['Hookshot']], - ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Firesnake Room", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Map Chest", False, []], ["Ganons Tower - Map Chest", False, [], ['Hammer']], ["Ganons Tower - Map Chest", False, [], ['Hookshot', 'Pegasus Boots']], - ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], - ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']], + ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Map Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hammer', 'Pegasus Boots']], ["Ganons Tower - Big Chest", False, []], ["Ganons Tower - Big Chest", False, [], ['Big Key (Ganons Tower)']], - ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Chest", True, ['Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Hope Room - Left", True, []], ["Ganons Tower - Hope Room - Right", True, []], ["Ganons Tower - Bob's Chest", False, []], - ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Bob's Chest", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Tile Room", False, []], ["Ganons Tower - Tile Room", False, [], ['Cane of Somaria']], @@ -81,34 +85,34 @@ class TestGanonsTower(TestDungeon): ["Ganons Tower - Compass Room - Top Left", False, []], ["Ganons Tower - Compass Room - Top Left", False, [], ['Cane of Somaria']], ["Ganons Tower - Compass Room - Top Left", False, [], ['Fire Rod']], - ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Compass Room - Top Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], ["Ganons Tower - Compass Room - Top Right", False, []], ["Ganons Tower - Compass Room - Top Right", False, [], ['Cane of Somaria']], ["Ganons Tower - Compass Room - Top Right", False, [], ['Fire Rod']], - ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Compass Room - Top Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], ["Ganons Tower - Compass Room - Bottom Left", False, []], ["Ganons Tower - Compass Room - Bottom Left", False, [], ['Cane of Somaria']], ["Ganons Tower - Compass Room - Bottom Left", False, [], ['Fire Rod']], - ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Compass Room - Bottom Left", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], ["Ganons Tower - Compass Room - Bottom Right", False, []], ["Ganons Tower - Compass Room - Bottom Right", False, [], ['Cane of Somaria']], ["Ganons Tower - Compass Room - Bottom Right", False, [], ['Fire Rod']], - ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], + ["Ganons Tower - Compass Room - Bottom Right", True, ['Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Cane of Somaria']], ["Ganons Tower - Big Key Chest", False, []], - ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Chest", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Big Key Room - Left", False, []], - ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Room - Left", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Big Key Room - Right", False, []], - ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], - ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], + ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Cane of Somaria', 'Fire Rod']], + ["Ganons Tower - Big Key Room - Right", True, ['Progressive Bow', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Hookshot', 'Hammer']], ["Ganons Tower - Mini Helmasaur Room - Left", False, []], ["Ganons Tower - Mini Helmasaur Room - Left", False, [], ['Progressive Bow']], @@ -128,8 +132,8 @@ class TestGanonsTower(TestDungeon): ["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']], ["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']], ["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']], - ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']], - ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']], + ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']], + ["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']], ["Ganons Tower - Validation Chest", False, []], ["Ganons Tower - Validation Chest", False, [], ['Hookshot']], @@ -137,8 +141,8 @@ class TestGanonsTower(TestDungeon): ["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']], ["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']], ["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']], - ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']], - ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']], - ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']], - ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']], + ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']], + ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']], + ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']], + ["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestIcePalace.py b/worlds/alttp/test/dungeons/TestIcePalace.py index 3c075fe5ea..edc9f1fbae 100644 --- a/worlds/alttp/test/dungeons/TestIcePalace.py +++ b/worlds/alttp/test/dungeons/TestIcePalace.py @@ -72,8 +72,9 @@ class TestIcePalace(TestDungeon): ["Ice Palace - Boss", False, [], ['Big Key (Ice Palace)']], ["Ice Palace - Boss", False, [], ['Fire Rod', 'Bombos']], ["Ice Palace - Boss", False, [], ['Fire Rod', 'Progressive Sword']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)']], - ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)']], + # need hookshot now to reach the right side for the 6th key + ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Fire Rod', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Small Key (Ice Palace)', 'Small Key (Ice Palace)', 'Hookshot']], + ["Ice Palace - Boss", True, ['Progressive Glove', 'Big Key (Ice Palace)', 'Bombos', 'Progressive Sword', 'Hammer', 'Cane of Somaria', 'Small Key (Ice Palace)', 'Hookshot']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestSkullWoods.py b/worlds/alttp/test/dungeons/TestSkullWoods.py index 2dab840cf4..7f97c4d2f8 100644 --- a/worlds/alttp/test/dungeons/TestSkullWoods.py +++ b/worlds/alttp/test/dungeons/TestSkullWoods.py @@ -26,18 +26,18 @@ class TestSkullWoods(TestDungeon): ["Skull Woods - Big Chest", False, [], ['Never in logic']], ["Skull Woods - Compass Chest", False, []], - ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], - ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']], + ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ["Skull Woods - Map Chest", True, []], ["Skull Woods - Pot Prison", False, []], - ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], - ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']], + ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ["Skull Woods - Pinball Room", False, []], - ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']], - ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']] + ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ]) def testSkullWoodsLeftOnly(self): @@ -50,8 +50,8 @@ class TestSkullWoods(TestDungeon): ["Skull Woods - Compass Chest", True, []], ["Skull Woods - Map Chest", False, []], - ["Skull Woods - Map Chest", False, [], ['Small Key (Skull Woods)']], - ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)']], + ["Skull Woods - Map Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Map Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ["Skull Woods - Pot Prison", True, []], @@ -67,18 +67,18 @@ class TestSkullWoods(TestDungeon): ["Skull Woods - Big Chest", True, ['Big Key (Skull Woods)']], ["Skull Woods - Compass Chest", False, []], - ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], - ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']], + ["Skull Woods - Compass Chest", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Compass Chest", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ["Skull Woods - Map Chest", True, []], ["Skull Woods - Pot Prison", False, []], - ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], - ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)']], + ["Skull Woods - Pot Prison", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Pot Prison", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']], ["Skull Woods - Pinball Room", False, []], - ["Skull Woods - Pinball Room", False, [], ['Small Key (Skull Woods)']], - ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)']] + ["Skull Woods - Pinball Room", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Pinball Room", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)']] ]) def testSkullWoodsMiddle(self): @@ -94,6 +94,6 @@ class TestSkullWoods(TestDungeon): ["Skull Woods - Boss", False, []], ["Skull Woods - Boss", False, [], ['Fire Rod']], ["Skull Woods - Boss", False, [], ['Progressive Sword']], - ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], - ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']], + ["Skull Woods - Boss", False, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)'], ['Small Key (Skull Woods)']], + ["Skull Woods - Boss", True, ['Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Small Key (Skull Woods)', 'Fire Rod', 'Progressive Sword']], ]) \ No newline at end of file diff --git a/worlds/alttp/test/dungeons/TestThievesTown.py b/worlds/alttp/test/dungeons/TestThievesTown.py index a7e20bc520..01f1570a25 100644 --- a/worlds/alttp/test/dungeons/TestThievesTown.py +++ b/worlds/alttp/test/dungeons/TestThievesTown.py @@ -6,10 +6,6 @@ class TestThievesTown(TestDungeon): def testThievesTown(self): self.starting_regions = ['Thieves Town (Entrance)'] self.run_tests([ - ["Thieves' Town - Attic", False, []], - ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']], - ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']], - ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']], ["Thieves' Town - Big Key Chest", True, []], @@ -19,6 +15,19 @@ class TestThievesTown(TestDungeon): ["Thieves' Town - Ambush Chest", True, []], + ["Thieves' Town - Hallway Pot Key", False, []], + ["Thieves' Town - Hallway Pot Key", False, [], ['Big Key (Thieves Town)']], + ["Thieves' Town - Hallway Pot Key", True, ['Big Key (Thieves Town)']], + + ["Thieves' Town - Spike Switch Pot Key", False, []], + ["Thieves' Town - Spike Switch Pot Key", False, [], ['Big Key (Thieves Town)']], + ["Thieves' Town - Spike Switch Pot Key", True, ['Big Key (Thieves Town)']], + + ["Thieves' Town - Attic", False, []], + ["Thieves' Town - Attic", False, [], ['Big Key (Thieves Town)']], + ["Thieves' Town - Attic", False, [], ['Small Key (Thieves Town)']], + ["Thieves' Town - Attic", True, ['Big Key (Thieves Town)', 'Small Key (Thieves Town)']], + ["Thieves' Town - Big Chest", False, []], ["Thieves' Town - Big Chest", False, [], ['Big Key (Thieves Town)']], ["Thieves' Town - Big Chest", False, [], ['Small Key (Thieves Town)']], @@ -31,7 +40,6 @@ class TestThievesTown(TestDungeon): ["Thieves' Town - Boss", False, []], ["Thieves' Town - Boss", False, [], ['Big Key (Thieves Town)']], - ["Thieves' Town - Boss", False, [], ['Small Key (Thieves Town)']], ["Thieves' Town - Boss", False, [], ['Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna']], ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Hammer']], ["Thieves' Town - Boss", True, ['Small Key (Thieves Town)', 'Big Key (Thieves Town)', 'Progressive Sword']], diff --git a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py index 533e3c650f..fe8979c1ef 100644 --- a/worlds/alttp/test/inverted/TestInvertedTurtleRock.py +++ b/worlds/alttp/test/inverted/TestInvertedTurtleRock.py @@ -18,10 +18,9 @@ class TestInvertedTurtleRock(TestInverted): ["Turtle Rock - Chain Chomps", False, []], ["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']], - # Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door) ["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], @@ -55,8 +54,8 @@ class TestInvertedTurtleRock(TestInverted): ["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], @@ -66,8 +65,8 @@ class TestInvertedTurtleRock(TestInverted): ["Turtle Rock - Big Key Chest", False, []], ["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], # Mirror in from ledge, use left side entrance, have enough keys to get to the chest ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], @@ -80,8 +79,8 @@ class TestInvertedTurtleRock(TestInverted): ["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']], ["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], @@ -98,9 +97,9 @@ class TestInvertedTurtleRock(TestInverted): ["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']], ["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']] ]) @@ -115,12 +114,12 @@ class TestInvertedTurtleRock(TestInverted): [location, False, [], ['Magic Mirror', 'Cane of Somaria']], [location, False, [], ['Magic Mirror', 'Lamp']], [location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], # Mirroring into Eye Bridge does not require Cane of Somaria [location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']], diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py index a25d89a6f4..d7b5c9f797 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedTurtleRock.py @@ -20,8 +20,8 @@ class TestInvertedTurtleRock(TestInvertedMinor): ["Turtle Rock - Chain Chomps", False, [], ['Magic Mirror', 'Cane of Somaria']], # Item rando only needs 1 key. ER needs to consider the case when the back is accessible, but not the middle (key wasted on Trinexx door) ["Turtle Rock - Chain Chomps", False, ['Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Chain Chomps", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Chain Chomps", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Chain Chomps", True, ['Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], @@ -55,8 +55,8 @@ class TestInvertedTurtleRock(TestInvertedMinor): ["Turtle Rock - Big Chest", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Big Chest", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Big Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hookshot']], ["Turtle Rock - Big Chest", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], @@ -66,8 +66,8 @@ class TestInvertedTurtleRock(TestInvertedMinor): ["Turtle Rock - Big Key Chest", False, []], ["Turtle Rock - Big Key Chest", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], # Mirror in from ledge, use left side entrance, have enough keys to get to the chest ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Big Key Chest", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], @@ -80,8 +80,8 @@ class TestInvertedTurtleRock(TestInvertedMinor): ["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']], ["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']], ["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], - ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], + ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']], ["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']], @@ -98,9 +98,9 @@ class TestInvertedTurtleRock(TestInvertedMinor): ["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']], ["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']], ["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], - ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], + ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']], ["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']] ]) @@ -116,12 +116,12 @@ class TestInvertedTurtleRock(TestInvertedMinor): [location, False, [], ['Magic Mirror', 'Cane of Somaria']], [location, False, [], ['Magic Mirror', 'Lamp']], [location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']], - [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], - [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']], + [location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], + [location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']], # Mirroring into Eye Bridge does not require Cane of Somaria [location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']], diff --git a/worlds/alttp/test/owg/TestDungeons.py b/worlds/alttp/test/owg/TestDungeons.py index 284b489b16..4f87896967 100644 --- a/worlds/alttp/test/owg/TestDungeons.py +++ b/worlds/alttp/test/owg/TestDungeons.py @@ -6,6 +6,7 @@ class TestDungeons(TestVanillaOWG): def testFirstDungeonChests(self): self.run_location_tests([ ["Hyrule Castle - Map Chest", True, []], + ["Hyrule Castle - Map Guard Key Drop", True, []], ["Sanctuary", True, []], From 5d47c5b31691f07b72a8f1c942a04532827b6d3e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Sep 2023 11:26:08 +0200 Subject: [PATCH 002/327] WebHost: check that worlds system is not loaded in customserver (#2222) --- WebHostLib/customserver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 16f8c8b261..6d633314b2 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -11,6 +11,7 @@ import socket import threading import time import typing +import sys import websockets from pony.orm import commit, db_session, select @@ -164,8 +165,10 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, db.generate_mapping(check_tables=False) async def main(): - import gc + if "worlds" in sys.modules: + raise Exception("Worlds system should not be loaded in the custom server.") + import gc Utils.init_logging(str(room_id), write_mode="a") ctx = WebHostContext(static_server_data) ctx.load(room_id) From e114ed5566abe4c51cae90b9215545dd5701315a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 24 Sep 2023 01:51:26 +0200 Subject: [PATCH 003/327] Core: offer API hook to modify Group World creation --- BaseClasses.py | 9 +-------- worlds/AutoWorld.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 26cdfb5285..535338b4ec 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -202,14 +202,7 @@ class MultiWorld(): self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] - for option_key, option in world_type.option_definitions.items(): - getattr(self, option_key)[new_id] = option(option.default) - for option_key, option in Options.common_options.items(): - getattr(self, option_key)[new_id] = option(option.default) - for option_key, option in Options.per_game_common_options.items(): - getattr(self, option_key)[new_id] = option(option.default) - - self.worlds[new_id] = world_type(self, new_id) + self.worlds[new_id] = world_type.create_group(self, new_id, players) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) self.player_name[new_id] = name diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 217269aa99..e2fda16b87 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -358,6 +358,21 @@ class World(metaclass=AutoWorldRegister): logging.warning(f"World {self} is generating a filler item without custom filler pool.") return self.multiworld.random.choice(tuple(self.item_name_to_id.keys())) + @classmethod + def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: + """Creates a group, which is an instance of World that is responsible for multiple others. + An example case is ItemLinks creating these.""" + import Options + + for option_key, option in cls.option_definitions.items(): + getattr(multiworld, option_key)[new_player_id] = option(option.default) + for option_key, option in Options.common_options.items(): + getattr(multiworld, option_key)[new_player_id] = option(option.default) + for option_key, option in Options.per_game_common_options.items(): + getattr(multiworld, option_key)[new_player_id] = option(option.default) + + return cls(multiworld, new_player_id) + # decent place to implement progressive items, in most cases can stay as-is def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: """Collect an item name into state. For speed reasons items that aren't logically useful get skipped. From 368fa649148d62ab9875cb63ac4e8eff252eefc9 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 29 Sep 2023 11:18:43 -0700 Subject: [PATCH 004/327] LttP: Update credits text for GT Big Key when key drop shuffle is on. (#2235) --- worlds/alttp/Rom.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index ef4f943575..47cea8c20e 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -925,6 +925,10 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x18700B, 10) # Thieves Town rom.write_byte(0x18700C, 14) # Turtle Rock rom.write_byte(0x18700D, 31) # Ganons Tower + # update credits GT Big Key counter + gt_bigkey_top, gt_bigkey_bottom = credits_digit(5) + rom.write_byte(0x118B6A, gt_bigkey_top) + rom.write_byte(0x118B88, gt_bigkey_bottom) From 1c9199761b85619538489f5fd4132e505787d240 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:23:46 -0400 Subject: [PATCH 005/327] LTTP: Key Drop Shuffle fix for dungeon state item removal (#2232) --- worlds/alttp/Dungeons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 045969be53..630d61e019 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -255,7 +255,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if all_state_base.has("Triforce", player): all_state_base.remove(multiworld.worlds[player].create_item("Triforce")) - for (player, key_drop_shuffle) in enumerate(multiworld.key_drop_shuffle.values(), start=1): + for (player, key_drop_shuffle) in multiworld.key_drop_shuffle.items(): if not key_drop_shuffle and player not in multiworld.groups: for key_loc in key_drop_data: key_data = key_drop_data[key_loc] From f33babc4206f6dffb4ec41e7a73057ba8dbdbc8c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 30 Sep 2023 04:53:11 -0500 Subject: [PATCH 006/327] Tests: add a name removal method (#2233) * Tests: add a name removal method, and have assertAccessDependency use and dispose its own state * Update test/TestBase.py --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/TestBase.py | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/test/TestBase.py b/test/TestBase.py index 1f0853ef14..856428fb57 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -141,13 +141,16 @@ class WorldTestBase(unittest.TestCase): call_all(self.multiworld, step) # methods that can be called within tests - def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: + def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]], + state: typing.Optional[CollectionState] = None) -> None: """Collects all pre-placed items and items in the multiworld itempool except those provided""" if isinstance(item_names, str): item_names = (item_names,) + if not state: + state = self.multiworld.state for item in self.multiworld.get_items(): if item.name not in item_names: - self.multiworld.state.collect(item) + state.collect(item) def get_item_by_name(self, item_name: str) -> Item: """Returns the first item found in placed items, or in the itempool with the matching name""" @@ -174,6 +177,12 @@ class WorldTestBase(unittest.TestCase): items = (items,) for item in items: self.multiworld.state.collect(item) + + def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """Remove all of the items in the item pool with the given names from state""" + items = self.get_items_by_name(item_names) + self.remove(items) + return items def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: """Removes the provided item(s) from state""" @@ -198,23 +207,32 @@ class WorldTestBase(unittest.TestCase): def assertAccessDependency(self, locations: typing.List[str], - possible_items: typing.Iterable[typing.Iterable[str]]) -> None: + possible_items: typing.Iterable[typing.Iterable[str]], + only_check_listed: bool = False) -> None: """Asserts that the provided locations can't be reached without the listed items but can be reached with any one of the provided combinations""" all_items = [item_name for item_names in possible_items for item_name in item_names] - self.collect_all_but(all_items) - for location in self.multiworld.get_locations(): - loc_reachable = self.multiworld.state.can_reach(location) - self.assertEqual(loc_reachable, location.name not in locations, - f"{location.name} is reachable without {all_items}" if loc_reachable - else f"{location.name} is not reachable without {all_items}") - for item_names in possible_items: - items = self.collect_by_name(item_names) + state = CollectionState(self.multiworld) + self.collect_all_but(all_items, state) + if only_check_listed: for location in locations: - self.assertTrue(self.can_reach_location(location), + self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}") + else: + for location in self.multiworld.get_locations(): + loc_reachable = state.can_reach(location, "Location", 1) + self.assertEqual(loc_reachable, location.name not in locations, + f"{location.name} is reachable without {all_items}" if loc_reachable + else f"{location.name} is not reachable without {all_items}") + for item_names in possible_items: + items = self.get_items_by_name(item_names) + for item in items: + state.collect(item) + for location in locations: + self.assertTrue(state.can_reach(location, "Location", 1), f"{location} not reachable with {item_names}") - self.remove(items) + for item in items: + state.remove(item) def assertBeatable(self, beatable: bool): """Asserts that the game can be beaten with the current state""" From 5bf3de45f43ce5dd1e57df9dfd2ee549f0047c1a Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 30 Sep 2023 03:32:44 -0700 Subject: [PATCH 007/327] DS3: Add cinders item group (#2226) --- worlds/dark_souls_3/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b78ff0548a..195d319887 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -52,6 +52,14 @@ class DarkSouls3World(World): required_client_version = (0, 4, 2) item_name_to_id = DarkSouls3Item.get_name_to_id() location_name_to_id = DarkSouls3Location.get_name_to_id() + item_name_groups = { + "Cinders": { + "Cinders of a Lord - Abyss Watcher", + "Cinders of a Lord - Aldrich", + "Cinders of a Lord - Yhorm the Giant", + "Cinders of a Lord - Lothric Prince" + } + } def __init__(self, multiworld: MultiWorld, player: int): From fe6096464cc242818697eedfdea2dca0a000fedc Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 30 Sep 2023 05:35:07 -0500 Subject: [PATCH 008/327] The Messenger: fix rules for two glacial peak locations (#2234) * The Messenger: fix rules for two glacial peak locations --- worlds/messenger/Rules.py | 9 ++++----- worlds/messenger/test/TestLogic.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index b72d454a7e..c24f60fbaa 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -182,8 +182,10 @@ class MessengerHardRules(MessengerRules): "Searing Crags Seal - Raining Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state), "Searing Crags Seal - Rhythm Rocks": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state), "Searing Crags - Power Thistle": lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state), - "Glacial Peak Seal - Ice Climbers": self.has_vertical, + "Glacial Peak Seal - Ice Climbers": lambda state: self.has_vertical(state) or self.can_dboost(state), "Glacial Peak Seal - Projectile Spike Pit": self.true, + "Glacial Peak Seal - Glacial Air Swag": lambda state: self.has_windmill(state) or self.has_vertical(state), + "Glacial Peak Mega Shard": lambda state: self.has_windmill(state) or self.has_vertical(state), "Cloud Ruins Seal - Ghost Pit": self.true, "Bamboo Creek - Claustro": self.has_wingsuit, "Tower of Time Seal - Lantern Climb": self.has_wingsuit, @@ -201,10 +203,7 @@ class MessengerHardRules(MessengerRules): "Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state), "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) or self.has_wingsuit(state), - "Glacial Peak Seal - Glacial Air Swag": self.has_windmill, - "Glacial Peak Seal - Ice Climbers": lambda state: self.has_wingsuit(state) or self.can_dboost(state), - "Underworld Seal - Fireball Wave": lambda state: state.has_all({"Lightfoot Tabi", "Windmill Shuriken"}, - self.player), + "Underworld Seal - Fireball Wave": self.has_windmill, } def has_windmill(self, state: CollectionState) -> bool: diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py index 45b0d0dab6..932bc13867 100644 --- a/worlds/messenger/test/TestLogic.py +++ b/worlds/messenger/test/TestLogic.py @@ -1,3 +1,5 @@ +from typing import Iterable, List + from BaseClasses import ItemClassification from . import MessengerTestBase @@ -5,6 +7,7 @@ from . import MessengerTestBase class HardLogicTest(MessengerTestBase): options = { "logic_level": "hard", + "shuffle_shards": "true", } def testVertical(self) -> None: @@ -19,16 +22,20 @@ class HardLogicTest(MessengerTestBase): "Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", + "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", # forlorn temple "Forlorn Temple - Demon King", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", + "Sunny Day Mega Shard", "Down Under Mega Shard", # catacombs "Catacombs - Necro", "Catacombs - Ruxxtin's Amulet", "Catacombs - Ruxxtin", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", + "Catacombs Mega Shard", # bamboo creek "Bamboo Creek - Claustro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", + "Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard", # howling grotto "Howling Grotto - Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", # searing crags @@ -36,6 +43,7 @@ class HardLogicTest(MessengerTestBase): # cloud ruins "Cloud Ruins - Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", + "Cloud Entrance Mega Shard", "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2", # underworld "Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb", # elemental skylands @@ -73,6 +81,18 @@ class HardLogicTest(MessengerTestBase): item = self.get_item_by_name("Rope Dart") self.collect(item) self.assertTrue(self.can_reach_location(special_loc)) + + def testGlacial(self) -> None: + """Test Glacial Peak locations.""" + self.assertAccessDependency(["Glacial Peak Seal - Ice Climbers"], + [["Second Wind", "Meditation"], ["Rope Dart"], ["Wingsuit"]], + True) + self.assertAccessDependency(["Glacial Peak Seal - Projectile Spike Pit"], + [["Strike of the Ninja"], ["Windmill Shuriken"], ["Rope Dart"], ["Wingsuit"]], + True) + self.assertAccessDependency(["Glacial Peak Seal - Glacial Air Swag", "Glacial Peak Mega Shard"], + [["Windmill Shuriken"], ["Wingsuit"], ["Rope Dart"]], + True) class NoLogicTest(MessengerTestBase): From 5c640c6c52fc9d27500aa273560b6912547201d4 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Sat, 30 Sep 2023 13:03:55 -0600 Subject: [PATCH 009/327] Blasphemous: Fix rules for platforming room in BotSS (#2231) --- worlds/blasphemous/Rules.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 248ff645bc..4218fa94cf 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -4193,8 +4193,9 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("BotSS: Platforming gauntlet", player), lambda state: ( - state.has("D17BZ02S01[FrontR]", player) - or state.has_all({"Dash Ability", "Wall Climb Ability"}, player) + #state.has("D17BZ02S01[FrontR]", player) or + # TODO: actually fix this once door rando is real + state.has_all({"Dash Ability", "Wall Climb Ability"}, player) )) # Doors set_rule(world.get_entrance("D17BZ02S01[FrontR]", player), From a3907e800ba12339bf7bd77a0c443cfcd9f77942 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 30 Sep 2023 15:05:07 -0400 Subject: [PATCH 010/327] SMZ3: 0.4.2 non local items fix (#2212) fixed generation failure when using non_local_items set to "Everything" For this, GT prefill now allows non local non progression items to be placed. --- worlds/smz3/TotalSMZ3/Item.py | 1 + worlds/smz3/__init__.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py index b4fc9d5925..28e9658ce1 100644 --- a/worlds/smz3/TotalSMZ3/Item.py +++ b/worlds/smz3/TotalSMZ3/Item.py @@ -181,6 +181,7 @@ class Item: keycard = re.compile("^Card") smMap = re.compile("^SmMap") + def IsNameDungeonItem(item_name): return Item.dungeon.match(item_name) def IsDungeonItem(self): return self.dungeon.match(self.Type.name) def IsBigKey(self): return self.bigKey.match(self.Type.name) def IsKey(self): return self.key.match(self.Type.name) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 79ba17db82..e2eb2ac80a 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -221,7 +221,9 @@ class SMZ3World(World): if (self.smz3World.Config.Keysanity): progressionItems = self.progression + self.dungeon + self.keyCardsItems + self.SmMapsItems else: - progressionItems = self.progression + progressionItems = self.progression + # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local + self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) @@ -548,11 +550,8 @@ class SMZ3World(World): def JunkFillGT(self, factor): poolLength = len(self.multiworld.itempool) - playerGroups = self.multiworld.get_player_groups(self.player) - playerGroups.add(self.player) junkPoolIdx = [i for i in range(0, poolLength) - if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and - self.multiworld.itempool[i].player in playerGroups] + if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap)] toRemove = [] for loc in self.locations.values(): # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT @@ -563,6 +562,7 @@ class SMZ3World(World): poolLength = len(junkPoolIdx) # start looking at a random starting index and loop at start if no match found start = self.multiworld.random.randint(0, poolLength) + itemFromPool = None for off in range(0, poolLength): i = (start + off) % poolLength candidate = self.multiworld.itempool[junkPoolIdx[i]] @@ -570,6 +570,7 @@ class SMZ3World(World): itemFromPool = candidate toRemove.append(junkPoolIdx[i]) break + assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT" self.multiworld.push_item(loc, itemFromPool, False) loc.event = False toRemove.sort(reverse = True) From 58b696e986bd0f92449c7ccf03746a6b8558ab12 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 30 Sep 2023 23:58:58 +0200 Subject: [PATCH 011/327] Factorio: use orjson (#1809) * Factorio: use orjson * Factorio: update orjson --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/factorio/Technologies.py | 4 ++-- worlds/factorio/requirements.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index d68c6f2f77..096396c0e7 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,6 +1,6 @@ from __future__ import annotations -import json +import orjson import logging import os import string @@ -20,7 +20,7 @@ pool = ThreadPoolExecutor(1) def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: - return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode()) + return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json")) techs_future = pool.submit(load_json_data, "techs") diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index c45fb771da..8fb74e9330 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1 +1,2 @@ factorio-rcon-py>=2.0.1 +orjson>=3.9.7 From d5d630dcf04eed5a001c776ab18a6831da7c449e Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 1 Oct 2023 16:13:30 -0700 Subject: [PATCH 012/327] Zillion: change test detection for running tests with multiprocessing (#2243) --- worlds/zillion/__init__.py | 2 +- worlds/zillion/config.py | 17 ----------------- worlds/zillion/requirements.txt | 2 +- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 7c927c10eb..f5e04b4ebc 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -10,7 +10,6 @@ import logging from BaseClasses import ItemClassification, LocationProgressType, \ MultiWorld, Item, CollectionState, Entrance, Tutorial -from .config import detect_test from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion from .options import ZillionStartChar, zillion_options, validate @@ -25,6 +24,7 @@ from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req from zilliandomizer.options import Chars +from zilliandomizer.patch import detect_test from ..AutoWorld import World, WebWorld diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py index db61d0c453..ca02f9a99f 100644 --- a/worlds/zillion/config.py +++ b/worlds/zillion/config.py @@ -2,20 +2,3 @@ import os base_id = 8675309 zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png") - - -def detect_test() -> bool: - """ - Parts of generation that are in unit tests need the rom. - This is to detect whether we are running unit tests - so we can work around the need for the rom. - """ - import __main__ - try: - if "test" in __main__.__file__: - return True - except AttributeError: - # In some environments, __main__ doesn't have __file__ - # We'll assume that's not unit tests. - pass - return False diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 2af057dece..4858ef3153 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4b27d115269db25fe73b0471b73495f41df1323c#0.5.3 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4 From e08deff6f9f0c2192da6f8e7b01fa22e9d930914 Mon Sep 17 00:00:00 2001 From: zig-for Date: Sun, 1 Oct 2023 16:16:25 -0700 Subject: [PATCH 013/327] LADX: Implement remake style warp selection (#1587) --- worlds/ladx/LADXR/generator.py | 23 ++-- worlds/ladx/LADXR/patches/core.py | 205 +++++++++++++++++++++++++++++- worlds/ladx/Options.py | 17 ++- 3 files changed, 234 insertions(+), 11 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 733ab314da..72d631da86 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -55,6 +55,9 @@ from .patches import tradeSequence as _ from . import hints from .patches import bank34 +from .utils import formatText +from ..Options import TrendyGame, Palette +from .roomEditor import RoomEditor, Object from .patches.aesthetics import rgb_to_bin, bin_to_rgb from .locations.keyLocation import KeyLocation @@ -134,7 +137,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.fixWrongWarp(rom) patches.core.alwaysAllowSecretBook(rom) patches.core.injectMainLoop(rom) - + from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon: @@ -239,7 +242,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.quickswap(rom, 1) elif settings.quickswap == 'b': patches.core.quickswap(rom, 0) - + world_setup = logic.world_setup JUNK_HINT = 0.33 @@ -263,7 +266,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m name = "Your" else: name = f"{multiworld.player_name[location.item.player]}'s" - + if isinstance(location, LinksAwakeningLocation): location_name = location.ladxr_item.metadata.name else: @@ -323,7 +326,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles - from .roomEditor import RoomEditor, Object + room_editor = RoomEditor(rom, 0x2A0) if ap_settings["trendy_game"] == TrendyGame.option_easy: @@ -352,12 +355,12 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m } def speed(): return rnd.randint(*speeds[ap_settings["trendy_game"]]) - rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A2-0x4000] = speed() rom.banks[0x4][0x76A6-0x4000] = speed() rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest: - rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() + rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A3-0x4000] = speed() rom.banks[0x4][0x76A5-0x4000] = speed() rom.banks[0x4][0x76A7-0x4000] = 0xFF - speed() @@ -374,12 +377,14 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m [0x0f, 0x38, 0x0f], [0x30, 0x62, 0x30], [0x8b, 0xac, 0x0f], - [0x9b, 0xbc, 0x0f], + [0x9b, 0xbc, 0x0f], ] for color in gb_colors: for channel in range(3): color[channel] = color[channel] * 31 // 0xbc - + + if ap_settings["warp_improvements"]: + patches.core.addWarpImprovements(rom, ap_settings["additional_warp_points"]) palette = ap_settings["palette"] if palette != Palette.option_normal: @@ -410,7 +415,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m for address in range(start, end, 2): packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] r,g,b = bin_to_rgb(packed) - + # 1 bit if palette == Palette.option_1bit: r &= 0b10000 diff --git a/worlds/ladx/LADXR/patches/core.py b/worlds/ladx/LADXR/patches/core.py index a202e661f9..c9f3a7c34b 100644 --- a/worlds/ladx/LADXR/patches/core.py +++ b/worlds/ladx/LADXR/patches/core.py @@ -255,8 +255,9 @@ noWrapDown: """ % (type, map, room, x, y)), fill_nop=True) # Patch the RAM clear not to delete our custom dialog when we screen transition + # This is kind of horrible as it relies on bank 1 being loaded, lol rom.patch(0x01, 0x042C, "C629", "6B7E") - rom.patch(0x01, 0x3E6B, 0x3FFF, ASM(""" + rom.patch(0x01, 0x3E6B, 0x3E7B, ASM(""" ld bc, $A0 call $29DC ld bc, $1200 @@ -537,3 +538,205 @@ OAMData: gfx_low = "\n".join([line.split(" ")[n] for line in tile_graphics.split("\n")[8:]]) rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high) rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) + +def addWarpImprovements(rom, extra_warps): + # Patch in a warp icon + tile = utils.createTileData( \ +"""11111111 +10000000 +10200320 +10323200 +10033300 +10023230 +10230020 +10000000""", key="0231") + MINIMAP_BASE = 0x3800 + + # This is replacing a junk tile never used on the minimap + rom.banks[0x2C][MINIMAP_BASE + len(tile) * 0x65 : MINIMAP_BASE + len(tile) * 0x66] = tile + + # Allow using ENTITY_WARP for finding which map sections are warps + # Interesting - 3CA0 should be free, but something has pushed all the code forward a byte + rom.patch(0x02, 0x3CA1, None, ASM(""" + ld e, $0F + ld d, $00 + warp_search_loop: + ; Warp search loop + ld hl, $C3A0 + add hl, de ; $5FE1: $19 + ld a, [hl] ; $5FE2: $7E + cp $61 ; ENTITY_WARP + jr nz, search_continue ; if it's not a warp, check the next one + ld hl, $C280 + add hl, de + ld a, [hl] + and a + jr z, search_continue ; if this is despawned, check the next one + found: + jp $511B ; found + search_continue: + dec e + ld a, e + cp $FF + jr nz, warp_search_loop + + not_found: + jp $512B + + """)) + + # Insert redirect to above code + rom.patch(0x02, 0x1109, ASM(""" + ldh a, [$F6] + cp 1 + + """), ASM(""" + jp $7CA1 + nop + """)) + # Leaves some extra bytes behind, if we need more space in 0x02 + + # On warp hole, open map instead + rom.patch(0x19, 0x1DB9, None, ASM(""" + ld a, 7 ; Set GAMEPLAY_MAP + ld [$DB95], a + ld a, 0 ; reset subtype + ld [$DB96], a + ld a, 1 ; Set flag for using teleport + ld [$FFDD], a + + ret + """), fill_nop=True) + + # Patch over some instructions that decided if we are in debug mode holding some + # buttons with instead checking for FFDD (why FFDD? It appears to be never used anywhere, so we repurpose it for "is in teleport mode") + rom.banks[0x01][0x17B8] = 0xDD + rom.banks[0x01][0x17B9] = 0xFF + rom.banks[0x01][0x17FD] = 0xDD + rom.banks[0x01][0x17FE] = 0xFF + + # If in warp mode, don't allow manual exit + rom.patch(0x01, 0x1800, "20021E60", ASM("jp nz, $5818"), fill_nop=True) + + # Allow warp with just B + rom.banks[0x01][0x17C0] = 0x20 + + # Allow cursor to move over black squares + # This allows warping to undiscovered areas - a fine cheat, but needs a check for wOverworldRoomStatus in the warp code + CHEAT_WARP_ANYWHERE = False + if CHEAT_WARP_ANYWHERE: + rom.patch(0x01, 0x1AE8, None, ASM("jp $5AF5")) + + # This disables the arrows around the selection bubble + #rom.patch(0x01, 0x1B6F, None, ASM("ret"), fill_nop=True) + + # Fix lag when moving the cursor + # One option - just disable the delay code + #rom.patch(0x01, 0x1A76, 0x1A76+3, ASM("xor a"), fill_nop=True) + #rom.banks[0x01][0x1A7C] = 0 + # Another option - just remove the animation + rom.banks[0x01][0x1B20] = 0 + rom.banks[0x01][0x1B3B] = 0 + + # Patch the icon for all teleports + all_warps = [0x01, 0x95, 0x2C, 0xEC] + + + if extra_warps: + # mamu + all_warps.append(0x45) + # Tweak the flute location + rom.banks[0x14][0x0E95] += 0x10 + rom.banks[0x14][0x0EA3] += 0x01 + + mamu_pond = RoomEditor(rom, 0x45) + # Remove some tall grass so we can add a warp instead + mamu_pond.changeObject(1, 6, 0xE8) + mamu_pond.moveObject(1, 6, 3, 5) + mamu_pond.addEntity(3, 5, 0x61) + + mamu_pond.store(rom) + + # eagle + all_warps.append(0x0F) + room = RoomEditor(rom, 0x0F) + # Move one cliff edge and change it into a pit + room.changeObject(7, 6, 0xE8) + room.moveObject(7, 6, 6, 4) + + # Add the warp + room.addEntity(6, 4, 0x61) + # move the two corners + room.moveObject(6, 7, 7, 7) + room.moveObject(6, 6, 7, 6) + for object in room.objects: + # Extend the lower wall + if ((object.x == 0 and object.y == 7) + # Extend the lower floor + or (object.x == 0 and object.y == 6)): + room.overlay[object.x + object.count + object.y * 10] = object.type_id + object.count += 1 + room.store(rom) + + for warp in all_warps: + # Set icon + rom.banks[0x20][0x168B + warp] = 0x55 + # Set text + if not rom.banks[0x01][0x1959 + warp]: + rom.banks[0x01][0x1959 + warp] = 0x42 + # Set palette + # rom.banks[0x20][0x178B + 0x95] = 0x1 + + # Setup [?!] icon on map and associated text + rom.banks[0x01][0x1909 + 0x42] = 0x2B + rom.texts[0x02B] = utils.formatText('Warp') + + # call warp function (why not just jmp?!) + rom.patch(0x01, 0x17C3, None, ASM(""" + call $7E7B + ret + """)) + + # Build a switch statement by hand + warp_jump = "".join(f"cp ${hex(warp)[2:]}\njr z, success\n" for warp in all_warps) + + rom.patch(0x01, 0x3E7B, None, ASM(f""" +TeleportHandler: + + ld a, [$DBB4] ; Load the current selected tile + ; TODO: check if actually revealed so we can have free movement + ; Check cursor against different tiles to see if we are selecting a warp + {warp_jump} + jr exit + +success: + ld a, $0B + ld [$DB95], a ; Gameplay type + xor a + ld [$D401], a ; wWarp0MapCategory + ldh [$DD], a ; unset teleport flag(!!!) + ld [$D402], a ; wWarp0Map + ld a, [$DBB4] ; wDBB4 + ld [$D403], a ; wWarp0Room + + ld a, $68 + ld [$D404], a ; wWarp0DestinationX + ldh [$98], a ; LinkPositionY + ld [$D475], a + ld a, $70 + ld [$D405], a ; wWarp0DestinationY + ldh [$99], a ; LinkPositionX + ld a, $66 + ld [$D416], a ; wWarp0PositionTileIndex + ld a, $07 + ld [$DB96], a ; wGameplaySubtype + ldh a, [$A2] + ld [$DBC8], a + call $0C83 ; ApplyMapFadeOutTransition + xor a ; $5DF3: $AF + ld [$C167], a ; $5DF4: $EA $67 $C1 + +exit: + ret + """)) + diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index 4c85e5c3d0..eec8f3127c 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -378,7 +378,20 @@ class Palette(Choice): option_greyscale = 3 option_pink = 4 option_inverted = 5 - + +class WarpImprovements(DefaultOffToggle): + """ + [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. + [Off] No change + """ + +class AdditionalWarpPoints(DefaultOffToggle): + """ + [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower + [Off] No change + """ + + links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'logic': Logic, # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), @@ -400,6 +413,8 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { # 'bowwow': Bowwow, # 'overworld': Overworld, 'link_palette': LinkPalette, + 'warp_improvements': WarpImprovements, + 'additional_warp_points': AdditionalWarpPoints, 'trendy_game': TrendyGame, 'gfxmod': GfxMod, 'palette': Palette, From 485aa23afd45fe50f4de62309c961ec465d3e2ce Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 2 Oct 2023 01:56:55 +0200 Subject: [PATCH 014/327] core: utility method for visualizing worlds as PlantUML (#1935) * core: typing for MultiWorld.get_regions * core: utility method for visualizing worlds as PlantUML * core: utility method for visualizing worlds as PlantUML: update docs --- .gitignore | 1 + BaseClasses.py | 3 +- Utils.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ docs/world api.md | 6 +++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8e4cc86657..e374a12954 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ *.archipelago *.apsave *.BIN +*.puml setups build diff --git a/BaseClasses.py b/BaseClasses.py index 535338b4ec..45190ac7b9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -8,6 +8,7 @@ import secrets import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import ChainMap, Counter, deque +from collections.abc import Collection from enum import IntEnum, IntFlag from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ Type, ClassVar @@ -357,7 +358,7 @@ class MultiWorld(): for r_location in region.locations: self._location_cache[r_location.name, player] = r_location - def get_regions(self, player=None): + def get_regions(self, player: Optional[int] = None) -> Collection[Region]: return self.regions if player is None else self._region_cache[player].values() def get_region(self, regionname: str, player: int) -> Region: diff --git a/Utils.py b/Utils.py index 9ceba48299..60b1cdadb7 100644 --- a/Utils.py +++ b/Utils.py @@ -29,6 +29,7 @@ except ImportError: if typing.TYPE_CHECKING: import tkinter import pathlib + from BaseClasses import Region def tuplize_version(version: str) -> Version: @@ -766,3 +767,113 @@ def freeze_support() -> None: import multiprocessing _extend_freeze_support() multiprocessing.freeze_support() + + +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: + """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.) + :param file_name: The name of the destination .puml file. + :param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection. + :param show_locations: (default True) If enabled, the locations will be listed inside each region. + Priority locations will be shown in bold. + Excluded locations will be stricken out. + Locations without ID will be shown in italics. + Locked locations will be shown with a padlock icon. + For filled locations, the item name will be shown after the location name. + Progression items will be shown in bold. + 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. + + Example usage in World code: + from Utils import visualize_regions + visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") + + Example usage in Main code: + from Utils import visualize_regions + for player in world.player_ids: + visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml") + """ + 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 + import re + + uml: typing.List[str] = list() + seen: typing.Set[Region] = set() + regions: typing.Deque[Region] = deque((root_region,)) + multiworld: MultiWorld = root_region.multiworld + + def fmt(obj: Union[Entrance, Item, Location, Region]) -> str: + name = obj.name + if isinstance(obj, Item): + name = multiworld.get_name_string_for_object(obj) + if obj.advancement: + name = f"**{name}**" + if obj.code is None: + name = f"//{name}//" + if isinstance(obj, Location): + if obj.progress_type == LocationProgressType.PRIORITY: + name = f"**{name}**" + elif obj.progress_type == LocationProgressType.EXCLUDED: + name = f"--{name}--" + if obj.address is None: + name = f"//{name}//" + return re.sub("[\".:]", "", name) + + def visualize_exits(region: Region) -> None: + for exit_ in region.exits: + if exit_.connected_region: + if show_entrance_names: + uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"") + else: + try: + uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"") + uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"") + except ValueError: + uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"") + else: + uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"") + uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"") + + def visualize_locations(region: Region) -> None: + any_lock = any(location.locked for location in region.locations) + for location in region.locations: + lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else "" + if location.item: + uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}") + else: + uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}") + + def visualize_region(region: Region) -> None: + uml.append(f"class \"{fmt(region)}\"") + if show_locations: + visualize_locations(region) + visualize_exits(region) + + def visualize_other_regions() -> None: + if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]: + uml.append("package \"other regions\" <> {") + for region in other_regions: + uml.append(f"class \"{fmt(region)}\"") + uml.append("}") + + uml.append("@startuml") + uml.append("hide circle") + uml.append("hide empty members") + if linetype_ortho: + uml.append("skinparam linetype ortho") + while regions: + if (current_region := regions.popleft()) not in seen: + seen.add(current_region) + visualize_region(current_region) + regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region) + if show_other_regions: + visualize_other_regions() + uml.append("@enduml") + + with open(file_name, "wt", encoding="utf-8") as f: + f.write("\n".join(uml)) diff --git a/docs/world api.md b/docs/world api.md index 7a7f37b17c..05b9e09399 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -559,6 +559,12 @@ def generate_basic(self) -> None: # in most cases it's better to do this at the same time the itempool is # filled to avoid accidental duplicates: # manually placed and still in the itempool + + # for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to + # write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations + # are connected and placed as desired + # from Utils import visualize_regions + # visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") ``` ### Setting Rules From f9761ad4e5baa8f3cc639df9c7eaea125beb571e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:34:50 +0200 Subject: [PATCH 015/327] CI: ignore invalid hostname of some macos runners (#2252) --- Utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Utils.py b/Utils.py index 60b1cdadb7..91584f55d6 100644 --- a/Utils.py +++ b/Utils.py @@ -13,6 +13,7 @@ import io import collections import importlib import logging +import warnings from argparse import Namespace from settings import Settings, get_settings @@ -216,7 +217,13 @@ def get_cert_none_ssl_context(): def get_public_ipv4() -> str: import socket import urllib.request - ip = socket.gethostbyname(socket.gethostname()) + try: + ip = socket.gethostbyname(socket.gethostname()) + except socket.gaierror: + # if hostname or resolvconf is not set up properly, this may fail + warnings.warn("Could not resolve own hostname, falling back to 127.0.0.1") + ip = "127.0.0.1" + ctx = get_cert_none_ssl_context() try: ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip() @@ -234,7 +241,13 @@ def get_public_ipv4() -> str: def get_public_ipv6() -> str: import socket import urllib.request - ip = socket.gethostbyname(socket.gethostname()) + try: + ip = socket.gethostbyname(socket.gethostname()) + except socket.gaierror: + # if hostname or resolvconf is not set up properly, this may fail + warnings.warn("Could not resolve own hostname, falling back to ::1") + ip = "::1" + ctx = get_cert_none_ssl_context() try: ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() From 5d9b47355e610e65fd339661951fdefada792eb8 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:47:28 +0200 Subject: [PATCH 016/327] CI: run tests multi-threaded (#2251) --- .github/workflows/unittests.yml | 4 ++-- test/TestBase.py | 5 ----- test/__init__.py | 10 ++++++++++ test/programs/TestGenerate.py | 8 ++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index d24c55b49a..1a76a7f471 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -54,9 +54,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-subtests + pip install pytest pytest-subtests pytest-xdist python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" python Launcher.py --update_settings # make sure host.yaml exists for tests - name: Unittests run: | - pytest + pytest -n auto diff --git a/test/TestBase.py b/test/TestBase.py index 856428fb57..4df6b80769 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -1,16 +1,11 @@ -import pathlib import typing import unittest from argparse import Namespace -import Utils from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all -file_path = pathlib.Path(__file__).parent.parent -Utils.local_path.cached_path = file_path - from BaseClasses import MultiWorld, CollectionState, ItemClassification, Item from worlds.alttp.Items import ItemFactory diff --git a/test/__init__.py b/test/__init__.py index 32622f65a9..03716a10d7 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,3 +1,4 @@ +import pathlib import warnings import settings @@ -5,3 +6,12 @@ import settings warnings.simplefilter("always") settings.no_gui = True settings.skip_autosave = True + +import ModuleUpdate + +ModuleUpdate.update_ran = True # don't upgrade + +import Utils + +Utils.local_path.cached_path = pathlib.Path(__file__).parent.parent +Utils.user_path() # initialize cached_path diff --git a/test/programs/TestGenerate.py b/test/programs/TestGenerate.py index d04e1f2c5b..73e1d3b834 100644 --- a/test/programs/TestGenerate.py +++ b/test/programs/TestGenerate.py @@ -1,13 +1,13 @@ # Tests for Generate.py (ArchipelagoGenerate.exe) import unittest +import os +import os.path import sys + from pathlib import Path from tempfile import TemporaryDirectory -import os.path -import os -import ModuleUpdate -ModuleUpdate.update_ran = True # don't upgrade + import Generate From 17127a4117c4e29bff467090ac2cc8da0e757421 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 2 Oct 2023 20:06:29 +0200 Subject: [PATCH 017/327] kvui: silently fail to disable DPI awareness on Windows (#2246) --- kvui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index 835f0dad45..71bf80c86d 100644 --- a/kvui.py +++ b/kvui.py @@ -7,7 +7,10 @@ if sys.platform == "win32": import ctypes # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's - ctypes.windll.shcore.SetProcessDpiAwareness(0) + try: + ctypes.windll.shcore.SetProcessDpiAwareness(0) + except FileNotFoundError: # shcore may not be found on <= Windows 7 + pass # TODO: remove silent except when Python 3.8 is phased out. os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" From 18bf7425c4b894f91b1b6582948cd2033d2cadea Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 2 Oct 2023 20:06:56 +0200 Subject: [PATCH 018/327] WebHost: cache static misc pages (#2245) --- WebHostLib/misc.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 6d3e82c00c..e3111ed5b5 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -32,29 +32,34 @@ def page_not_found(err): # Start Playing Page @app.route('/start-playing') +@cache.cached() def start_playing(): return render_template(f"startPlaying.html") @app.route('/weighted-settings') +@cache.cached() def weighted_settings(): return render_template(f"weighted-settings.html") # Player settings pages @app.route('/games//player-settings') +@cache.cached() def player_settings(game): return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) # Game Info Pages @app.route('/games//info/') +@cache.cached() def game_info(game, lang): return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) # List of supported games @app.route('/games') +@cache.cached() def games(): worlds = {} for game, world in AutoWorldRegister.world_types.items(): @@ -64,21 +69,25 @@ def games(): @app.route('/tutorial///') +@cache.cached() def tutorial(game, file, lang): return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) @app.route('/tutorial/') +@cache.cached() def tutorial_landing(): return render_template("tutorialLanding.html") @app.route('/faq//') +@cache.cached() def faq(lang): return render_template("faq.html", lang=lang) @app.route('/glossary//') +@cache.cached() def terms(lang): return render_template("glossary.html", lang=lang) @@ -147,7 +156,7 @@ def host_room(room: UUID): @app.route('/favicon.ico') def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static/static'), + return send_from_directory(os.path.join(app.root_path, "static", "static"), 'favicon.ico', mimetype='image/vnd.microsoft.icon') @@ -167,6 +176,7 @@ def get_datapackage(): @app.route('/index') @app.route('/sitemap') +@cache.cached() def get_sitemap(): available_games: List[Dict[str, Union[str, bool]]] = [] for game, world in AutoWorldRegister.world_types.items(): From 9d3872568856cf5f57d35d9653a937368393a22a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 2 Oct 2023 20:07:15 +0200 Subject: [PATCH 019/327] WebHost: update ponyorm (#2241) --- WebHostLib/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index a3695e3383..55669f1018 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,6 +1,5 @@ flask>=2.2.3 -pony>=0.7.16; python_version <= '3.10' -pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11' +pony>=0.7.17 waitress>=2.1.2 Flask-Caching>=2.0.2 Flask-Compress>=1.14 From c7c94eebebb3d8c71723701378e9b59977bb29c1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 2 Oct 2023 20:07:31 +0200 Subject: [PATCH 020/327] WebHost: fix indentation (#2240) --- WebHostLib/templates/viewSeed.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/viewSeed.html b/WebHostLib/templates/viewSeed.html index e252fb06a2..a8478c95c3 100644 --- a/WebHostLib/templates/viewSeed.html +++ b/WebHostLib/templates/viewSeed.html @@ -34,7 +34,7 @@ {% endif %} Rooms:  - + {% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
  • Create New Room From e377068d1fcc1f7d09035ad5a65e66938de75273 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:17:34 +0200 Subject: [PATCH 021/327] Core: more gitignore (#2249) gitignore versioned venvs, prof output, appimagetool and sni downloads --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e374a12954..f4bcd35c32 100644 --- a/.gitignore +++ b/.gitignore @@ -33,11 +33,14 @@ setups build bundle/components.wxs dist +/prof/ README.html .vs/ EnemizerCLI/ /Players/ /SNI/ +/sni-*/ +/appimagetool* /host.yaml /options.yaml /config.yaml @@ -140,6 +143,7 @@ ipython_config.py .venv* env/ venv/ +/venv*/ ENV/ env.bak/ venv.bak/ From 24403eba1b167c1e3f45f57531a42867c301a76e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 2 Oct 2023 11:52:00 -0700 Subject: [PATCH 022/327] Launcher: Allow opening patches for clients without an exe (#2176) * Launcher: Allow opening patches for clients without an exe * Launcher: Restore behavior for not showing patch suffixes for clients that aren't installed --- Launcher.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Launcher.py b/Launcher.py index a1548d594c..9e184bf108 100644 --- a/Launcher.py +++ b/Launcher.py @@ -50,17 +50,22 @@ def open_host_yaml(): def open_patch(): suffixes = [] for c in components: - if isfile(get_exe(c)[-1]): - suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \ - isinstance(c.file_identifier, SuffixIdentifier) else [] + if c.type == Type.CLIENT and \ + isinstance(c.file_identifier, SuffixIdentifier) and \ + (c.script_name is None or isfile(get_exe(c)[-1])): + suffixes += c.file_identifier.suffixes try: - filename = open_filename('Select patch', (('Patches', suffixes),)) + filename = open_filename("Select patch", (("Patches", suffixes),)) except Exception as e: - messagebox('Error', str(e), error=True) + messagebox("Error", str(e), error=True) else: file, component = identify(filename) if file and component: - launch([*get_exe(component), file], component.cli) + exe = get_exe(component) + if exe is None or not isfile(exe[-1]): + exe = get_exe("Launcher") + + launch([*exe, file], component.cli) def generate_yamls(): @@ -107,7 +112,7 @@ def identify(path: Union[None, str]): return None, None for component in components: if component.handles_file(path): - return path, component + return path, component elif path == component.display_name or path == component.script_name: return None, component return None, None @@ -117,25 +122,25 @@ def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]: if isinstance(component, str): name = component component = None - if name.startswith('Archipelago'): + if name.startswith("Archipelago"): name = name[11:] - if name.endswith('.exe'): + if name.endswith(".exe"): name = name[:-4] - if name.endswith('.py'): + if name.endswith(".py"): name = name[:-3] if not name: return None for c in components: - if c.script_name == name or c.frozen_name == f'Archipelago{name}': + if c.script_name == name or c.frozen_name == f"Archipelago{name}": component = c break if not component: return None if is_frozen(): - suffix = '.exe' if is_windows else '' - return [local_path(f'{component.frozen_name}{suffix}')] + suffix = ".exe" if is_windows else "" + return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None else: - return [sys.executable, local_path(f'{component.script_name}.py')] + return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None def launch(exe, in_terminal=False): From bc11c9dfd444b24e988d46326752d0d55189cba5 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 2 Oct 2023 17:44:19 -0700 Subject: [PATCH 023/327] BizHawkClient: Add BizHawkClient (#1978) Adds a generic client that can communicate with BizHawk. Similar to SNIClient, but for arbitrary systems and doesn't have an intermediary application like SNI. --- BizHawkClient.py | 9 + data/lua/base64.lua | 119 ++++++ data/lua/connector_bizhawk_generic.lua | 564 +++++++++++++++++++++++++ inno_setup.iss | 4 + worlds/LauncherComponents.py | 3 + worlds/_bizhawk/__init__.py | 326 ++++++++++++++ worlds/_bizhawk/client.py | 87 ++++ worlds/_bizhawk/context.py | 188 +++++++++ 8 files changed, 1300 insertions(+) create mode 100644 BizHawkClient.py create mode 100644 data/lua/base64.lua create mode 100644 data/lua/connector_bizhawk_generic.lua create mode 100644 worlds/_bizhawk/__init__.py create mode 100644 worlds/_bizhawk/client.py create mode 100644 worlds/_bizhawk/context.py diff --git a/BizHawkClient.py b/BizHawkClient.py new file mode 100644 index 0000000000..86c8e5197e --- /dev/null +++ b/BizHawkClient.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import ModuleUpdate +ModuleUpdate.update() + +from worlds._bizhawk.context import launch + +if __name__ == "__main__": + launch() diff --git a/data/lua/base64.lua b/data/lua/base64.lua new file mode 100644 index 0000000000..ebe8064353 --- /dev/null +++ b/data/lua/base64.lua @@ -0,0 +1,119 @@ +-- This file originates from this repository: https://github.com/iskolbin/lbase64 +-- It was modified to translate between base64 strings and lists of bytes instead of base64 strings and strings. + +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode +if not extract then + if _G._VERSION == "Lua 5.4" then + extract = load[[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]]() + elseif _G.bit then -- LuaJIT + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function( v, from, width ) + return band( shr( v, from ), shl( 1, width ) - 1 ) + end + elseif _G._VERSION == "Lua 5.1" then + extract = function( v, from, width ) + local w = 0 + local flag = 2^from + for i = 0, width-1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2^i + end + flag = flag2 + end + return w + end + end +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder( s62, s63, spad ) + local decoder = {} + for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode( arr, encoder ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #arr + local lastn = n % 3 + for i = 1, n-lastn, 3 do + local a, b, c = arr[i], arr[i + 1], arr[i + 2] + local v = a*0x10000 + b*0x100 + c + local s + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = arr[n-1], arr[n] + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = arr[n]*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +function base64.decode( b64, decoder ) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs( decoder ) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) + end + b64 = b64:gsub( pattern, '' ) + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n-4 or n, 4 do + local a, b, c, d = b64:byte( i, i+3 ) + local s + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + table.insert(t,extract(v,16,8)) + table.insert(t,extract(v,8,8)) + table.insert(t,extract(v,0,8)) + end + if padding == 1 then + local a, b, c = b64:byte( n-3, n-1 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + table.insert(t,extract(v,16,8)) + table.insert(t,extract(v,8,8)) + elseif padding == 2 then + local a, b = b64:byte( n-3, n-2 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + table.insert(t,extract(v,16,8)) + end + return t +end + +return base64 diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua new file mode 100644 index 0000000000..b0b06de447 --- /dev/null +++ b/data/lua/connector_bizhawk_generic.lua @@ -0,0 +1,564 @@ +--[[ +Copyright (c) 2023 Zunawe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local SCRIPT_VERSION = 1 + +--[[ +This script expects to receive JSON and will send JSON back. A message should +be a list of 1 or more requests which will be executed in order. Each request +will have a corresponding response in the same order. + +Every individual request and response is a JSON object with at minimum one +field `type`. The value of `type` determines what other fields may exist. + +To get the script version, instead of JSON, send "VERSION" to get the script +version directly (e.g. "2"). + +#### Ex. 1 + +Request: `[{"type": "PING"}]` + +Response: `[{"type": "PONG"}]` + +--- + +#### Ex. 2 + +Request: `[{"type": "LOCK"}, {"type": "HASH"}]` + +Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]` + +--- + +#### Ex. 3 + +Request: + +```json +[ + {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"}, + {"type": "READ", "address": 500, "size": 4, "domain": "ROM"} +] +``` + +Response: + +```json +[ + {"type": "GUARD_RESPONSE", "address": 100, "value": true}, + {"type": "READ_RESPONSE", "value": "dGVzdA=="} +] +``` + +--- + +#### Ex. 4 + +Request: + +```json +[ + {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"}, + {"type": "READ", "address": 500, "size": 4, "domain": "ROM"} +] +``` + +Response: + +```json +[ + {"type": "GUARD_RESPONSE", "address": 100, "value": false}, + {"type": "GUARD_RESPONSE", "address": 100, "value": false} +] +``` + +--- + +### Supported Request Types + +- `PING` + Does nothing; resets timeout. + + Expected Response Type: `PONG` + +- `SYSTEM` + Returns the system of the currently loaded ROM (N64, GBA, etc...). + + Expected Response Type: `SYSTEM_RESPONSE` + +- `PREFERRED_CORES` + Returns the user's default cores for systems with multiple cores. If the + current ROM's system has multiple cores, the one that is currently + running is very probably the preferred core. + + Expected Response Type: `PREFERRED_CORES_RESPONSE` + +- `HASH` + Returns the hash of the currently loaded ROM calculated by BizHawk. + + Expected Response Type: `HASH_RESPONSE` + +- `GUARD` + Checks a section of memory against `expected_data`. If the bytes starting + at `address` do not match `expected_data`, the response will have `value` + set to `false`, and all subsequent requests will not be executed and + receive the same `GUARD_RESPONSE`. + + Expected Response Type: `GUARD_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to check + - `expected_data` (string): A base64 string of contiguous data + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `LOCK` + Halts emulation and blocks on incoming requests until an `UNLOCK` request + is received or the client times out. All requests processed while locked + will happen on the same frame. + + Expected Response Type: `LOCKED` + +- `UNLOCK` + Resumes emulation after the current list of requests is done being + executed. + + Expected Response Type: `UNLOCKED` + +- `READ` + Reads an array of bytes at the provided address. + + Expected Response Type: `READ_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to read + - `size` (`int`): The number of bytes to read + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `WRITE` + Writes an array of bytes to the provided address. + + Expected Response Type: `WRITE_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to write to + - `value` (`string`): A base64 string representing the data to write + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `DISPLAY_MESSAGE` + Adds a message to the message queue which will be displayed using + `gui.addmessage` according to the message interval. + + Expected Response Type: `DISPLAY_MESSAGE_RESPONSE` + + Additional Fields: + - `message` (`string`): The string to display + +- `SET_MESSAGE_INTERVAL` + Sets the minimum amount of time to wait between displaying messages. + Potentially useful if you add many messages quickly but want players + to be able to read each of them. + + Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE` + + Additional Fields: + - `value` (`number`): The number of seconds to set the interval to + + +### Response Types + +- `PONG` + Acknowledges `PING`. + +- `SYSTEM_RESPONSE` + Contains the name of the system for currently running ROM. + + Additional Fields: + - `value` (`string`): The returned system name + +- `PREFERRED_CORES_RESPONSE` + Contains the user's preferred cores for systems with multiple supported + cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and + SGX. + + Additional Fields: + - `value` (`{[string]: [string]}`): A dictionary map from system name to + core name + +- `HASH_RESPONSE` + Contains the hash of the currently loaded ROM calculated by BizHawk. + + Additional Fields: + - `value` (`string`): The returned hash + +- `GUARD_RESPONSE` + The result of an attempted `GUARD` request. + + Additional Fields: + - `value` (`boolean`): true if the memory was validated, false if not + - `address` (`int`): The address of the memory that was invalid (the same + address provided by the `GUARD`, not the address of the individual invalid + byte) + +- `LOCKED` + Acknowledges `LOCK`. + +- `UNLOCKED` + Acknowledges `UNLOCK`. + +- `READ_RESPONSE` + Contains the result of a `READ` request. + + Additional Fields: + - `value` (`string`): A base64 string representing the read data + +- `WRITE_RESPONSE` + Acknowledges `WRITE`. + +- `DISPLAY_MESSAGE_RESPONSE` + Acknowledges `DISPLAY_MESSAGE`. + +- `SET_MESSAGE_INTERVAL_RESPONSE` + Acknowledges `SET_MESSAGE_INTERVAL`. + +- `ERROR` + Signifies that something has gone wrong while processing a request. + + Additional Fields: + - `err` (`string`): A description of the problem +]] + +local base64 = require("base64") +local socket = require("socket") +local json = require("json") + +-- Set to log incoming requests +-- Will cause lag due to large console output +local DEBUG = false + +local SOCKET_PORT = 43055 + +local STATE_NOT_CONNECTED = 0 +local STATE_CONNECTED = 1 + +local server = nil +local client_socket = nil + +local current_state = STATE_NOT_CONNECTED + +local timeout_timer = 0 +local message_timer = 0 +local message_interval = 0 +local prev_time = 0 +local current_time = 0 + +local locked = false + +local rom_hash = nil + +local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") +lua_major = tonumber(lua_major) +lua_minor = tonumber(lua_minor) + +if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then + require("lua_5_3_compat") +end + +local bizhawk_version = client.getversion() +local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") +bizhawk_major = tonumber(bizhawk_major) +bizhawk_minor = tonumber(bizhawk_minor) +if bizhawk_patch == "" then + bizhawk_patch = 0 +else + bizhawk_patch = tonumber(bizhawk_patch) +end + +function queue_push (self, value) + self[self.right] = value + self.right = self.right + 1 +end + +function queue_is_empty (self) + return self.right == self.left +end + +function queue_shift (self) + value = self[self.left] + self[self.left] = nil + self.left = self.left + 1 + return value +end + +function new_queue () + local queue = {left = 1, right = 1} + return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}}) +end + +local message_queue = new_queue() + +function lock () + locked = true + client_socket:settimeout(2) +end + +function unlock () + locked = false + client_socket:settimeout(0) +end + +function process_request (req) + local res = {} + + if req["type"] == "PING" then + res["type"] = "PONG" + + elseif req["type"] == "SYSTEM" then + res["type"] = "SYSTEM_RESPONSE" + res["value"] = emu.getsystemid() + + elseif req["type"] == "PREFERRED_CORES" then + local preferred_cores = client.getconfig().PreferredCores + res["type"] = "PREFERRED_CORES_RESPONSE" + res["value"] = {} + res["value"]["NES"] = preferred_cores.NES + res["value"]["SNES"] = preferred_cores.SNES + res["value"]["GB"] = preferred_cores.GB + res["value"]["GBC"] = preferred_cores.GBC + res["value"]["DGB"] = preferred_cores.DGB + res["value"]["SGB"] = preferred_cores.SGB + res["value"]["PCE"] = preferred_cores.PCE + res["value"]["PCECD"] = preferred_cores.PCECD + res["value"]["SGX"] = preferred_cores.SGX + + elseif req["type"] == "HASH" then + res["type"] = "HASH_RESPONSE" + res["value"] = rom_hash + + elseif req["type"] == "GUARD" then + res["type"] = "GUARD_RESPONSE" + local expected_data = base64.decode(req["expected_data"]) + + local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"]) + + local data_is_validated = true + for i, byte in ipairs(actual_data) do + if byte ~= expected_data[i] then + data_is_validated = false + break + end + end + + res["value"] = data_is_validated + res["address"] = req["address"] + + elseif req["type"] == "LOCK" then + res["type"] = "LOCKED" + lock() + + elseif req["type"] == "UNLOCK" then + res["type"] = "UNLOCKED" + unlock() + + elseif req["type"] == "READ" then + res["type"] = "READ_RESPONSE" + res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"])) + + elseif req["type"] == "WRITE" then + res["type"] = "WRITE_RESPONSE" + memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"]) + + elseif req["type"] == "DISPLAY_MESSAGE" then + res["type"] = "DISPLAY_MESSAGE_RESPONSE" + message_queue:push(req["message"]) + + elseif req["type"] == "SET_MESSAGE_INTERVAL" then + res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE" + message_interval = req["value"] + + else + res["type"] = "ERROR" + res["err"] = "Unknown command: "..req["type"] + end + + return res +end + +-- Receive data from AP client and send message back +function send_receive () + local message, err = client_socket:receive() + + -- Handle errors + if err == "closed" then + if current_state == STATE_CONNECTED then + print("Connection to client closed") + end + current_state = STATE_NOT_CONNECTED + return + elseif err == "timeout" then + unlock() + return + elseif err ~= nil then + print(err) + current_state = STATE_NOT_CONNECTED + unlock() + return + end + + -- Reset timeout timer + timeout_timer = 5 + + -- Process received data + if DEBUG then + print("Received Message ["..emu.framecount().."]: "..'"'..message..'"') + end + + if message == "VERSION" then + local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n") + else + local res = {} + local data = json.decode(message) + local failed_guard_response = nil + for i, req in ipairs(data) do + if failed_guard_response ~= nil then + res[i] = failed_guard_response + else + -- An error is more likely to cause an NLua exception than to return an error here + local status, response = pcall(process_request, req) + if status then + res[i] = response + + -- If the GUARD validation failed, skip the remaining commands + if response["type"] == "GUARD_RESPONSE" and not response["value"] then + failed_guard_response = response + end + else + res[i] = {type = "ERROR", err = response} + end + end + end + + client_socket:send(json.encode(res).."\n") + end +end + +function main () + server, err = socket.bind("localhost", SOCKET_PORT) + if err ~= nil then + print(err) + return + end + + while true do + current_time = socket.socket.gettime() + timeout_timer = timeout_timer - (current_time - prev_time) + message_timer = message_timer - (current_time - prev_time) + prev_time = current_time + + if message_timer <= 0 and not message_queue:is_empty() then + gui.addmessage(message_queue:shift()) + message_timer = message_interval + end + + if current_state == STATE_NOT_CONNECTED then + if emu.framecount() % 60 == 0 then + server:settimeout(2) + local client, timeout = server:accept() + if timeout == nil then + print("Client connected") + current_state = STATE_CONNECTED + client_socket = client + client_socket:settimeout(0) + else + print("No client found. Trying again...") + end + end + else + repeat + send_receive() + until not locked + + if timeout_timer <= 0 then + print("Client timed out") + current_state = STATE_NOT_CONNECTED + end + end + + coroutine.yield() + end +end + +event.onexit(function () + print("\n-- Restarting Script --\n") + if server ~= nil then + server:close() + end +end) + +if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then + print("Must use BizHawk 2.7.0 or newer") +elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then + print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.") +else + if emu.getsystemid() == "NULL" then + print("No ROM is loaded. Please load a ROM.") + while emu.getsystemid() == "NULL" do + emu.frameadvance() + end + end + + rom_hash = gameinfo.getromhash() + + print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n") + + local co = coroutine.create(main) + function tick () + local status, err = coroutine.resume(co) + + if not status then + print("\nERROR: "..err) + print("Consider reporting this crash.\n") + + if server ~= nil then + server:close() + end + + co = coroutine.create(main) + end + end + + -- Gambatte has a setting which can cause script execution to become + -- misaligned, so for GB and GBC we explicitly set the callback on + -- vblank instead. + -- https://github.com/TASEmulators/BizHawk/issues/3711 + if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then + event.onmemoryexecute(tick, 0x40, "tick", "System Bus") + else + event.onframeend(tick) + end + + while true do + emu.frameadvance() + end +end diff --git a/inno_setup.iss b/inno_setup.iss index 147cd74dca..3c1bdc4571 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -74,6 +74,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 @@ -122,6 +123,7 @@ Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignorev Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni +Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft @@ -146,6 +148,7 @@ Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe" Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni +Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot @@ -166,6 +169,7 @@ Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopic Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni +Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index c3ae2b0495..2d445a77b8 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -89,6 +89,9 @@ components: List[Component] = [ Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), + # BizHawk + Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, + file_identifier=SuffixIdentifier()), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py new file mode 100644 index 0000000000..cdf227ec7b --- /dev/null +++ b/worlds/_bizhawk/__init__.py @@ -0,0 +1,326 @@ +""" +A module for interacting with BizHawk through `connector_bizhawk_generic.lua`. + +Any mention of `domain` in this module refers to the names BizHawk gives to memory domains in its own lua api. They are +naively passed to BizHawk without validation or modification. +""" + +import asyncio +import base64 +import enum +import json +import typing + + +BIZHAWK_SOCKET_PORT = 43055 +EXPECTED_SCRIPT_VERSION = 1 + + +class ConnectionStatus(enum.IntEnum): + NOT_CONNECTED = 1 + TENTATIVE = 2 + CONNECTED = 3 + + +class BizHawkContext: + streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] + connection_status: ConnectionStatus + + def __init__(self) -> None: + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + + +class NotConnectedError(Exception): + """Raised when something tries to make a request to the connector script before a connection has been established""" + pass + + +class RequestFailedError(Exception): + """Raised when the connector script did not respond to a request""" + pass + + +class ConnectorError(Exception): + """Raised when the connector script encounters an error while processing a request""" + pass + + +class SyncError(Exception): + """Raised when the connector script responded with a mismatched response type""" + pass + + +async def connect(ctx: BizHawkContext) -> bool: + """Attempts to establish a connection with the connector script. Returns True if successful.""" + try: + ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT) + ctx.connection_status = ConnectionStatus.TENTATIVE + return True + except (TimeoutError, ConnectionRefusedError): + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + return False + + +def disconnect(ctx: BizHawkContext) -> None: + """Closes the connection to the connector script.""" + if ctx.streams is not None: + ctx.streams[1].close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + + +async def get_script_version(ctx: BizHawkContext) -> int: + if ctx.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = ctx.streams + writer.write("VERSION".encode("ascii") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + version = await asyncio.wait_for(reader.readline(), timeout=5) + + if version == b"": + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + return int(version.decode("ascii")) + except asyncio.TimeoutError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + +async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]: + """Sends a list of requests to the BizHawk connector and returns their responses. + + It's likely you want to use the wrapper functions instead of this.""" + if ctx.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = ctx.streams + writer.write(json.dumps(req_list).encode("utf-8") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + res = await asyncio.wait_for(reader.readline(), timeout=5) + + if res == b"": + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + if ctx.connection_status == ConnectionStatus.TENTATIVE: + ctx.connection_status = ConnectionStatus.CONNECTED + + ret = json.loads(res.decode("utf-8")) + for response in ret: + if response["type"] == "ERROR": + raise ConnectorError(response["err"]) + + return ret + except asyncio.TimeoutError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + +async def ping(ctx: BizHawkContext) -> None: + """Sends a PING request and receives a PONG response.""" + res = (await send_requests(ctx, [{"type": "PING"}]))[0] + + if res["type"] != "PONG": + raise SyncError(f"Expected response of type PONG but got {res['type']}") + + +async def get_hash(ctx: BizHawkContext) -> str: + """Gets the system name for the currently loaded ROM""" + res = (await send_requests(ctx, [{"type": "HASH"}]))[0] + + if res["type"] != "HASH_RESPONSE": + raise SyncError(f"Expected response of type HASH_RESPONSE but got {res['type']}") + + return res["value"] + + +async def get_system(ctx: BizHawkContext) -> str: + """Gets the system name for the currently loaded ROM""" + res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0] + + if res["type"] != "SYSTEM_RESPONSE": + raise SyncError(f"Expected response of type SYSTEM_RESPONSE but got {res['type']}") + + return res["value"] + + +async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]: + """Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have + entries.""" + res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0] + + if res["type"] != "PREFERRED_CORES_RESPONSE": + raise SyncError(f"Expected response of type PREFERRED_CORES_RESPONSE but got {res['type']}") + + return res["value"] + + +async def lock(ctx: BizHawkContext) -> None: + """Locks BizHawk in anticipation of receiving more requests this frame. + + Consider using guarded reads and writes instead of locks if possible. + + While locked, emulation will halt and the connector will block on incoming requests until an `UNLOCK` request is + sent. Remember to unlock when you're done, or the emulator will appear to freeze. + + Sending multiple lock commands is the same as sending one.""" + res = (await send_requests(ctx, [{"type": "LOCK"}]))[0] + + if res["type"] != "LOCKED": + raise SyncError(f"Expected response of type LOCKED but got {res['type']}") + + +async def unlock(ctx: BizHawkContext) -> None: + """Unlocks BizHawk to allow it to resume emulation. See `lock` for more info. + + Sending multiple unlock commands is the same as sending one.""" + res = (await send_requests(ctx, [{"type": "UNLOCK"}]))[0] + + if res["type"] != "UNLOCKED": + raise SyncError(f"Expected response of type UNLOCKED but got {res['type']}") + + +async def display_message(ctx: BizHawkContext, message: str) -> None: + """Displays the provided message in BizHawk's message queue.""" + res = (await send_requests(ctx, [{"type": "DISPLAY_MESSAGE", "message": message}]))[0] + + if res["type"] != "DISPLAY_MESSAGE_RESPONSE": + raise SyncError(f"Expected response of type DISPLAY_MESSAGE_RESPONSE but got {res['type']}") + + +async def set_message_interval(ctx: BizHawkContext, value: float) -> None: + """Sets the minimum amount of time in seconds to wait between queued messages. The default value of 0 will allow one + new message to display per frame.""" + res = (await send_requests(ctx, [{"type": "SET_MESSAGE_INTERVAL", "value": value}]))[0] + + if res["type"] != "SET_MESSAGE_INTERVAL_RESPONSE": + raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") + + +async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], + guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: + """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected + value. + + Items in read_list should be organized (address, size, domain) where + - `address` is the address of the first byte of data + - `size` is the number of bytes to read + - `domain` is the name of the region of memory the address corresponds to + + Items in `guard_list` should be organized `(address, expected_data, domain)` where + - `address` is the address of the first byte of data + - `expected_data` is the bytes that the data starting at this address is expected to match + - `domain` is the name of the region of memory the address corresponds to + + Returns None if any item in guard_list failed to validate. Otherwise returns a list of bytes in the order they + were requested.""" + res = await send_requests(ctx, [{ + "type": "GUARD", + "address": address, + "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"), + "domain": domain + } for address, expected_data, domain in guard_list] + [{ + "type": "READ", + "address": address, + "size": size, + "domain": domain + } for address, size, domain in read_list]) + + ret: typing.List[bytes] = [] + for item in res: + if item["type"] == "GUARD_RESPONSE": + if not item["value"]: + return None + else: + if item["type"] != "READ_RESPONSE": + raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}") + + ret.append(base64.b64decode(item["value"])) + + return ret + + +async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: + """Reads data at 1 or more addresses. + + Items in `read_list` should be organized `(address, size, domain)` where + - `address` is the address of the first byte of data + - `size` is the number of bytes to read + - `domain` is the name of the region of memory the address corresponds to + + Returns a list of bytes in the order they were requested.""" + return await guarded_read(ctx, read_list, []) + + +async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], + guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: + """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. + + Items in `write_list` should be organized `(address, value, domain)` where + - `address` is the address of the first byte of data + - `value` is a list of bytes to write, in order, starting at `address` + - `domain` is the name of the region of memory the address corresponds to + + Items in `guard_list` should be organized `(address, expected_data, domain)` where + - `address` is the address of the first byte of data + - `expected_data` is the bytes that the data starting at this address is expected to match + - `domain` is the name of the region of memory the address corresponds to + + Returns False if any item in guard_list failed to validate. Otherwise returns True.""" + res = await send_requests(ctx, [{ + "type": "GUARD", + "address": address, + "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"), + "domain": domain + } for address, expected_data, domain in guard_list] + [{ + "type": "WRITE", + "address": address, + "value": base64.b64encode(bytes(value)).decode("ascii"), + "domain": domain + } for address, value, domain in write_list]) + + for item in res: + if item["type"] == "GUARD_RESPONSE": + if not item["value"]: + return False + else: + if item["type"] != "WRITE_RESPONSE": + raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}") + + return True + + +async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: + """Writes data to 1 or more addresses. + + Items in write_list should be organized `(address, value, domain)` where + - `address` is the address of the first byte of data + - `value` is a list of bytes to write, in order, starting at `address` + - `domain` is the name of the region of memory the address corresponds to""" + await guarded_write(ctx, write_list, []) diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py new file mode 100644 index 0000000000..b614c083ba --- /dev/null +++ b/worlds/_bizhawk/client.py @@ -0,0 +1,87 @@ +""" +A module containing the BizHawkClient base class and metaclass +""" + + +from __future__ import annotations + +import abc +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union + +from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess + +if TYPE_CHECKING: + from .context import BizHawkClientContext +else: + BizHawkClientContext = object + + +class AutoBizHawkClientRegister(abc.ABCMeta): + game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {} + + def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister: + new_class = super().__new__(cls, name, bases, namespace) + + if "system" in namespace: + systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"])) + if systems not in AutoBizHawkClientRegister.game_handlers: + AutoBizHawkClientRegister.game_handlers[systems] = {} + + if "game" in namespace: + AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class() + + return new_class + + @staticmethod + async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]: + for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): + if system in systems: + for handler in handlers.values(): + if await handler.validate_rom(ctx): + return handler + + return None + + +class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): + system: ClassVar[Union[str, Tuple[str, ...]]] + """The system that the game this client is for runs on""" + + game: ClassVar[str] + """The game this client is for""" + + @abc.abstractmethod + async def validate_rom(self, ctx: BizHawkClientContext) -> bool: + """Should return whether the currently loaded ROM should be handled by this client. You might read the game name + from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the + client class, so you do not need to check the system yourself. + + Once this function has determined that the ROM should be handled by this client, it should also modify `ctx` + as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...).""" + ... + + async def set_auth(self, ctx: BizHawkClientContext) -> None: + """Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot + name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their + username.""" + pass + + @abc.abstractmethod + async def game_watcher(self, ctx: BizHawkClientContext) -> None: + """Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed + to have passed your validator when this function is called, and the emulator is very likely to be connected.""" + ... + + def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: + """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" + pass + + +def launch_client(*args) -> None: + from .context import launch + launch_subprocess(launch, name="BizHawkClient") + + +if not any(component.script_name == "BizHawkClient" for component in components): + components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, + file_identifier=SuffixIdentifier())) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py new file mode 100644 index 0000000000..6e53b370af --- /dev/null +++ b/worlds/_bizhawk/context.py @@ -0,0 +1,188 @@ +""" +A module containing context and functions relevant to running the client. This module should only be imported for type +checking or launching the client, otherwise it will probably cause circular import issues. +""" + + +import asyncio +import traceback +from typing import Any, Dict, Optional + +from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled +import Patch +import Utils + +from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \ + get_system, ping +from .client import BizHawkClient, AutoBizHawkClientRegister + + +EXPECTED_SCRIPT_VERSION = 1 + + +class BizHawkClientCommandProcessor(ClientCommandProcessor): + def _cmd_bh(self): + """Shows the current status of the client's connection to BizHawk""" + if isinstance(self.ctx, BizHawkClientContext): + if self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED: + logger.info("BizHawk Connection Status: Not Connected") + elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.TENTATIVE: + logger.info("BizHawk Connection Status: Tentatively Connected") + elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.CONNECTED: + logger.info("BizHawk Connection Status: Connected") + + +class BizHawkClientContext(CommonContext): + command_processor = BizHawkClientCommandProcessor + client_handler: Optional[BizHawkClient] + slot_data: Optional[Dict[str, Any]] = None + rom_hash: Optional[str] = None + bizhawk_ctx: BizHawkContext + + watcher_timeout: float + """The maximum amount of time the game watcher loop will wait for an update from the server before executing""" + + def __init__(self, server_address: Optional[str], password: Optional[str]): + super().__init__(server_address, password) + self.client_handler = None + self.bizhawk_ctx = BizHawkContext() + self.watcher_timeout = 0.5 + + def run_gui(self): + from kvui import GameManager + + class BizHawkManager(GameManager): + base_title = "Archipelago BizHawk Client" + + self.ui = BizHawkManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + def on_package(self, cmd, args): + if cmd == "Connected": + self.slot_data = args.get("slot_data", None) + + if self.client_handler is not None: + self.client_handler.on_package(self, cmd, args) + + +async def _game_watcher(ctx: BizHawkClientContext): + showed_connecting_message = False + showed_connected_message = False + showed_no_handler_message = False + + while not ctx.exit_event.is_set(): + try: + await asyncio.wait_for(ctx.watcher_event.wait(), ctx.watcher_timeout) + except asyncio.TimeoutError: + pass + + ctx.watcher_event.clear() + + try: + if ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED: + showed_connected_message = False + + if not showed_connecting_message: + logger.info("Waiting to connect to BizHawk...") + showed_connecting_message = True + + if not await connect(ctx.bizhawk_ctx): + continue + + showed_no_handler_message = False + + script_version = await get_script_version(ctx.bizhawk_ctx) + + if script_version != EXPECTED_SCRIPT_VERSION: + logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.") + disconnect(ctx.bizhawk_ctx) + continue + + showed_connecting_message = False + + await ping(ctx.bizhawk_ctx) + + if not showed_connected_message: + showed_connected_message = True + logger.info("Connected to BizHawk") + + rom_hash = await get_hash(ctx.bizhawk_ctx) + if ctx.rom_hash is not None and ctx.rom_hash != rom_hash: + if ctx.server is not None: + logger.info(f"ROM changed. Disconnecting from server.") + await ctx.disconnect(True) + + ctx.auth = None + ctx.username = None + ctx.rom_hash = rom_hash + + if ctx.client_handler is None: + system = await get_system(ctx.bizhawk_ctx) + ctx.client_handler = await AutoBizHawkClientRegister.get_handler(ctx, system) + + if ctx.client_handler is None: + if not showed_no_handler_message: + logger.info("No handler was found for this game") + showed_no_handler_message = True + continue + else: + showed_no_handler_message = False + logger.info(f"Running handler for {ctx.client_handler.game}") + + except RequestFailedError as exc: + logger.info(f"Lost connection to BizHawk: {exc.args[0]}") + continue + + # Get slot name and send `Connect` + if ctx.server is not None and ctx.username is None: + await ctx.client_handler.set_auth(ctx) + + if ctx.auth is None: + await ctx.get_username() + + await ctx.send_connect() + + await ctx.client_handler.game_watcher(ctx) + + +async def _run_game(rom: str): + import webbrowser + webbrowser.open(rom) + + +async def _patch_and_run_game(patch_file: str): + metadata, output_file = Patch.create_rom_file(patch_file) + Utils.async_start(_run_game(output_file)) + + +def launch() -> None: + async def main(): + parser = get_base_parser() + parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") + args = parser.parse_args() + + ctx = BizHawkClientContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + if args.patch_file != "": + Utils.async_start(_patch_and_run_game(args.patch_file)) + + watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher") + + try: + await watcher_task + except Exception as e: + logger.error("".join(traceback.format_exception(e))) + + await ctx.exit_event.wait() + await ctx.shutdown() + + Utils.init_logging("BizHawkClient", exception_logger="Client") + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() From 6b48f9aac55b672fa17c91abacc58f3c72070a24 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 3 Oct 2023 02:52:58 +0200 Subject: [PATCH 024/327] WebHost: link to stats from the use statistics directly on landing (#2242) --- WebHostLib/static/styles/landing.css | 3 --- WebHostLib/templates/landing.html | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index 202c43badd..96975553c1 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -235,9 +235,6 @@ html{ line-height: 30px; } -#landing .variable{ - color: #ffff00; -} .landing-deco{ position: absolute; diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index fd45b78cfb..b489ef18ac 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -49,9 +49,9 @@ our crazy idea into a reality.

    - {{ seeds }} + {{ seeds }} games were generated and - {{ rooms }} + {{ rooms }} were hosted in the last 7 days.

    From cdbb2cf7b769eb6614d63bc2233e1ba01635f5dc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:47:22 +0200 Subject: [PATCH 025/327] Core: fix unittest world discovery (#2262) --- test/__init__.py | 3 ++- test/worlds/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 03716a10d7..37ebe3f627 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -13,5 +13,6 @@ ModuleUpdate.update_ran = True # don't upgrade import Utils -Utils.local_path.cached_path = pathlib.Path(__file__).parent.parent +file_path = pathlib.Path(__file__).parent.parent +Utils.local_path.cached_path = file_path Utils.user_path() # initialize cached_path diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py index d1817cc674..cf396111bf 100644 --- a/test/worlds/__init__.py +++ b/test/worlds/__init__.py @@ -1,7 +1,7 @@ def load_tests(loader, standard_tests, pattern): import os import unittest - from ..TestBase import file_path + from .. import file_path from worlds.AutoWorld import AutoWorldRegister suite = unittest.TestSuite() From 78057476f309c3c012b5534b83a2d25c6b3b2a42 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 3 Oct 2023 05:19:09 -0500 Subject: [PATCH 026/327] Docs: python 3.11 works now (#2258) * Docs: python 3.11 works now * change to py 3.12 unsupported --- docs/running from source.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/running from source.md b/docs/running from source.md index c0f4bf5802..b7367308d8 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r What you'll need: * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version - * **Python 3.11 does not work currently** + * **Python 3.12 is currently unsupported** * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler * possibly optional, read operating system specific sections @@ -30,7 +30,7 @@ After this, you should be able to run the programs. Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) - * **Python 3.11 does not work currently** + * **Python 3.12 is currently unsupported** * **Optional**: Download and install Visual Studio Build Tools from [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). From f6e92a18de0b5cccd8a5b439f951c24ebfff6489 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 4 Oct 2023 11:23:29 -0500 Subject: [PATCH 027/327] The Messenger: Fix items accessibility region rule (#2263) --- worlds/messenger/Rules.py | 3 ++- worlds/messenger/test/TestAccess.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index c24f60fbaa..664bf5f6d7 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -263,5 +263,6 @@ def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master") # add these locations when seals and shards aren't shuffled elif not multiworld.shuffle_shards[player]: - allow_self_locking_items(multiworld.get_region("Cloud Ruins Right", player), "Ruxxtin's Amulet") + for entrance in multiworld.get_region("Cloud Ruins", player).entrances: + entrance.access_rule = lambda state: state.has("Wingsuit", player) or state.has("Rope Dart", player) allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index 452ed1189f..e95a81c5c1 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -163,6 +163,8 @@ class ItemsAccessTest(MessengerTestBase): "Forlorn Temple - Demon King": PHOBEKINS } + self.multiworld.state = self.multiworld.get_all_state(True) + self.remove_by_name(location_lock_pairs.values()) for loc in location_lock_pairs: for item_name in location_lock_pairs[loc]: item = self.get_item_by_name(item_name) From 6c4a3959c36a47863ec2a7ef1d9fe51d51dc9733 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:52:34 -0400 Subject: [PATCH 028/327] Docs: Categorize Commands in Guide (#2213) * Update commands_en.md Commands re-ordered and put into categories Some commands were better documented / explained more clearly Other formatting changes * Status command moved to General category and elaboration on getitem command * "Multi-world" -> "Multiworld" * Moved game-specific local commands to game pages --- worlds/factorio/docs/en_Factorio.md | 6 ++ worlds/ff1/docs/en_Final Fantasy.md | 5 + worlds/generic/docs/commands_en.md | 160 +++++++++++++++------------- 3 files changed, 97 insertions(+), 74 deletions(-) diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md index 61bceb3820..dbc33d05df 100644 --- a/worlds/factorio/docs/en_Factorio.md +++ b/worlds/factorio/docs/en_Factorio.md @@ -42,3 +42,9 @@ depositing excess energy and supplementing energy deficits, much like Accumulato Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client. It can also be queried by typing `/energy-link` in-game. + +## Unique Local Commands +The following commands are only available when using the FactorioClient to play Factorio with Archipelago. + +- `/factorio ` Sends the command argument to the Factorio server as a command. +- `/energy-link` Displays the amount of energy currently in shared storage for EnergyLink diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 29d4d29f80..8962919743 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -24,3 +24,8 @@ All items can appear in other players worlds, including consumables, shards, wea All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the emulator will display what was found external to the in-game text box. + +## Unique Local Commands +The following command is only available when using the FF1Client for the Final Fantasy Randomizer. + +- `/nes` Shows the current status of the NES connection. diff --git a/worlds/generic/docs/commands_en.md b/worlds/generic/docs/commands_en.md index e52ea20fd2..3e7c0bd4bd 100644 --- a/worlds/generic/docs/commands_en.md +++ b/worlds/generic/docs/commands_en.md @@ -1,96 +1,108 @@ -### Helpful Commands +# Helpful Commands Commands are split into two types: client commands and server commands. Client commands are commands which are executed by the client and do not affect the Archipelago remote session. Server commands are commands which are executed by the Archipelago server and affect the Archipelago session or otherwise provide feedback from the server. -In clients which have their own commands the commands are typically prepended by a forward slash:`/`. Remote commands -are always submitted to the server prepended with an exclamation point: `!`. +In clients which have their own commands the commands are typically prepended by a forward slash: `/`. -#### Local Commands +Server commands are always submitted to the server prepended with an exclamation point: `!`.
    -The following list is a list of client commands which may be available to you through your Archipelago client. You +# Server Commands + +Server commands may be executed by any client which allows for sending text chat to the Archipelago server. If your +client does not allow for sending chat then you may connect to your game slot with the TextClient which comes with the +Archipelago installation. In order to execute the command you need to merely send a text message with the command, +including the exclamation point. + +### General +- `!help` Returns a listing of available commands. +- `!license` Returns the software licensing information. +- `!options` Returns the current server options, including password in plaintext. +- `!players` Returns info about the currently connected and non-connected players. +- `!status` Returns information about the connection status and check completion numbers for all players in the current room.
    (Optionally mention a Tag name and get information on who has that Tag. For example: !status DeathLink) + + +### Utilities +- `!countdown ` Starts a countdown using the given seconds value. Useful for synchronizing starts. + Defaults to 10 seconds if no argument is provided. +- `!alias ` Sets your alias, which allows you to use commands with the alias rather than your provided name. +- `!admin ` Executes a command as if you typed it into the server console. Remote administration must be + enabled. + +### Information +- `!remaining` Lists the items remaining in your game, but not where they are or who they go to. +- `!missing` Lists the location checks you are missing from the server's perspective. +- `!checked` Lists all the location checks you've done from the server's perspective. + +### Hints +- `!hint` Lists all hints relevant to your world, the number of points you have for hints, and how much a hint costs. +- `!hint ` Tells you the game world and location your item is in, uses points earned from completing locations. +- `!hint_location ` Tells you what item is in a specific location, uses points earned from completing locations. + +### Collect/Release +- `!collect` Grants you all the remaining items for your world by collecting them from all games. Typically used after +goal completion. +- `!release` Releases all items contained in your world to other worlds. Typically, done automatically by the sever, but +can be configured to allow/require manual usage of this command. + +### Cheats +- `!getitem ` Cheats an item to the currently connected slot, if it is enabled in the server. + + +## Host only (on Archipelago.gg or in your server console) + +### General +- `/help` Returns a list of commands available in the console. +- `/license` Returns the software licensing information. +- `/options` Lists the server's current options, including password in plaintext. +- `/players` List currently connected players. +- `/save` Saves the state of the current multiworld. Note that the server auto-saves on a minute basis. +- `/exit` Shutdown the server + +### Utilities +- `/countdown ` Starts a countdown sent to all players via text chat. Defaults to 10 seconds if no + argument is provided. +- `/option
  • Supported Games Page
  • Tutorials Page
  • User Content
  • -
  • Weighted Settings Page
  • +
  • Weighted Options Page
  • Game Statistics
  • Glossary
  • @@ -46,11 +46,11 @@ {% endfor %} -

    Game Settings Pages

    +

    Game Options Pages

    diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index f1514d8353..3252b16ad4 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -51,12 +51,12 @@ | Setup Guides {% endif %} - {% if world.web.settings_page is string %} + {% if world.web.options_page is string %} | - Settings Page - {% elif world.web.settings_page %} + Options Page + {% elif world.web.options_page %} | - Settings Page + Options Page {% endif %} {% if world.web.bug_report_page %} | diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-options.html similarity index 82% rename from WebHostLib/templates/weighted-settings.html rename to WebHostLib/templates/weighted-options.html index 9ce097c37f..032a4eeb90 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-options.html @@ -1,26 +1,26 @@ {% extends 'pageWrapper.html' %} {% block head %} - {{ game }} Settings + {{ game }} Options - + - + {% endblock %} {% block body %} {% include 'header/grassHeader.html' %}
    -

    Weighted Settings

    -

    Weighted Settings allows you to choose how likely a particular option is to be used in game generation. +

    Weighted Options

    +

    Weighted options allow you to choose how likely a particular option is to be used in game generation. The higher an option is weighted, the more likely the option will be chosen. Think of them like entries in a raffle.

    Choose the games and options you would like to play with! You may generate a single-player game from - this page, or download a settings file you can use to participate in a MultiWorld.

    + this page, or download an options file you can use to participate in a MultiWorld.

    A list of all games you have generated can be found on the User Content page.

    @@ -40,7 +40,7 @@
    - +
    diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 9a8b6a56ef..d4fe0f49a2 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -149,7 +149,7 @@ def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: class WebWorld: """Webhost integration""" - settings_page: Union[bool, str] = True + options_page: Union[bool, str] = True """display a settings page. Can be a link to a specific page or external tool.""" game_info_languages: List[str] = ['en'] diff --git a/worlds/bk_sudoku/__init__.py b/worlds/bk_sudoku/__init__.py index f914baf066..36d863bb44 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/bk_sudoku/__init__.py @@ -5,7 +5,7 @@ from ..AutoWorld import WebWorld, World class Bk_SudokuWebWorld(WebWorld): - settings_page = "games/Sudoku/info/en" + options_page = "games/Sudoku/info/en" theme = 'partyTime' tutorials = [ Tutorial( diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 432467399e..16905cc6da 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -14,7 +14,7 @@ class FF1Settings(settings.Group): class FF1Web(WebWorld): - settings_page = "https://finalfantasyrandomizer.com/" + options_page = "https://finalfantasyrandomizer.com/" tutorials = [Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.", From 706a2b36db4b1f6fb5c759e320a0e505eedfd0bf Mon Sep 17 00:00:00 2001 From: Seldom <38388947+Seldom-SE@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:27:57 -0700 Subject: [PATCH 081/327] Terraria Old One's Army tier 2 and 3 missing Hardmode req (#2342) --- worlds/terraria/Rules.dsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv index 5f5551e465..b511db54de 100644 --- a/worlds/terraria/Rules.dsv +++ b/worlds/terraria/Rules.dsv @@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity; Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh)); Get a Life; Achievement; Life Fruit; Topped Off; Achievement; Life Fruit; -Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3); +Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3); // Brimstone Elemental Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental; @@ -410,7 +410,7 @@ Scoria Bar; Calamity; Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar; Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode; Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array; -Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem; +Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem; // Martian Madness Martian Madness; Location | Item; Wall of Flesh & Golem; From 426e9d3090136f6803e1cdbb178acf794f75bd15 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 24 Oct 2023 08:16:46 +0200 Subject: [PATCH 082/327] LttP: make Triforce Piece progression_skip_balancing (#2351) --- worlds/alttp/Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index 40634de8da..18f96b2ddb 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -102,7 +102,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\ 'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"), 'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), - 'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), + 'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), 'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"), 'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"), From 78a4b01db517eb9cd85e440b920d0725cfce704c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:59:15 +0200 Subject: [PATCH 083/327] pytest: run tests on non-windows with new names (#2349) --- pytest.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 5599a3c90f..33e0bab8a9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] -python_files = Test*.py +python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported python_classes = Test -python_functions = test \ No newline at end of file +python_functions = test From 90c5f45a1f07a2dd954f36d7713fbbbd840bcfb5 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 24 Oct 2023 15:50:53 -0500 Subject: [PATCH 084/327] Options: have as_dict return set values as lists to reduce JSON footprint (#2354) * Options: return set values as lists to reduce JSON footprint * sorted() Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index d9ddfc2e2f..9b4f9d9908 100644 --- a/Options.py +++ b/Options.py @@ -950,7 +950,10 @@ class CommonOptions(metaclass=OptionsMetaProperty): else: raise ValueError(f"{casing} is invalid casing for as_dict. " "Valid names are 'snake', 'camel', 'pascal', 'kebab'.") - option_results[display_name] = getattr(self, option_name).value + value = getattr(self, option_name).value + if isinstance(value, set): + value = sorted(value) + option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") return option_results From 58642edc17f447e18d9a8c04e3cf803b17b97355 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:19:33 +0200 Subject: [PATCH 085/327] Core: allow multi-line and --hash in requirements.txt --- ModuleUpdate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 209f2da672..c33e894e8b 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -67,14 +67,23 @@ def update(yes=False, force=False): install_pkg_resources(yes=yes) import pkg_resources + prev = "" # if a line ends in \ we store here and merge later for req_file in requirements_files: path = os.path.join(os.path.dirname(sys.argv[0]), req_file) if not os.path.exists(path): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: for line in requirementsfile: - if not line or line[0] == "#": - continue # ignore comments + if not line or line.lstrip(" \t")[0] == "#": + if not prev: + continue # ignore comments + line = "" + elif line.rstrip("\r\n").endswith("\\"): + prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line + continue + line = prev + line + line = line.split("--hash=")[0] # remove hashes from requirement for version checking + prev = "" if line.startswith(("https://", "git+https://")): # extract name and version for url rest = line.split('/')[-1] From e87d5d5ac2ac82f185c1d5c9ffc318dc917d2256 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:27:05 +0200 Subject: [PATCH 086/327] SoE: update to v0.46.1 * install via pypi, pin hashes * add OoB logic option * add sequence break logic option * fix turd ball texts * add option to fix OoB * better textbox handling when turning in energy core fragments --- worlds/soe/Logic.py | 8 ++- worlds/soe/Options.py | 25 +++++++-- worlds/soe/__init__.py | 12 ++--- worlds/soe/requirements.txt | 54 ++++++++++++------- worlds/soe/test/__init__.py | 15 ++++++ .../test/{TestAccess.py => test_access.py} | 0 worlds/soe/test/{TestGoal.py => test_goal.py} | 0 worlds/soe/test/test_oob.py | 51 ++++++++++++++++++ worlds/soe/test/test_sequence_breaks.py | 45 ++++++++++++++++ 9 files changed, 180 insertions(+), 30 deletions(-) rename worlds/soe/test/{TestAccess.py => test_access.py} (100%) rename worlds/soe/test/{TestGoal.py => test_goal.py} (100%) create mode 100644 worlds/soe/test/test_oob.py create mode 100644 worlds/soe/test/test_sequence_breaks.py diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index 3c173dec2f..e464b7fd3b 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -3,7 +3,7 @@ from typing import Protocol, Set from BaseClasses import MultiWorld from worlds.AutoWorld import LogicMixin from . import pyevermizer -from .Options import EnergyCore +from .Options import EnergyCore, OutOfBounds, SequenceBreaks # TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? @@ -61,4 +61,10 @@ class SecretOfEvermoreLogic(LogicMixin): if w.energy_core == EnergyCore.option_fragments: progress = pyevermizer.P_CORE_FRAGMENT count = w.required_fragments + elif progress == pyevermizer.P_ALLOW_OOB: + if world.worlds[player].out_of_bounds == OutOfBounds.option_logic: + return True + elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS: + if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic: + return True return self._soe_count(progress, world, player, count) >= count diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index f1a30745f8..3de2de34ac 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -38,6 +38,12 @@ class OffOnFullChoice(Choice): alias_chaos = 2 +class OffOnLogicChoice(Choice): + option_off = 0 + option_on = 1 + option_logic = 2 + + # actual options class Difficulty(EvermizerFlags, Choice): """Changes relative spell cost and stuff""" @@ -93,10 +99,18 @@ class ExpModifier(Range): default = 200 -class FixSequence(EvermizerFlag, DefaultOnToggle): - """Fix some sequence breaks""" - display_name = "Fix Sequence" - flag = '1' +class SequenceBreaks(EvermizerFlags, OffOnLogicChoice): + """Disable, enable some sequence breaks or put them in logic""" + display_name = "Sequence Breaks" + default = 0 + flags = ['', 'j', 'J'] + + +class OutOfBounds(EvermizerFlags, OffOnLogicChoice): + """Disable, enable the out-of-bounds glitch or put it in logic""" + display_name = "Out Of Bounds" + default = 0 + flags = ['', 'u', 'U'] class FixCheats(EvermizerFlag, DefaultOnToggle): @@ -240,7 +254,8 @@ soe_options: typing.Dict[str, AssembleOptions] = { "available_fragments": AvailableFragments, "money_modifier": MoneyModifier, "exp_modifier": ExpModifier, - "fix_sequence": FixSequence, + "sequence_breaks": SequenceBreaks, + "out_of_bounds": OutOfBounds, "fix_cheats": FixCheats, "fix_infinite_ammo": FixInfiniteAmmo, "fix_atlas_glitch": FixAtlasGlitch, diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index f887325c60..4cc3c0866f 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -10,12 +10,8 @@ from worlds.generic.Rules import add_item_rule, set_rule from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial from Utils import output_path -try: - import pyevermizer # from package -except ImportError: - import traceback - traceback.print_exc() - from . import pyevermizer # as part of the source tree +import pyevermizer # from package +# from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments @@ -179,6 +175,8 @@ class SoEWorld(World): evermizer_seed: int connect_name: str energy_core: int + sequence_breaks: int + out_of_bounds: int available_fragments: int required_fragments: int @@ -191,6 +189,8 @@ class SoEWorld(World): def generate_early(self) -> None: # store option values that change logic self.energy_core = self.multiworld.energy_core[self.player].value + self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value + self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value self.required_fragments = self.multiworld.required_fragments[self.player].value if self.required_fragments > self.multiworld.available_fragments[self.player].value: self.multiworld.available_fragments[self.player].value = self.required_fragments diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 878a2a80cc..710f51ddb0 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,18 +1,36 @@ -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8' -#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11' -pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64') +pyevermizer==0.46.1 \ + --hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \ + --hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \ + --hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \ + --hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \ + --hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \ + --hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \ + --hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \ + --hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \ + --hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \ + --hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \ + --hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \ + --hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \ + --hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \ + --hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \ + --hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \ + --hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \ + --hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \ + --hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \ + --hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \ + --hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \ + --hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \ + --hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \ + --hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \ + --hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \ + --hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \ + --hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \ + --hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \ + --hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \ + --hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \ + --hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \ + --hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \ + --hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \ + --hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \ + --hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \ + --hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 3c2a0dc1b6..27d38605aa 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,5 +1,20 @@ from test.TestBase import WorldTestBase +from typing import Iterable class SoETestBase(WorldTestBase): game = "Secret of Evermore" + + def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), + satisfied=True) -> None: + """ + Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True. + Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True + """ + for location in reachable: + self.assertEqual(self.can_reach_location(location), satisfied, + f"{location} is unreachable but should be" if satisfied else + f"{location} is reachable but shouldn't be") + for location in unreachable: + self.assertFalse(self.can_reach_location(location), + f"{location} is reachable but shouldn't be") diff --git a/worlds/soe/test/TestAccess.py b/worlds/soe/test/test_access.py similarity index 100% rename from worlds/soe/test/TestAccess.py rename to worlds/soe/test/test_access.py diff --git a/worlds/soe/test/TestGoal.py b/worlds/soe/test/test_goal.py similarity index 100% rename from worlds/soe/test/TestGoal.py rename to worlds/soe/test/test_goal.py diff --git a/worlds/soe/test/test_oob.py b/worlds/soe/test/test_oob.py new file mode 100644 index 0000000000..27e00cd3e7 --- /dev/null +++ b/worlds/soe/test/test_oob.py @@ -0,0 +1,51 @@ +import typing +from . import SoETestBase + + +class OoBTest(SoETestBase): + """Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic.""" + options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"} + + def testOoBAccess(self): + in_logic = self.options["out_of_bounds"] == "logic" + + # some locations that just need a weapon + OoB + oob_reachable = [ + "Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip + "Levitate", "Fireball", "Drain", "Speed", + "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57", + ] + # some locations that should still be unreachable + oob_unreachable = [ + "Tiny", "Rimsala", + "Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances + "Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275", + ] + # OoB + Diamond Eyes + de_reachable = [ + "Tiny's hideout #160", + ] + # still unreachable + de_unreachable = [ + "Tiny", + "Tiny's hideout #161", + ] + + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False) + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic) + self.collect_by_name("Diamond Eye") + self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic) + + def testOoBGoal(self): + # still need Energy Core with OoB if sequence breaks are not in logic + for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: + self.collect_by_name(item) + self.assertBeatable(False) + self.collect_by_name("Energy Core") + self.assertBeatable(True) + + +class OoBInLogicTest(OoBTest): + """Tests that stuff that should be reachable/unreachable with out-of-bounds actually is.""" + options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"} diff --git a/worlds/soe/test/test_sequence_breaks.py b/worlds/soe/test/test_sequence_breaks.py new file mode 100644 index 0000000000..4248f9b47d --- /dev/null +++ b/worlds/soe/test/test_sequence_breaks.py @@ -0,0 +1,45 @@ +import typing +from . import SoETestBase + + +class SequenceBreaksTest(SoETestBase): + """Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic.""" + options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"} + + def testSequenceBreaksAccess(self): + in_logic = self.options["sequence_breaks"] == "logic" + + # some locations that just need any weapon + sequence break + break_reachable = [ + "Sons of Sth.", "Mad Monk", "Magmar", + "Fireball", + "Volcano Room1 #73", "Pyramid top #135", + ] + # some locations that should still be unreachable + break_unreachable = [ + "Aquagoth", "Megataur", "Tiny", "Rimsala", + "Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape", + "Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57", + ] + + self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False) + self.collect_by_name("Gladiator Sword") + self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic) + self.collect_by_name("Spider Claw") # Gauge now just needs non-sword + self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic) + self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead + self.assertEqual(self.can_reach_location("Escape"), in_logic) + + def testSequenceBreaksGoal(self): + in_logic = self.options["sequence_breaks"] == "logic" + + # don't need Energy Core with sequence breaks in logic + for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]: + self.assertBeatable(False) + self.collect_by_name(item) + self.assertBeatable(in_logic) + + +class SequenceBreaksInLogicTest(SequenceBreaksTest): + """Tests that stuff that should be reachable/unreachable with sequence breaks actually is.""" + options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"} From e5554f8630b0b73e2b22ce33ab592ba70e9d9d6e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:34:59 +0200 Subject: [PATCH 087/327] SoE: create regions cleanup and speedup (#2361) * SoE: create regions cleanup and speedup keep local reference instead of hitting multiworld cache also technically fixes a bug where all locations are in 'menu', not 'ingame' * SoE: somplify region connection --- worlds/soe/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 4cc3c0866f..9a8f38cdac 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -224,9 +224,8 @@ class SoEWorld(World): max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256 # TODO: generate *some* regions from locations' requirements? - r = Region('Menu', self.player, self.multiworld) - r.exits = [Entrance(self.player, 'New Game', r)] - self.multiworld.regions += [r] + menu = Region('Menu', self.player, self.multiworld) + self.multiworld.regions += [menu] def get_sphere_index(evermizer_loc): """Returns 0, 1 or 2 for locations in spheres 1, 2, 3+""" @@ -234,11 +233,14 @@ class SoEWorld(World): return 2 return min(2, len(evermizer_loc.requires)) + # create ingame region + ingame = Region('Ingame', self.player, self.multiworld) + # group locations into spheres (1, 2, 3+ at index 0, 1, 2) spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {} for loc in _locations: spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( - SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r, + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, loc.difficulty > max_difficulty)) # location balancing data @@ -280,18 +282,16 @@ class SoEWorld(World): late_locations = self.multiworld.random.sample(late_bosses, late_count) # add locations to the world - r = Region('Ingame', self.player, self.multiworld) for sphere in spheres.values(): for locations in sphere.values(): for location in locations: - r.locations.append(location) + ingame.locations.append(location) if location.name in late_locations: location.progress_type = LocationProgressType.PRIORITY - r.locations.append(SoELocation(self.player, 'Done', None, r)) - self.multiworld.regions += [r] - - self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player)) + ingame.locations.append(SoELocation(self.player, 'Done', None, ingame)) + menu.connect(ingame, "New Game") + self.multiworld.regions += [ingame] def create_items(self): # add regular items to the pool From be959c05a640363a1a57303ced968ca998b721d3 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 25 Oct 2023 02:56:56 -0500 Subject: [PATCH 088/327] The Messenger: speed up generation for large multiworlds (#2359) --- worlds/messenger/__init__.py | 6 +++--- worlds/messenger/subclasses.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 4be699e9cf..0771989ffc 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -82,9 +82,7 @@ class MessengerWorld(World): self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) def create_regions(self) -> None: - for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: - if region.name in REGION_CONNECTIONS: - region.add_exits(REGION_CONNECTIONS[region.name]) + self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS] def create_items(self) -> None: # create items that are always in the item pool @@ -138,6 +136,8 @@ class MessengerWorld(World): self.multiworld.itempool += itempool def set_rules(self) -> None: + for reg_name, connections in REGION_CONNECTIONS.items(): + self.multiworld.get_region(reg_name, self.player).add_exits(connections) logic = self.options.logic_level if logic == Logic.option_normal: MessengerRules(self).set_messenger_rules() diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index c5d90e00c8..ce31d43d60 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -32,7 +32,6 @@ class MessengerRegion(Region): loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None for loc in locations} self.add_locations(loc_dict, MessengerLocation) - world.multiworld.regions.append(self) class MessengerLocation(Location): From e5ca83b5dba69a6c6045bb3317e9f69c2d7af176 Mon Sep 17 00:00:00 2001 From: Felix R <50271878+FelicitusNeko@users.noreply.github.com> Date: Wed, 25 Oct 2023 05:22:09 -0300 Subject: [PATCH 089/327] Bumper Stickers: add location rules (#2254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bumpstik: treasure/booster location rules * bumpstik: oop missed a bit * bumpstik: apply access rule to Hazards check * bumpstik: move completion cond. to set_rules * bumpstik: tests? I have literally never written these before so 🤷 * bumpstik: oops * bumpstik: how about this? * bumpstik: fix some logic * bumpstik: this almost works but not quite * bumpstik: accurate region boundaries for BBs since we're using rules now * bumpstik: holy heck it works now --- worlds/bumpstik/Regions.py | 8 +++---- worlds/bumpstik/__init__.py | 19 ++++++++------- worlds/bumpstik/test/TestLogic.py | 39 +++++++++++++++++++++++++++++++ worlds/bumpstik/test/__init__.py | 5 ++++ 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 worlds/bumpstik/test/TestLogic.py create mode 100644 worlds/bumpstik/test/__init__.py diff --git a/worlds/bumpstik/Regions.py b/worlds/bumpstik/Regions.py index 247d6d61a3..6cddde882a 100644 --- a/worlds/bumpstik/Regions.py +++ b/worlds/bumpstik/Regions.py @@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int): entrance_map = { "Level 1": lambda state: - state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9), + state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8), "Level 2": lambda state: - state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17), + state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16), "Level 3": lambda state: - state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25), + state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24), "Level 4": lambda state: - state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33) + state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32) } for x, region_name in enumerate(region_map): diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index 9eeb3325e3..c4e65d07b6 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -108,7 +108,7 @@ class BumpStikWorld(World): item_pool += self._create_item_in_quantities( name, frequencies[i]) - item_delta = len(location_table) - len(item_pool) - 1 + item_delta = len(location_table) - len(item_pool) if item_delta > 0: item_pool += self._create_item_in_quantities( "Score Bonus", item_delta) @@ -116,13 +116,16 @@ class BumpStikWorld(World): self.multiworld.itempool += item_pool def set_rules(self): - forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player), - "Booster Bumper", self.player) - - def generate_basic(self): - self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item( - self.create_item(self.get_filler_item_name())) - + for x in range(1, 32): + self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \ + lambda state, x = x: state.has("Treasure Bumper", self.player, x) + for x in range(1, 5): + self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \ + lambda state, x = x: state.has("Booster Bumper", self.player, x) + self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \ + lambda state: state.has("Hazard Bumper", self.player, 25) + self.multiworld.completion_condition[self.player] = \ lambda state: state.has("Booster Bumper", self.player, 5) and \ state.has("Treasure Bumper", self.player, 32) + diff --git a/worlds/bumpstik/test/TestLogic.py b/worlds/bumpstik/test/TestLogic.py new file mode 100644 index 0000000000..e374b7b1e9 --- /dev/null +++ b/worlds/bumpstik/test/TestLogic.py @@ -0,0 +1,39 @@ +from . import BumpStikTestBase + + +class TestRuleLogic(BumpStikTestBase): + def testLogic(self): + for x in range(1, 33): + if x == 32: + self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards")) + + self.collect(self.get_item_by_name("Treasure Bumper")) + if x % 8 == 0: + bb_count = round(x / 8) + + if bb_count < 4: + self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}")) + elif bb_count == 4: + bb_count += 1 + + for y in range(self.count("Booster Bumper"), bb_count): + self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"), + f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs") + if y < 4: + self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"), + f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs") + self.collect(self.get_item_by_name("Booster Bumper")) + + if x < 31: + self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}")) + elif x == 31: + self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points")) + + if x < 32: + self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"), + f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs") + elif x == 32: + self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points")) + self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards")) + self.collect(self.get_items_by_name("Hazard Bumper")) + self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards")) diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py new file mode 100644 index 0000000000..1199d7b8e5 --- /dev/null +++ b/worlds/bumpstik/test/__init__.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class BumpStikTestBase(WorldTestBase): + game = "Bumper Stickers" From dab704df55dae017f1dbbdd6364f62d23b92ee60 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 25 Oct 2023 21:23:52 +0200 Subject: [PATCH 090/327] Core/LttP: remove initialize_regions (#2362) --- BaseClasses.py | 5 ----- worlds/alttp/InvertedRegions.py | 2 -- worlds/alttp/ItemPool.py | 2 -- worlds/alttp/Regions.py | 2 -- 4 files changed, 11 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1b6677dd19..0bd61f68f3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -328,11 +328,6 @@ class MultiWorld(): """ the base name (without file extension) for each player's output file for a seed """ return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}" - def initialize_regions(self, regions=None): - for region in regions if regions else self.regions: - region.multiworld = self - self._region_cache[region.player][region.name] = region - @functools.cached_property def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index ffa23881d3..f89eebec33 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -477,8 +477,6 @@ def create_inverted_regions(world, player): create_lw_region(world, player, 'Death Mountain Bunny Descent Area') ] - world.initialize_regions() - def mark_dark_world_regions(world, player): # cross world caves may have some sections marked as both in_light_world, and in_dark_work. diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index f8fdd55ef6..806a420f41 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -535,8 +535,6 @@ def set_up_take_anys(world, player): take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True) - world.initialize_regions() - def get_pool_core(world, player: int): shuffle = world.shuffle[player] diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 8311bc3269..0cc8a3d6a7 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -382,8 +382,6 @@ def create_regions(world, player): create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area') ] - world.initialize_regions() - def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits) From aa73dbab2daa1d316add35809a655d559c9e9940 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 26 Oct 2023 00:03:14 +0200 Subject: [PATCH 091/327] Subnautica: avoid cache recreation in create_regions call and clean up function. (#2365) --- worlds/subnautica/__init__.py | 40 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 7b25b61c81..de4f4e33dc 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -65,22 +65,38 @@ class SubnauticaWorld(World): creature_pool, self.options.creature_scans.value) def create_regions(self): - self.multiworld.regions += [ - self.create_region("Menu", None, ["Lifepod 5"]), - self.create_region("Planet 4546B", - locations.events + - [location["name"] for location in locations.location_table.values()] + - [creature + creatures.suffix for creature in self.creatures_to_scan]) - ] + # Create Regions + menu_region = Region("Menu", self.player, self.multiworld) + planet_region = Region("Planet 4546B", self.player, self.multiworld) - # Link regions - self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player)) + # Link regions together + menu_region.connect(planet_region, "Lifepod 5") + + # Create regular locations + location_names = itertools.chain((location["name"] for location in locations.location_table.values()), + (creature + creatures.suffix for creature in self.creatures_to_scan)) + for location_name in location_names: + loc_id = self.location_name_to_id[location_name] + location = SubnauticaLocation(self.player, location_name, loc_id, planet_region) + planet_region.locations.append(location) + + # Create events + goal_event_name = self.options.goal.get_event_name() for event in locations.events: - self.multiworld.get_location(event, self.player).place_locked_item( + location = SubnauticaLocation(self.player, event, None, planet_region) + planet_region.locations.append(location) + location.place_locked_item( SubnauticaItem(event, ItemClassification.progression, None, player=self.player)) - # make the goal event the victory "item" - self.multiworld.get_location(self.options.goal.get_event_name(), self.player).item.name = "Victory" + if event == goal_event_name: + # make the goal event the victory "item" + location.item.name = "Victory" + + # Register regions to multiworld + self.multiworld.regions += [ + menu_region, + planet_region + ] # refer to Rules.py set_rules = set_rules From 88d69dba97badfd04c18a0905aa7de59ee418018 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 26 Oct 2023 00:51:32 +0200 Subject: [PATCH 092/327] DLCQuest: logic speed up (#2323) --- worlds/dlcquest/Items.py | 2 ++ worlds/dlcquest/Regions.py | 5 +++- worlds/dlcquest/Rules.py | 54 +++++++++++++------------------------ worlds/dlcquest/__init__.py | 19 +++++++++++-- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index 61d1be54cb..e7008f7b12 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -11,6 +11,8 @@ from . import Options, data class DLCQuestItem(Item): game: str = "DLCQuest" + coins: int = 0 + coin_suffix: str = "" offset = 120_000 diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 402ac722a0..6dad9fc10c 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -23,7 +23,10 @@ def add_coin(region: Region, coin: int, player: int, suffix: str): location_coin = f"{region.name}{suffix}" location = DLCQuestLocation(player, location_coin, None, region) region.locations.append(location) - location.place_locked_item(create_event(player, number_coin)) + event = create_event(player, number_coin) + event.coins = coin + event.coin_suffix = suffix + location.place_locked_item(event) def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index c5fdfe8282..a11e5c504e 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -7,41 +7,25 @@ from . import Options from .Items import DLCQuestItem -def create_event(player, event: str): +def create_event(player, event: str) -> DLCQuestItem: return DLCQuestItem(event, ItemClassification.progression, None, player) +def has_enough_coin(player: int, coin: int): + return lambda state: state.prog_items[" coins", player] >= coin + + +def has_enough_coin_freemium(player: int, coin: int): + return lambda state: state.prog_items[" coins freemium", player] >= coin + + def set_rules(world, player, World_Options: Options.DLCQuestOptions): - def has_enough_coin(player: int, coin: int): - def has_coin(state, player: int, coins: int): - coin_possessed = 0 - for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]: - name_coin = f"{i} coins" - if state.has(name_coin, player): - coin_possessed += i - - return coin_possessed >= coins - - return lambda state: has_coin(state, player, coin) - - def has_enough_coin_freemium(player: int, coin: int): - def has_coin(state, player: int, coins: int): - coin_possessed = 0 - for i in [20, 50, 90, 95, 130, 150, 154, 200]: - name_coin = f"{i} coins freemium" - if state.has(name_coin, player): - coin_possessed += i - - return coin_possessed >= coins - - return lambda state: has_coin(state, player, coin) - - set_basic_rules(World_Options, has_enough_coin, player, world) - set_lfod_rules(World_Options, has_enough_coin_freemium, player, world) + set_basic_rules(World_Options, player, world) + set_lfod_rules(World_Options, player, world) set_completion_condition(World_Options, player, world) -def set_basic_rules(World_Options, has_enough_coin, player, world): +def set_basic_rules(World_Options, player, world): if World_Options.campaign == Options.Campaign.option_live_freemium_or_die: return set_basic_entrance_rules(player, world) @@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world): set_basic_shuffled_items_rules(World_Options, player, world) set_double_jump_glitchless_rules(World_Options, player, world) set_easy_double_jump_glitch_rules(World_Options, player, world) - self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world) - set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world) + self_basic_coinsanity_funded_purchase_rules(World_Options, player, world) + set_basic_self_funded_purchase_rules(World_Options, player, world) self_basic_win_condition(World_Options, player, world) @@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world): lambda state: state.has("Double Jump Pack", player)) -def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world): +def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_coin: return number_of_bundle = math.floor(825 / World_Options.coinbundlequantity) @@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, math.ceil(5 / World_Options.coinbundlequantity))) -def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world): +def set_basic_self_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_none: return set_rule(world.get_location("Movement Pack", player), @@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world): player)) -def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): +def set_lfod_rules(World_Options, player, world): if World_Options.campaign == Options.Campaign.option_basic: return set_lfod_entrance_rules(player, world) set_boss_door_requirements_rules(player, world) set_lfod_self_obtained_items_rules(World_Options, player, world) set_lfod_shuffled_items_rules(World_Options, player, world) - self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world) set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) @@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world): lambda state: state.can_reach("Cut Content", 'region', player)) -def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): +def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world): if World_Options.coinsanity != Options.CoinSanity.option_coin: return number_of_bundle = math.floor(889 / World_Options.coinbundlequantity) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 392eac7796..54d27f7b65 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -1,6 +1,6 @@ from typing import Union -from BaseClasses import Tutorial +from BaseClasses import Tutorial, CollectionState from worlds.AutoWorld import WebWorld, World from . import Options from .Items import DLCQuestItem, ItemData, create_items, item_table @@ -71,7 +71,6 @@ class DLCqworld(World): if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: self.multiworld.push_precollected(self.create_item("Movement Pack")) - def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: if isinstance(item, str): item = item_table[item] @@ -87,3 +86,19 @@ class DLCqworld(World): "seed": self.random.randrange(99999999) }) return options_dict + + def collect(self, state: CollectionState, item: DLCQuestItem) -> bool: + change = super().collect(state, item) + if change: + suffix = item.coin_suffix + if suffix: + state.prog_items[suffix, self.player] += item.coins + return change + + def remove(self, state: CollectionState, item: DLCQuestItem) -> bool: + change = super().remove(state, item) + if change: + suffix = item.coin_suffix + if suffix: + state.prog_items[suffix, self.player] -= item.coins + return change From b16804102d2f32301c38f1a3aacd7054ab764eed Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 26 Oct 2023 18:55:46 -0700 Subject: [PATCH 093/327] BizHawkClient: Add lock for communicating with lua script (#2369) --- worlds/_bizhawk/__init__.py | 118 ++++++++++++++---------------------- worlds/_bizhawk/context.py | 6 +- 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index cdf227ec7b..3403990832 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -13,7 +13,6 @@ import typing BIZHAWK_SOCKET_PORT = 43055 -EXPECTED_SCRIPT_VERSION = 1 class ConnectionStatus(enum.IntEnum): @@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum): CONNECTED = 3 -class BizHawkContext: - streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] - connection_status: ConnectionStatus - - def __init__(self) -> None: - self.streams = None - self.connection_status = ConnectionStatus.NOT_CONNECTED - - class NotConnectedError(Exception): """Raised when something tries to make a request to the connector script before a connection has been established""" pass @@ -51,6 +41,50 @@ class SyncError(Exception): pass +class BizHawkContext: + streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] + connection_status: ConnectionStatus + _lock: asyncio.Lock + + def __init__(self) -> None: + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + self._lock = asyncio.Lock() + + async def _send_message(self, message: str): + async with self._lock: + if self.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = self.streams + writer.write(message.encode("utf-8") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + res = await asyncio.wait_for(reader.readline(), timeout=5) + + if res == b"": + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + if self.connection_status == ConnectionStatus.TENTATIVE: + self.connection_status = ConnectionStatus.CONNECTED + + return res.decode("utf-8") + except asyncio.TimeoutError as exc: + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + async def connect(ctx: BizHawkContext) -> bool: """Attempts to establish a connection with the connector script. Returns True if successful.""" try: @@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None: async def get_script_version(ctx: BizHawkContext) -> int: - if ctx.streams is None: - raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") - - try: - reader, writer = ctx.streams - writer.write("VERSION".encode("ascii") + b"\n") - await asyncio.wait_for(writer.drain(), timeout=5) - - version = await asyncio.wait_for(reader.readline(), timeout=5) - - if version == b"": - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection closed") - - return int(version.decode("ascii")) - except asyncio.TimeoutError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection timed out") from exc - except ConnectionResetError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection reset") from exc + return int(await ctx._send_message("VERSION")) async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]: """Sends a list of requests to the BizHawk connector and returns their responses. It's likely you want to use the wrapper functions instead of this.""" - if ctx.streams is None: - raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") - - try: - reader, writer = ctx.streams - writer.write(json.dumps(req_list).encode("utf-8") + b"\n") - await asyncio.wait_for(writer.drain(), timeout=5) - - res = await asyncio.wait_for(reader.readline(), timeout=5) - - if res == b"": - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection closed") - - if ctx.connection_status == ConnectionStatus.TENTATIVE: - ctx.connection_status = ConnectionStatus.CONNECTED - - ret = json.loads(res.decode("utf-8")) - for response in ret: - if response["type"] == "ERROR": - raise ConnectorError(response["err"]) - - return ret - except asyncio.TimeoutError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection timed out") from exc - except ConnectionResetError as exc: - writer.close() - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - raise RequestFailedError("Connection reset") from exc + return json.loads(await ctx._send_message(json.dumps(req_list))) async def ping(ctx: BizHawkContext) -> None: diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 465334274e..5d865f3321 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -13,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, import Patch import Utils -from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \ - get_system, ping +from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \ + get_script_version, get_system, ping from .client import BizHawkClient, AutoBizHawkClientRegister @@ -133,6 +133,8 @@ async def _game_watcher(ctx: BizHawkClientContext): except RequestFailedError as exc: logger.info(f"Lost connection to BizHawk: {exc.args[0]}") continue + except NotConnectedError: + continue # Get slot name and send `Connect` if ctx.server is not None and ctx.username is None: From 6061bffbb670c67f96c9fd164ff949e4f6ba4271 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Fri, 27 Oct 2023 14:12:04 +1000 Subject: [PATCH 094/327] Pokemon R/B: Avoid a case of repeatedly checking of state in ER (#2376) --- worlds/pokemon_rb/regions.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 1816d010c0..f844976548 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1594,7 +1594,7 @@ def create_regions(self): connect(multiworld, player, "Menu", "Pallet Town", one_way=True) connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Evolution", one_way=True) - connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, + connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") connect(multiworld, player, "Route 1", "Viridian City") @@ -2269,23 +2269,28 @@ def create_regions(self): event_locations = self.multiworld.get_filled_locations(player) - def adds_reachable_entrances(entrances_copy, item): + def adds_reachable_entrances(entrances_copy, item, dead_end_cache): + ret = dead_end_cache.get(item.name) + if (ret != None): + return ret + state_copy = state.copy() state_copy.collect(item, True) state.sweep_for_events(locations=event_locations) ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances) + dead_end_cache[item.name] = ret return ret - def dead_end(entrances_copy, e): + def dead_end(entrances_copy, e, dead_end_cache): region = e.parent_region check_warps = set() checked_regions = {region} check_warps.update(region.exits) check_warps.remove(e) for location in region.locations: - if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy, - location.item): + if location.item and location.item.name in relevant_events and \ + adds_reachable_entrances(entrances_copy, location.item, dead_end_cache): return False while check_warps: warp = check_warps.pop() @@ -2302,7 +2307,7 @@ def create_regions(self): check_warps.update(warp.connected_region.exits) for location in warp.connected_region.locations: if (location.item and location.item.name in relevant_events and - adds_reachable_entrances(entrances_copy, location.item)): + adds_reachable_entrances(entrances_copy, location.item, dead_end_cache)): return False return True @@ -2332,6 +2337,8 @@ def create_regions(self): if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances): entrances.sort(key=lambda e: e.name not in entrance_only) + dead_end_cache = {} + # entrances list is empty while it's being sorted, must pass a copy to iterate through entrances_copy = entrances.copy() if multiworld.door_shuffle[player] == "decoupled": @@ -2342,10 +2349,10 @@ def create_regions(self): elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len( entrances) <= (starting_entrances - 3): entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if - dead_end(entrances_copy, e) else 1) + dead_end(entrances_copy, e, dead_end_cache) else 1) else: entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if - dead_end(entrances_copy, e) else 2) + dead_end(entrances_copy, e, dead_end_cache) else 2) if multiworld.door_shuffle[player] == "full": outdoor = outdoor_map(entrances[0].parent_region.name) if len(entrances) < 48 and not outdoor: From 0f7ebe389e0d08419b79684c9816702e34c39d19 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 26 Oct 2023 21:14:25 -0700 Subject: [PATCH 095/327] BizHawkClient: Add better launcher component suffix handling (#2367) --- worlds/LauncherComponents.py | 3 --- worlds/_bizhawk/client.py | 38 +++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 2d445a77b8..c3ae2b0495 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -89,9 +89,6 @@ components: List[Component] = [ Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), - # BizHawk - Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, - file_identifier=SuffixIdentifier()), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index b614c083ba..32a6e3704e 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -16,12 +16,22 @@ else: BizHawkClientContext = object +def launch_client(*args) -> None: + from .context import launch + launch_subprocess(launch, name="BizHawkClient") + +component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, + file_identifier=SuffixIdentifier()) +components.append(component) + + class AutoBizHawkClientRegister(abc.ABCMeta): game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {} def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister: new_class = super().__new__(cls, name, bases, namespace) + # Register handler if "system" in namespace: systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"])) if systems not in AutoBizHawkClientRegister.game_handlers: @@ -30,6 +40,19 @@ class AutoBizHawkClientRegister(abc.ABCMeta): if "game" in namespace: AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class() + # Update launcher component's suffixes + if "patch_suffix" in namespace: + if namespace["patch_suffix"] is not None: + existing_identifier: SuffixIdentifier = component.file_identifier + new_suffixes = [*existing_identifier.suffixes] + + if type(namespace["patch_suffix"]) is str: + new_suffixes.append(namespace["patch_suffix"]) + else: + new_suffixes.extend(namespace["patch_suffix"]) + + component.file_identifier = SuffixIdentifier(*new_suffixes) + return new_class @staticmethod @@ -45,11 +68,14 @@ class AutoBizHawkClientRegister(abc.ABCMeta): class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): system: ClassVar[Union[str, Tuple[str, ...]]] - """The system that the game this client is for runs on""" + """The system(s) that the game this client is for runs on""" game: ClassVar[str] """The game this client is for""" + patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]] + """The file extension(s) this client is meant to open and patch (e.g. ".apz3")""" + @abc.abstractmethod async def validate_rom(self, ctx: BizHawkClientContext) -> bool: """Should return whether the currently loaded ROM should be handled by this client. You might read the game name @@ -75,13 +101,3 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" pass - - -def launch_client(*args) -> None: - from .context import launch - launch_subprocess(launch, name="BizHawkClient") - - -if not any(component.script_name == "BizHawkClient" for component in components): - components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, - file_identifier=SuffixIdentifier())) From 3b5f9d175885350c4a1ff2fb66c6db784f651644 Mon Sep 17 00:00:00 2001 From: Jarno Date: Fri, 27 Oct 2023 12:01:46 +0200 Subject: [PATCH 096/327] Timespinner: Fixed generation error caused by new options system (#2374) --- worlds/timespinner/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index de1d58e961..24230862bd 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -49,11 +49,9 @@ class TimespinnerWorld(World): precalculated_weights: PreCalculatedWeights - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) - self.precalculated_weights = PreCalculatedWeights(world, player) - def generate_early(self) -> None: + self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) + # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true From 16fe66721f9c2c6871e73f251b1fc76c00be0240 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 27 Oct 2023 05:12:17 -0500 Subject: [PATCH 097/327] Stardew Valley: Use the pre-existing cache rather than ignoring it (#2368) --- worlds/stardew_valley/__init__.py | 4 ++-- worlds/stardew_valley/regions.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 1f46eb79d7..177b6436ae 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -100,15 +100,15 @@ class StardewValleyWorld(World): return region world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options) - self.multiworld.regions.extend(world_regions) def add_location(name: str, code: Optional[int], region: str): - region = self.multiworld.get_region(region, self.player) + region = world_regions[region] location = StardewLocation(self.player, name, code, region) location.access_rule = lambda _: True region.locations.append(location) create_locations(add_location, self.options, self.multiworld.random) + self.multiworld.regions.extend(world_regions.values()) def create_items(self): self.precollect_starting_season() diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index e8daa772d8..d8e2248411 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -429,7 +429,7 @@ def create_final_connections(world_options) -> List[ConnectionData]: def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[ - Iterable[Region], Dict[str, str]]: + Dict[str, Region], Dict[str, str]]: final_regions = create_final_regions(world_options) regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in final_regions} @@ -444,7 +444,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options) if connection.name in entrances: entrances[connection.name].connect(regions[connection.destination]) - return regions.values(), randomized_data + return regions, randomized_data def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[ From d595b1a67f72aee26d921c43dd07fc06c15d616c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 27 Oct 2023 05:30:32 -0500 Subject: [PATCH 098/327] Docs: slight adding games.md rework (#1192) * begin reworking adding games.md * make it presentable * some doc cleanup * style cleanup * rework the "more on that later" section of SDV * remove now unused images * make the doc links consistent * typo Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- docs/adding games.md | 437 ++++++++---------- .../archipelago-world-directory-example.png | Bin 51175 -> 0 bytes docs/img/example-init-py-file.png | Bin 82760 -> 0 bytes docs/img/example-items-py-file.png | Bin 48373 -> 0 bytes docs/img/example-locations-py-file.png | Bin 66617 -> 0 bytes docs/img/example-options-py-file.png | Bin 40852 -> 0 bytes docs/img/example-regions-py-file.png | Bin 65088 -> 0 bytes docs/img/example-rules-py-file.png | Bin 84761 -> 0 bytes docs/img/heavy-bullets-managed-directory.png | Bin 84024 -> 0 bytes 9 files changed, 181 insertions(+), 256 deletions(-) delete mode 100644 docs/img/archipelago-world-directory-example.png delete mode 100644 docs/img/example-init-py-file.png delete mode 100644 docs/img/example-items-py-file.png delete mode 100644 docs/img/example-locations-py-file.png delete mode 100644 docs/img/example-options-py-file.png delete mode 100644 docs/img/example-regions-py-file.png delete mode 100644 docs/img/example-rules-py-file.png delete mode 100644 docs/img/heavy-bullets-managed-directory.png diff --git a/docs/adding games.md b/docs/adding games.md index 24d9e499cd..e9f7860fc6 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -1,214 +1,206 @@ +# How do I add a game to Archipelago? - -# How do I add a game to Archipelago? This guide is going to try and be a broad summary of how you can do just that. -There are two key steps to incorporating a game into Archipelago: -- Game Modification +There are two key steps to incorporating a game into Archipelago: + +- Game Modification - Archipelago Server Integration Refer to the following documents as well: -- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) for network communication between client and server. -- [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md) for documentation on server side code and creating a world package. +- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server. +- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package. -# Game Modification -One half of the work required to integrate a game into Archipelago is the development of the game client. This is +# Game Modification + +One half of the work required to integrate a game into Archipelago is the development of the game client. This is typically done through a modding API or other modification process, described further down. As an example, modifications to a game typically include (more on this later): + - Hooking into when a 'location check' is completed. - Networking with the Archipelago server. - Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection. In order to determine how to modify a game, refer to the following sections. - -## Engine Identification -This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice. + +## Engine Identification + +This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is +critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s +important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice. Examples are provided below. - + ### Creepy Castle -![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png) - + +![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png) + This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case -scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have -basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty -disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other examples -of game releases. +scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have +basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty +nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other +examples of game releases. ### Heavy Bullets -![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png) - -Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files. -“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually -with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing -information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never -hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important. -“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam. -The directory “HEAVY_BULLETS_Data”, however, has some good news. - -![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png) - -Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that -what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which affirm -our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less -level files and the sharedassets files. We’ll tell you a bit about why seeing a Unity game is such good news later, -but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler, -that’s another dead giveaway. + +![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png) + +Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files. +“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually +with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing +information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never +hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important. +“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam. +The directory “HEAVY_BULLETS_Data”, however, has some good news. + +![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png) + +Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that +what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which +affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, +extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools +and information to help you on your journey can be found at this +[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking) ### Stardew Valley -![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png) - -This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways. -Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news. -More on that later. + +![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png) + +This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways. +Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good +news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx +and MonoMod. ### Gato Roboto -![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png) - -Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for. -The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. - -This isn't all you'll ever see looking at game files, but it's a good place to start. -As a general rule, the more files a game has out in plain sight, the more you'll be able to change. -This especially applies in the case of code or script files - always keep a lookout for anything you can use to your -advantage! - + +![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png) + +Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for. +The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For +modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful. + +This isn't all you'll ever see looking at game files, but it's a good place to start. +As a general rule, the more files a game has out in plain sight, the more you'll be able to change. +This especially applies in the case of code or script files - always keep a lookout for anything you can use to your +advantage! + ## Open or Leaked Source Games -As a side note, many games have either been made open source, or have had source files leaked at some point. -This can be a boon to any would-be modder, for obvious reasons. -Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it -does you're going to have a much better time. - + +As a side note, many games have either been made open source, or have had source files leaked at some point. +This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for +"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time. + Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical. - -## Modifying Release Versions of Games -However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install directory. -Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, -but these are often not geared to the kind of work you'll be doing and may not help much. -As a general rule, any modding tool that lets you write actual code is something worth using. - +## Modifying Release Versions of Games + +However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install +directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, +but these are often not geared to the kind of work you'll be doing and may not help much. + +As a general rule, any modding tool that lets you write actual code is something worth using. + ### Research -The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification, -it's possible other motivated parties have concocted useful tools for your game already. -Always be sure to search the Internet for the efforts of other modders. - -### Analysis Tools -Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools. - -#### [dnSpy](https://github.com/dnSpy/dnSpy/releases) -The first tool in your toolbox is dnSpy. -dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#. -This won't work for executable files made by other means, and obfuscated code (code which was deliberately made -difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need. -You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to -modify. -For Unity games, the file you’ll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below: - -![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png) - -This file will contain the data of the actual game. -For other C# games, the file you want is usually just the executable itself. - -With dnSpy, you can view the game’s C# code, but the tool isn’t perfect. -Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation. - -#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases) -This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2. -It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have -to worry about). +The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification, +it's possible other motivated parties have concocted useful tools for your game already. +Always be sure to search the Internet for the efforts of other modders. -You'll want to open the data.win file, as this is where all the goods are kept. -Like dnSpy, you won’t be able to see comments. -In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from -creators. +### Other helpful tools -Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets. - -#### [CheatEngine](https://cheatengine.org/) -CheatEngine is a tool with a very long and storied history. -Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as -malware (because this behavior is most commonly found in malware and rarely used by other programs). -If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level, -including binary data formats, addressing, and assembly language programming. +Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to +existing game tools. -The tool itself is highly complex and even I have not yet charted its expanses. +#### [CheatEngine](https://cheatengine.org/) + +CheatEngine is a tool with a very long and storied history. +Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as +malware (because this behavior is most commonly found in malware and rarely used by other programs). +If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level, +including binary data formats, addressing, and assembly language programming. + +The tool itself is highly complex and even I have not yet charted its expanses. However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever -modifying the actual game itself. -In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do -anything with it. - -### What Modifications You Should Make to the Game -We talked about this briefly in [Game Modification](#game-modification) section. -The next step is to know what you need to make the game do now that you can modify it. Here are your key goals: -- Modify the game so that checks are shuffled -- Know when the player has completed a check, and react accordingly -- Listen for messages from the Archipelago server -- Modify the game to display messages from the Archipelago server -- Add interface for connecting to the Archipelago server with passwords and sessions -- Add commands for manually rewarding, re-syncing, releasing, and other actions - -To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive -from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary, -avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in -case the client or server make mistakes. +modifying the actual game itself. +In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do +anything with it. + +### What Modifications You Should Make to the Game + +We talked about this briefly in [Game Modification](#game-modification) section. +The next step is to know what you need to make the game do now that you can modify it. Here are your key goals: + +- Know when the player has checked a location, and react accordingly +- Be able to receive items from the server on the fly +- Keep an index for items received in order to resync from disconnections +- Add interface for connecting to the Archipelago server with passwords and sessions +- Add commands for manually rewarding, re-syncing, releasing, and other actions + +Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's +servers. + +## But my Game is a console game. Can I still add it? + +That depends – what console? + +### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc -Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers. - -## But my Game is a console game. Can I still add it? -That depends – what console? - -### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright -holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games. - -### My Game isn’t that old, it’s for the Wii/PS2/360/etc -This is very complex, but doable. -If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it. +holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console +games. + +### My Game isn’t that old, it’s for the Wii/PS2/360/etc + +This is very complex, but doable. +If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it. There exist many disassembly and debugging tools, but more recent content may have lackluster support. - -### My Game is a classic for the SNES/Sega Genesis/etc -That’s a lot more feasible. -There are many good tools available for understanding and modifying games on these older consoles, and the emulation -community will have figured out the bulk of the console’s secrets. -Look for debugging tools, but be ready to learn assembly. -Old consoles usually have their own unique dialects of ASM you’ll need to get used to. + +### My Game is a classic for the SNES/Sega Genesis/etc + +That’s a lot more feasible. +There are many good tools available for understanding and modifying games on these older consoles, and the emulation +community will have figured out the bulk of the console’s secrets. +Look for debugging tools, but be ready to learn assembly. +Old consoles usually have their own unique dialects of ASM you’ll need to get used to. Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these older consoles to the Internet. -There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer, -but these will require the same sort of interface software to be written in order to work properly - from your perspective -the two won't really look any different. - -### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that- -Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no. +There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a +computer, but these will require the same sort of interface software to be written in order to work properly; from your +perspective the two won't really look any different. + +### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that- + +Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no. Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be -working from scratch. - +working from scratch. + ## How to Distribute Game Modifications + **NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!** This is a good way to get any project you're working on sued out from under you. The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you -to copy them wholesale, is as patches. +to copy them wholesale, is as patches. There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding the issue of distributing someone else’s original work. -Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play. +Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play. ### Patches #### IPS + IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's fine. #### UPS, BPS, VCDIFF (xdelta), bsdiff + Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes compression, so this format is used by APBP. @@ -217,6 +209,7 @@ Only a bsdiff module is integrated into AP. If the final patch requires or is ba bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp". #### APBP Archipelago Binary Patch + Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the bsdiff between the original and the randomized ROM. @@ -224,121 +217,53 @@ bsdiff between the original and the randomized ROM. To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`. ### Mod files + Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere. Mod files come in many forms, but the rules about not distributing other people's content remain the same. They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be -generated per seed. +generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data` +so that the users don't have to move files around in order to play. If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy integration into the Webhost by inheriting from `worlds.Files.APContainer`. - ## Archipelago Integration -Integrating a randomizer into Archipelago involves a few steps. -There are several things that may need to be done, but the most important is to create an implementation of the -`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder -in the Archipelago file structure. -This encompasses most of the data for your game – the items available, what checks you have, the logic for reaching those -checks, what options to offer for the player’s yaml file, and the code to initialize all this data. +In order for your game to communicate with the Archipelago server and generate the necessary randomized information, +you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations +and show the basics of a world. More in depth documentation on the available API can be read in +the [world api doc.](/docs/world%20api.md) +For setting up your working environment with Archipelago refer +to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md). -Here’s an example of what your world module can look like: - -![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png) +### Requirements -The minimum requirements for a new archipelago world are the package itself (the world folder containing a file named `__init__.py`), -which must define a `World` class object for the game with a game name, create an equal number of items and locations with rules, -a win condition, and at least one `Region` object. - -Let's give a quick breakdown of what the contents for these files look like. -This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago. - -### Items.py -This file is used to define the items which exist in a given game. - -![Example Items.py file open in Notepad++](./img/example-items-py-file.png) - -Some important things to note here. The center of our Items.py file is the item_table, which individually lists every -item in the game and associates them with an ItemData. +A world implementation requires a few key things from its implementation -This file is rather skeletal - most of the actual data has been stripped out for simplicity. -Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the -player to do more than they would have been able to before. - -Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool. -Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning -that the item appears once. - -Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World` -implementation. This is how Archipelago is told about the items in your world. - -### Locations.py -This file lists all locations in the game. - -![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png) - -First is the achievement_table. It lists each location, the region that it can be found in (more on regions later), -and a numeric ID to associate with each location. - -The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression -locations based on user settings, and the events table associates certain specific checks with specific items. - -`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear. - -### Options.py -This file details options to be searched for in a player's YAML settings file. - -![Example Options.py file open in Notepad++](./img/example-options-py-file.png) - -There are several types of option Archipelago has support for. -In our case, we have three separate choices a player can toggle, either On or Off. -You can also have players choose between a number of predefined values, or have them provide a numeric value within a -specified range. - -### Regions.py -This file contains data which defines the world's topology. -In other words, it details how different regions of the game connect to each other. - -![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png) - -`terraria_regions` contains a list of tuples. -The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region. - -`mandatory_connections` describe where the connection leads. - -Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create -something more usable for Archipelago, but this has been left out for clarity. - -### Rules.py -This is the file that details rules for what players can and cannot logically be required to do, based on items and settings. - -![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png) - -This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future. -The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class. -This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to -indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it -from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name. - -The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these -functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`) -to certain tasks, like checking locations or using entrances. - -### \_\_init\_\_.py -This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago. - -![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png) - -This is the most important file for the implementation, and technically the only one you need, but it's best to keep this -file as short as possible and use other script files to do most of the heavy lifting. -If you've done things well, this will just be where you assign everything you set up in the other files to their associated -fields in the class being extended. - -This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit -cluttered if you put these things elsewhere. - -The various methods and attributes are documented in `/worlds/AutoWorld.py[World]` and -[world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md), -though it is also recommended to look at existing implementations to see how all this works first-hand. -Once you get all that, all that remains to do is test the game and publish your work. -Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing. +- A folder within `worlds` that contains an `__init__.py` + - This is what defines it as a Python package and how it's able to be imported + into Archipelago's generation system. During generation time only code that is + defined within this file will be run. It's suggested to split up your information + into more files to improve readability, but all of that information can be + imported at its base level within your world. +- A `World` subclass where you create your world and define all of its rules + and the following requirements: + - Your items and locations need a `item_name_to_id` and `location_name_to_id`, + respectively, mapping. + - An `option_definitions` mapping of your game options with the format + `{name: Class}`, where `name` uses Python snake_case. + - You must define your world's `create_item` method, because this may be called + by the generator in certain circumstances + - When creating your world you submit items and regions to the Multiworld. + - These are lists of said objects which you can access at + `self.multiworld.itempool` and `self.multiworld.regions`. Best practice for + adding to these lists is with either `append` or `extend`, where `append` is a + single object and `extend` is a list. + - Do not use `=` as this will delete other worlds' items and regions. + - Regions are containers for holding your world's Locations. + - Locations are where players will "check" for items and must exist within + a region. It's also important for your world's submitted items to be the same as + its submitted locations count. + - You must always have a "Menu" Region from which the generation algorithm + uses to enter the game and access locations. +- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing. \ No newline at end of file diff --git a/docs/img/archipelago-world-directory-example.png b/docs/img/archipelago-world-directory-example.png deleted file mode 100644 index ba720f3319b9a21dbb89e59b9395d698c21e67fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51175 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

    +9Ru)s;B2p&w|5 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 J3H0W}e*;7}euDr2 diff --git a/docs/img/example-init-py-file.png b/docs/img/example-init-py-file.png deleted file mode 100644 index 6dd5c3c9380bdfc1d98cf1bb91484d0b62e00f96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82760 zcmbrmcUTi!7d{#kQBcH!A|(VxR6s6 zR+85SfoNnvpwq9;(Ew-m)F_`oAP$hSysWNo`U)BrXFX@RfUi$%OlzoJPHI^(+wLmO zU?@aa)rcgHnZ4VZ&G(+z(Khv2n}2Qo{JmWh`z?hVHeWbeK~^%)nPVj@11@-AL%M}; zSbiK(oiU6i8G1{&3)3>UrNyhgj;Q#0t{fZ<4hW+cIzIThIJdaiqzv_UB(E8ei&ONU zEzJ34a+3Xy{4?!UfWG}_yV^eVnc+W2zy}V8b29&P{CwJhhGX5=6c9`d7)z=pl;tSK zq9_3eq*`L4fw^PA`k0V?^=`|d=hK1?t^d6<(BTf#*^6xbIvSE(%KQ7cmZ^2O$LzS1 zCYa29v-hSK#OPEGi;tf`I6+kZy)i`KQ;O+C;AYRe+<>H{t+y5<2@MCXl*6o`!l6kY zV!?10FMBMpJ4LAH(rj{#1uuD&mNJlKUQio!h>g$qZ#$qb`c_U(ox|?dPEIO!)ze=b zQr=v>OXD%o!#CwS6a6lCr#F4ccMwTHAelpi<>G{b_ga?Lze$fwFBqJXzV|;3m1Fqs z-@t%o+umH=&pO_78+*;++CogWQ#K8zkl*D(n#flXJ43P1CdxKGjri(ta^SegXCzpM z)j(~jJH-dFGM{^KO^{2o@gmU%zlOV(N#~57@gmY`kOqIgH;+B@7ha&cz`pS%q272w zWO?QWQ+lVR@PRRdh9l1eaiFex?0tAmF>?Fy3eob?fjcw^NuV!A)cr6DMz9wUwg-G9 zSVYM?RWZ&^nxuM2`}&7JU4LFYZ9j{7M8ok8(J#>P>xtkQjsb`2y&ox6_Ta?aD#DSy z|NKl#_Dt5k&s4x8Kf}iS{GpLPuZ{AHCj01-)})0Su@?nE>j4+k!9IvJbbZzUJEr_s znc^|*R!C08q(KJIEoTG6pJwP)pE&U6)@JroglDl-9WU^T(iaNY@Y|2PZTsK2M>TpI zb+xZmFl@4kJ@GsBPv^eqf9>mtb*Gof-Tq?T|1;c%PDW~fAXWuh+$nw3?MH zb<&{ZH+zv2dNUDy)|>4e3>F!|cNv_Mk#46M%7r@D1kxXunEe%eFj1Wqf6_Ot*k-&u zXmquBz1VErYu9FzusW7b6=9M>t++Ub6Y8J7vxLP8pH`3T|0JCda?#r4;*I4}~bO6|riR@0*-EE(Vrm!NKn2+w%qL+q#(hRm z;|Yr5_*ERb8%5e#B9{tDmN6Hj)rc2b;zW5@rk@4vLewDy+yb zmTdJ{Hr%H^tk1mV>^~X`asT2BEp#{4Ob6vkq;g$;BM;YKTWTX5XPDpPAKUL0IyNte z1q|j#PAF?!g|3zQz;Bq8d#9I%^AXB-&dH3lPI-2S)jGmcNls+lai5F(P#Vv>A1fxe zFa14XxwPh^5RRU7KkLrcz6n+2_dUarPc|NDC1r#YEivl8&M9JDV>2S?VZ~aEqkoy# zroM87YeQ!||a|`;)8wNZc@ zF4V=EuDghN+ggo}_>Vpgi|<1%4#$|rR)ud5=-}bp&miEn3}l6bddn{Q&e_W$kIZ<9 zbZMbXjhmI==%1AoyU)|Lux;bMr0q#8d2o;KQh}E5cFy{rM+Ny~ctOkGHjv!hd&3Y;qk*3FLd8~q@m=VVf!PS_qS7OI3{xME5|vrWYGDKe+U;&$N9uB7&S)n#O@I^x>JaEbi) z9+~Xx8wzfbJ_>gn80{`n)mEu zN;=7J3yv0MNcvBNQ~2A?7}?f6G?d`{#eKT%&0lQ6o^O-Q{R^ig4$6Ab%hJnD6USqA zb`(Lx56ZEAaB{uof;kyNo*LVM1KQdny!A4^v}t?}1@6ws=maBSO;fI^*N8`t3+O7n zb&XoqVXpF`;2_w6=kie1-eiNHfOSEb_i5XKccJbt9{m{zJm4o&eLd& z3PfSuqf%u$zb=T)DC*D4s+!G)mf_|Hc57SRrJHHe<{lxczfI+Dv4}ot$p7~jp5J$* z;Rx7X89+`^5_{7nbA;^s7FWiqP@=m(&PqRecbaA~!+q@nxOd&CATiW(rWzH6H5SSl zO;$8nF}!PS%Dm&u{%>0}$|j)7^70>e$$;&sjXEm-=Mxf7&UytVInJJ1G7SITUdSgp zKKRH&(XO46`X^R{LSzM-ivjrg-)-yMe}dP^NmVv#j{klMdgy*BCNXC?^$Kl30@Lm$cpWC8S+nM8( z5t<>pW!$IMGrk_~#yZxz`595m=(fI!4_$mA`>e7D<_soX*!jo%N2bi-J5M|(>GwDP z)o)-0`^eDNr}4&{O$Q@^)EwsE%b^!#4qfm|epF!m_or%U60$&--9Jj+I(-kTP@s!{ zlC#1lEL3z6>AvW|S}=1%z|6va!~=hS?nayYML|%?$W}G>psH8Win>zE4W&ZV&Q>{Z{KXfqkM55d_{Bf26p_P6?ggN41@AcM;*fV2^8RT6)rkD1O>lGFl7&YQ(mGt>w z#&eAgMM%c%(FCCn(7h!%pV4`LhEM9?aYMqwlJ4tXQVqq2cp9p(c~DR<#R|qt;iI|^ zjPVCq`1&2M!)QqBgPaEq4iH(kw=)a*iVoPF0R1>pE8B|?c=?=C!(+&ofTAa?%q#ZV z)>W-X8>j!M)z{r)-~+nNEjSqG98&GoAK*1x5`OvvU29XYCq|`sGY4V5FN^3x4ZoQk zU9t*j9+_EM`A`|$qgy<~_0iF^n_%O$`q<;qUC-y0>?2!nXOpk0e$%FX^AzJy&Vov3 zh9~1)1dB%ZaU@=JU;Nlr!I~+dt5Tq4!RcMNQZ%#- z&A2FlLODz4_G_fMw}_g=HfC5zW7Zfux5lOR#ji{2JokebSgaXQ)&A6PG&HE&i$33o zx-k@x1U%oO{kuShoHPGCU5F*tEGxL2Q|3Nl=z`sBSkI9J^v+>Wl_+R=;f8tg!5)fl zenr%{z3e?q23sfG)dN*~6AN`=HP?k?ST&ZK|HAgJ#9;GRT-e>bN68be_Yb@#6#5tV zgGn7b>weX|jfi>}|1aX?D>af>v@&e?`YdlodC+t<@`1$N(6&t|s{+VMDoIxLuWRz4D~kfgiXMgp{ypB!1|m(19DagW{Vu+<`9yobcvkC*8RcMT7uD8rW4K_Z*N zoIhV&!m^Vug}_D5=(p$;cqR2|=6~6kj5%b_>VZCtCe*KXA^cN+G#gFLd0p29O;@Rh znp_z3Be;@kHhn@LDI8p8N8NmBowhWO=vg{E1r18uxq#o69t5=!>-G_SPM1|#T~&2( zM#+-V9_#DuGeZhAJ1Il;{C~QCLnO%v5;)b!;Cp%N`E`hx1_HY>`cMr$jq3OGjGy9& zZMFPsab!*PZLfOxHSfdupam4EWfJ^D={nwT8!?hiuKBBbj4;|_CV8;*17$Q(OWNhb zEEG@ruHf8RWwTlc-b3HkQwr|Wg-bv!C3fa2(6Z)hnK+{b^5l^x$ZK5mn~r8iwks|z zk5Bx_6(NIhU>byaIW<-@TdX;=FR*v47b8gYm~H7!P#dXk)`PO=LOAWvN+PBRU&O+C z5~Zf99wTFA1Mj@~7s)*DUR((%5AKds1d>!)RKQ)MevH`a0xPx2Pc}N67!dp?uii>kED$|KF`m*<*QY2`l&CGxF)TQ%;=+fu5gtU)v1*x25Ol94Fk! zsNBRs6MVYo2iLz$Nr-zWh`K$-Mo@hD$tYaaDaGA9?R>AZrwr)S%-^|YCja#0VNe|n z$2Y41tVcat$6vYU_3}0n^KrH+ua`c(OL>Wem6VhSACtamX&36!Xiv>~7=_7w9r?Jd z{%)}T<)@`j^Y6n%?cbmIHh8p*lx+duqKqt*k1Io~OR*i>nJAIM45gVD=1Zo^RfUmu z*u*a=@rw&0=MK{inhmw`HMMvQv~HO=rFLFV1mwFxLU&?fcjC3K1V%Nc#BRn!Uf&jr zIXc@s58H2oBn2$j`=?K}`K*^-E}-s~Hs9d_jWznzGR$1}32^n%Bz34hX5S&*7IMUm z+%9K6etp8Pfj}=>1wh=_-!_Y0(pB{5af3h52ZrfzDvus_3UYbAdS`vhgL9R{xZOmT7rBv_>Ouc@_)82|2kbx`32*+SnmumG zgY$o!fvisHG+EXTfQ*nib-X!)aEH{YRCa(XwO;bRF z?TmdRVVF!OOEVnP-8QqnJp=cf8hy$_^M>Rw;4R}p*&YlUHYxC z55kX6#~GK7Sk@Tp)i}Z5>G34W$i?Y4e^SxoNidmL5z%vQKh_B=y28#WyW-l`PS);F0@hN}LL#-xt{aC)&;%vU~2T*PrE4(0Hegxxx6Fga{B9Lb+UhLsNa!y7? zYgk3AFjYrfvRFj`_+oh2AymE-g?lJ6B9bA@g&io zx8=4d5>fn1*Lm;^?T$J|_AJMMuI!lH(>JQh74YxES8{EI&2)wJ7X5E4)y|Q`s0Cs{ z-xo*{_AhVH3lcAdBovG20a7H=b4LNF$OA%G8AIUMvUH_zRH1%v(Yx5f^wf%)giM3H zI9=#$ODp^mm6CJ@i!7`WFpyGyLeDkfmP_w$Km8=Yg=OZ_Nhz!4QvU9ue10kNx^{bl z>Skf2cl-W%_QOnrY+Xkh#yl0ZZ`6W)jnwT8 z;;E1yL0DwJY60akHK8QcIc09u7hMTU)ecL&5@sy46=4^8UPf4C4$9N!1`_-%qf$xD zQ7{dy@u^iWU2&!1;AJKETU+N8fZ9}VEliYssxOiw_lZiws%1?;x0Z%FUtgXUR(SnE zu?l|qWt0DGggRpVEtLGL6-M(?@mEDfh2<+PR`(3!v0v9-x_ko?6d6s;QQ>kBP(-#T zZlV{x5`dTuY+vdIhZzmM!5ikt^=CpLNBGAyxi}Zeb9-0;S{bXQcOFkUKRZO76L`<3 z%eXE+p8Z$-N^?pdF^wG0GUlQWNQ$ccLTT0p>bwk%01Gg(5_sCh64GueA&fJeV1lRO zfK_HHED;WQo8SaxFT{Tw*Vwh{$=fk zVC-6*jN!a@2!9?doia4GGld1_5~6~M$H!hZMG&d&4?%Se<6F_D>Ta_hP|D<*Kcrl3 zx1w^RTwh=GzEnwcqOZ`obvoI7S{XVW9W}~h0&PJH)63mcuXF$m7@c`4E=mCL{4}v| zWFg?|xql;|zwzldsEAIU_IjXNsK?RiBF3YV))-LRm6y6j=xEUTr!-vKVEZX_wvFsk zpkBPL^N`{#9o^eTy0`TndYLmN^z0G0Ai91M(I9sg`oY}{!lm2ET|DthaB2=N?7g@q zZV+vlTxTw}41a+Kv_zht=sS0*0O3DCClmD}C!M}9!RQ6)P;I`Lj%$+WA4jEx#9_-% z$H@zp0@iZ4pFOsBrl-4?_llasO~wJa+u4RZIS#k-fU{3?E)(#R%)Gn_PQ!hHck=dB z|K1^z7DYh<;a9^v)Z+I$VhtbU!f)m)*~wOYXYRRU9|#G>q7oyN<0Ba93Pq)n4-~P0ClWEcq0}ihCLVOfGRR<&Yz7?2!U78MAS$* z4Sy=iJJUMdRP@bHk8ZPw(N2%MyVz79b4>yi4?P{1Sf`#bKFM}w@SGG2ryO99!x-xI zP(SL9LUd#Fubd+!7H-&lD(*=*d_KcR7jfiA*;*>lLu?!AWd*3O4LdoooZW49&oIXL=dA z?{c2EmCE|XujHc^9HUM-j^3ajRPzfs!{JRH?MX0Pf*S+qNfgAbw@js9e=UTO z4lsk3ZB=hZ3P3|PAkcX4CU#Fw@OZux_v8iX5~>I<`t)||Lb+kD+e(_?Y6>++Xx%&z z?V#^ z16sOl1bcQQrPq9-{VQ{_U_dCjrChP^52-l(p%_BV5|Z$~C^3f|eD!tSXP!Mjy-p*& z;`V?kxxWLdV__f<9@nT`hq5!30|Iny8Xzl}Vl*gs-L2Wily^^CEoGko3X4qWFu;#c zyXOorPkz`gVh$8u1fUyY`l|m9Q%k=_R*Le(OTeU{D-Oimre<H@&({`E~;;4S-k6tz3vReCExXbQ;SY8WLUkU$t@9@N%A6O-bX zybZBA3+tzfz-}J?*lD^_SgoB>a%WJ#(k6u0<605(K(7qL)1ZPwMf z$2u73ubAT^_iD3Y=8xYUL<-dnG4O!OBBa5OPR`ws*>fPREZ ze%(ra!2OPgCkap-z7Iu7PoAtc5E3fAEWY{XWwhTnLE9c50k3x{5!^ruE{e2!385P7 zV>E=x%9Va{a#yHP(XJVMxh^KdZ5IV942Sf9+AwhY=u0`AAfq=#-KL7`N(aJ*ysSqG zA;3&eSY^eOUbc(sx?x%MRB=Jhk<{i4`OD1E)FiYfzz3ho93A&{zX}Pu9p8^UT8EJ5 z#hSClJj65ncP%McUSxIj(Zg7fc))y=O4jbH1#dBPM{w2@3_@8)`mBwtPOhWkIieR} zE;nvQT&3nfyEVGUY<{o2Vr1Qharrc~zS-+gLD~z78QT|&-E&87{zH7;5ZkAdx$OlB zv0Ft`4DEkF-PTaR3Je_+s%E%>!LbTjnujuqgzCIulW zK!)e}rYp32&-S{mO4_MQ;UX_?JKp>OYFjc4y&%&th6D%oIS1X$U0P!9>E(A=*s4aY zO%!HG`0yn6ICux?pM;P?>+x~}?Y&&9`p?>7>GN2H-GR?7hWWXgy#)d%h6KvMO?xBY(lw#pRJ}5-9f=WOGMLTUZCEMh8?&0CUS#4s8cYV^;Jo zy4wCzNIrDkx)w4^$NUs~J~HnXAS!lNnMiK?V2dI>z3&zUD%+>@1RBpDLo2ovmckhPRRW(+s4xoUt z0Z~QVw%xF`eLFn=_iSF|W$oNeTQy96jZlLbHh%T=B_!;w;M2rP+PN!7Oolt2<^$CT zi3dzpopxA2iWslH4*S3p_Ia@I6(EsIemIzf+Ev^d>MVQ6?uE|M4~hu+Qx{|oOQ;6b zK-$IE0sO#{Bi*U2_x9$n3T>h%mb|YddaPIaY45|11EdjTW6fEX7+ohit?;3|q5^+T zQq)^aF?^R+riA2R-?n?-g@$9!7~8&B*q#t(c7}u8KQpvCnL!ot#`RPjlWUWf{gj_# zjTm8wKX~^EMW?D$0(G~^+izB$oh2s$NF*vz_Wt4|y*Ve-DO#C9g`5bwPnWV>A~~#4$#;F-ErU z12{e_br^1CjD6!siuzV9lr$(;RFY9qIj?h#3n;8(8<1I8Sh&Bag|?tcaPW6v05B%1 z*z0!7>+#v?b;~D4ldmsR*TXERBAzH|@wMkVp=?W^8-8cOa_J57*aw`K*`M70cq&dw zl4J2M=ncU5F6lXg20zC-3FJC)+Z>u)7{(nbeb$d+CI-*W>#4Zu)Fi~~VgrYQL7#jy z#1wr>T-s{1k9Tw*mn9~)R~t5Z@id#TB4{|G<2mL*xdK;i6Kp}1mKUY7{eb*HNy^*(W#(~^96L=h@%mxCBK8@KJWhV&ibEXzL+_~I+ zYtejG=khh}n|gGP*9LFe`m@nq89IRzI!apH@_8yM!!HG(A5$$fxgc7lm$bOVijBF8 zJlYY$ogYu=8o@9pEPU!=B9bRXXntD5hCU6G`amQ!)}dYUq35kDFF;meojF-g`&r3a z#WnyJw~T$xrPr%gtRGou9P#;GnpWN=z0z@>(*&xH#sZVC7zI;zYzr$dxv+e?vF0*i z(!Z$ylxuj5J0})tYdsnJ8t|NV!ZQ^#9SYXfnAOSlm!uJO$F%b2wsV$Clh|fx`P=x2 zse?Iwa@AU}#qdHFPCD~Zb{3m-S#1jihTdZeKEWmEW>h{d(^pP9a7AEvDp2QJ#E!#Y zdxeMs+^{tcr3K+7f9o@tF2hhyMCz0V<~hwt>y>U% z71~jA1l=$Py)(FN)E{^sAX3gfzOv_+_x5t$TP?1Q&7u*W$h>E%t2aU1$z5u!IkFo< zI@%Qoli|^WC=6oy5lb!xrkJC3!s>a512Kl{3N0YttOS779bs}ed;0pE?d%kUgoFUH ztY^UuKhIW1Et2~6@POl-Ajt7GV!7JVmhAun3}mxp$Zl|0W~-S0g`aYhbdjNd+YIxfUpzf0(5WK_A% zs4{;|rFz8&AJofhP{Iv}NeK_}(rbAk1<We88$PntSx7t`Gv$3-n60T+qyTFUCQ zmGt~}EfWtbU+;WfQSK*s&XF4IEz0ESW64VcI7YCh?O+#y_RmE9!(Jecs4CWSr!a8G zTg2Vw+BgrO=iDd}8RM_vB8R(GcDq`<=-~UO<7m#)THlV))z41VEKJe@Bs(?1M)mFR zGmw=cfJKG}Z*p%-aYOT*;3{)*71tvy*c3zWyLgVG0?ZU{UA7f$|NGk;c{zZT+5@Ho zTttfj_LDcci#tP|;XX(}+Kv+3LVY>J#;S_PK8I()ipSo%{kUo%`wWLG1|omA{qw0f zfOQ(L#~L069&Bfj&-6u$q7Vn(09CZ$;N(=)WBz^qZK^Vhju|Pcrn#nB$nO5*Mj?Iu zJ7P^B`FE9X{7#tkXX40Gp(q%DGZF)H*|lHk@?uVe@0%{)tEjk7m$bx(@xm4qSG+;F zHOtcFV%8q`cwi7}!t>X_Ml&KTaOhvZaz;&YqX(!OK)E(X7tA&wAufl`3%^>~ z*o6OP_-=7_o7G}3QhmxRS>4n-CG)61+?0Z62+|B@qANTM_o9q906yIM{_fWI%9EE*cDIaR7K?}8#bEQf zNkb6&%4EJ%3A<*udSSXdzuVpJ99f>R1iEY(37@$>9nl=PgGpL;u1`4O;dNPtNXxSv zt+!M797T=vZy5{&_z^ZxiBgP?hYYAbJrA(F3X@Ob+#e%37WqN6%vtBZeRj(Gu%4o+ z@;Np3Mhh?3{yCX_{PxFX-gpvhxj2Y<U zVG&k%S@-i=h;RhPY^43+*>m}Yc@32IW4q_tg!6YV|C!quEvXBlA8$HWn^B=FkBx_= z#-!&gb%=5aYW0DWv>}?}dlF_wDu^#Tw6GDkQazq)eH#yQKXvk_=Nv`}Y~*~{c<@_M zbYU+6UHya6N4(0fs9XzN3g>{z(`X?>T@(RQaI@m2-KVEL?R4N?44v`0hakuFzB-|@ z4C{EfGX54<##L@-pxn5xqkG?M2oH zy0g$R(Q1wL`^l77!wGA4^y^xPZWqiMvDGz&ZmjWfn`l}0?fV%%#6nvsX>;i8ZQthW za`UG&hzd8%`_PByDpl|g4D`t_5INZzs=ei@v=xoHQ|yxXrB5cO*YuYOwFL0VhEDNX4N z%{%BPW6?}Qm=_<@!x?PZFCfv|sek``c=3B;;x)NTz?^9tpUaHfuNfaSsf)XjX=btc zn><+a4lq5Gu%E4Zukm~0{>SvYdYw(dv@+*pPI~2Dx6KwJY+hGU@z)d7y5eImnG{Wc zkXsHH%wD-;upH`8y|6D*>CGXOIYJKiPyY-F`I z{r}h2JAQHfz9T)b=O^aNsklw4gO|M~fyzQz2<5foD6~+PK#;WOqMzA|9U;yZs<9I2 zWMB~dn6u&NwIaOLi(#ofwrsK8IR?HzaBcF%WN3rve%u4;-Jy=YFhS;^lozEj;nhaR zwsL8CE23#bXXhRlYR^-M@J;ekxW=MR3RB|l^xKA+Y$mUR(2StN?33g3`@5$#h+lJp z#W&Krdv5)H{OOT$JU~vky#Q9nADiD_(qIo%gCHtDrHv4V!kc|n&Ngo+xl;D=u%MMb z!cf>wOOaVtpzEv{N6t2$aD*Dc;c8H*d?3RN59>)D?auhGRA9F9_z}t}stUZkyv8Ob z&H)+No(1{={$*6(tQX#zpE!bS$~Vy_43t_RbZQY4l6Ted$J0J#y!$@`6wGN7zW?fa zR&z!QZzPGyh|<*s=>5uFX>Mdu1__X2Zp9xUDLSLlwgP<~$3^z8QLFDn*oV zxFq4fmF9MDf3F3+M?YWQ4Ib-HmaZzQiG>VQ0PC(RMkS%%*afex_QpiY=%>uOc^&`5 z4Lmsy52-{*{?SN9+P@lMkfOXhT5N{!yenPYN9u0<)o3xoXA$tMmhue`Gxw_gw0EH4 zxB%`OCuo*D;AWT3O-x^^n25aiZ6;DoGB-~z4L7e3U+jiIQ-y>C>qo{ zTPwNo&Xv+;0H>UN-zvQ}5t%go%s5ZAx}qX`IPXc1H^PBSEJ2WYv1Z)Bs1096oansy z4L{yllytc27O{55q_zjz#1Lzp2FCV*I$aOJjr*gcnyy}jWU+zlyf0Is-1g5J-3}VD z6iGKB=w(}}>74@euKuNY;_Cy8ehOjtjf(Uk#J|xPYo@4Cv|a4uHQ${gY7s;UUi|6S zocHWKMlr}!{C-Q5uWoQqpN_v^eNl}@pq6tjpnH=wT!qOhb|6g(=>pd(j+gK{4!1%f z@BTBYjQ8nf(vN6-F31EOzvo|yT@`T+bnQSFN}EeL2F!TQU3FVLoyAWie0z%$yITA# zyt=x2Xo}ed{dEcD>8UBEWR55AQpU$qPjtJ{#ZR0|wiua-l?L#d;Y`I@NTBAiK%H}lD4?W9aQlzS%%xK|M z&5#qg;<^?3;Qe3b#c0#JUY@++s`yUFx&E7As14m?4_@f(pm!k269nkDtVgtyzjpbC zU5r^8Z0O+5VmAa|$=bG;S}U3FCFD)6Xs`)6aVpy_fx})%-b#cP6!K`0v$Z5$7N* zLF6|4{%nq|nJ@m*2#7fuG>fXvY}$M)=sCm~bc3n`Lk{bQ{l`tmy${6dtU((AS-#50 zetY4mM_XO{lu!b8>+6~$u7E!H__=b6&w>xV^xiwW>n%u$I4@WSC~J<3gimC*6n<_O&r zja^+k4fOo%BhLSN=X&(3@~18L zBx6JI>SV*-__e3UL@*3533pKD^~T~y6OHh->pfzh&B4^GEre9tK_?AwPMMd0cbgTX zIBSwr=WK%MQ5x=g$Mpl*xQSRUj!L|Ob#V0lhCly{$LiJr^@&el8IdF04XD60FXnh5 zZ`g>viA&9lz67+o(wM*RiLr5yU*Ll%ivYU(l@)0>7;&V<>}*w3{}kD2$}QIJBNh#9Hux`{;bPxwo_E{xUK9HZ#`$=Axfi`K zvY;Ym5MaRCLMR3fnBrH&sFy~k%u6$+N~bdxoSfF2HvNhN3Zf_`=E3*9V@F(rWt`8h zCWFVi+=j0jw0X_;xzX5j`l9Y>F6Q=%CF6VjJiTtyUY%$8U_Dsp?ng8GtT}S=Gjea` z6Y}P-UAys|Sg8~9$`uzEpCnLdvw-(($7Qg%N88R}m)Pso!O~jrFw7tiPf;?iH4ln) z9ZS|OS?$3+{^-2G2QC=t<-rLGNj5j+`aWJIrlX zhcX+_;l_ikU%jdC2BQW((AkMB%%Hz`fH8a-TsG{&EjiSWS>#)wY z$9Zw&;~;6K_Ro@M^u1x2B{&!daSQU6D8bM4`#A>m7?-W}qR=(Vtw#+dkQRKscJE|u z0MVZ@oJA+L9-W2itggz?nU_qiUm!54dkJR#j&kvhnb-{-H)1~jQO7hfGQX=~yEz$7 zLJ)>)b`S=&_RR(7UBB-HU3Iu9lixc$HT-FPcX?{N5DBbH&~DOMP$vy%Je^sv`=u2e zNZxYHkU|%sdyZ=ZR@xhDynh6hg6lLeuEtIfS`S-OMdYK8E8%X#r<13w8iP;$#-63H z+Tc(L8jgiK=0g*b1zTCyErwTH`Hr#n76XtwEl6qAAdTP;hnYt|G?kB+gt#baX-DlI zoR;RCpEgYznqSvRsCf>KR1DXDvC8puUh2Qu)l9^|7vqxj^%Fgp+Z>6nB5I*<$ry?E z-xn(|>?Z@5B!IH)g$|_}6{~~=&#)*CR=+$b; zfl)tNS-jZ>ReA^C{PdA#Kd3@MKQ&3UB4v{oPC8w*<6M8|u0KZ0~6n>0;HtaK&_t|7b8og>>q zu8)7ig3i{y<<0t9IFZxs-jPGsX@;ypHC}#VJ}1{CJU5q>-Bi!)iR0aI<*%!b4ucCSnNF7^Bya?=A1^!0&g zXLaBSn5>b8qv~0~mc50)nF09VFDZ`|J;dy2aU>f;FFwciX-pD1-nw8bJ3RQ~F%iqZ zM3`{!oWXjB{uHcuk~ye_OHYS1~dRzj}u*+Hq6ZsvQ22~ zykbH8XP_zB4K0 zN3Am6zX%kc@W7G>;{1Y5!{a42l<_lM)?;?C)0bjva!v8BOj+kST{Qkm%*gVDA9j20 zM$zY52RRRi6InP;)61ar#qp>BPw@kz@Uk_~E5V&f)TGo*d-(}YE;9_&FGM!wwfHaD z`|m~9lF<;oEnwI6r_d*w2b<;LEO0x)TlpJ(&84hs!CJ?*#|8l{vTQk^+{=L_tCF?i za&5~4vFt+?tZPmTlCFoU_&#Syvlash{dL{$1Bm*hCaZ)|v%iA|tpdmbs2`+(;dC}h zeW_KWDPMYTP^aO?&m6G%#-hdI1}5mE^rPLEK>^3>s5Gj^7-3AI7n|FQ@fH!BXiv!A z`Zec%OL%AP>n~#Y-vu-XmQ_F8@a}WV1l=x2ukQqR>0e(?`|th?PZLv)eY_DNn4*fe z-XR3HXcs1&pgQR-qe<`0nFex4Rm0Db5-IP=C*Y|U2k#r^&fkSo!jrv*A%qc>S*;s~ zchcBv7EW^(GO7XIQJEi_u}amE#B&wNx=oe%{YTkw;x@6vd)2j;yBr9W+lC0I{1GX$ z{L4#=CEou0izVSP+z(kWo*jPRtkMxXR$Huj@(jU_B*6miY!d>luh&0RAaR#hI!pAr z7JTv8jUos1GP5Zl? zzDuAP$!esqP6&YZ7|5(yyqUgc$CL^;q6Yh(e|P}oIOgwIiVW6A3Q1`GiNw2oUTGd) z+$+@%Ul4P1T0RfqPszU_$lve(8T}-GFAyvD9TqDl@!Ukrrd?O&nQ_IG@UR4erKOgr z+O*bu00)wp70<7aCQ0O!e%NheG14Sj&4KlIh@aD2bu!gs9tNmsJ7eWrw z7^ob7+^2yl^i1)Bhcu{O?e`OMK0YYb1z_RG63aA-kk8PdO5;~rmdeCx=G0?71#=juep#u0H_N4_c#?1&YJ0CpKI9roAzS92s@-OIvdlLDukqPFJ zsgoRsId=)y7bf7W@mE5Ims5&=K`BWbdjF;CF|%_K!!x5sbAm~|l<0Csi?{P9@l4*i z3Do<%8~?5aI0$%6-YmQzcFNYG;1fwb5RT=jxLhu%NR^(9cP;w!IAHsV0=lOyB2vE ztpnZZ{WRm988K0mgPSU}7Q0Q4Jgy#*+E$kIo5YakiWwHW45mI?AAEvt$o@`o@+;K< zTrmTvY}sJ=TJe6z0Ip_jk>IelTq|(_f-p{&-af5HFdA}OJC2#5|E9mbIFPe{xFC%T zSglv=>gsA($P7q*z{kg(sImn7DexdrOsx{s@Nfj0-iGDhfwNEdBhYwr{oQFIgv6qclMSkgx*f>LK*gVH`^B;D7i8{Kq?@@!$5q8t=X7H>(01}C z(8!Bye{5`kSatIv4-&Zj*Jz(g2g?l%(LU7)E_NvDhZ71~^2Qe5@5^jC;E6lx!-@vG zmC2XE#ptzGZjjMraUu8407Q23!qpHCfZI|>p-ZTyw@!+dm#x^Pc~9PFC#;#;|G}Dh zHSGQUb?V=Hn(0fVeGbGPQFYwt=(%evm*Nss2Mr^JM%}#zShf?Ea2DUi!lABC`wv^`{}Q-gtTp<_#gpSZZ`+y7{Ebex ziGch_HMlEQ+kdFV zT@NvJ|90)Sq09mhsP_4_J*~%%OQuWYe%d3-oXFE`1eBK{gF(M zjxJfKrVv|6FZt1}`RR5Alnr#>5We-_Uv6{v)TD~Y5|L@Y52`mD`1X`< zB-;FOFI9)#&^g$4U)27if1U$Kl9%0iA9aA*`dB!u8AF60TO3SNb9{aFyC(nh^70QHA!42Uvha+C$qvWOT;PeS+&RRv@!+5Dl6EAELYd45*VBJ>i$Lqv zv}XK-7RFqPr6%<~vwRjmE%hS83i_~5$&?#-k4JXz(P1||BavX-`ivJY=~||9aIa}# zDfwUq<8`>3nl$6d*)sY+o%F4w>Bv7Xv)nt8J%{jFyoD#Q{?+=m{#n$o>A#-|W7iM$ z?^Oibg?p>A%65De_dI81=G^8=itm-Nvvd7^C;k6o>`lO-?!x}@uO+2IQMPQANJ1*x zSW8*T(n|J1iLqqg$FxYYmO@$PQ9{|WjeS&>5k=O)*aw5LWF3s*cgFHO^}g?Q{r}T- z)z!@Tp5;F0-1q1H-1qk!W=lpb<<(mu@Pv|KuDc z2}kPKLzba+yQV(|@v_osne$dgjUe7S|00|?ape@9)~$Lh?tomx(k>|z z`bhrWQgNC=r)C*wBHj2ni;o{fPV|&|lCtProZYU@_!IRXK!c*)qXP9@7gO3gze56y$E5s#UQj~TvgusQ2<>Ytr8aO7~D1AQoP4Tg#Zm}4Roemnv ziHhH>{24nBQ*0VY-F@@Nx+<{w*~(yg(Vq!Qfn<b7P*0d+osBkc<|H0$=1xbRy; z-)K_(Z@!-$bq34Z;hl_{q=B3B)ewi8f@!}^BemIys-GfoWRchA3~uVJt-d>+J~sBo zCULAY*c@yQ8sgk;#}!r!S+``$~per>{{t&%&X;K&Z}zJ`12>+t_ZB0rYPv=ubi z?BHP4cU~a}x?S);#isWtgYGoFrrFqk4?d%<`M+eV7oYKW-{qbEue$$|BZnv)dA58* z*`Rq0=Pm#L%8;g5{*>LIHOs!go(+zYQR|{9rb#$*LQ!>z8v3`$%-W$ljGHl_jd5=b z`+s%qe~qts-tr>XPf`Fk9C<;C6=Cr{%k95aSEU^zqo&cHCeHugx%5!aym!($$7`;V zMX0|M)ORRBp?;uAkZ28mB1;hk^mHO6VZvsn3lCE}{@{WO7eI>i-a()24I$NGe;Tf;^y+RUF)+wXY<`#YJ zU-wAycvZSh?M0(br{xjSRfdBIav5Q)bp-`7{HmCg;bHI(!W16u-%yQ%rHB}ZH@bVP zXxzU0DyQYZEks?3dG?tw+0V&TnklNcC@(2wYmwf0wz*arnlZGPDOL!K!5O>npITIq zrHqfJEtX2D_Vtpxa7zWt{qxr~zz~%MBCa2L9;b0ELqgo%keo>O3|H(l1B(kO3|swN zQRCz1^jU&o&NUR~7+`*@zI|n{35rnRH|Tn&S{KV#$n2f*-l7YCJ!1N%Y$MCa+?GXi zE&}H4-X1lNiAeIU$Ocw2P_dVZrS7`BOJz=kPV&WRk8NodPluR<)tp~YT1u;BPSN3?P2$VA`w5upN*C# z>{#{QQYiAmMhw<38TPpbE&kw_P3VtqIvU<64g1E{kU(q-7L2F5Gf~EncK%1@F$UAE zIb%Y=a#oc05arC2K@MoH;}c+CRW8&8i95^+hX`-OyA6&_F=^5n?*Euu=s=}~%^XH*vXDSjh&1_T{_BE2(DzFm^ zeDyi=dHmP=`{&II_!eh652z{bng4e}v~roWG$G5$3(v;~bpZ>c8#X)>|By*&8u!8Z zda=3hL(&{&dhb;HyAQaA!s6o3(YW^R)SMYBA&@cwBh3Pf4jgO~3rEWD!m0Dbvy$Z# z=~4Vgs3SPshqAJQv9(S1Y;8ne8*f*Egff|{uqza0+B zZ6DV?I6H`kw-k6J(1l!bV2NacB-hE4)9>aSBqx-@!URDmKLX+zakWoX+4Jdo6Wl@q zoBK0Dm)3+tEn%kRDWD<;hodv{K=PwNpXwuSx8H+?_9^_%22(1Yi^f0)COv6V>*frs7)PJow!D2X;4?_@NNdOsEN zFU&)#vTZ5YAYowB>mchO$&v@(k3kCh1Ij9}qErA&l^3c%0|Yv}fkklnusWNO=+N-r zV{o?zx9Of$p5^o4H*|G=Px!b-xl5w|>>8%0Jw0}DWDu@F#G9JJ`Dw7J#E4|)fHY8e zTj!0+sr4qhmeqB6HLUHK{`J*oUv9uiSp(I(J-u-;$4c{zwUOh5Q^UOg6?br!M=LIV z5N^&Ni%7Iez}(k`*TbWI$a(y6XggQOUJ7r8Y~MYLgt4`{`_{yf6zt4WI!>mVP@g&J zro%Oq=cm+|0K?~0dS!c9yz0?R8%xa~eoG;8yH6arbe8K>;UQ|v#pV2%;-6>3?p#+? zoH|gm(AV&2S!v6}@MUco^dh4LMHhfSgWk2Y-rL@coEvuAkr&{HQk?B8-OHG26-UQ+ zu%jE1O;80Ne8T|Cg_-G?1wM0^yJ`b-#Qd+H|FD7^_Bm3z`Cb!FcK{4qU=K$`0#U4t zcvWJ$SmVr5)#y82)$agFmMrhX1ulip{{6}8HbES|tIZQAG0cUdB38*!MWcbr@% z8d3s{kTp(5wwtV>loDaeR^pWB&dQ1V@H`qYAVSX^eq^Ip6%|M0&x!ZI4Zmkitmh9={8 zZN1Y4$IiLbgs_+>Zse5>$4?&kh1PjHgj#U6-rttV80K`t(17mY;O@Wtx65T!c zaH;3)?BR|4CHG5Q6?c+P@?>W_k@^E;_NJW!QRVULT4yV+qBB1K{E0L5GJ-p_@EJLk zoy3n71?Qn8KT?;#`5He-v4`Yd+^GhZ3Tq`60@OFQRkwboShBuz*RN2R=Ue)E{T>{? zQ|G;|r0x0-Z&~~-uAv*BJ{HTsyF4U-yhW-vE(}|Fd%fd^MS?>mNdbXRLisQJVx5FZ zeyfKe{B+@PtQo@*GASMLW8kxGK>K1hsldvRD;kdXxMLUKUV=v7d*xJ~frHh3al$_+ zji#0rGG<1Uq?%)s&8d!9qOWH6jZ{`tOm9Mz`S6+kMLQG0=+kdP;6F$B%GNo5!vX>v znkaMLMFCR38Xno%d@X0&F|)S+N8iI%W!j2xl-LxbqDQSSqn6E1@{jJ9AM)@eOiy~= z?yK=&Z7{Q3odRCo8P}j_a!*nSQ?n2)M{+rDlGx(dEtKW%e4X1IP5KSu3<|hIimKS| z5*{-XS%K+^f8-KdJoH?hGbMt2OSRoHFRQxu+1XHRJkC}YKXx72R~4Y>>Xe;{V?}^= z{R3kXNa-_P#1bAvA|aLEBPT(!;*KqDB#tZEv&EYAL@H(QanZ5an9)gjq%ex^Ja-$ip`=M z1n+lw3N?1$X&JNUQ6604drw&I@Aei*7@>}fLNk5)n@0bcOZQK80q;z6VeLyk7AOi> zIXtbAn*hmdsEdtIRr=JS!{5Cp@e>~tybEyryynng1*S`YG^T*)y2FJZI}MF>$29fg z23;4NuH(blf4lm-N8fQu3PY8QPP`h+fdU3d$J&oDw7EJoH$qRxkAHkzn7Plsw%kd+tB6bsr3bwjsNJQXr2n# z={Q@WNmDu#=V{)6t+MP@I;{x2^DR+paMVJI>mA}prd2IyFo3c46pBE^kDWuSx6Dxk zAC;R~JPbSVxuC7sp#tSN{;vK7y)ue&ieX>HWT+3nPq|NgDdmQ|a+xRN!sNuALc^U@ z@VZA%BN9~Vh-!>G0VO5upr{Ppv@yd3%rAKMi)3%;zA8y;mdF$$Y0#hIHnW`h#cGye zpDO$eH@cf;7ERpQvN7hc6@ zdcI@e4CI_nsp;ZhK;NSo|H&WJSlv6J#2H@_c(uh{W#KEM(Bk9+z>N?wBX8lt`A*rc zeLdzDi<8A6aBO4uVoEh$F(SPb#vg4oiSK<$%di}U4t0EB} z0$gKb%|AuB4K9zQm!agLkN))KFz+O{Ma^4l%Fn+R88PJLUE92qjVBRKf>GJD~fz_m$zAO zTQ(P;%?pBiPDjv5*^0zjQ>kn4rdxOGsKap+E=gC8jKsz|bGs%MId>p3M>3WDtTL$7 z=RDwuExk0U=Zzn2#p7e=__{Je#aqvPjL)w0?&=->-5CA+T;UhHHE+#|#9|9PV*|%j zOUBQXU#Y!)%Ib@_Qv{hQn+N_kH7cZgCT&vlG+q@NTo@(Kd9V9Ebe64>7mp}Bj9}%b z%vr^*vz9%=(2fQJlB?ZeTGI0HPE}U-ChH0z=ibxki(ecdQ?$y~d|=dUY}foZ`8cT+ z+Cggf&(nPGe^OaZ4C%gPNGk-p-j4stJb98Wh{w@5(4gdU% zwBh9ECwqetYm$r0=_jRnIrmyUCp>5%S5qVQH1enku1?S%9eKnn(x@X3m{xt!oTr>UWt0jmI0e|i{RPfQgz7pK(Q+Bmni`eKbTVLHvGFUlL4V{-d0 zo-^P7(ly&0id8)K^@RD+MX~zIf{gn&9r8Hd=W~--ap|i~nW5?3F&yw4SYi~Dg{_5* z!^49uHl0hiMy$rv&0{K%oR+FLup5iOEn|9f59q9@NN7~Q6NyJc=dlzHxZNJG@#1$Y}dL}hjVr2UHf-FV>- zzfzhL%XAIMc$L%wHp`WXQ!3^$A^vHLSj@wrm9}1I^fLd(~#5n`Ly@c zo8yh5H}MQPv5QZ#+~1-2!x|qPe=R#4HIjh$zwa9p`BgQglFBQW5@a;6Pe8qLE#6U8 zBunZk4_Ug=d+=W&483KD9K0|;`}2&qnUQj{mk#V9HEW7KY8ngEIh7pHzp{2bEFpu( z^5tQKW!jRqN}q57x>}!2VEJRkZHRq5Pf55~9F>P#+7*@;7!O?3Q!flxjkB%Bc|{*t zff3Qm-8)}AM%~>^PUk64_ga)q4?|>%bkACb6?PsVmLJ@_<3zV0l&3Tf=F^Q; zBEDFY69UtUY)GTKwp;B}z=P>3djk#@H9r%p9pMVLkc&7wf^E70BUo2??L3=nY^QmJe+(BSstc6 z&0SLuAa=PFEGKf_3*}3VpIVxm8b$3P?p148?L&foL$LQuTJ^gl7owpd7qBcV3c>@{ z=h5)xM%OXsF69r(_~UMBP;>LNx=w?DwAc-Nkl!=r?pSy=_iV4|8bbFZlmjbGeZTwN z+!`NwttvbZbYGgIA2a|9Z;Yx)S2jdesic})`PkEGjq(v$)qKhhVU3`4I1hB+3cB-V;sBb&sAJ@0yG%OIoZl zg{PtTi3me+RjV=?wL!#)JB!*X;bfLYr4=m2g75_U>xycsVnz+~+8U4L|6U0xxjWkb zw6!gpwl?PvTiZ{wwe%F4tvw5U)XPmaVS0s0nGFtP#lNYt_Z^_THyN8Pu)}kD0b5s8 zC)hexb$7-m3o9zO=OWgXO+V46xU zP``q6YkSi#UU_V#9{e#p^=+)xCwzjubNkH|!phVUSAyW02ixI~RbqD4{RAuF@E3rdRh5^%~q!*`=1HsovD_s3oB86&a54 zS9MoXH$7BymR9rzt>h5Q)@0>zpOH!e;@2J0iO}vqky5V;4P>xh;^8aRv9#CQktggx z_<;5V?eoAr4a5#kA6e6vEJ^P$ChE=4dFC`)-1VM8Zx(>QEWBr`?KMW1CQO|8aZd)& zImLca&1OBw z9I(Mc7CbWOYNN``bw{tBy#c*!GI&|_9JA!Fdyr0RpVi*YlH3w@NF52Nc-h!^`HmdM zvR!|+WO<*zt7rt9AcZi_(bmKDv4~94(Z9)C>4=M)xiO!}w4GjrcYyTFK7eL%MP0 zS9W%NwaQ*!!|Hv)7z;cWJJ2UkNH09XQ!`&ZP((05wiaextMOj(f442IF9P6&zi$x< zowehPL{$vo2rws47J)foHs3TS``2jZWC;tifA|`4qE{|wqEjRtH{{kWT!>z_ECIfqn$$11xK3alvLt9_hhWX7gqLGYd<&!lM?D7w=5SN6uEdQROUl~ zg_HhH;N{`-8sMJR{-jL0=1dDb-gKAMqe;~bQ72847fwIgQ5(0SeBhFfS11k3flmk6 z3BnAKfl4ZV`HVuI?@&iYikuRM78JyvmM;Uqsw;4LedN?t%niY@3rjx@y;W_YYYFjs zg-j<9y2BjJ+DB;mLx%4Xo^@Fy>Ggh6SD|Qg`%UyI7i9yX1JA>W0*Ol4CyDMpKS-5v zWj5z(NwZmq^4k>};StrAg1H^5K6S3;$TS=7Caz`YEH8nUf_qQd3E$FBr^x-WJcsBd zd&Xivb-X^b*=Y!(?f2WKcd4!NJ`dyi{BG+Y=UBnb?_u_4GUERp{ya`o2U{heFKruO z{g+y>GfQ>tPNGHifL~y>=$}gBHaF2vZnL3ZTrz5x405Fb#<+%G@M4G@p4IJ@>#M$J z9up&|L%0&FHGNv)qeRr3qoak*1~==TU5@8=f4Zi5k+(~SNZd_rIJ0?BXBtox5|DJR z(T8)jpcYZ@WApb@Glhc>_6&Q8P$loDLCH4j?=9`Ph8!R^*G9_8m=In(kpP+^zLO5(e^Y1H6_2}%fdL>f{H+_8|w_L=}xX$ zHMT7B5mZ^S@(&vN==qE2TcN)+Tc#5ocW1#CD%J&p#@5ef==5c$NPM>J&6`LtADLbc zDwCX6B0l#`#Rtd&y)%CzFE(d%4`$q-OQyk?0@5R&P?Q&Yo>9FyeNa}t39)2vyXlSe z7U;`N8r|sv$T}zT_wLA*WYRVN8zQ&U-I@K|cQ?b2IeX`NS=VMTyL+;m^(H)_oJM~*~= zDFp!$SWi<_T<3)%ZB&?C>vPs-*kVBa8Qzq2uH5_nPGBUJiqsO*iO0V@Z|*%e*f+L_ zVgabSdv|0kF+L)x%tE=byk33}lTgx3OH!X=cn%t6^K;sDwX|hTMSrPhb3e(`g9S5y zOyyX!usUtmwO%LkOx{ z<=!LJ#oocGiY4vwdR6R&41vL@o*?O$dLERlF(Zjn!D|*508}KOWYeCiBTg*n1tDZl zTE{}dh-PK~c1g2iH}^oW(7d!6*p&*l1L*vU#+hk73PowabMsK_{#~t5r%AnsjddQ( z&!N00xHCh97rBJ4+0Ew)PKirF?VVC}7Bu3pL~55+TOPh-o6{g;ev3KT zB@s90M0cDMdB%h;?>>w8qq|McL z2st+{Kd`_4Tc*`3?2zhJs)`oG4i*B+b#z(hPucBKW3^B;f?n#~SA0+Az;_vl7GZgi z6JnE{-|ijm5hZi6nB1<`vjnT4r?F*SLDs=g=Es@Ywqpw4Q#;goDXPu(43R}dzC!vT zMnVDEx6dlnOc!z)`D5g7+#&YsQbNj&gi2|;XW7Rc-Bs7eWhHfeN7&??`^*_G1m~p8 zPg8XDM%K}$UpkAQzCM&mS@*o9-MpYwo=8ZtcrTJ_(k6PJ)TqeMv3z2GLc~W5%F@YS zqjE986lz{f94=7IC3l%HB;yr(dPmN4bJz#?qMq6rzp_JGe>J?$64@^wl4jAQf`HBY z#-OBgR2$Df)FBj{pPqZt$G@R=kVmgDSUX(;2^wItH?bqTgAw7eLuKACal{Ov*^$x? zqMY|hYFwrD*fsy;s3bXy;c`vr+S`s+^p*J{uTVWzI!vFzhZ2Y9?1Vv;G1J{9eo^ip zz`n03jPRGqcV^I;xAEghsCzZ7dhhkK(}SO)UVok2yTr$}=V}SxRQ?W;NnAQwtmk~{ zBJ;B4z-Vk{RT)*#M?-xfAZgTVYT&!8&ZzR`zbPNT)T05iCpjsR)g!p;iKL5VhPiyKe4HoztL3sBiCPO)Ag#{6$Uf6j*{Jm$gP7zZ-e!Hc>5G# zsDJzlEx%*Sa3IswSt82|>^&7bZ^r(7{@?e(NlaQ|G!4y`S@9J*BXS|6#ed3vbd)+y67=r&+%Ehn#W3FApe)YnT=3W_63COHt!%hW^06@!qC4uy!w> z<*i)6UAHtn5`5;JBx0J10`5{Vs<8BTqy9#PNZZ(}ERh>hx{D@_lMQpFgQinM?F|Tf z)uQX=y!-sP;=Ikw5vOD>i_SdfNUoFDW*1*M@6arJcFZ)^EB?pOeD{Z>pU%_g{WBzA z?Z-ZGotsm*#0%|Dd}14?)y!Qr!?&p}{qEgj5N3gm)72WIUw^UM&_x>DNTu(jQT2#- zFLX};%FBb@5Ff%MGx`V`n%|`+8Mr0he@c1Up*LQT%UnVJY^9^hg}xgRzAteN zz5SLt1PxK#4QPeB#sxrs$}P;5sRkskt0>|qmDjNzSiKWAc)9}nxtCddVuDs-pG>n| z66Y_ z`uxE{BUj7NOg^sCx-=gNapRl#iK^ogH6@h`dZ&2=4k4@ZzprOFOmd5A0Gev9HCxb! zjevU5Y+(9Pw~eg&Zd(5z1t{1%TzXOu^}hI5CN8%upIRgixR*AK^a{ESobuyd;9y!9}+a^;DI4?yC-3;_}k zoVx)(k#?!SX@+Bh-w-(upW8=1=$puDOo83A=c^eYx#X}-_uiCd3>FNUBrN}S+G#{* z*=4}IFdAat_QLZ=VR+6;!MKb%D@T7riiKfKK?Wyx|MG<$LedVqGfmTiE?`$Db`^-S z?K88w&)q^!bp|F=(#jweYb^2n;De1Z+#bAlgTeYKotE<>ujP?9H?lGWY?VY3ZXpak zA8OBtumt)sL_Ng>>jCrkh^%{U1T`<|r;7)HO=_1?m4CH`xG_NUVPRXuko?{-V&LBL z9R{umZ(}jfNZG%+L&^=V8IQFbVDGNDLEYOBo!s0vD5cjTovATZ0%5;-V8YM6+<|D&lbVs3|%v(d*|Q~_jYAbmj$mR zCedCTJ&Ek9j6PiWg*WAAWw&7r{34Gq6wGhMA2GvQ!t|^wfW6mK$JZhrFsEgg9jsOX zt#gz*ph*>IL5aP{v&|W%Jn7-qu`Nz2Lo|1Wp0orDoY%*#QZ=IVty6HUXNc z@x|8mFpV(60WBE(v3!GmIUug~RuoWqMs#C?(lm$Rdr^w^k1N#2qc?^4MN%n+-f5KQ z3Y#uOM=fw6teR*ROy1tf^>8RNx}x&dq(aRrF|HPmtR$&NRwq-Kw1(`Wx_8j;3y9kt zHQ8&^1oD|Gf}AJ68@^;Tyyo^X+oc9drYbz`^7qo{+R~>;(y8~nLV)A`^kWK>J2)VvJ2h^{DDep?#RHb>WBjh7c*=$-8IdrdxU zhm<#oYdz^?MW`|o%{slD;^3Zs$PkrrW0~Qj8Lx*~O?X|0y2wRZd+!gXc_r!nB zH_dccMiFCwb!h!}x1-E8Y4EirsC8 zG==E2YPpsdkG>FIKHP_ipM{}@h=Rews)%TPt_tFsRmVM+%3g_m&`Nsq^>=9x=U^C?gG~`380T3C_~`XN?E3s%gaEeHe>vKV zqCBpYNjNoPJku!Uq13oY-2^>f+GtpzJCvSA`HxE_ekaw9zo@ZlW}c{6)7v4^7jaM! z`AS5w!f#IJ1e^4^xPX<_)M)r!2g!V_+;{4*$qdJoCgQX*H}c5}aAYiP<7vPz8&zzJjyErhF1L3Ikn;Q$0cCTHLYT}PdTjhz zNE-z=w?-72`9_5F(x?re;EH(owA7 z-3g59jiRQx*sfc=C!pYBgHRGUXv?=^bABJl+%p4^P&lPV-6ydxyD?{n!3m2Uvmeka z1W^UQI@P!XEUm{o0?KWV$jYBPbp5^1*pz_Kcw_~t(Wo%uwU8$Frznf|crF8T9) zf(gq*=#fn@I^HjicPal{<3#Z|1 zQC2m?%GhPuuawF;<~g_X^Y}ui*`wbhY&+&ua*Zcl=foJ%{T@HXe3Vd-0 z1`TX4@CDiepBX)b+?-JeLdw&ef;T5YV6Nd=iI+)!M^*6Eq-@=$gi#`}Srv7&gAPgh zTdKG!?hh!v_B`vqIc?vEx}G=>M3HwaoDyaM zsRQ!jC1{yzi_@?6!*5htd;jo`=l-zCJ`LzR$Sw1d5UrVib5j2?+Rgqfi0huw$ek(r zft}i7j}Apl5=v-lRLcyG+VZL9r2m4yrZo7=UTTd0YDoHD@Ymd{-!9I9t5x21;v=q~ zTIF|thnDPE*}7-jI?6&^8HxC4qQ$jWM&p+;l&RYEPDUoRtUx7 zhAjmL%hN4)v8xK;u?quztOfK#Cn(!8f5_7YG4Bu>!+E}j2}z&(ejhN&Rlfnd^Tu?O z-AP)dG1CLYmwHU4HTuI+Ri%Mf!J*C~TETBlMyTqK!FBO`1$xogr>ZX=(P`Ze9XkBo zjHdFe?wd*t`O(!r`L8K}-^8+i->UbpnD;xD-ge1A^f(gn7du_deX6{hteSOpzu7MJ zOITeQC^$n}x)>7n$Z%Jxf!ov7?)s}bTUIqa(o%SwC-g2fy=^j0pOFJNjq^pg;(4*n zwS9^KsMPa-AaMncI`_QGm#UR<%(xjHa9DD@3w}FT6=C! z8{F>|L*&QSzrf$SP4M^bPw>}31AoV9;4g$pDDgFaZ}w}$=kjp}b;!Eh;!%1cqQSz! z940+_mu+*hin^+vXX)LCm#&i~Rngqd7ZNmsos+)Lp1(SoJOC@8k)Mrr5B~*(+}i&G z5JtQp`1}V5|Csp*uw{zNE_=crm;>#yr3wCkY-Rs?_VF~lW7_*ckzYQWmMpSa_KOMf+U>-=pX8X0n7z5hEyoQ^U5(B6Da6@U?xY6!g)=$rzq2i;vjQ$K(kq?&^hKkv!dFa3Ax;g_+WB}`iP19yt6ZTBfQWy5RO zMv?t6P~wl&7M$(o)Ey-GCbQB7O;bcZ-Gcp>`9e5NQ^THPdfS)p^>@(#+^|_m%XW<$ z-P-`U+f8Qm(9kOuh--VWa1f0FQIAWZmxY}*TC4G=Z{k+>1{_`=K!~o#9iVEgshgFU zZ57@pyXjGQu zHE7?L&Uq5xSuz$ANks>Rysuob4fv?s(-j|J#zRz2nGb!v-^N{ebaZJiNk-igHBh`C zxh105ETFWCqaophlZCK-h?ATQj6%{6)!P%#U%A4@!Fjrs^H!J0hudES9jgk8=Vcl% zu{(r_%Ni$=I}-R#i^wEjo7RAKDC&6G{zfPf7D|*U09?%yb`>$|y!58*u;E=3}?d zgQu+D($xa$?vxI<-3~3d#Pe9@wZH?9Lq^dB=+59drW49|)Av=kuG?8y9p68<_pywc zW#WJW_54~TSV#4%s>{iU!>HCr+dJ(`@{cs8XHLH9;BM29>hrJAgO~712Sxjp@oH`K z4`Z!3bFXM*kctvsogVaih&&xATYBwhh-2=)Br&&LXro|fkoeP8QfFW+S=JLx<{5YK zwKK7G_j+nGuf3kPsth7en>|O`zTNidwj}Ui5-hc$S)d*W%~$fv`j<)$Z~cNSezR3J z0vt5B_2q3Zfb32S0AF$I3n+{KFD(GuM1GEZ5}qbJKhKuf7G@x zA*AzL_rGd0p{EhJn|B1XY_ct{Gvp2#WwSo~NAaIQ9{uAKC< zIF#<|-8pGpcxHBa(RgaoKqW3=!b4FS<)ry*`(D+Hn?jaM$mpK*$TgGm3n;|^mJ%ey z^;J7!Lj2cG)bHP$|GDx>;b}eg^t-%K{X{>N;9F)(Z%+^E&HC-Wb0g-L2L365?cCP} zzi7O8NgkfjZB98D%ji(7g8HLS^h;p_w-|!|;jcDPfsB|o!4^zm5k;I)4P-i)PidY?+^iKzqH&6Z zJ!)OBv@r_c4$Fw?O0s>_bDF21Vh*aQ5*Dzib?&X21C%l8?D4EQX|w#DLY*Gh!cc>a zhGEB>+9wN}PUvQ7(tKNjtZov#Iz#?hF*sEA_|f*Xr;-1x_+^YXI5aS-KN>8E=O*l7 zwnPWN9WSr$J-WSwA28GWUMeKa#@DNUSRH#%C<2fXDoO`2(T*Q(mNbA+bAZR5VTBpgjyoU#zF;DobZgAWTX;SaUy_>t# zcH>*ES^HGMeJD35m4YC-;nVDHMxlkw^wolUdiEV!XF zFkdt{UROky^%@+7?2O|*A|zRV70w>9pnw z*qt>d)#)=GKHTzg-;+(|MQGXpHtb^Zj%qA!Da_% z_fCn=Z_oM%(2{MM7jIVgA2B<2t*h(Pjcz4HP1`*gY~BG&k>pFaXP2(@h>+fMmR0e! zTy%wUBLnLyAEso!d&SN5mN7yhr9|R2hfP9Jb29KRl<*$jQxble7iDcx{(Um>m&a!3 zoDK5FeWslqBrWQLr5*4Wdg?FZPXufCnjrLB*S6L}&3&K%C9^x|FkONeW1+2YA)#Ix zcr!N%+UK?X)q}DP{eouwBc`;eejWJ#09qi54MI{zE1JvG0qd_<`#>W9+I6b%`rLHI z%XJF*&~l;0a`?xU539s%$)=IR)W@qt6yn%AITbZDliau!B>M6J*dGuJ0o(O7-km~r zy_a_cI)`K@u;&0EGum%h%}kjHzUixxZSa_2m44crRx zdnNQ7A$u2rFd$p&39zWJqQt1ubAwr%V1c&DP7EgOQbv~i4udWF$u2U**&dp5H zX`vHtU@jUEF1-wh48X6tRD>BU`$%KGvTmA$vHNDGpgoEVur^z^ufL22%48w!h@%j* zHUktBL|^v|zE^}bUsqTx@X)fi8}EZ#5|>OsM9L>XhZ7vjn%O+Zm?hIuK1{YLkn6T( zJ-hp`cDupdZe~3H`_Hf2vJo%WneE|ie8?Mc+ttUb_!Qogtqegxwz*ho_jUBgtdh|O z0`5_c76+Gqtm2*rJl$Fu4RC;lOBjX5q!sgCw0#-Cx`SIw+W5K(>60@AAU*?a?wCN< z0w}q!AUw%I!1PwrB^f?05Rbrn4|=d=DnK)>j92B>9=u-y9M{?W^l5Jx$gh03l`)2# zi3PTEozu{0DaQ;Yyc!FKuJv8|RN`_}7W(KUcax3%X(rt}FiWO~cYPG#-~d=`b=B7Zdue0pGzE;$PT#OP7knwY|#?Pd0Ka4Nfa4#0@`7b~&^j{@D$IFwdWS%hA6`j?&lH&5?Jm(2E z{Vwzu-f(nVW@&uD2TjcEG1Zh?^8QIxMc}?O&fKM&0<32NbHp}_3fLXUE{(lW_yKIW z#*vs=)ej(xRw>b;REf;_hahO!3QmdyPj7Vsl26^*?{5&$y9;<%$Lm{;E2Kt4Lp0@} za~f(w=j*=8%h*;YkZJn{3BU2%wzG5Fnx+#@gLsRg!+`6r>~5~jHct8fPjtosdkPJ? zHGj8Em+A?ZCh&yq^xvu^v=g+Md|1;Jvb4^#Q=}D(y)we1%lub?eaO91_z_X5CsQDo zeq)D9cRjnzuFWoLkY1;d-*Nu%f~mJlF<`o*&%cj`>8z)7_lOCPp=|^Q{L`eP ztCUpWP0gE9WHXd2$f)QESTGtUXJ6o&s#wcV2V3#AymYS(e~Cc^BGW8n#;4nBODI|O zpnstGbYsF7!@2%b`|TING$q{fDEQu4tKSM2*S5UK_DjH~C0m>zoKx~3aX$9iA>gYl z1i}@bBwnSi6*yqlS4nk6fMvFg^Fmp*dK*(@MvOyoGhv3C;_7#yq0R%ySEPXCAKK?5 z(X4O4wGRQX^4~HJ@8P+3UIC+At&gRM3>Qcsjs%=UC9^5NU!q9)XX2#Y z!_Hm$v9-Sl2REyY%?7@%5zU)UpCEyt6u?(OR`q@0J+_d9FD(3dk#l;-VKI-M zanHH~e;1il2+}My)-vBbqVAvLQy#}lE)*N+RMeRMEs%^2fee(0z9PR`wAXcGR^Hq| zeISLq3Y3LmW_E9O!L(tsY*I<}RTGxS_Uv+3uHRW-NC_tNKXK9VJs;IT3)R8r@h-Oa zlWbn~1i&3aTNs5d9me(b({;_ZYx=kX+bg5;{#=F;{@i-MR zwgxx3C?+j=;8GRe^`7Xu;JmC^KC1=}T=oE5+FVn?d~m2wE=pZia zzf0pQSQ;C)6bYHh96(5oqS+t)JM_m}yR5hY5_VKP_75tFVPB2-rd%GdY>*{gupt=3 zzv&YVAvA^aCG>12=bh>;hT(z3->iv3p3#0JqnLCSNmf1eT5P`ujXyZ!X~lbJk61hT zN{zNJM?A)UT^jb;--PW%SX~*6Md5AtO2!G)4 zmA}e~=?6dt?xMqPRicl{E9ZhqRK`bhDYwKjWn-NrWevj!1+>s|d6}2NylFTtm@Qmc=eibuve4)7p4U(5vd&5ix+dzt zA@;sR>O$UPYs5$UgjPj}xyRQZE_K&**m!}`Ec?J>x!kCSq@)e9K>`x^GKng;; zw7=loR&oXuUk7yB6Z%M)0kUeht_xO&(^q7q9D9L@HIq)u-uD}>SyoK<9XYN)yqsKD z9AP_$XbW+At?L(X&@jAJ{ZVX=v%*(Mcqv1i?EYhWQJg9QyN-DWABa^y-L3YN`Pia` zsyg)2TvSogq`1jHx5CD^jQG(>VYr70@Ocdd_L`h9{c`10W;My;9R^EAK_I_tTj`!E z+hD7B>I#^I0)1y4AG#NL+T2iw_F2_}kho4@GYdYzy`tSc6XF5)ly(SiNs#HxhUrpQ zcmjn~f8$WOQFy>Eo$i{s?m*^aXE&C}qPgQ%dGCgdGWX#2J-Cn0`{If7jWDGfOlXyw ze=YNYIGF9UAEs~5eUSH1jH@YhZLi0x(X!8eMB+9=xd9esvek>ZHjTX(e-Vbog>!@b z;3o=>ffex_4=tn3Tr3>~VDdR#*HK5tm0n8(`EmUEI_Gt^-Ra6ZhrTw!9CB7w(X%#2 zkfBwdXLr%WurvC5+vB)?Z>7Tz0r4=Pao^rl%N1fLXS`NW(zFRX-naB@lx=HtsWiSk z2G@Q#ibXA4_Tl6s@J3+si6D5H=T?E(*njbCv76%C<4x#0GQplEsZZ%Ub_Ctzva0yE zVB)qRbb{E6t6LNf+WAftDxBbV-K8*|t_qOP(f=wf0@{Zs0;p!EZF&a!^5F0Er8nDw zkk^2wo1M-EAz_UEf2D5#=cd-Dlq1jRApJ|1{;O*$+rA>8{dnzTyUc5)!dGz4@?jEPU&DAOR`h3~H@<$^Zyu*J_|JaIw z!_(;o)Aw8M29{yF!P_Uc^wS@?UIfsGTCaTfKj=~{t&y4gq(2Cia5>gD`_(RWe}0o;S(ZIwQAj2U!q^8turq1&z4-HxIb#ft`KJw0oC^`d+68`J5vo>jy z@*=)d7P;i5i&IWc!;2mx1Dewao@Fn^Wh&N4vTw+L&dh$qUEz^$;^X27l6j5Cw0gcK zjqW@M6L|p?n~*em?Ut5U*?ndSM{{xezIkbLECD8WbT`XQXHeDtDs3+FBGKGk5!`SPhY^7U0^r3)6;gx&)P zkF(TXkY21FHa4>mbUMpL0_11c6`VIQxfvLt`b2nhz>>sPy4ti2BXr!wGHE#~yp5Xs z`o0O(N;Tos0}Wcz)hC=NjE#e>A%$pIqs<=srAhW9A4f}KEaiC{l}Dgpr#C4tGhPu@ zB+vrj_`W-zsDi2*C5xA-y_Pj56W)VE9Bx^%Vz|40*zWa6e|BTKYngNd>C9JA$1OfZ z2)G~R##!;Jsaa>#&Z80))2Jph_imHdbXGP-rTB*KXjS{w9 zrAc={bE2HZT_@xJk@n`{Q2+10_z;y;lu%?Vp(IOLGl&+7l(J>3DA~6Z#xluL5fh2f zNTKZ6mkcV5C8jVY`)=%GAB>s#J!gjA@9*b)&hMP-I`c=@Wv18ueBI0Aemw5`8VD7? zSIrfTCa6P>`)5DW4YgIuck`nfkWt;XM;>Mj`|RU}S7dx8OBWUyP~yX1wP8Y(Ka;sX4U%9kLmGU-F)0}s*qmx+&@nChSJ-VaawO2vSQ zoITz9hO-|b0dH(cXO1feGT{VNtE>Sz?^Gem#L|c}Wsni`s?gBMF!0;F%43LoAmv)Q zTY}7s_q5Qd^ddN%CdA#~Rlqob_}+Q_yxWx8RA>XlDHM#Hj<%ykr*?^6cFy`g{c2zM zhS;Vv8I*N^CTG)@Ba{^*XP3+*-FNm>bi~ZHDsTg)y~33B6E~v!zUga~L=gUvgfjH+ zG9%*ngn`e$D4!DvTAVBAfX>$BVs*(Sq7D5{VEKf@#<~P#tJ*>~Ori0VYj?Ab7k%_L zE77cOTNMcm?wJk1C}el&1`s#7ya#EOTUvD7+>+c8N(D|r1S!QAPcbnTdso;fCiwz} z#rNxrD}Fb#kQi=NUy9iW*aSn$`rcD}#9PW6Wnay(^%?K@X8XAbx8Up{doAfr&S*{C zt1JlHtg_l5C6e#xpg&u`S&vtkn!ad^z9Ydf>0Zrh#G%-2D-CpNTRx(;Il4m4_CgB) zht;YYtdCEZa*=&MKx}k}*ZE9=a-zOmzKLXhf*bFl6I`G0T+#4TyBDK4=8H0?L| zJk5Rohcmkb(0#;PgS$P?!>&K2+&X#WMZR^SnCh78z1j2X0if}K;Tv9;CWIII*Zs!f zqc^oF*Cddd3t(@c^hn3KVsz?zTk7v~ll+)DtG>VC{kV2ip8WA`w!(c%Vvztkt|Fkp zA6ZdPp2_a%Yf;@8@0C6(RiD*g!mbxR$PNI|b5be*2BxZpb(a-*CXx zp0&fFH+M?k(hTb+2k=jij8=~N)>1=CS5dS#r|k>~E9k&2mIRgrE4>I9>x{lDGQ9D0 zmG9+-awLF}+dnBvCaL+A37xA7_pK1{bkSe9JT9B3mQd|gagZ7D1*qZQUMH9IAuw)A zB}vT`t(z3AkU%7Z*crjVNw)2xTu*em_l3%m%ssPaZDuE#bMs7*PfMe>eLx{8?8_Uf zQUqDl`g8MI$`c;z-el-IDrY2A|0n7!18VCmOT<1QO`cxkT}&J%LDz5nCWG~t!6atdZjHJ>RA3=w$#OL?0S{!}%d+5;&XsSB{Qm$>G=G0yr-JsH8^&e9L}m8h zlKq_7;OVMLmOV4Lg7nMEM#S*Q%yZ!r$*mAn?_;6WlxI_|c!@b7rKA($hWS!+Z+ z`;FheHqrLkJl`Tv1t-Nl_I&0RSJZi6y>Q~OC!X3dMN&hDM^;h=T5}08ZZQtZc^3_H zxB>K@-mv*e$*3V?Hlt`hY7NpV7 zL=`WjY(&|wT?qR!QukG2o1CDXd_b$y&NDIw=NvCuC< zURP63lNtNR#d+4yT^ziU7AGD{us`>$7sRH22wd<&d-75F`+-Nu!*WR{pQna9Y^31b zn6yI`cqzgwCE_Xl-jnDNr>lhiL!)JA;Ig@WTC^-3o>5f9^_ki5q(FCX?&Eft zfGXx7+FSzCpgw4|+O^Ou1V2z^bXKewQ>Q3Mu*#@PBI$0w>ZN*-ML2iTnOfvOXzW-O z;%|&g@)$;O%taWAFOh;pwO-P%3IO|G`Q|MXNyNF|O!e3Nl3r$#C*#uQ0~#SC!n5=S z8SV_ss`B&NWuwfPEF%jLySLgSWJD^U+J5ZJ{hvTD3;6k~3xzMv>aX@$i@Q#mUF3wH z99b~C{W6^z3bdCwRv|vQ!!2!tePX=uGfqjK=1CU*szA{#zenuh*f{6 z|L$6p^}gYc4l~(0NuUt`_7iFw;z>XiaAmuH6FUoT-pcX_7EbYj7FW}d z@45%T?!92TltjDFe2%Qz-JX0cZP&y{%7H(D5!To``hX)=eg$+%9z?40HsE%r{w#9B zAI=msMWO?vcL?JJBVV=bhu5q)5c{!Dvw`;um!*1^2PvQ?M)hG#iw#Tb_i=N z`31MW><+rlLgOtQOaksj1frZCr_Xw^1M-}#Wn_1>uI*~yd~_{>#_@-71o1=G)}X=%dqwItD??@bH+q10>};_XSq?m57;Oau@=ai>iLNcS+f z+LsutHE4I&VSC>o@TiCIotD+a(6rW0q25~P>nZ$=wV-xnz|gSwEZsrX zoI)X@tJ8|^Ly2J*{kcOS7_Lg<5AWA*6GHQqD-t&qE}du*j2D{C#WA`m7ZWLn*~d3>D;2T=hW2SH-x6G%QGWd3TKpGC51qYA_oLrRVI^02n{YH z>g|77LId`I{KNnx?h5+_M!y`S&{x?qjuSKIo zuZGXF3fEfa4`1dQxaDZD;h!NG64b(_6{eRpK7^}C)*HQ5L~z(te3E|V7T zkf5SUwqL@onEWoX9R~uUv|oa4$FG&g#&%s!e;P|-s{6|TfuE+8hnNu}H|>odNS;!9 zRmJJVMI#4gHtpi1dTTDce?M1r3u2JnklgAJ6Oq1yw^4~8n{&`h;%Lj{`XXpjU@z0w ziu7qlXu%St3O@mTmh8VS3OsGe&bW2)t|q1(OxllS?`T>;B}}xFAT7zUo!g*Z%tc8TF=sw^re|r?;w; z_rXdt*sTaKnQR-wOv0e#>&h5;Chf3NK*Qei+9gI?3G)1`evrW*1nZ7+AFE}lvOp1? z8((8UvFk^NVWq;dcgwPMIk3SlPnM}8RZ;y70oQT#$QMMGZ$V_K?d^GKHvlci6=exY z(;TeAdHCnAESnBtIrrBD@dFMtlEx>xJ3DI5RR4>SjDl5SUI$hQ5Yb({$1efK)GpLi zv|h}*!nY0KhYG(X%n3tkgZNO%t?$>duelxThk$x?zqMYv-^=9lGC*g;)arLOzwO8t zM&x=3T;x!9Uj}qQ81g*jAc&$9Nwp(P62BjqA$T)pjgGirNf}})t(`o9+fi-#sRe}hlS{e_?W{u0C>d;iP(`9F1xr@?0Q-c*u+F2o%P zjL7TA&i`(ZE<+6VfIM{TZ0M%yA6xKW8HJO}&`Fv-I%p5y+Dzsg!x~kNGr|pKN$8~H z`G4&;5ZJQg+lAf!^;H`93XpRpOo|Z#&A=`H zb&CJ3xc^`7(0{@E=#o`e&A-e=_v6Hc{O$&4+mXeY)b9Pm|Aj>Ko)$TnHa&{HIw=5P z(Gzq#A5rAXMN6Kblo|ImF#-@moz89A*Q1~LFtC3zFL4K~Yh|}($aI)i+?UfyqboMV z%v!PMqB=^B23;?jBK0n zh-&fKA*{ck^8DAwXAql05VZC9E0De{pUhM*j1kTIz#j<&GjTBX5)|j`^rL)OS(R_n zW(kEAu!ZH`KvJ@wrtPDtlv|9Qe4iOrLQ=eb64!|?TggWPSFx#G-YM#V3NCBk=+Vv< zy_ZQ@(!P^wbI;cwfmaihAZcw$R)|Jw`afln4#%#B1F2h_AsgTooj{7`>!XO+kJ1%R z5#5(B14D_V^#-fNfEq~SYa{=lB#+Oa87Jd`9xR~)!-q6^97mp9tPD0hKJfW)L1{F4 zhyG6YVs7CH(UKxJPKF!flCY}tM4_*fjRsnv5Qk9&73{zqj}w+hb%%bukj#~l9NNl?`E zcSiZb$6?gpHx))VoJw@VS(M>lC5Sj|^ej;UVwXX7g933SEHlLE%SE6dEYU1?gzUGE z#8=*(1(Fu28%Yk_NT;S0-_=`M%XZU6-~IR=BUd5kXp%-v&ekHrFmm||(@qAS1cBV{ z0qb3~@cTy!d4b0?3o<`d#+)*5j_+o6P-mO&WtMytNCo5|8q4Ue-)B&vx|QeG!b?A6s%wzIr7W+o@Zw_~kLB9PJM0qzZC6u@J()f+u7?0_^xczeB2!A*eHBNEnE zSd#%wbm+KA*pYW9`*E)0^4_3=6}OTo2D#p4!#b=l1YgbN{+NF(7KmV`i|(!;DQ*r% zRQtX$v#K(?vHtjF_siP$3x^~iWvGD89sHn(jh1692${V!Gu%IvVZLp|-R!pfp#VT9 zq9ZYQGc&(uM${w}3FpS&u%bk{$h=#c@0S7}yZo$%$$|S%EKQhqe|@_5=EVE#>Xx*D z2Xc3~K5lI1k~*=>wet_}9RD=Gqp!1u!BEl>(-etx3IC`1U!>wyY_7+{raom1PY`6O zQjc|ec}z~5qDYTkK~lcsZk)Tv)h*S%_nxHo3apdx&SZFMnDhONYWA{(Y>w3Hoyuty zwb3+Y)At&mdT&)TMQ^eB?taN7%0v&@?XGS&kI5baR*t9F-aiT#5_6#IO-Y+O7XHC= z`p2y2ia%h<8A+oy%Y-t{T$4=uBR3$cJ^`yfPX>N6^WEQkX(4sinoy0Q-)Dqb1pTmQ zUbLiBv0wK)Z7Vrgt_>X!+;q}P*dQNx%||;xEnlBhGR<)sl(!AFB!3%$61TWY@_L_l zXsgC=|5p*D?KiDqlc;rO3IDM=>=`wo?BI&Hly60tskWl2u!duGRZU(S7bv3?5o&L2 zX;p)|?db+ZX`+wcivi?^sy~&H{`<)D`guYD(dbNE;KYkjk8eb}B(cUGoO4Hh_J(@u zMTMp`9H`n~VdgxeEoN0$zVACkrIW)Q_f#n-sC`r|%DY@svL2;?`gP-qVPt@h zz%I8Q@gkFheV+3i{&?tCVtsdX3_soqT=#|g2UP~)BVJypI@ zlE`574#`dk3O6&M4HQBkDSE!kZ;0EBKR9{1i@vR|7b}j^Ie<^7S6bt(Kzc}mKvQzY zxK9ewAm&>^OdInzCR(dsHBe62us9?R%akOhuf?6R?!M_!Um6@v<)mLwf(~gH!d+;m zmDRIJo3)SY9hr|=FvDd@=m^287rnkQhCd3d6$2WD2=2p_SuHg2xWy#}JIm6XFL~ny z%;&pN_)dx7BPeFX-r*MfkwDCa66zZ+IL#7hsEW036J+VFN7u6#LSXqRVu+pJb%j}h!xsoHWD!}w4 zIgS?-?fG-*r}hGKA@+Zq8c=?K7QCq^W3QN=@f_NDA{(<87u&sbd&+a^NA|?x!&w zGK0t!`)n1g+hmdjh36meaWVtLmX?+F%ByABXdm6w%bfa;EuQgjCiz-#uW=x}&g|Lw zWYZ^rUKAf`&r+w(h#-2>vgS44m-yJ)<*Ch|;q38XgCfT((ceK`x>+vZr-8!Q>N?1a z<(#UXQC2_s`d2&>5P+i6FmsuzQat%4oc;_i4T(eQ5E+0*lL3wBrW=U3!XjKnS$D{E zP#4v^n@c!zltbfZ*vTA=O;tI>ydeb!uh|bjv*(x7j@|gVb80TY5K@n1J_gKq%7L$I zV{Ecfu1CGe40qD#?VkJT1%6F9Nzv+F4E1rO)fN1%bn&xePse*?%U$OFVe+=k(yCkK ztCLNvymp-PpDSZDpP$)K+L+-rMJ)ykJ4tHf4;{7VMnE5#MzR?J8VsbyUo*9Ar zw6ku@$>z|U>V5~@j44khZ3k#T zqwR9jpeh!T(yVFOhyioq7`Fz_mO#I$9D118eDhQH%CcZ6=ivG^p!)0+0&Zv?i)T}; z06fPM6)VVo1O53p;6wIq1#*z;*FNx@+aii_(ud&f&!ulav#6>Db6B<@qB@#e-Ko(c zQn{{@1+kmSd&MFgU}JUz>d$TVzz4~p?f)ufeauv+^)zDMxq({(G#$@fC^V_RoA>Ot z`R215$Oons*e?O7hI7C42lee)vvE|hB=usaCozYK=)pLbU^RgP({h8O{nZuw&4b#9 z*)+zg8iazsq&}?;i);08w%mDcb6>hmw5a2#I;65ssM1n6?*&S2^Ox!~ThWHb)}n^D zZGL3~l#YKUHvs^M?vJg=<-q2)5G8PKJ2kHEH$&xNJ?31+)dK<$RIZ`xsxwH;60XmStHUY6-{K9BPQALbU zm3lqZ9~|DVJlb%)>c*lo8vQ_Od;*F0bQoBaFLbkcy%OyFyO7#5e~SLz`7YbBg3 z1nRJGSxj+QC{%>t>3M{_k$fpU>6J**$0nH1ODV|njRGK?e~f+V$Nr;t;8XRv}m^lp;zwGXIb^8 z^Ca7jau!I*L3n$_g$dqi5%>sJddb2Rq57i#_hupLvUmpf@`-m`_k*J1?_0swy@kU=Qr}|Rs z7T3+zuJ=VWob{$&tPHqp?6N@-rjpLYN^?{$R=ko&_AMDXsAb3jm_Y>JQ zj0l9t<@?JwXV0yb`x4rg<3i&3(Ral_<*TvuhjcU7)vW8O!>v-+y8Na3&b-Rd9WEF3bvQv4=R>w0(h5)XSq z>jH8UrY3mc;p?YA6nLoQbuI2*_KrB=X_q$|AhhItvZ`-zQHfs$a{ZR$l&xaQN-@S@ zd^xzsg+?uf?kqy~O_UjR!$!hE*K{nFi<({a?8c0y~7uusR?*=C9<~Gja z>d6zuKGvD|fx)Z(9V>}D*)&=k$UpiUsb|mX%L<#Is)(VGmrU9dJ@&|~xbeeq(d7ey zA)#m2%DlV1EUXkpiIZ-+H=18s%cj^2HI9lWzXn_j=MR|`R1nuYi2XRVGPQz$q}+P! zDMXqaLCWQzi(*>dIo)ZOgs>fo7L>=%4y)HlmxTI6_#7Ta;hdHqvT4+99BR-z|7q~W z6n6@@-Pc$YRy;s3llb`u-c8{q#DK5=WYDBJtll=gds)7pFrwNr)^bBhY{SESw+Ww* zA4`ah6RwYf>Nx%E7ge0vOHJ~%2NY+T+0m$>nTzB6`qmvYDrba6Am7SF&wb;NkCrA7 zMnipu*-mL{MxCLBQ_lRK>j01M!sV~h2^;n?m6=u{Dq{uDoN1Q@S7gQ{H-^!^^S$h`-i1mX z(t;>Tz31Q@cz|+XiqTv4y&BB9xfK6T7;R@qQUG<&a!+wUWRPEO@Q!^+j%5wrYLGSH z+3y8_gaS>rpjcD&DNdADlAk1|d)5%CqV7U_o&$=&%dSb~yrCOvM~Whg@XXyz#bTfD zBg_cB{JS5(IxEc|)MTo6REWm@Fk73SsJzpi6WXd^x<}HCFY-{v*c}+0PVTTvNLkdI z4KX)61<7j=CEVtMdv0gr|BlY~)(y$BD@Bp?10pmd*%At~b3eg25N`e#L*Dk}I9rUA zDL6$+vb7o>z|JK3ggZT5f!KT|9Z;zhVbUfo7xfb+z3VvPg#i&StRHppm;}TG5U3*5 zy%fu-xymqJSq6@>T>)GNR!|7EU2{Fuu%f5lU!_;<)&=^HUp)~2FcOU7_3|<+a56S) zhDJUYg(!D?bSQq#cSN+lMZRXlS*~+T_f{?zS~B_^OpX+c7I9F~8XoJzT*p{<`CqU~ z#|Mdxe_RXSEnjBgfoz=bv~A_RxL28S=?AND8EFQ93xzo%Al@TE`yeT69{>`5OutwZ+jZ6hyz`+K-DosvrGmX;Sx@*A%n=_qHHI)fLbvyJ9lnYvaR|{ zw=4_v0~0_55P22MQJklJTTO|l>9|MtqZxrp+`_jQ?^nSDa=XFIYQv!{&-ov<~r z2I+;v!Gdl6zz`nK-u-}0t8VR(j#xu>vlubjQrQnVbNA0U4b47#C?^VLNbD);`&f*z zmM`Ty0!g3C4&3Q|%1r%=NWY>8v6~AnaYG=w1Eb@E^o1PAN10i0wl6^UFDYk)7r9)x zxZioBHWouJ;f?<{IVMr{byPx;@sY3S$;;}g%3LI!pZKOTgn;8yJNt|R?5X|F0I z5w{s7)bYS;j9eU-djy#|2OhEr5AziFw*JC2N*D)>_{2^VVQz1*bmlO;i5A|{;$f~; z`YvdDi4X(@Hu2}#eSap)oII{$ZY>o=tzjL$Ni@f(I!N784~tJ)M0ptve=`~A@nUqe zy#(TI(3Ay`H<^!id#$rt|7_eQOPj02RDoXM|Hl!_>{cNx<5`r@VQnRsid)P|HYpPcru1|C#TdELUt zD*D6Hy-hene+*)3mtQVts?UkxLywZe@ZLPB>)Uxn@OCf-%~_A}xDa#FG$&k@mqPwE z%)OAccWom`Ez`sMC(3wGH8e)nsh@XSh4DaXXfNL2U5xOlhbOycJ=QHFBWv;g&oKGK zvmT#)LilnaiZV|al;uH&)^DU z^~lVH6Cntn%ea_evv*06h{YgP{y}1yx2_iY2)X3todNhT_gof|vhfgRR8U2#;_Whm z?_RbP!L#k#4-X2zQ1920Gi7QN*--Gu1@;YrslB$md(9F2FzXS|Y7bQ>ysJcSugIAs z+Si)|sak81OZL+in#^{vV%){CYC>z(Pa!OLx$}@Hy^xMzp-$O5Rn;xjyZy#(!zZYj ze?o|kZ|irT)sLLH=VOU0V@6EEg4^5fd@o(@+*zisb(l*oE!q0_Oo7LO5%Fql2v$C{bNZRtCNp@EHE!>8sgwe-rxiDRazn3~DF`|KZ!^Tr=1 zq4&Yf?N46DM1VHi$(6>K)x$nXTnziHp}0izIXX1^qyOOme5prEmE`F-CH>uuDDRT6 z`ZnxV`uCdaRaWkyNbv-vceupUsGK@dhtrV)3QF@pzXRoS>-`@9pq(m~LBZfbiQ-G0 z558hVtW|D)Io<9RteU9>Rfi^FL)@Im6-gVk6*Ej?ttzp-ejC#2n-tnzELpgt-fnZX ztV0nme|R)EzKpIi4)xvJUAcK*CO7YkY#cpfe7r^x(&0eNsK8J?LMa=dPe+am#U!yk zLwD|{P@B$2-=vTYWz}dEk7JnBMb(@}uD6)f=O&n9aU|Q9mBkTGVXtuPiI|lXOf7u>&JsFrY(>}k;+h~Wc z|1H9xdAr9SQQ~mM*rMDLj5Qnj4Lqwh{X{|wQ)+Vx` zYW(j&%`0@>Qed3^%+W^%4)Kp~UfZf(`HuRcGkfa)fZoC&0uThn{Rk`_6M#RDf5<_n z+Pij-Et46V=~6df3u)>6=6^ml>t;Azn}2tiE+D!c|Bu_CEh*Y(6XKx9ZF>c0NALuW zg{pqSC`pTDx#z?zoeT*UUpsIg^ago3)%A~N0?4b6nK^e4$jGY4O5<`;b(=>6l4fl@ zXH=ZYYA`VF1_c~R=h>qfgC=0k()1s)Wme^*nI9HrPl|g%Cv=l3I{y+S^hA-YHv2;OM+b5=A|$z^y|lt|&^=8= zq8~G&WMP;HTcgYwRl3YZ;tiyhZx*LrZdqJx^K);WDDyXB4o6JcS78{*mVI~-sKiYu z9jhCSNKX)U*~Rwyc-F=r!n?RezrCroZ>XrpLzU(w+}XbP>mRbuT2@KFx1%L;fVfCG z_ubXHbvdrm)0TKWtfBYPjj~AKBrqE8f_QV?MN z-|@c2$b}U0k9QkA70r4JpO~ue3{hYDcD{ZJ7es&d2BEewWC0Pv-B4wLs&cUyT)20` z^4%*7Qg=#DvzckSw;g*X@JwYGt;r5hvVazj}ydJ>F>OGf z__|sZxjZ8N8?+kF$$mKNn2>A9wW#${C(_yn=T{NgGB#^{tW=`)k(G+r*GKQ#?-hM+ z^drovG7zzzXCPKp4&h%D6i!H`9%SY;Z;jO}&cAcYO3*ixH=mogtje%Bhi@{DQ^)m{ zRcgLZK)(-iQq~lzQ$N2*PDnul@#xLwAFT>iDjzBuZ;K6PO5Nw(s}Z&MV7SJ96eLO@ zNpb33uMVF8JVA?AZ)&Zr~f7xklQum z;H?b&qgo;5w04He#q~Pqp9k8#$`eK{dVjmIYP;Cl2j)C%R=BPe!~%M7@6^a=TNpOd zq0GFlXnH^wnl%8}Sqz=w-#C+eHW{s`vd?PhMOihuu0@nb&9 z(1HU2i22DNxsVqVh&Yt8L%ZaQ4xskUIQ)3&cu#+37XpEo)!Gy>U38`wsd{6G@``1a zLX|i+kUxR+Jp04!=p}FWH4%1BzTzjlRK}cU*1#!?$cD4wl6N0s5zATsX2*tkpS zZIc5^a2ouusi}mx3easP^9c3~KPElvGSfAKHb+%kM2srKLOziH*9&=wmk~BIe#)b{ zp8tJ_On-Y#+vY)vSQvN7pdL))qOO`wB{h^cE}9VLK=3=4iGr630(QYs%=ApUVt4bG=p=VprN4a3D z00Qx+=6W5yDje}&_8FxG*jbGW2rJ^6lfGy%NKz=|8o+C0&kNDLMh-ov&x02ZD2$yL|$ zN)90D3wC=5U=}^1&+0c_K1B6|)?1h_e%xfy8E{YeKNoBa_e$UNZTU3eu z!nPzEu;N1Y7Nv8*;AV0lsekPLxa9TA^r`vl{!(t!`)Ez;8^j16PeLCX)@6A>hS-la z4n9%udBCpII&g$qQXao8hkR+aODpj8q%4~Ic)xloG2cB`GiM6mUbzu-&g%XsYm)nUA^~rslQbn zC=F4yEpy;tGnh=OwNVAW0miM6Hj{*V8+7%3aQXaGh$_G*B*_o9Le@CPz^>$FtLpCnASD1b6Po4yzoCZa)mTxOaKE~h>aJq zUj1Ag(Qu8aUM6|~h|*Q%9bi2#B*>ltc@%^LK_*8aw^vc8&)*`MN&62zYi;qCZ%daE zXZT`4z!b(hlw0_(m*JH6frDWyi}rj7x2prgXTTH+d3^fefZc2xnNphHUtZZREP}&& z{4dTXj-^>FD$;D%a(AT|U^PCD1hdP6I|Ob(3}|>28c|)HKleoP+A560+(kq$LN_uc;f=WMRfC9%mFZQYnI8-!&|+&Y`YyaGGn% zc~h-V$0pfvE@;L4;J~ctA)jvy8C@qXc!0nX^`wD+E#GduXdV%~+tcg<3WS+02jHqb zhW5@?Isf>$m8o)721vD#?&$u-E-bcnN}el_0aFc~uF}(Ez*M!7z{&Ze1YDhhi9n&* zFDNWnX;oShp);>0P}iY&y+`70MUevgy(pxl-|ThSLQo~;!oPcO)Zk(In0hN0YGeK* z7v(B=?}U7b6^`ZzN8TztHt1>ULrh+00+CV1Dw~#wO3<1FcGeYds@u z&91?KaCeu14#7P}*NvrN{Ke*dldj3%}xQj{q%IW2{ zTr_Z-pUP)=I3-6XT--C_>zb9(R+Cl$gvJ?J;7cFX;5!&WZgqA7ZBs-WJ=zaCWPdiD z6YV~_v`-~ObR zQPmQClcc3e1g>3)b^`fJm?PH#OD=}T=PdZIP7|anpWflKnwu`ANI}w37**Cc*!6{Ps~mUMPRp zo;>K~E%~`*;o}AYbw_hTH3rufFVt5b+e{url`AT8Sae3^;sGA&vR7l0zk-7oE-`$$ zIpN)(%O5T235wbE-v<;VZkVj)^(VE$O~QK5%Br0G#Fgm~&t9@!IMpGuou0B3BVoTD zMYhJr?04#&w``N3NtwY%iVlX2Jfz#b9UPAr3W}v`Wm#5fPHT13X=&Mfy8xUPh&H2# z_1ux)Ww8TymnoIh74w%k!;DMK(eFPiK+4WjBW*5(H0=;y z%^g|m-C&ZLwxO=IhJgu)_!q$W^i8(HVl-b}hU|Bb%rJ95KKuu{Spm-mx$DT)+1WSw zvbBOj3HM@Y;pSeQxNAFACyTuRM`Bg;xw{0QYIkkcQ*y>QU*fn8Fr#(}dW~kk8%tgs z5rx!~$)H<>$lSQajaSmIB|?vYhD0-hsZ81w7kPPg)Ekl#sh^?A2BY;=#SuvJ3yOO@<=8*NUNXKm|qh~UrYthSIt+p0u&KV_*Sa!{YT)fe# zX|v*Qe4-Gxvf@F_x05`#kq&JurSeAQT`69BM2Ph_JQ8ZdK&nK)>+l8|-(Y8sGw7&4 zrW!LGEtTqD8E~V($xyrW9K;~YDz?}?Ugy(u(r<=oPjYk{4bSK-J~1M=#hH9B^v_!$ zu3J3xq4dN@P%ycB?Tf+Vh2gfRR4;k;tm)4Rr}U?eFYUGS&y6m!G``J&2)hm>rMfG| z;;U`gJ;9X+T%m%ikqckq2Se&gM-JAp+;J;`J4tNV;@xUruUZ&7jU zt3H2>n2}@WJq0zmdw7dPP!7xs!od!V;N9}v+b#9>jg;=`Sk4uSZL^lIZHVZ;Z9!dU zpawF>=m|iek~tV2ZkOi2zU&qM8Go~+_xnBCH@$tED^Mwt9?B|P&~dT2 zO4gr3L@lAk(UUW!F+6}8MgO?&+2WbtS+@Elr`6mU!3nz^9RX}G{ZJjLp zxS8tjAzy;5#N3s%{*V&?!QTApW~K-C2lfD1HlV>t753mU;fg8F`}fXCKZQz5Ba2MYqc64kVxe$D5_qYV`6odb>wkMAW; zXM;Yrh1|lX*REw$UDGd#FQ}3nu0S7{%>z(O&UOI-!kr>p?QtlBs1lb3fRwW6uU}Ig zb@O&;yM=-N$L3mcrWCEBAfbXzFy}&h0MQZ6zANL-cU>^%ajGe;lpL+zvoo`7P7!lC z3e=BZybK}v<-xpk8f?n2QiEtm1<(*}zf#KbNMN9SoJ=&(6prB*9@Y7fods0tH$bKsG_U&^2Zq6< zJ6m9RloC&aCaDR*=RV=gZI|ba{C&%FJIhZj35EdSIZ>wOn%Qk{vGysTW4u|CW6yrL z)4*lc9;%BC_!Y? zIB1CfD^C5KwL{w>PNsbqrbs~@O~?j@9?F?b(b;ml702WW{c#?u4_j| z1dCT_XBZm;n0tG;LT)j&{s}u`6Br=2HgKf0xGw&klFR$ng0Wr4>f$y1ijvRQLmg+B z_PcXL|V6t|9Xig!O$nSSbjWRh-3_0c*$a zN%aqloH3FnCd|2&Gx^lR2ia>ceW~r0%p*->m4z`KexyfceX!MH6a9OV*?Wm|-$4W4 zUH`Ge8~<+LTNZDTjK830_v{+4N?}JcaZYi8 ze53MhX@EmUO#CNFMY&P#!>NnDfC7b&x0Rzej+FoM=uOxzH0mMPxk#!a@oFz1kEZY-Voz6`Yfac{A%*hr0TAbMOG9$K5(l0?EdEjDJUq5aKSGsh<^iI1;O$Gmnk*us8Xm#l|7$uSKmd$hAsbM!(GN7`4l7=ID#psYa>g*Fey0P zY;|>qMLUky(3Nq}J@U11t@hr&1%6b_yXOe@ugx<2{*O0g=wqdqos`*#} z))+|2jA|KHtO(C0@5sTKT4;jjD}LS;TWqF+2iT@V6RWU-spl_s2wTq!2LPY^C}mwb zY$sU4c?W0J>)ccll{TX)fIP}rk>P?+jU&4?a>&;PS~Z`tV;Wy-21J6OHl4sdsjCM% zN_L9bKSKzg#ere#`R}4m+o6nL#rMj|kQDD8&7kePNC(Mw9=RNWS$W3(E=B_I0d9SK zs`+9^)4k>?K*ab=W79n3&@h_3>hGJOpoB4c-X1DEu?t~9Kuj6XLbiOIlgrK7wE@pO zsV^Iu`(Kd78@tb9muW4)%4|3g2sd^xs_J^zGf(Vxe5Y4?q*Lft->#3eBnqh9765A# zs@B1OG;Pq6nKKd1zeY));xS<)w^!)ootXG0%y%B+RBOTfxsi6MFCibDl(k*4I*u`V zK8j9HZu+3^e=sw{tdXu=H0SYoAPMU~U)fq)F4=t^^rl01=v6vMR>X2yudkHsgjKzF zbKrI-kA7)*UT`&0Jn_2eoX0hlwB@bkn;rKb!5ib|K;Tp02U@*LDK!56u=3NaYQ{5% z@G(ROjP(w;+ma+!FK;m@*?c#50lB(~mm3l^Ep^j>qr6|$Vz=nL5c96hXjU(R5blUe z#nC!QnawT5N6crcT72DXCJL#prPbuXCFT>&4mFX(sps%1{}c#<+N9+%krNVWWrY&ApPb+s+~ab z6EOx}7XE*3a`izPcq7|hPlR-yaOM#-V8{Cqb~1UOO-B&=^7{I-1l zDBG?qH6n@5oV6hNkd#8$&MqDaE9m0M=xB;&G z^oIgw=bpU6^bC;Q*)&h>gKw-)UZAU)-Zn5QZiM%@P=O(T|9MF(NR;{*W^!t=e-~{8 zTEe(D#s@@*`Bpt=W$S$WQp71G7JQI=qHWfE`g&cAJ@B^+R>tU<1K0r(N6GY+c$FS>8sz+21C&#; zf!SY8r3Np3rO5@I`BTsQ(vdw=v--Nq{gtq+K5tFLIU~3z*W{^$UoB$E>|B3FkzAJx zrGz{Z;~p!leU-bqVubI@?DozF!Tl|9Qc-Vg-_)xAawePRBwJM|nvL{DPyic$#roCD zP%L(_Fe{~All^=W`=c!`fHB*N1x>&Oq@5X)G5i<%<{CTL0W6aZ)1|x}_4iGQ@wu0F zGHGLXteIP8U+-j=9EqDQ1vzx9^o*xBtM<4o>3bvG-gTcp@_YXY^vyM`C{XW8K_<`M zF=_4^Ge(>V;(d8eljq2pV4>4&dpNrsCSCuz_QG){cN0L@auVU3&*oINf*9aP!;hE| z8!JZgAY(oQ!hLZpkMrs_#4LV3-MMMR%#CwDybyc39lAT)TZ}O~IaQt=RPueIf_Cg| zmiH`;-6CjmnIt!}E#4zroq!)q9B!eMg$90#IrOlV{g z?dXu{@*(C^6?&tjN7mEqb~84MGUDeu<@ah^2jqI?o2 zV6P--+obo+Y>MrbB}`3w!O?=-prClvDc}6~?m0(ALoQVbi`~VOPeaAMolJCoTizI7 z+-Fn*s-m!|RN`j!QYHnMma;d0g^w&Kz9(CVXq_X7c<=7iXxyn9k`^_SaJ7%ee%hW7 zh7XdT+HU#3TLciZOj?0Wl6G&?| zE0zp!;(uewn|B-rQAW7q8&6PW?9u#}pJ4d&&D}e<{bh&S|Hlz#M`oRm^WT_ein5yj zeHii28OiW~(*91Rof=&)vn@CGQ+?(ajVHDF7d%0WULp_w*YC6}EVrQyR9MGWM=$Qy z`?Y1552n&@=KrcD`ft`;x%dA@g8@WIq_|Am-?u&?FEr;`wR!!#0Co6{xjbf^OD_)s zC;eJCi>9GCdo&@HP?YNU^Xj9o73J1!8bfb%%X04p_)fZ{#H@OE$2F{l03+_ z?q=Z)1noJRFiCUZ#%0U>f{%2){k>b0<)k z$je9$qL-k|tfY6As+9=(=N5JdKmFE(_GFT4O$nTm8d1;J#thj;5jLj)>u_YlEs*LR zvw!P3E(IzM#s3YUg>@jhVW5&@NPip19F6-nu#9ygZ=mu#>$Q+Rrn+}I3ig@ zF!p^W`tm2)mBp4Qn@eEanhyRrV^f(Cj)oU!T_Pn3H5(@spIBxe-%t&%GK@%HunI66 z;55+RA-)3)nin?}`G{<*9s)WJPOtV#V{IrC5wW$Rl)~6RGNY){-xv=`DOsHquxHQm z1y0MBqACy6%KK(HG|H3EwmC?%mebK+s*|&kd$+S`Xc91-O!b_D+qWP}S0}D1CWD$? z2s6(#ZehSl1DOesMDGC`i;o{9jQB)y^L|o7??Q9|R@CN&h=Ruy1hIMG)?v0wLq+jB zQpXE2G?jN&B^()wH{n5m(KrPM z%=*kyk;*bC{>@3qi;;{gH}v9FZ|p9KcnkWFfM=W`Ul~{j3;6LE2~ICnr@J37-uFn- z;X(W)We(?4Yg}$M)zq|C7~ZomHL9WUaay3QUu-`Y6TwuR=PT z4g_uj#d-s^?#ZrW#EpQUj@0#C5`UiYWf@;QfuY{J9~f%>Q)6X5=I?4~nE$))5b6xY z(V*iixlp-RxC1{#@t}wZ{Q95iGx!E>L3=D>V}KsC{tQ91aJ?Mm7wf;lHl~!f=|f~U zUDP(cIS0gtR`7rnWHv87S24kfv6;!q*)ak~e7JNhHi6JlA~yn(K5y_3y5zMqBPNrc zpGVdfT!N1f>xs{?568+yuql9)J+VLPS{>%YmxY9m`_xZG(=SZq4S!|>vu{^V`%gT8 zC6dITuhil+gw3$(t%>z5S(F$iOkOFkq=Y8Ke0 zOaDx6Whz*O;4}Tw4KlET3vw2`9@Ab(jJqgDj!dd(7`FcYtm1@%mx{T6c!c>a^e$iq zkLJ#x8(dlNIIM1|=p?)uQ{wi(@a600=LmvtK0Ld3*>kKqg6D(PyAb7DXDypNwNbiu zy&$hzw$}G9cx5L4=h{NlfVsE<;STrom#6mG{U4Xk|J|f0X)wEN6bF1c@Onl@E!uzA z3+n9w%W|^o-{QhT(|=YcpL&qfKLhp1=Kl#@-W?ixxnubVpGZKId*XR zQt_|ax`Vaqz&UGqPWH%K&Iy5S^4E*9)5yuYVW?3i9E$$o0Jv9(iY2LtBfwhAazZL0 zoQ6tD!}EkgzVMF@CeZ8kF;@_UKO)N;^TR@iJ3bkExDscqMi?Ibow~FA-scY>yKzmJ z>=%0K(d9CBS$)Y3lMtZnp&BLV`2UtTjDbx$4j27je-V7X=CN6+qQAxpkm-r>j%m+* zc=GvGp&%Efn3b?DMAPW_Jt=F16j6Tj5TTzEInGKvKrXdC=-bTtH0~E#>0Wb8n3GxP ziV?Q89Jh+w!~Kq+6EnqX*J5YZ+40}R;tU>p@ul>S*=lDA&Z?=*Ft#_V1a*Z1DBO+_ z|=rm@t@YKysc#+A0U;%Jg04j&~ZU>W)=rpjvicC>sZ=?#kR_=@}M5!{r1hh`V7`l05{|2u8746EIgfq z-GH>#?+>NUf3?2)OER=uuf>J+W|nDM|Myg^sSLVv;(^Ur)#UpN%N>7_QiCILH($bN z55zpOxYj|fl4e1 z8j@R|k1$RFEhR*%lGR$4O9;A&@%;mIXy27aoseYY;xSD6%&=7(?TVXRL`H}*M*(ix z9D+<*2KIzRTeNh*!0&l7gBGh&pN!ntvigY2z|QE2Fq?)l%H5Btd!A=^3YF5JNI8-u zdqh=r%tZ`xZgBh5!Skwo0`t)H)&@>26Vh=EUwVYf$sHLuOaFDICb!g<{Ltdc9&kX@ z=f6;+`UI`Is+MiZZ)^&XMC8eE$_WM>qDpwm0p}rR=(9oQK=0~)C6N^4ib;h`zq(bI z7uHSvH&05;)&7S$aiBRUUIDNfePyUV&3HI%1k_mXUE7>;Tx*FxiaVl2{$mXfV-F4p zV>$+dUc=0)7v*$^W6?Q!K#$frpiudV|KhvUh&s9|o`#bz51;?h9U}NT1CLEYsxn0C z63;|wFCqK4{{Bb)N}vzRou9AiM=Zn~998(}P#I)4qqRgGdZuyO<;nvU3TTTM3|toI z%7t4hbb5vX@G=YfO2Fjh;7&zj8)eNFhu5-%@E9*Z98)@abYTQgi$8I@+y6CakMP39 zjq<@Qzq2cjiH0ff)hY~grHpTiEhR_ojt73n+?nU`ImIjJ4-oA0x&R6>h>PzWKu)xk4_K#eQV<{!T3Z{k1^?OjP7 z=ms@obgpjvTtE4S{6>Mle^5i~*ARYTOrBjrIF1GdHU7S_lnlf}ynehFIPY$(Jky{{V?KQvA^YwIQ zQC9%w;^EzY5t&?D2!KV`HX$~{mxCc}+DLHv6*UZBfkE#<*!C;h_%9|)>Maj>DImHn zs%X+9UBuG89OR-^oDu01{#wmtYHB*+CO!%bD#9 zq@oTeuUmzAfdh~X(JU+_oL49dz-7kX9^apIpms^(FSYun9!zl#8MA$WTd;!=#KunA zw-u6Mb$PU=I(_O?gjNVIq^d7|Gk-3cPQp+JT~Rj-f_$f)vZ9MaHG-0xBSU~GB87wh&BtBrbh7d2+O0F#?BR! zSiE;S3f3fvzV{j+Z^JDw9HSc z>=nR?DvcEk}eYbYAE zS2mR&!GVLn{%z`!$5-w}O>Cnie~d$x&vMnQN3s2-w*Nz7vjKqJ$b<&0#q*cHr=}N; zXRq3xhF6C;5}c~u*n`)^_4cD#iO+vS>cDb|g81GZYiJ|`dJ+r5oz#9_`Cpq;_kY`* zjk6Dk8RD}g?LgkVDfGhlC*m8|e)W08p2%>lUn}~{GSTMzg=S~IeUuKaw6Kj@r2E_ZjRF~`&|-xAJsYcBLO38VJvV?v<2P3demA;bg(mR4O25J<%54a zMYzxTIL4`fCVqHX8O!4AA8Afup4nsa?w3*jYR`bk+hOF7;e{yzU0O`U9EhDqU0lo{ zH|wmc1q+7;^~cPqeOj#9rb`Cj?#tl@%WI7l#)dX<)4F+gHoUTKVanexyNnsA|MWPs zQhWq>u!k9f$`C-#A`&79BU3v5kVw$THJ++27I5*5Rhv%;BC(C0I z-3xJ_C$^s{%-^=z6X(JjJ*eG`Akc`PM`AiMlp&fx#kk@3R;ZH<1VLLWc1gxTJuWkt z$*FS)NYBjhfn}A}dTWljzn_52<=j~Z%NHSA1ll#?xzuaZVhkYf_Bi&3_6_%D5HWWS zg+G;-XcnQGsVakQ;(s=PWY?`$#*7jOM2RaI$cx=ZtlhpRc$^l?!dp14N&++b3je_$ z(&J9)i(oE8i~eKYk|YdqXV|Rj9*cM_)2oEFKX?8DC`JuAGfKM449@2ZueuFJ!DS58+9ZKPM+9XL zEY)b_QOs$-@kLu5$OFmep`hHh)lu2vGvnf@F6o=$-mK zQLY@3Tfg@<_4U_ovk@0s@p9w}y(ZpCrSu|bYIwR~SQ@*r;kwn4#e2v*2YkV4?Rwq{ zKZx2UhYzqUDYnQ9UvLSM&}zRYEA6k6ll4%A!wnf> z)Kk?C#ICJ6)teM}&Tf(Kg(^-TFI4n;Mq-DJk(@$$Nm8s`6NoF5TMuQLOU~YlBmWMD z9CYW?bpD+GM0|she2=?aN%P zh2LXcn|S360q6aW4CuVY9Pl{@dz|{kUq-u$t9;um?*xMZ{PV`1mwjj8pX3e3L*j>l zsK+BZ8FLa=xD@+vYPse+J#MIiYSho|8doVfaLqByA7b8NzoHY@JAXJVpep(=X1#VsudP||I5;7v z2&*h5SM|O1Hqx@b;vL#h{){b2#Ft=2PB>rbcDkENv! zg750e;j4970Osfww8aZy^j#M61VO(R?Cn1_xzJ=*&7-=sRXSKpGnc%GSIc*^3A8d4 zYknBDS3MEb-=?LtRh$1)DSa6TmW}}}F@7X2>BS2J!~_mMVV7PS|67_^a5EJz#@|ej zdWsAHvOK9PYWdf&vYOk;Vpo<8V@gT73vZa>>mtruLhEPN>q1NL}E zYGL!3#AhoFbVJzuQ>l+SO)Zwx;Yg)k8i9NaGufhTf?Wcw2Ff z-@}9)wa6DnBlocNeEAK(ur$u$jU3Wto#@ilXXJ9kst^VD)aqYT9x89Uv{EaYtu2JV zS{DR9lqP)d*7tEcA#ELc@C?5tO54|ua@X;(M1opaY+|XrxKPZGcuqMt9}DV_0YTO# z7e6d?Pn|MQg+sd{S*&|{Eb~}|f!6Gpnmby3d@2_K?>7XU3D0|>l{ZrO{|WOuW6eR8 zO_-trg!-lf(2En|H|^~F{|-j|NXdDfG2|`jaKk#eVY%sFb4R)H8$84ndsZ}2+r!{G zq-s+xfLs)V&an%x`U2$5yS#2l=*DkKxR)SYd$?pqS7GceXkr7LN9rYzvb46~`0O(v z{&KctVpT6Vf7y!DhsAFDo9i|O5>DevT)z!{AaA0e1;UFBt1Gyf;$PxWg_6OA%v5+l zRM3lpgatph(aLPlNK|d4+?X$$#zWQj<^_WHQ)U{krAx1(!oayb4io#sx5CIjVxQsc ztL4k2;8vO^X+ZYRr~8G_GjH9B}ioiR(?j zG_|iI&b<#~R0qo(+ldkUO7yw8gvk=&l#mGte}A&f;FSkM4%epRDX=VWDt$v~tPR8M zPS@`J@-}3y?`cAQX8~=Ta&#J^J__%U7>ulKN)=wfUuP&#IyJAf0Fua>xsSssUvwbDGMruUDTL zjPMxCbcHK=9*LYD-u#lu4zoC-xLP*(^W-9&u8a{<4}h z1%)tzZS0aJ!+hWIj*H9UJ$piC6CC7`KuecL0K{m9`B!5#M%saqaq+; z6Mbz*$#_Wo3%wQSEs}qdrMM;*7#qCnX4#r>Nn|H)^bN_O{O7zhxjgKH�G4^eBwZ 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

    ZDG z$uhgd|INT1t$l~s1Z7Mz(n766*$ke24i1G1=MO9!LDe(4j4d^2Tu$E5K62hl%GDy3 z8od<)y9!5QXivjCc$PHTO#10xY#C81=?g!Oh~;fFr4cw`#)G+C zpii z+X(x)M$RdjLfZtZ(IM_yGvVRqTn4dh4JAXwNH&lho1C5(hb4PvMIlL%-Nu{AbHo<&%e=<#*_t@c5gr7-Nv}>o0<(8t#(hcmH zxa5=;k@tku%h+akh54vhb&>)THDc|^Ss<$@7JJEwa*o{~HCceshf zC}nXCAc&RnfUSQPI>eXfaO^X0j9uIi`bCgwz4vaNw^duq*qw^@yegu2U%Glov(;SK zitQprE6VJzFBI8c>-j@_<)yGUn;yOh5oxW@YIM6>k&|9n?_+gcr~Y~yw?hPs>A=tr z#Y}*Q&56D3Ekimgs4A6O^ayKd(SjG!d%%GyJy?N*AC*xXqqbv9-{r)|Ki2(n73ApEHDvp0|{gKw)XA!P__N;8)%UEd<^cPB| zgP2NkxJHxc_`km67PvH6{j$FWT zWVkr?&nz%XIlwY6%7EH!FisGbC@l%B-+SoW7{=>J3#}W*aj*{>ovS+mh-YDSptC6Sf(? z5~zK%lDa5;b+o*^bwdk~dlMQdtz(r`2d-o=2X^8~MzBRi`JIG>gqgWQ-r%G=M^HS< zSqd`xv4-l@(p$zgqzU%&R2kGOuGRmbf=+s$~EC)h@vVr%jY(!OlzzbBMG(7WSM+Yh=@I#2t6ceKdRdz35 zj$;4*K%K|VGWQd@SPZSD@^!9Vp2{^vq)#+NJrQx%m z9h=-9b$V4zbp%GD~ulFyWj7o@;xkyuza!#k|j_ zM?wpqebKWW%tx(ys^s(bzDBqP`K*Q(*Q5NzzP5y@w#-I7k&$uz_AVjjhn4-Vh?Bud zlC%M2+tC4+YUC_(o-~!gpccjINTWR-AheG%3Ov5k^BY#A8_Q}4xU!bVZh+ZguhrCk%Mini$|!gU6yCsxM|bhgx0Fnadss zu>J->zr3{)P+pbHvl%Rpg!!kN1Zrhsv8a$L+d&g?^bsxOFqH9Lk2%nC5bHrMSfb6M4mH735jV1q&x1`=Rmz z$~ZR`Z{{ycm&isgE!oTln3>AM#ksb!oHDa{IBMSoj*&jz*hDNlW|VgyLB9CYyEooj zLH^1JJVuQgPT+(E8lR3^F%H~iQ{-fG1Ziqg@1!Pi0Xh^0a*p%7h`?QFNgVOF7VwTj z%oZ1}vNNr_CO@mW;GHSD%xU7C)1~bdIZ(#uGqGMAn2e7>!6p@ta*PfxYWG)lXT9;P zdi?@Kk1}{5j;*1TX6QQ`o4jX4R0w`$t+Y&g3TGOck4%|}bf~{lVYOm@sVy7GMojF+ zbD^=eWzz%UvG|DNr#Hbe+qhvxGybypopqsh5dgo9WTOc<AT&|2mING=GoU+}>P6(E{;sBVo#6wz|vf zZ*G{4_C`NWsC@r3^29;_Y&X)I$1!jI49FrmOaQaIM@#Wms!n8Ne2vihd|J0{V!`uh z%Wnn9$@$yRTEdvy1ulQ_a&_iYV0HLofbc(w##>;}m^rv3eP)Z4lvH(1nAGTeaN6K|wW+R^tsKRrPLcxuku$aBO ztt#%A#-G%4K}_M;{{Bu4wgv&xaH+uBWzAabF-#0Bwc|A5eEW|1qNyzW4Po>uIS-XH z)p|Q+bnut6NMQye+0%k%e1bl=&35j{(81Ge4V~<7aF7gNY%#}gmfL`bgP9NB+^))j z`88iNeO0KPnNM9AKtLC-`fP&PP^t`}!Z@ca9&RQGCsncrn{p5=oudH!3f5lJHYY#R z?YmxUja$`}oRF)-Qhx7ox%T6gKi;1$B0HLUZsKUAqWX|eGC6UsqUem}@tZZax0%=weKkoZ1UbF*c0Iy5w_!&J{Px0WsBr|>e;k; z?~t4O`%Xr$LJM|xOE3LzGGjF=4kQrA4#j;!dw?>)f-PvjPH5~PbkuxxwGgBLM)!~8ZHCVsDL+pkj!m?43!_`UlXuuS zvl&dy$5hb0wdlbKmTVWdqb=5yZq~O!OtIwNmFdv14?2*jkt=wLpVEgg5t$ttO+q?ZuvGh4AKmpa`+82l7R`bgbEo1#lDOJt02>b zs+lkIMPk7F-C5#`LQI5hZ(&45NvTMe`7 zcnMgW!J`SC(|0TBIMa!j@&98TOdx#;x`=IhekJ^i8l-XY@0WkIn%4l#T*eSEM?dc2 zRBFvb^^wcL8yy;4vOX&C85_qAo)=?zUjDBD<~_zOys^PKR&MT{JF0zS3sMpQ{9S?; zw;?ArKErtFJDao0!%V9~oKHOCq59ilkDYv!Q_2IsvVywE;UqGrpg7i!WsI3zF!5I$ z1`Ogvut3zr1MU8P!E|rd7$euw?ThJmYhLtcIWtLq{qE+5zYJZ!<4PZ!fqzAe-v>k1 z7h&C?bNqH)@cUZp>`?PX;Fhl+BUO{^bR!t6s|Qz!#N-)&3UZO8ObDkI)+1JxR%cf} z+EDW<(%PReaa>w#?r>dkEp=|9{mHeZ)$w!NzL&ez;^dqR3ApT1M(_Ch_TV3ZRRce# z4=5xw=UclB&SKxRq9PfHWz-pe;;2_PfEhcY8xq8u~oi!SJ{&d$^COwLwCOBLz8 zm4{q&M>H!}wjdcnKj`>a64%#23c}{Gj$M*=Rq2sBzjmV75*GACA&^=*?)uI@K9b$%&H^MdJ?|cH#ISep|1zSASxwoI%#3Po%802k}7A^fooMf7ITc;;ME>V zWVb^DoE@V*H4`Wq>3@P}%u8sxY;raLVpb^Z-M~7J?AAWb32Ptx%!}dENX7KisIk?C z*;DVrC9-rb`v3@8(KwsrxEI=)qRD1ZLBu6N8Rt=IYz-49{i20i5;O>ad>K%wWXD7( zZADMY^t!>>8WdVwipz5HlOmV~H^T+o{vKdoQ+Fl<*r`i|)8g6mk9xU&v#%7idp&0t z#Q9fO0XA{8RJ;%P6+ewJCL-ou$tfiUX0o^^1|J7H9HY zize(yVe;4+IX>41RN7@yhPL+pQd|>9q*8nFZ~F2U`qHdT43sfCKT!>^mb2CQXHT`y z&*y9QT--0@I!LR~rT@$4iqJGS!(E8xH$CW#N|4TY7S`AY&B!5pRIdXm?gqhYj@c*nSs?ZpF`?M za=lWbhB@I%m2xdvU#?j6T^Euf{_`aHs_!KYSAs22bNK71AE?}wEEFEz5q;~{DO?q0 z*=!SRFV01RvV8|aZw$2@l~DNUIetDzlU@?~zHNSDZeTn5ao3Uki84xg$`@^-2d&c! zlCyP-c%3BPrRX4U&&O!e+mxVS-aw-|<>ijoTb9#m5?&aR7L50XH^tU)17`-wAOT$# z!e(IVnOZ%3TBkjV1-5=O^UTfg2=Bx9!29W}KeS$)+;MJiT!nP=zMFi!CsAQl$xeF9 z6$Mjg-Ml)-+S{dTq#M0%Rl0E=KU^>T%hXd%UdWbuNlnT7BmE#zqJXn|MWdd$q|_&? zpA~S01C!fc{8NJ}fBD_U8Z-OoPJH+$8a4^EG#2k-LAK2sgro2?BUxQjqOlU z!szVwS6lNY%uIDkGnrbHNh#mz%2L?cw$$P#9?DjhYaG|B&nHhe56yZAF&n zwl2tI^xE3Qno1SE-@oZTB2^^#EcC)h6ZFvgFd*9sZbm>(8du!Jjy%IWu=%#Nx3Xm@ zNa8gTD}OoioT;jOt`5{ors|gP5Y(&;o<-<#0$xn)dgFXWA1|hVB&gV`6OeRA4~W_9 zU}kTYr~r^%j>kqNN#$&U3B>9p?>et=`aa2t9rMV&%NUxA@J+4V1S=yX03ITz3E)-T z_6PkD_1VV~zpeb4wLMUY3)%eUk(7KwHt*7k?+z0VnHLB?t04cZ?4B?y;nmrjhOb)R zJ?W>-D6P_eooJMc53K#5dj@NUk2r=>2&k>EQtZw;eV$YWj5M9rD-LVrKMWmJ(yWwA zOUxk)EL2dudEoENP>(%dnJyKWpHgUD$zs?@OxZf_Y02!FIYhlhW%^V2Zu| zjxz5-m;xnfoq=vtHWO~6LojG2yyY)I_(uHOdprja1cth{H3z5H@eebGIQgg4`o z^f>WYCju|-Xdw$NTTzHJRlrFwa_QUX6$SElkdhj!%piNLU||P7k7`|vxH-Uq**7fd zmzQeyZ5B)HZ9Dc+Tq2tn4s7CDBXAno?uhME(iI9kM@EIBXRMdR^uKaD=Oy}^LWh!D zt7);PJ*)a2dnCNae;;4G?Y%0^Eo_AMrd~R9&t~RhUUXrIQkgFKF#btMIp3zL9 zQm3_AV=28Z^x3^$vdpZSg+ZYyz)k)a_-S7Zry@Wt`+Z2bVau|bHa!bx_$%ZC@F=uj z=hzk)x%~U-Hig%R39qL3kmi3=cd%pd^_UNr!>BF1FU{(XDrKMjUaoQ@)bdH50CV4mVjeD?=ISziv-f6K}Y#Zv#dn6#E|*|)4CtK-o1-~y{{{uTNn;Oh`H zwG*(TgIVH#7{>pHI^KWs#x&RD@IQq&$HO4Q5E?8w1of^mh>xzm~S3&aX#sz!D zuv=#{aUS5xMqSGpzaLZW=(AE*vMwrv+93H)PJUnH6`#36#5Sf`+v+)17`(a z6>3?GK0jYSla}UPjq9cMS5<|HPV@ahQfK?ed>w0$xdhx5NfObu^3)Lg;zE?s| zz_4gcWoIUDTIte7Yd6b8v~^Tc$y|ZP4kZA{9Rf+8zUOz0cN=*`yJHo+Z*5xu?zbjs zBBu<{_PE6p6fv(*5;G};YyTTlQfI69ekcv3No9ILbMpX%%#y3_9OQy25B}OKpSC(11lDpC0`caW>wU(Em;+{_Xs9W zfF(=GLcz%uJd3r|+o-hMg`gAA_gA{8Mg2TR+h>_;k;2a>R5DHlsl2~OIYP*}+q1eT zclNR_a*bihDIbEfW4)!-WR#Y)eLs1DEUN~I=j@sB)T z6O>gZRa$RS$}j);i=mz;o{JBj_1*ENdR9I5Z2n+3KVq|lm>v9i&>5Gx|Kn2WeyzpC z2~;pqHmVt$BG>c4)h;PdcB0rU*nm%V3|DXWuR7cdJ8rgy$yGT_D}zuBDtGULD#;J3 ziug*J+)y5#032%xgvhaDGOqE<)deA@9$|iEiO1gCIXpOkb0&s`=-?bL{O!K4)eb0M z&3Z_>SS3wtD>`M2@%F_hhq9qx`&LaBLif03qzJ*sZ5U@mLDqP1%&JQ8g#T28 z-_1I&<0~z47|%D?DN6!v>f=jVWzfihKGO#sYLKDv?bJt*OrzPXW4?{UMWW=3x}dv@ zWJ=QL#LuZo3sTInZzQ==^?|S$);|B_FAla&G1pGK_#Vq3||zTtzTvUWE=3} zqy2Tgf1$I7xmC*qQ|LPA#cQ?(FaLVEv+p=CryTPc^R(HZ=2>-$d#b{O7o)2SQ6T`| zGo4_TJNDH52At(FtuvVuo%tZCz=@ z^^$quefrNhVa|IvFyrVe%8+4U2m4@AKSRE6^i7Tk%;o=;l4H0$T9C58;iE{dB!5Wfe@SR9+Y8+SA2&3aIme5Y>|8l zy8iySi|`f^kyEH>CZrNh2SoIF-rIl%|0Ud3MhBv!xh`oDRh`Q`aB(S=nil^ix%$N$ zg-ArD^6qwSF?7lSIx=k9d>2~A^)&9cnzl=S#7SV&kj)y4{XuL0KHI}E4NzlhbF)@`&=u}qs3waJq}r|=xP!Vy>8x$?0a8X>2|VO*BI_%BzSD&}6) zrv%|wSSP7|e+T5`W_a#^%Ol2WQ?%zB-|R_Wl6uye^9X8z%w6ykZ@$!yEzVX>6%Au@ zF{7M~izs7!mR{+T@?5F?W`*N}_SJ~YH>XY~_T5w-BP+wSnaG`>?M_Z(&}}bxk}K1ve#K;#P=ap&CPnA2-dED z4x(^x=iL4t9O3{}5>kslwJTZxt`=(4Z3{Y^cOQ8%rSmtl zvYBO`_$9@=X1FRufTPLn=40#VR=FpI2eqad&2oAC_oAw^Wvs$Y8;^EM53?7??w-is zyi)4jC7=Agd?}@w^m4%`#XQ&`Zv4gQEbg~gG1=4=##Gd(cx-U7e~Fk#N-@{{A0oSM zB8!j{+?=)8ZPo&-qmU<;>u2Lns^Mekn? zU)i@<*{}8fIq(DrcvW-tz+hWgt`&sLD*z4Ef#ULX>c6j?+K%qijmXT*#5hyQXA5V7 z@Qj!>%KgM4z;sVsmJSbZ1VY2+XC4QYU8!4A6Cyn)bk1+U zLwf)ZyV>eXppgZjxJg+1c_L|sX8r$3 z?A-f5HHkV8Fh$*Xn=U@p^(j)}0#I?=R<{<>-Ru1Gr>%5gPA&T3$t{8IlNt1LXKQ#)b~0||Nj1{@?w&HEwfXUnfbD30 zhwAK(dByJYK-EP3PBo?x4&Ro{bFb3H4KQ?Yt04x8CZt%2$X{E<&sP8UelrMUY&mo_A>0Ii@4uu4O-&azG?)4p-7`^mSStGK2 zQ9sr_s8SzS33^V=q0ASAY1K9nNu1MZWOrf-@zcuEH=4Hr%Up4eEWX}o0fec2 zC-qRe{pVkg1a3e7*mEEKyzF-Q+^gL9t|Pqgw)c7Odas8d#KEtZk6d3SXP43yxnNrt zPgpaGg=ZGS<3_@1vE{nHSL!E@<77vdY+^}3o|DFlO_p<&UJP?pb0OOal~oajQe%I~ z`<&sp(?&{5o*AoLlGK&0r+Iv3sI$29Ph_UbrGOhI!7;#|)$jR4P582F$U@Fg;l13c zP55soaYl{pT2s`wJ&O6`@9m5ycNLfLsHdH&WRv!eVl)RRvne-c4?kvl3B&3)_av?4B=}m{s)k^V*|5xBR^~(H~J&t1Iouac7#8 z=9#!q(%q9l8+e4QT*Fhj^}y^~{^CuxbA%3k5rIkAaKW5~i$UFGQXQSA0re7)qw|Q1 z-V?8OxSjdi^Z6wWTTic>rfQ#;N2v~4$E+4@KYmqjbF9yrn7Bk;?jbLkNoHmv;)rLj zI(Cz3JE&nE6HA``db6K@tfq4%Md#*}qh#DJuQKLYV%B?57Km>^S& zKW+NJab*5g(ZPuor2EGge`%^rj?-NW3x6*i^bGgQWO)`J9j2FW66AhHjl)MoHfKO=4;KiEh?s-CMo zq0X_}1p3}PKfB~V7xb#-6i(_w ziioiGZNOVUp(zX8!wSDX{euUvJg;m6a<2SHV{lQd+7~-$#5W>>Po|W0k4fPfOWrB< zwrQu_V$D&w%rt;d563cAwT}0hte54Vn#)?K5^$O|nMMeiAY(^v$ zk@QC0*2XT)rJi&n%-9VbCb-y``Mp%gC{6jBTcwok5HdPMcR-R;FpX4`nLLipqEdp zW`fZ7K;UGlCwx+`yvDUO*lfLVxJ%`y5Av-agH>WU?WmI;f=%Qm+x0#`#!F&fjuP~!rZ%qK}BlwE#!qmK? zTNQ6Gc5JxOf~hnTgaK z^rv5uKsy1YaqBRv&=~`$(1yDln5HCt5jKO`kx&O}wTR3F4}FN@yjwpz5pwEC(^gLT zTUndoMUU4 za@@zNPomkFGYeKzYoc*UTwW0*v>%>M>ZT?dV}+;o3a=^yU_5nlnJHVH+5|gD^bu|CVJ&{os&l*d%<13|({v3Q`-str8E zu;Imzda0^oC^LOM@M7^@L{hPfye&4%931SZAX~g|&IW)Jp1n362WE76eXNc?3?wf8 zq7fd-D=Oybzr9^WB+y5mUGqi?7E~=N5<`Qsn?!aM+^kEcUMk!+d=Vtbbka&PIO1`F zdBj7l8k~<|*~2&svq2g7E*L%<`Gm~?S*~aTDh9f<1Pf_4gI~+jjB*c7gGPUY@8jqf zF?4=V$sc1B0BM?(Sr*m}yeG0U861sTpQljR1?Of!XN&?PpzuF@x z_18eP{k2#~e0XMNy^kzEYv1Xf@#bXquYO52Ld|- zHCUD~c5I#cWy(**Qs2Q@V@4yHdD4ujz#V8LWKE(AMQo@}>%jS~w~Icn)f&J4A0IK- z0{Z0qmJJj{(gOzVXccuafmDs?9o9vP(t{9vxis8BJF!VdeGuP% z8s|F^!vE$x|3B5y zmC|9*av;Ty6R1y_O|X<>Yoahq8NTS?4j}ELOd2&P4M?w+-FTr>@|Mo%e9r@SGrr?p z=Q4|g5PFr;4{DNJ--@6+5y)1T!RfKzsxxD`{kq=l81lPV{{|CDbc$94D_Ej6K#l?A zt_jT;#Zkp#AP&}AuW#RL{eP=(GqW^w0muTGT0Re1@EhVi^$gwb_T+k+@3UxS=fOs= z=uDjbPomIZtaP)R!xt>!S&bozSC+I`oDw36vTUw>0qUBjG}1e+?e{R$(TkaU-}=f3ZDsraqSBwh5-1W|y!=%BRb+ zDD|BWn8ou)LGz^Q#=fO4=`#BvYeY1yayw4+ZJ+HMGit9pTDU8%)@U#T5vnSQ#gY3m zmI6FQw%*!io!}vk8+KPT`o$T3j$zp+)!T2R_+r%E})m}WlM4bMn*`CK|$dIJl zTm8Jd==hI^&LA}dT#)WoEVUBL-Nl$6A=d4X*x1&DM zbcP@*O(+}&s^Ma(yZ+9?=E6uX zpYlY?h<(d>&tgc^F7L_?abttP{z}S@Q#xT*RfTnDYBz-?--J&tpAtQW60M|vG(SDu zY_z0vK$Ae5A6J3y*?ikxuopK|t_UqZ^2SzZE{5`QV4Qh###_g+sG(+)iSq7>AJ)3?-l5YQ)T zQ(rN!;ZY@cE^5UOC|7P#3UF1+%TkO7-kkVHrm3WaA^5P9>N)3_$IU6ES=zkkEIkTX zS2dxq>M<9Lp>%oY_0=9f&abikx)JBL-eB6F8Z)0@GF_Ap=8su#3qlXF95#T8y|S#Z z42c!3qsrtH|Nk{TcTEGt;+hyV7aZ&pm09}^x*6o=x7Jn+^4ZC>wKYy=4-kRI;=xJK zYOM1VVS|)OU>KMa#`Djpg1TNNFwkWp)!%QT=!AjoHIP8(dft=-t!ENTNdN zo8>T*nZ!vTD)TAWpjDWU{@&dbL-tLT{!!Jl{-uxi!Ixasp#shwtmdur!2j!VQ^*~B zWt+5hi!_jg)|?1RY_AE^6`Bt&8t&Y?F*vccTtWZlDO0L~g4F*syi<3b=drE>8-V|x z@tg!YTjMoU3?-}yLXWI92@Smgujc~TuD`?8cM*heEwls=t7{P_1u*hL@c3rtzG9qW zp-c2`<_rK5FLUtIetQ2 z7xtXBhsMQfHp4w`aNZ38!fawW)x3JsW(Uf(KeIp|pUr$lYe z6lY6S`{@HEJ3O`Kwg_zPS>pdTT~_sBg9QW)uGf~)AJ(mS=+@}$x}Jvxx6$sZU>sfY zI!Y+X_4!$F2GhU3wLbm$pEm9ZD0NRISnfy_6&f6L0D|U{_1$#Jer?`Fxy9!r^@G}? zqF>A35@hud_dg1E=8CX#Mdqh~H~$YA{(lL;UBU4j=E-?JR|4Xun)Q?p^JHP+G&Re# zp=07Gu`JWsLMcl-ysWSuwX%048Wp~piMm4_sK0W?tHv?Ft1o_cXt@O3j)1f^4n!a{!(>li#PG?+vh z$`mAZ3BF~!Zpzm1ZRDpMpP{#Udnrg`$7uVIMN`RhX9s>89c`sb@oCJ}g_i2+vl+C` zi+wqYduouk6|I1r{d~h`7zoxg<@w;ey4gwzmQ&D|P%CQgT$13>hbKyim4&lQ{#7;i6ufpv&Dts)ChpTo*w?z*b&q zdD(s;uS@6JMA8mU7;fcNDQmIy$2Oe;cJu-ZPRkcU894QaM@E#D2lpC>R^6j^k4O76 z)#0WMkLd003g;^y5-5?pj@^~5+`qK@X@PkT?o$VqQVrUrys1^M(~vyNL&cZ@e)}*j z>B!des2T7h&?$p?_HUi=P{8Inwu+R13AM6;h>2||8TQz$eQb%h76&D=8 zyH!|VHp#Y|PAm$E`%=suyYma-fhaxf=ZBD=xEt=u{Gbe6$klKn$d*~DPoqyR?{3L+ z<7_zpC}$IlW3V=)XA{i)njREPgA;})olVJ9-ne)A%b#H?-(le+FjhjElQQ5G`VDQhSV@1o1nb9{K=jCDQ~QfPH3ng zycA$ci}a!EGey2!Fec|fH#?LyUf=0$w_2Ctl=Voqw-oWsmR@Vz(*M1kT;k=%=Hwb} z{FCdfzEEzv+=%7(?|f%_yY;%Hkyq-~v3e8U{j$58&B@4mKsG)zwLRjJJWOOBZFK}E zyR6$J86#n#p^nkJiu|cwX&eU^ZxkIo{~(RS)vEnw2lY4kvS}{6D|RF_&?u`;G2nhO zIJ6kKkXcH|wJfHoE+c6Ky;bbu#}|8BPuCAD)(}ENrdz5N3~}wWI9&9LS$B%Hd|B~C zn3$Iy^tPbYjM4+rs1G!b>^5X|0m<`W9vS>4$1>E&w%lr-js$NB>yE+lDv|p(F^h5g zhOJ2{5=fW3+BjFl=(27f!)K*3AL)Gn*)nD(=8V7gca`x6;V)%+^$3*Ok++M5N_tvq zRmPbMDU;L4p4Ora0o|Hz!T(=-R~pvTwXOrSa2ya@2b3w;TBub(E67YxwANOksEB|N zkV%9P<{=>oEv={w!J>i!2`Y*M5CKUTOd>@RB|sR25CREvgb*Tx5JINAvD$N=^W1xX z-XG@_{_s56?7h~vR@Pqc`@QeCTpchQpfcyrB&4>^ImWINX97jAlD@$L-}3>x`-Q;} z@Zq92k&@1Bs=3S1Ve37>0`~1NsO;SuV(OvI<1(nfZ?mbRZGTvgtO?}nqNq&H_8)yNsAzU->JEmZeAek4zV(IX2rV>j#n5Vwt zJJ(A|cdMTuV6eTNp8TPa{chKHp{K4CjVxbRoKd)(tgnd?yGUBBM)5~Pkjm%M`qco^ zinvIh0=%)a`FLlYP=4pJnvV|#EL&MlnFKlRvfM8;-1gv5$frx60(_y3Yoo5~1F0{0 zj&)Www=sSF17M>%(Y4gF(5;KVu6Q0HcXCd-v&-WL-d-SUY!>L5YOA`SQW`2#I9hI3hVAc7p5 z>&zcuuCF#z+$!%!wj?bqeqCR`OD0pv-vAzOJU%JkObU4hrZhp0reSCS_pXo&V2MlY z>O1og^Ck*7i7CtI(|(=QdPt&7Q>{Q%_GvlN!}q9OVQ%YPlRBwqW~V3M^a#ostU#2j z0hjsz>9%i)Z~pa2&}WNlJ_R(HZVB>X+o&gyj$s}o; z>O$IAc^_GDqF9In0J>5cDdqlHw7J)^=>Z2mSOGW%f0w@nmC1B?k^ZaRjnf2Bgn(Q* zTj!EAYhVIdI_pW$f}wEVG`$bj1i8Ogd=w_q5HdHNSj1geAhjI_^+XNP6u!oJ^oJ?$Tl zR?Lns{!XhpLT)y}L;Ju46l&QSa5*2uxSQoOQKgO>T#3Ati;;bdLan?Sct7#;^k?KZ zHfth?B=uXM1di|g(TH^fY@&Z8=%n4H9L>C|{KPkrsM!GH@pEe_p zFY*$+F&>9imERhk*Uc*#)iz~GeVzNLj!{3`6dIV$YrKl!M0;NXy4teFbs>*6s97Fz zH@WG)M+BfXjR;Q*ZdFx#RrAij0sDoY0iQOa&Zc>soFPJvA4a!~1b*e#ea6GWS;rDc zkql|>My$`R`mv-B40q%cX(@*_UQx)$vZ7ty?i3t?^UrJwNCfp{uwtLAX#370{bv8{ zq-xJpVpu8frNNk-Lu)m+bnmoZ+V55|4~(c8C>FC6iy^8S&O3=hPaJmvH7u_V;vGJy zEQ4vx!g#z<_=N(B6y!aZ0%MgiyOnpYMc1`TREM~VNp26z#C*BV6|rg>c%&TrTq2x9 z2%+mlMzbgU5{t*J$&XddQ-y8-9I%#gPUz;1k1?NIp5$-gX=nA!{w6KM-{%d%m+5CR z#Er)ROIG^^!s~Aso!NaHoR%=NtVwy&`v`b*qp!xvc*#+6Jh#LRJ;ibCpw%-$6Ybm? zJeob*N>Q?ejurvZe>v0 zGk+Cje8GH=OY#XFnTq(CZR}_BMmg-6(%#qa1G4l0R2H?6@Ji#dJbt5;4=8_jqpTwD z)-rQ-bJX^1iSnu1c0^^*k~>mkD+dfLYc)f@mLATadL0;}euY84E36`JG6(_1-6!;& zQZxl0DDof4x;DHDB>-|ebWLb%V}k|pRM}fYeylJHccDoC>O)n<4_8yYcIH(&Q=}b^MzwdVcY~jpF@G*ZHohih{XTpZ zD@>j8$$C;chRg3T$-ikC?;rXVIPM}gHV2lwmYPGob=cQv_evN09d@7y^DERNS53fa z=81VusT{kP+NOy#A&>#-0G|SO+BlPzVb5)!OS%2;icZ$i=T7o&=OB$QtDUw;Q-cac zaNPjxJ(rf3#o{ir!lR3~%BMb|EA@_uJk<_s`$>SSq#i<8QOyQwh9X3v`pdI#?R_e1 z<#$%%U|OcGN$JxaCL^SG=oC!PE&7FeieR3=t3H^uxI8}=pfd*XXhj8V zzfm-0CPsy2lt%Gl6m$Cs*p_;EUQ1FgyU!8l@jQeDL(_ z95aH8H%_bidZxFY$FLLw$YWv1nboR%D#ReHJ=s(Ge^}0Bgk_X0!F=X=F@T`hZf zPK?NTx-$T9s2 zHLkgG)o}naUCnx~qED26Rmg(=MMu@v_69h1{X6@gYKj-r3g0v41*=(tRsSfoXvMFv zn0;@4EdSYx+qD)@S!Z1?hsM_gsJBYxA#ozhj^uM6qzwl;Y!hfsN zvw9JrSpq;>7T{Xu!2!w4I!g`RSN!RMX-k#Kafr7PgdF36C}c5 zh2quLATIlx%^%5>9w1Br)j0ilF7qQp1pnWV@aojJ{3T#Z%m;iO81;Xk*by};*~;fi zr0(YQlFggGK4`4nvEbQT>UXLhHFb!j{mvtO`vm;LyNNtk*MMk9|4P5t=~sfCw(KJZ z6q^qko03F>yZ>as&{s=0|Y^@In`;B{#R^X+%K^K{WFr0_1}Z; z{uQEvvcLZP&xzK5_+{6E|L(h_zn8wh zm%jh!rOzD-`WDYb?$5fz|MVk5so&+M@x5`Kl#A9_jCI1mY*mgAVKbnOBX8*4K4y6Mawsyl6PL2lVKM z#w)c-3VDBVdQp4=Xl(R<{)uH^Gqm#5D*3AK6{u%Al>_8*@%Z=94MfRl!l+d??>_+@ z`wsQNgFNE+UOG_0>Y!rORW7>kPHls`GE^hT8g7k$q!Krddf%2v^M#mWG{deM$TF^i z6ty~m)AzbhjK*c#mWE>9Is3rb@3Xo=#jjc>|9OY^8n{2~u4OXzano-qo24P-(D$a2 zUt_<}D+qqBVIj{Dy+$y$8l>+jk`4REllDbyec}da;xM*U{Q9oX4W<>JOhvv)Nbc;mjI>uf;Y;?Mab^Uk zG{mG9n7~mSI)Z$0Yd(|DXAmeJQVEgfOK9Pz^6bF;&8+m?{n3zwnZyi=Mdn#vwZ#SU7rg|MYFd_7HKd3?0*Q2!4sS2#Su(e2Lg2VNlA3O9qr~Yk<;gHMC zjyRdjF`tl~0fgoaYqx{QapGMn*SVAKFlErSD|=Y4BxX29i~!18)_0@T`)F;bWtMbW z-vwQ{wWG`^+)-+X(!VCBHCKVkL^7}1!s0L-c8%*Lykm%ef^^(ljmi6ITr=<3{#&#E z*ey^>FOQEM(aMW8?6T3!yTc+i1GEmE0hA(9qw5N-#YYn7L&X%H6KHx{W4*XfHgUa5 zdgV!c6j-D`k6rgw?iyG3>TFAtGaq32b_Cn{i&Uj+*B#(=nr*gk{Y}9zp*H^9K~d5M z_95Pi1;Uu=>jL5D8sFE!ip@5MDOQBqrdgq653BykICM-s`aY;E+|`obF&xr!ybWvC zQ82t+B;VF^v0GM@swaJC@M)|g1&P?8kE|JES0$PY9!^AImNL)2PEzAoCwr~j` zG}^QliXZilybh5bF!1Z`;+joVXnfCPeF;?Dt{IBw6nUEWXo+i!<%@J(`#qrT@f{-q z4!v9d6MEXAAf6--fx~X+>#*bJV`C81z29Mhc)vYwL3)D0{G@FpWJy|CGj?es)qHW` zb7gIud@djmm{H=;S~U^o6fge z#-72_TXMZvO`o1gd@3_qMi~nHrJ~33UpgGYL9>?G9ms6eSr$&+ZJG4B>L{Qf9|%=n zQFNu!s>HYEQoH12d1f@{sUYp~-jy%lsL>0k-#{U(xM`o8a?6C=La9n#grBSX62q`zKBhOFgJUX93tm^HHvba{%%e1ME;lIb_Hc%3Q9 zZ_HgO)(4WJzyb;VNY9sa4h8cX)g{SmF@j!rK5o!eHSHXijgEjU-bidA?M-Q})!lM6 z=FafVMpC}R$iV&t;9QI00*&^mn-H(3r1+@CPfUX$8a3i%Ebz=LD$f^3Tr_&55uQMq z=F2Ubd5QbnlL-uzVCE0fiZcnmACv6XF%?fPQ6E0v4GFDD z#YX~_Ek$eMnV?KOzlj)b8Y=qU)`VM(#7C0J{nAQ1t zx=|x)PeEUa73FEk9zI9k**hz&dW}6TK##rON-!so=88R+{Edunb3Jr&x)F_1VWxXS zbbV@zz7xlfzc~EOBT9V7axin*FAzncU);xTl=P)>F$|^Z>0SQWXUWvq_PQPvlfWF( zSTHdf_QH1!aCI;{2CA0!(=ngtwy;FNo7X?>Jbzp^d49Jkn^aY0m|h{b9hgj;I7)R~ zcL&Z-1PYa-t==EgljQjP6Vya%SIwUD!mU*%2`+V(0r02)NMw?0;bx%iXK%LFIFyYC z^=|p;Ew|x?v1W9~yA**R7HVji!J!9sORvqq8jEjsv)q!HjEAu~haBRs(bGs{9 zToTwp(m47+*f_!#mZy?XsD<=kW^VApJ3D{4POd~Mr_m6m+qMtEqp;nS(0ypCk6pL9 z*^-Vo3cqA<)=Edaq(s>mNePcV4XT;b+p6c|9lpVJNqo%>VB7ob-Rp_<{#Mok$!|2n zcNU65W%Cu6)Onk7`RNI)4;=EQu3wa>l~=T_40zG=W3Ox?uMjuBJJk@Q{S4is3Oug! zhrNWC+lClV>KEP6%%cFceLX``a^t+xD_upzhwh#%UxTp}gJ;rNWgYA+k@* zlA1`I>Uw=C-;r|%Cm}#Pp;RwJyOdyC{{Rf3i z94@{sL|%trfD05b8FwDhjIfg1ITP^^bH<7ZSd}}|`owRS zSFeGB)#WQ{HKNE~qE@pQcT2d4l{VI6H4%7K;>TPz^AW$ml_VgmtMqyLvl`(f>y5dHQjrpmxsz|M62UW7#ZO_#IzsRO%Q&8 zP2`-ix0+nADEN_=dq**Rr{pZpnkXnQq2GZKO1KNs!Rb8(Fng#(-p6pS7(EWM|BK3rfh zpfNLQHMq92z&uNW;Sf0a^(&ILh)0+J7jJxezu-~81Bvw~$s%KQ?=-C?iYQ4$Dn&sDkeKMHWnNW=pNpzMP_w?l zB diff --git a/docs/img/example-locations-py-file.png b/docs/img/example-locations-py-file.png deleted file mode 100644 index 53c4bc1e29055166793b095eb85c4f21620d8cb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66617 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 t^R*> zc3*%(`4(%O2=IIUgu2!c_H|V6?hpK|O&Ah!8Bp>ArE(Fis#8%4KM>5Z^%NF>B!JH^ zXU+MuwjE&w$eRbmbZFSd=B)?tfi7KbBX4Rh+Iv=Nos|=jEDZS9dmHzmpaP_lGgB|L zR)D2A2)HfOEn%8#epr1rm!?fnDh`~S_g zhWsb;`G2lT)POt;6AqLZz)lNX|3Y}hP6MPHh3oJ6j{%3(I>$&RD<9-3h4K66Q5T(n z;M?}hgR6hjo+IrpY^byT{_HRJ$fn$bpuB_FqQ47JNRTa%H2Kf#-Y*i}>3r!2&i-kv zdYj2#NV^Fd$ei-`o_5$v@Ngo6|DE-pzPfsdrtcCPkC_a7uNQDBf_3{8No#y!&_n~E z5IIEU40ydTWWngJO%1pi_5)k@emN73P4hi3nia1!$cX^n1ONjITghn7 zo?^EjjuQhfoXrUPx%Kt-5YU`}&jcScnQ${VjAjBY>t2zULfKF83qlEC>YcwU+g3xc4Eke-{MW8?Y`9a3*k4AZQol&ug3w zn2N?Gty4jOvP(fIn~}&3>kbKEKiN5auab7kU;l$r&&+ISrVs>wo)5}!(*$>truKo~ z{)f-p=`%;cbDWvAa2Mfe5f3HLM!0B6uX%oi1>f>(wmhr>iG~Ue*LNa#6)jgg1(mn#qvKL zJBOy9T2ES{cjuA-3!W%Z=*e; zEH&lO@p&;#SaYXX=KcX*>eNU>%bTN{hikQ(TF3i%Kg0F!Q{#;YViOqL{lFO5JDqQf zn?v$0_eg61cknVAt}R`4xeQzyN2ctV(7&cR!Mc0Ul*2RYNv(dO6)?x0Z9*NzGjK0! zxpIU2ESx@|`BRx_|5jgWh8|?%ARAqpy@kuN*Pp0@CrsX)lKv)arX&(p#B*Y{ z+kaQKX2U1lPz9r>i&kzU)?;7phgdO6uc-|juJB?N>XjEVbB^Bb>h+i+M*$ld#Xbk^ z_R}vgr|z(@Y0-Zu2b*^J=AU?Qyy0c=Sz;ctUS^2@MpgeSwL&I&DwFQEa-`&t>FPydeqP#Wf8wxjZqUa3b@Wa{-7Va^7?-g8YtMc!?5!kW8 z!cFwzKJcy;tgIm*Zp3Fd1b8n(ZE**@JR!}eGx@aDh1618N&U=^)rv@`i8<)fl)lqU zj7G!aY%H{hivsp|L6@CnbgnNXm|0dtx5U4xUv_(!dK}hk?P>#tCs3iv~y7P$%4K^_||yFTq1*oF%R&*4>KMrtt2)T>ZWu6MnK0q z=|y-|TcN)>9jCM+S5zOecV4-=700U8LOa}EsN&g=1@B9~S05Dj`4J}EV% zJ~-%1LxArc@!lC7&wZ_W*#R488KL5FR8Q&XIgyjtwr_OzhJc$=>P??2%N8n@yJlgd zuf<*!wf~l@Bfy|UIT}k(^l0qbU2rw;vYBdKv)M8DG|!?`@XL|M{pd>M{VjzLIbu8Y zqjijZ5935s*pJjo@ke{#i7m=m8ir9(i6fV+T@S79TxjN9N>4^=I!jlv9Uh;~8u8Q? z-R$7y$5L1uPdvp&B3W_o4Gvs$jxc8Bxp_U(0uR?3v!x*QN3z(M@M%%vz6#B=`C1T^{D=Cjixu1*AmVhfc8YwCOu^Xs}<_E+Rzh> z90ICl>8#paFtyx)inDJ9(aV{;LONu=sh9*6gQAPzpSC4M2PjoEOF_sQ$E!B>?p@h$ za`C6!+3K@BJ#Q=C1ZBfh-&14NOe$A}bP^LX(0!;HJxCSsFk#P$o@Ko+-<&7KtRjAE zai??;^~XDSzxuw=Je#HIQNH2#0#Xtx=5pJ7s2bK>i6F+-P^PA;%;3 zmSjdnu+Jg7x!t4Z1?S`mB+FOLD@T_ajJAhPzar|;O#3}XF=I*(qqFYBIjF+))eS%UqSIM5 z7i?ef5}|5Q^iVsgbZ6v)Y(WZ_?|C~nU5y*B9Gsm;qdy{ogRJDIx}{_pl?r$J?0H)q z0@^YOB3V)nUkBZaUs~T%x!^FVYI2bfS=iEghLRrnX|guwY{Zyc?FfYKV7_C^Y ze{d1s@sz>PAex8Bc&)Qn@0P@LK-Uymg5fqw_M}Fsd>oe7=;4p*ZKB@v#6o)=63zua zvKWZL)FE23yH53@yJWQVK93tmeaVL-`T@lP9+g!#rP{nm&6DvFjlO${(h+aQENPz6 zza70Rdi?!`uOB-C9hRUta4`9`%zcnAFS@}vZ#l#Xy|u za~C7}Z5Q5(aJb`vH0x}rTouv|P3gY4SQ8*JqJC?)@bSNtyf?97?zxQb8f43*%1U~J z3YgtWBZi{x)_nT8b7)M)iYs73YnY0{qy1V8MU6t_w%q$E!q-`~4P@A^RI57SPscm0 z57DH=e;cCDi;P?~NM+6XEOG0g^5S2v8Vm{qe0~71dxJorPyz^~$!5*~lTN)$bXQ1h z;&K<*L~AwgIu3`!Wlz#Z*v%m${P7^4rO;ACE^i1}&M()cI_^4Qk^h0(IrE^Yft=H# zk@#%L##MhQ*mHU0NCiEq$?>6`l8!4iNUj<;23veUt=QqJ>O`!z{<>0HSX);Y()*h0 zNnoRL$=9C}2(WaR*L^RVS1)~DK@Eu==iLRuh;>Rca5Jw3jYI60O57VB!%WiW!S59% z+00Nw#XpD$!jtO08P1oWs?_;qzc%*kDi^IU^YdLUS`Wo*+Lp0;L?SVr)6AS?_5)7_ zEQeh!K;p?f+H@q(A;1CcOj4(}O`)o5Nn~BFv8&BeFm`HC5iv~sPSoK;1;KnZel!8t z`)@x2?-;?%%#1Z|3VS>|@|@DuntjzJ&72jOXcc)iUEF?I@y0LM%@-X=noV@NbFN{K ztF!pe7GdGAG3<{1P?r<+`y$;48H3Wi@a43Ci0aQ19{HRjBqJPlcwvQh%Yn&19JI+Sg?wUrNx8y(%~(V7T;-VfNm7ZU+84UHGvcW4PK``! zt-d9V$(~TLAeP`7?1u0QnmkAT&gqH7C2G-UF}g@3GCk`ypQ5cCROQmz)MTz^`J7f_ zx&3i#I~`7>eqpipRT-r}EkR7O4hgA&yLS;crce?}(G^M=8O%}inRKmN?R?Me zdNkuZ=^C(@U@$QgxU}Qgdn9nAy(#E<;lKTonZvm2`%B8xJ0m)p${$(|@ld~mG_{+* zy9m8(?f~E0gMH9GS~?o&I3xGpe)lZG;l^dLY47;!s~e#w$#Y3`wi}JAvPa7nyk01? zeMj=L%FTEQ$i6op)t>D(On1osG1ZSI>RYMkaKoS_NV$v?}oR+@N%M`m5?rjf=x3%J+=F?jWUnsJkr zT7f14Gk=fA+W~sx zSbiztJ! zc5+=Lqssh?(wjW=j<}h^&tS0h2A8C&*nSW-`Ihu4F1AUT{jV?%gB(_8zLIknNHp?c zI4M%cg911hi(e#~idAXUT9GOLjPlE^pY{tK6{ong!9N98em$z9;^HKYbzINT!e#|6 zJvh;}V}OP$MHd9=pc_5a?E9KgwnT;SOsK{PQ+N#u6_21?f3al}2Hc446!i0VjE{J_ zT#zZx!HJ$b5z`@rfEj=pG91Wk>4UU#&1=`C0vO~(YalzXt;Nk`*dXZPM=vrV!%_vK zk0vHXRpLPZwBw7-2qmMVG!={Y?{(vYHf!7Wr6VT`W{|Qi4d_u}=c9+gNxO7qKP<)| zxM_)P{hY+r3XPSg9ZZDj6LEwhm~lpAuNF@`W@$5;_#_9M1{H^++*w>Ql3Y75SFm%- zU@!xFRfa+5;&UmB0<9ZMsKh>{9VML0+KJ;8&2&tENdai+(||k0w3IE;Vi#B!|?->vGfxHq*Prn$K;~gW79C zW|BN4bGC1f!$Y#}Cu#;OgIVQp8qG3zF^VPq6R$zG3b?H5tQjL1jo+aF1ns!Q=&D=L@+xMH|jv#n=>KZm*^yv z6LlptITuT4k@T|G2<+82Fv?*x@f|(5z%yKnzOKlp@g!Fw-Z<6yV$EolDo-=K&jdfU zC=EA@{j^x);%p(7J#SbAvs|Y41D~$)lAi2Y(Pc1S+%C2*_?hp8=yUBYnSL-5iI*r- zfcfF}3zPBZgr+CqV9%2c^O`BULhx)167`)KZkc$kC@nFHn3rDF z?{HmS(B_x~gVSVsuhM!SAW#i^2ts)lW%=UfZnLQQ4bWe_2ze>IM$wtC{?cdMHfi)Jv!mW%xlYzte$-Z=N(+f; zC?;dntmhfq#b=To`pXt|IDZcNipA&B`RKmK;g|tfADn>R##3+k*t7e-bExo9nJj=y zcsRIsID`i zysP|LI`TNkF(3~Y==4Sh2L>%dwWY(MTDW{+<3UXOud!f0-PO&hKnt)*8g7+iEl{Y< z)h3ff&Gwi7AsgN_a$z1LQC<0WUvWe$`0W1xH)6AMId(hHxL+9vG8#Xx7_9Rhb7+tu z?qytQ_@iTkvXo*d7rs4;9uZvfR{SbKVxPeno(d-##R^t{Y=C*r*vnEVAxL7Hw^#5h zJfKyVtc(s%R>G1{)Qufbo3@NB$!#*9G-7smg0T)fw7%dbGwLSY&UGSA|Ke#EPH8iy z*v1_Qr+1pnkA7AzbF2@DLaBs0!)LwOl_dTKp&!$56m)`7_E?~|{TaF%ITb?w&gRRr zP2m$Z%$sK=PO-eIaa#DGdUp_PKQo#IS=02vdL32vatn0$NDH(% zUXuA5j=HzN5$kMm#n?5d|1h`0_3O{k6~jqU0Kq#Pjn{{9-dP)6t8fk5=WF-Y*RQ(= zPt-xWZ^)v((zS2Yc-E`;+F;(8Mehd>!E@$Yl5W?2Yvhnl94>l25^$%(hqhTF8=T?v zLvt^1q!QNLQed*}Bu4Ku6o`PVl{nm=5X|@pZX&kG%nZ}weG#XcZceWaEkeq19_1Z` zqJi}8i8ATk62&8&SMfZE*dO_5qDic{aNbxV=sAr)mizgu^fb~(@0jJmVd)XxJO45L zwCP&L(6;Wev1I!%9tXRdFFzt+9d89%3~?F$2H^7D{(ZUGBNySifJm7a1Oy7b+&7%b zcUV0mxt)2pVd4CdVx=a9l^M;~%GV9mIt}yiB~nJdZY3uaqbpC)xJ_b0BNY|~8c@Dc zb@Woh@$m?~NuOLX-N*MX$3(wVs42nS+ru zorbBkAq<2AkJF8<-Nw4a&S1Z8%dF%bvSS{Mk>@WxCAB*1CL;)}3loxAim^n-zhVKR zy-@~QXyrVtn2{!zAL6qP_{f?^xa5+fea*v!8A&hW>gwv(dL1mt%*ovkPhX>w#+1((I{VF!yGdwxB%+^~m%J-kQMCpS(2cKMC zHNYLJ6!fPfSA`3FmVU4(s>|$TGq|d}qzJGdlavw54z%KC{Zu59gW6FMa4>n7sL))r&ZK(uUotmWMeu% z@0L|Sh&ixq)fXrkRA2w%CeRbP=h4Hks^wTBx-P2-tiaTzxr}5^oZyHvyUjMn&ZosE zP!^%|Z%pk2$SjDJ`FyQh+nE!L7T;IYl1}x2)<$L;O7)(EFLmjkh?~9Dag%9Rh8x79 z3pyVy;cW#~5V;gh@6XPlllF_Zr?5tkt=8g>#*bJ&30deS9xZhr$DDc6vVJZ9NbdAs z&OB@#5}PH?9&%7J#>S*CF=fxHvCj^kvl8#yJ!pR9?=l>_BQo-JEXkTDqGzvG-EUZe z5U@PAU(0h})-9_%&!4^o>9r1Ke!JAcAN?7tJhOqSfXj=!p#8`JHPw5CJ>K#fKK23# ztisBKTi_Lx52s}o!cX$7xeTt)a@pEP96f;ZLl9HQhcOaUI7?HVBvB;Mzi~7h3|t7qZ1p!6PGm^y z#LXYIR!70qSw(kUZ7O8re@wgWv@-mi?LaiscKiB`AQFiZN->0#N0)Kii?DvvWk$}M zNa`acWha*NI7f79F&Rqb8Fyorodq=QJ3elM+mVvy4^!K}?_VzgFTd{*H=>LmEo|>V z5ij)t2bhANFX;PWEtSAzlCzjw2Yy0PcA3*AR}Z?lP4W(0t_;LNaU`>|zF9t! zS&VBxEZ6D}J)jm)!dfFiyU^AY4c(4=DC^oznlTNY#vJV5Mwr|hb|$S9+zHD6vSwc1 zB2)d)trdf{(i=oFUDj|FEbYIOBq}tJrMxBm?$Y1ciKae-9L~moDy559`Z{UC=l#6Z zb|F=wRH@6%W(mZ-TK=VV5##ovcvrdM`kJ*~8TH6m=P}XdLUD=j9v1MQ1sUQA!`T~G z%-wy)@KbIs9X`{FW6J`;*iOH_#u~l?Yk#c34r%%olyt`dH1sG#ywqbrb_QEt25*G8 zKeP2NzuYm;deYNPI7kL1)i02{<)=nMJtD1?+|l^z7GfQaPq}EvCi?ocD#-E_J@)G( zL!t$-fBRSPP36~TWdF+jCky=?>nH_s|HQoz(DffaH^T0Lw6@Ra313*5IOC3eH@*OB zyVK)TL0?cy%)FPiFP*)&S*ZWWUnLRL2FoBhDq2!;xpkZ!6KA<;hO3UImMgVoX}e$u zJk3rVl=;wl=s#QCOZy0PjieOPOD#7=sC-GG6hAQWyjrbfdmhUz{CRuh>ptLlygXF< zi*F&>xgusT*M`#!56vmtUd)L!7v#ZX5HEF}mGzkrEHh@lF9w!vhc&623|9(3x9J5s z1tq^u#IE94Q@2Zu3Rof)T9XNd;h^gS7YxZ78Au~pHldS=INyc3KzhuVWc0RFB5~*b zth5owFBuv>(5An7s|oEmdCVLO+XlbBm9tIF(82f>)=RZ#>LrPY>&E>`nH@V669KL2 zmj}sKBhM7jr45hxnD=BVH0q$!wbnVt;_j1X!w|F^5=9^3~0((ntuTQWF`& zSnfD^LDSMFEMVHbwsv73K1z`=csthzGeU3>ZjnJXrh{)S8jvQ~%CW)1l#Ec%uKC05 z@s`sAVUs9f!MCfhB^AyrBQspA{DIo}fA;80+~7Fz=Rs=5UEs+JAt3m$OmZLV7o)9h z760b&aPnWYNBzbgG*r7%@jcqAI#$+YapiQK7xU6KbEpthp(9YBe!iWnU+~=)oo6ul zUZH5^`{f5Nu2UsatxsFrXm$KgYbEx!94WvdyS616e*xr+Zx~{Y;)G=ifoH_={kLoD zTXdwou&=%k6q(f>(*7}EKEDjrKnu361kOX`u(1XzJ$RREH*DM4~c-+r;IqkUXB2* z^on#n@0U9BD33o>-gGeYhBi>7}c1YK5u#k<%j7F1?PW1UNUuhF&o z+tOTAw{uGUfqXc*d@miv^~=lryQg}CwtC7#^`gK$+nW~p0F@i+G(Ob>=$e6+)bZQ( zvq#9=#ZQZQbqaakJBwwO8Q$SbG!lEy?=wx*Z!$Nz0ZPf3UT)3pg_sc~3w>>Fq^Rxt z@>P#+l$CwCbx2S5dxAehok7}nlOR~G(wH5!!4=ceQB)a{t#~xgyRtm33h>B!IIZMo z;=5VcJ&H_?QUaeux)>GNEt>-GzhmlZLIEvgzcntRefwjW)sVC(pclg&gOSB%N-+$&a4-t2r(rji?ew;N=0+E7wa#owDC>rC6!M8g+}yO$zVpx-%d3e_T`XVL3Cm6K+^1+x_ZbGqE#K z?+%;1dgYXV%bvFS8BY0VCiyi=75Y#fI0H29q96J&8;23gxLY*i zP-v>tV+G4rXx$Ai{t2bjVO0a2UtsXAd;ge#3b@LKE;AO-Q4oPT`3_Ob@p_2xdH`!F zB3WcC4t&Ana9=4-VsgFj9Ty3g$Db=KzPR-yPsm7F`(}~>Z@k|1OAO5d%Df-DQ-6$H z=+eK?qkliXh}UEVE90IZ_{Be=*e9WcP7mW${nYUm=g-LP=&`!2?d}UsuBkcFrN)Y3 z*SGX;Z0S82`d&_|^{XMlCR@nq22vabcP{B%KR@{6$GiN30ue#M?9ELN>{Nq}r0@Dl zBZI82lXmG-zx$cQ%jT{l)N%5s&Qw>Ea{V+{7rJsa%5V4xrQAOZ7Ak_lAOX9pIx2U| zV22`RU|aR->KXR01j75fNMr0hvr-Yo2dvy2Oyl_4`GV-w8i}0ghg7P`!roVvsso*& zrH-y;!(8%8aiHte@|1C0RCIEuVE-2*15a`-AqX1&7z{b=n1lE*-E!8%V(rQyhv*YU zV!2*AGZ`_j>E-59K5XTsYBxlkACI!V-q{GK^`_eGUrKSG@K`ab${5h?OBs^(-8 z;W)@NDAIO5c_G30(PFV>o?Y`Lf%Xfxf86?zb;dNzjdb@Tru#Bqc9uc@PpJe$UL2%U zsoMfHL^Dm0R^yhl3T@QR3lqu<)6P@i+lg3irIEWYx+Koq<_;1nlvAlX$;A^dA>O3P zlSF`>h|7c7tyI<`bwE}(^q#vLY(6x5ztk@(>pGIl4|?zI>DWu{@#FnfYLfPL|g zk8rP)D)mH9(W910@WIeoxgYf2s?;CcPhAZby$-ZD(aLtAy0{d<$Oov8U{(&hTB#4Q z*GdcR5;M}dyxk4ZqJ*S7d8AGUBor~oWe3tOV$r-+_b_g$MgrfCq?X&KJ6rXhz1YC3 z_|lxZb@3&u#DZzM`SGf`@v3X9udGrwQ2xm$;_eYS7n-kC)(@qmpI?2aol@!>8>{-^ z=FOWI-|}40%&h?jnpVE3c+VxA;$-qu@O`m^l%8IBtQJ>Lf63ET+FL5>J)>dQLRl8O zZ>ulgq)f`5Y~h22330F^;F$&!wpl;|H05zNrbSE`$2N z^(sGq*?6Fmvuc+<;}Ad9Di7#(bTn+I8qVg15O}i*dSe2btBuV(5tl9q`4mZZCK0Be zQT^Hda1jYGrficD(J9l%>~q9BZN4r}IBaf>0kvIvHL?be2CXW@5wBOZpMbyjRvx!9 ztNgKF7^_=eYS>t6m~XS%eD}7+FVw1R;l3{$#hfT>+d(FpG52XFUxJDc71za{TXCOn zfFd7X>$a`VB+>GLR`Ic3vfOf0G2_vAP#d*O@m>UP151A&8n8}E6C`5>A)ran#0uzo zLLYBJ7jLGLqi28uQ~Yq$cb(M8w);$~x8HZ&&g^yoo^HPQK@9TaDUPy1Li@e%=B%1_ z6G<7=tRaT2k=n3qL$3wJ&x~5NAD`Y4%jw#Ab90!frRQg@d)|5NJ2#(Pb5heR5fm)i z-|!gqiw*ZQ2UzI@1NM`0A7&3!8J9T;At6+nb@YC9kk}#WxEC$uk1MlxZbpO7RGo?- z2i#w?9pw`aTXY=l87@x9*W*(!)=$-k*}32(LGL9fSFR&!uUM2F~OC&EX z2eWQ5Sx3~dbKYCG_1sVWo8Smb0i>*`T4$yoe7(qcSGohmPRXcBef~a(G?3^L%N&#in;9PjMv}3x6ptKh2e= zt-Sfdbgvm~zXZVECmeA?vkukVso+~^-c|dJ0SvOTpzH|b*C__aP@O4xU7H26VI-!b zx4W0lb}zmKSPGDe%~tn9l*;m172C5axz(wWdH0zj`QG=flvej`)uA66F%_lG8O`sv zoprSBcFa?~op<((@{sq<&uZD9mCBt;nLd>8>ipv9{Yh-zT5K%#ft|dN=k)0P!Mm!w zH$va*hrTCkhen(aWx;B!6JQol2;U_g?j$XMq|@}Ji|p|dc;{VW(CHrs7sna9e<3u zWdYsa0pI{l+9_z-sJj@{Ty;|!Gpp#SZf$M7Uk9`?6Qqof?;YfF+c_WdAe589Xrp@~ z&TaP9#L~v;TB)YoV5Pok$0!sp^onsC>^rnKbAnMUpve7Lwev-) zTTqeS5@ntZrGQiQ_}Xj_aMA8!J~IbH2dxleH$PD*#l^hW2U(qwEv1Nn&|Tph=D^DS zpr5FqWURv;0GtGXTBxDfYV!?10D9Y969n81llWob-e+xr(&d8Tw{_AZYbN-YI&DhV}o3P`uu9DU8?4chP`vNH`H^T-!1gPlxrVLM~xR#$8H zhRcmHWT(Y_`%1yEUfx!FL~NIC-aKY{({mD#440#fj{ShT?LXF>S#^&o&kx zJ}Uzl2)!yGmtjkHMLG41ILL#~IT?_kd5KwKbH4pGR?e(}g`IbRVRv5Mn4W-x>(1__ z-z?zq0j#l+L_Qt(hK?GaRs^38uY-j18_p%X>H=?+O*B`Vm$5KH=UGAtuc3ldvh&)< zRA+Cs-1dw8ny1go9BfFMl=8-JW|o&HZL2J#V{C;Ioum6I;=63FpyaFOMNj1Fm*4^a zGZwTe0*YKlJv!#Qp%O(z);TV~Jq-}a3ApiDf!~$t>gY$OP><0PaS_(`NA$f1IS5d$ zD}NlMy))DRX^eRVC~2~eMG8g4Cd)L(D;rgXS)TKUl#G~_*mzRvutkdDx3IjtaAVwR zyPtc*6MK!U4Qi>Gy)I4fweOepf(b_C9rq)B*~1dVP#StSY-VpW7C+Z3<|k0$`Z&sG zjc?e<8Dur?>+0I;Q%A)`KF!5c6gAh7s=b$b^L#=K)P5+T7EAuvU?Tlul;w8-`K4me z0Hl3u@j`#!t@n|)0m@MIhG>c$fwgnWjW=0Gx0Z_&NuSmh-#KxRMMr<&3TF!413>5d zKkD9Lqe-`aJBrjbEHZskWD47ZT3&O0FkfxmWAWmn{_QHA?~#HR?fbNEPea%#5fn;S z!oKi`|0BI5dD zl-^_;?T_{yi(nvUQYh%W8$EU21VzDv9_wSa@mdOdzP-QS?2;uvCh%- zn@k~NlY<+{o7EK;Lv-2rdk1xvK8C(L5ofpJ;fZeP1GPPj11*HIxg9$bTJybP_#V!~ zr$?q`YxW6IYyHv%Su*6Bpo0wD3f3LclYAD#XQ4G;R=0D2NBK>=J5xp23VJ(J&s_?t zK~i{FHbE(@j)DXzIyYVzMW&zgeLMt64|bAtyLuQWG96UT6!KSkhP;lY=VP~`ZlR5C z(Zz(j?e{ZgiqvMn9zUJ>8c!+Z)z93Q*jw_AKbF1{q6W%3%wjd zd+o~3guCpnG_-4#<@@>D7gcSar0STdYp}9U%+E7L@n{3`64iD-s*MQ}4NB=#bCMD% z-yEs;g z7${&G`XJ7al*v%r7&gwLh!8&y0CY09d=v#`oQU&orOlbkQon{@lBYyaE7KMS4-qf$ zdgOHy8dY@JVnkMY`(2Rdda~f!JW%NbE65y*Z8@-o`z}PVy0;YBLmP|1FVnciOXVY2 zUKzrIa0{vDB;x4!H0sTFHQmZWZTL<w!vD19FUnm+!}6AN4Z9%5sv9Q3@B&co)B%cG ztXt1|nnB0wdc<{gZzD>Z-YeKj_pHiiO>muwt9J~I1C#=YAEET?o_F$zL3^sl(qlm0 zTgGd4#A0|lVpg&4!6xp9ixz9q59OiI(iIp_<^TgHSS?x%tt*a%+%GZ`^?DpGP3y>p zuN*Nw2)w;2PEHuH0G&q?ETyl{`k6qpg5gwL_D{N_jK0y6Py}q#ECKwlVDB9-AVl$z z)4aM9%i#jLLaed=vPNdT5>#BHHJgte@Ha(@QH5G?)9AIVBCWkoBlmzh2p3b3w(cFX zA@9$HYG0|Pw7)yr_6XYcc;rP57mGQm*zmF9u7Zm6O8^w*0<2WgR0}j<4$_X1@nEML zns1rktUnw23LpStw2M9hbUyM8PzQAFeZ=34SY+gx?^cyaf~0SKIG<&-T}6;A?KfIE z<5|T3?ZusiL~6N!@nEroOw?}M{w)Ttt2@(8(ONW2OiU`d!ayukhQqUksM!~%I)-s# zFu@;1Qp%l#%=E4;llNGbB3?ZVQ24$B@Ulio7#{0##u^>e$Ctr>`vb4jO}!DDJNC?7 ztQkK?YgiYPb=8aXlXY*4RdZ;6oG-SUR*X^BmN3x1ZU&I-OUG%kGsq1vQJWY09|TU)E?D;^4VN)N8eu_;o#lqD`bp+0Xe7 z^j=KRfkALy9Rkk}--~uHB*367!xk6M2IM>)kToa?SgPu2+(6l26GW^nx9u3MPO)p$ zUh*P70|HJBt0BbD{GJ@jbK=xv{wKib<3JIQ6f$dJQiU40^MdVYQaK52LmxZF40d$K=TwAO1` z1CE2(JG!`(_L_HH?nzZ*(=o$pl*+;L+?hVBtQ|N!=?8F$WtUgl@TsWd8{!U*_=O*&G(uvc<&Sq^0)1!7X1-o-DOB= za^YN)jiCYpm{!v!AMEm z$R_6a*8q8CG%J=kyS}Kq49YCH=hf?eUFN#A>tJD=IE|d=Ho8-v3C#1MC#q z`w2ET;!P1=K(QMXij#om2Q3}@?u>TY zPkM7zqVo8n@=7iNr=HY^MVMFq_NqQl<7iz2D1g7^5&p68r{pU-BFkoIH$%nOBU>y9 z*CZ#1(t{B6s<$zaz<96p_`RAkULmK~c)G&ay>QUs)`#1U9Wm_fOnZ@UmV-!^PlMw~ zJ-<_B_&1yphoa!S8m4yGZOSw2&Oy0bEx>T zY~QH<;?+OZDj{0mw*?(A*QPXX3JFGc_&P}WCbnBke(e6<#8L5K?^E$A`i#wuZFw}$ z{bJ3sQ72eA-mJS#4^W5iXRH?##fRZ7(-kqNZYQV|FADSatvhUh6@Nav@!|H~5QC+6 z1npcSeHHtYp&RNvDyb1kA}P0g9%S5dyx9h7lghHRHJBdeuzDcI|q-?@B(rKgMe+Xg7NX(;P|f0Rt87N_0GPCP3Y*f;5;nl;}Kw!b66EZ%Y5BkZ53wy z9Dlo^AfA1TGt68zD~xCQ^vi_%oNInIE4=Pyr8x1-zGT|uPn&yzzYB6)xHtTRp8xSr zKpX-j=>UJDLv;_CrBb=gyt0eEVSs zD5#!2^#UlJblt}*{N4j?n-%Fqudq{4yGWV7)+mezXf_Npe8y);a%B3~Ch&vXtkH9) z>T7BqQ@{GtV@zsr4rkN0t3`}BoJ9<$tXNLr^DyIGzA5M0o5sn;^C65OT))RP_k7F? zWYyx~>PjbOxXl3h)2{q~w_AE3DvBhCP;^qOC|*A9tL1;UX_pigRRC38%*}Z#k*%R% zQO@A=aN5pMvf7f9628=Ti(F|R*RL|itubmPfSd#sj3W zOsmxvifY9sm1r)tl;mil@N1KFLNw4ezsq5eJ z`B=)Q8%)PYs&xy_X*$J)I7&&{Q6wHdtqfF01?A;4%;2ls4uJ*13!vS$qqs%X(SF)Z z?!m~0R!e{Ru7&i(vx%s)s;R0s6I{d#LT$r|mlE%b)#biIqr}DwG6PhAZPJpGl7Ou) z9-o~REp~Cw+|jPBm7LhMuctXjq={4WB{`V=9^7DAopg~2XMk4S6^NeL^iHyye=*X7 z&JD7AV|uF#x-!9#CwACOqiU>l*C9n>aAG1g@$!-}Ho&c@;;N6(02l^sRH~guh31Lh zn4IXYx^%$dPAN-M>z?MbcZH)}R5DkVQsJ|7^wVj&Vf=H_;#wogd;o&pjpUU*;0$P* z8OTu7DBLXZ^V#276}1Q=2d`wix8%JY?vNm8nP-l2Y!@X;S4$Yq99Y~V;d{0fcP{(M z=J5>o81F44)p$-?vcnf;*x`HT>``E#c0anGYF77{Z1|} z`aO0lS~7*w*5Kwxaa;1(aj&Pgq@(c81WlPjfndU!lq(IJZtM+&?QV+6@zS&#hO>Xa;_YS zdp|rnv?~ywpfOH08Q7Yrx+F!(kQ7sQjz(^4uQ@Ytt4p7!yGH;qg)G_vme<+QQJFaH zmxYKaC$mfkOh(b$wB%4SzzfroGDlxdsY?9%r796e+GxzO7=F3?dVklyNPXq0C+_}q zi;vY92Z**@<5U2Y;F*U15RNER8)IWV13c ztZJ$c0RGVA z1=W4%TiKCadvhdidQ&%!k?zh_LPa`M!*+G(8#Y&b1mJ zzHL1u_Vs@Ia4D6WerajxjwU}8k(q8{DBHgE$~LtOUu5>q@kJ^W=A&VvaQ_E?(E!0_ z=flzmuhi-wbhfpvzrT@U%#}eVO{qa-idDewFH3%Y{=MKK;)f=0=dD7w!_Mi(+xsV{ zFPIIea~j=mzMjLz?I#LX6*Gh9?glr(T@hYhG{&B(Gwnrv6PfLGVXQM0igh^mehDLY zO$~0jZ{_hXr%PhEVVCaU7Xle-H9bZTCTJy%J1S2;)*#)BwKs>lWt}WvyiYJNOm(I) zUDxq;C5d7Lui39ml=j!w&%~f62d9QSt7VpJDoj_lyeu&=9_YQnO_{rQpa{33>b|VX z+N|umvIs1(PG^#%9WzVl8NKM&^67g)&}Zij$?TF#h=P*Kh=N#qI%p&5hS__6a|4@9 zR055_E&MN{2xPl%at2`!XaK=tsrz&^s^>J!zQ5FtSgIKd3oI^uT?S<+c44C%YH{_l z0NeRI_to^88Ievk6OGZ4aq!1JYpAAW?{I7BINx`tKL$py$2-iql}-5~^y#N~c(#&e zKJ(mFUHg@Vp4;sKO5*<@P$SEZyEh87K+ZZSKg7&2?3~Uj@vSEc^V~mNRq3f-MigX- zNlyGY#-!OmWfFZ~z2z68;FmCAcxD=%Fb-^ z^P`MpY~e-5aY`9Z*~_(0!Jck3ZbXl5JhI;{$p@TX9zH4U+=P&vBs@1~sIw@S30U?r z4qS7c-p%724>a3!(KA4kK9si*n;Q3LaBH@3ln9*absmDND$^GU%D1vz%87 zz6iZ~KfdU>Tx$(BhGrROy1JpI!WtOBjPpM0$l656m{dTknX&luEPz8LIbwHfVyPOW zinx>5pB{48tsK5}_dT#p{Ov^oF4i>dzF4C(sEP;wIe$Z@=t$_On80pKLTQIbE08eqb%fT^2cTsyD>O@=Y_x98|WfY0J|Ws&!Ha zVIN}4!9TO(8$DKSr(e{6W@9k$o+b;degSMktlPfvB&jJDR7rJx38ew#7+uStY23<$ z^}+S8wh7qb=M63WLTjzo#pT?bN2P*i7nZU2;2NEmjl}w7xg>TB_J#Hu_Do1ueF}9^RmIIig>te9qiSynMf|W+d<-`h|9ImypmGBY+uAqmii2Sou+a8KbtG>`BunA{S5g^53Z` zKK4^uw1E0w~#Cv?y1agXXH$S z+QZI=&@TUWqN?`n6ZX1I_AyC8`AO&(CTNnD`tc_Lg?_q-N9p;KeIQ4Q?!~+7lZ~@2 zm+|4KU>KI1=^R3PA0&t{chiVn1a=v*klro+{zd_0(1Nw!$_kxdZ8(QmU@aDt5Ignh zv^~^|{3PkGYIu;GPH}vYXrceDAX`~F0&A~nc<}Fe%!jjNwm$YjR+Pdo%j_pL?o@KF z5OFW$5fE}^IlE)I4yJxpBs2_vc) zUJI#mXW6PkzQbMQ275udkDag64HJ~Bzhz%-{_FS|iq6_%CaA_L1+wes#%Laa)S3P{ ze_qBT?SwP@aKhYKqqinVE%vtOfm*@*NDAiulNh5+aiULPtx1{)CZB9fADo6ykLfk% zF{5Ps17m0H&)!MOstm#zk;u{~euoOG%UPq#h^@p-FswYGdph9PxNdkT4h^+Gob71z z-K@U}HE~6EY?YpqN!C&4XVcMVzmv6<8OGar!7xU?y~?h4KAqR^l%QLOjE_oz20xqg z>lb(0f)=_z>X(}^NM$x}#n2HWHXf3!(a0>B13bMw=TXe(ShC#w=`57H1yKx}Ax6-kL22+YnB|M5m{h0<+L(Rv zDThDGhrw;L7iv)^>=kxAhn`-%pHA|ROX{U&$Se^vB$#k;qB*-vpdo|I?z6O|kE}rb zC2`6tcB6T_?-{$9YX9m)1C%d_6whu>Z20Wf$ARh)FnhXwv_0&xW^T{U{)uBaqQ>ue zli$8OfHUS5nuvJsz*L!a36=$Tql?+Q7nYEi!&=g(ijf9pSeZ2p&T$2K?)gNxK?iKu z{^j^38r9UaG%x=6%Gy|x_tVLu-tsaQ3Ml5)@S_R4vW!Kn>tMD<;5*RfK|{s&iTHV4 z%U-!}n^(*($@?9$I5%+AHWHcivUi*po5tx;=Kk6p7KfGP zeNtr%K?eeV!j8;J{P%Vf`Nw;o2uIpPe7j)7(}^+8k8 zWlsk9F|iLgCZGEw_cJxJBBnoc8*q*cVur#{K?;1FM;|krmzegq5>D?d_P7rWl0DcD z8UJ|(r-T7aFzPaIPOC=lP^gYUi_? zR82mc40)8^y%_^d*qsw@+y^{@2C_2xYMca1V8(Ok#15@t5^TU$m&r9zR29r>CANCJ zW-@TXTJ&6QtYmmBq?>cFSheTwz;eXx6h_--ZXcVBmsU=`96RHY*YTxkj$oKQcgUVD6x5* z&JdlJP?F4_nMlSwN@L$Vp7PzlQ(jy?R4@oU#Cb1%L-PgodwfScH{JZ<@m)B$;6?t3 z=aGF&Ue|8?{ww_U5Y25r$|4uQW4_S|iv{hgKibMpAvLYQ!7r-)#&NzM+(W8D11 zAi+Q>4hem9=(<>L*7PTZ^}DfP2=aFEe#+q(KWG!+Hh(`C=;~iE`~9-@9{H18K}ZnG zSJ1*0GsY~`LGx(+UFP>78!IRIwb>koW9*=lxrs`B@Ew$X;5j=@g zBp)$xRCuYsX~6pI9qUQSyT$|T$74P`X%;+##T;cl9}il4_@v)iqY@S@kJ5yE$&un5 z*ei2I!RMt4iW+{9gXNmqp+oq~MlTO0<~t4@Ep+avJ;>>6S~m=0Saj5V>I)qd zxz3NyRazUro3O|dB2+W`Oa3u8PHNO$r|


    jUPINF&z_tUaQb#kbxe7>XjqbnJyF%2~b< zj#(6>OwqqLOsg*O>JpS3E_ltRn;Yc8d`>7~L`C@d^Ql;AZwEJAeK0EI+og~fKoEmI z>QhOgTwixl1zxoZYO7(P2TBj_HkvKRrhV_gKBuEP}-zzG!Vb18#Q^}4`hZWk&~0&537@lv+O~~64zykFhBSFxYGS=V?&1;uVTaxRz3cuZi{1{#cDSF0k6FVO!(xBJs=oq4$b5+V z)`K4IC;;)j?<)5mz8w+#b2{liT_`uYQJ@a?sW{@+4S)LeOuA25Q!X7^O z(TcjOtDGLDWDr-RU%o$bwY9!B;+h6SP0c^#<=_&SanI_pTBg=F?}1Y2j5sudt^lOk z`?nii*`8gZ?q-mZ&Az*nm7?hFk*E%pW_>TUuf|-r_E0pl?_46e-Ean^^^MxIAJb~tdaOUEU$#KtQfz4r__C>@A-1?o2fn4{928}H@=T?&(=`vmDSh} z)D4x2?4NaEkF+x^Ct12GF6`P>k7JkYH&BbKg=e+5Jg094J*-AG2=6pHY;{#}DZhIM zbk4s-1-HrQ>xreXecVSe^%^OEhQ}Iyr?Ck@;#YNG$gIlszMoODw>^HZ1}0YTbV$B+ zwW)C;Z~Gi{Nltk6PiurdpMfGOa}$Fa8esYnh`WtDr}=px!&dD|XCYy327!)G4AB6m zbf)t58tmt;`M#kOdk3Je4=9q@Zg%OK?a3^+eow~-Pf{L3N}UpN!tGo%jj(nVLAF{j zjdi$9jYHQp7U%n-(?D0MQofD55g2w@GU%Liw-0N(pgX_Tza@Cx^-`{hyIDucbYXOT zO#L$2%bSoqVC9SsDB@k|wn9wj?kId`mfXGs zh3KIT<>js|o`=JkkF*k>FYT&0f>yhd$4$rtV1sYn?;Y7x(?!nYN-P!YnVR*kN!?4A z(D6y`ve}-=OFox>E_7FNApnWXPN}WSV^5@Qu@4Ews%ybM#O(f{3sgg?pd%rlJ5|D> z(T
    HWDr60>+a$z3C6?Ri7LOp8p&WT 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 E0BOldqyPW_ diff --git a/docs/img/example-regions-py-file.png b/docs/img/example-regions-py-file.png deleted file mode 100644 index a9d05c53fcbf230d8ac946b9b6ef3065d5c64d23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65088 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 Date: Fri, 27 Oct 2023 05:33:59 -0500 Subject: [PATCH 099/327] Tests: Add a unit test for slot_data (#2333) * Tests: Add a unit test for slot_data * use NetUtils.encode * modern PEP8 --- test/general/test_implemented.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index 67d0e5ff72..b60bcee467 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -1,6 +1,8 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister +from Fill import distribute_items_restrictive +from NetUtils import encode +from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -31,3 +33,17 @@ class TestImplemented(unittest.TestCase): for method in ("assert_generate",): self.assertFalse(hasattr(world_type, method), f"{method} must be implemented as a @classmethod named stage_{method}.") + + def test_slot_data(self): + """Tests that if a world creates slot data, it's json serializable.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + # has an await for generate_output which isn't being called + if game_name in {"Ocarina of Time", "Zillion"}: + continue + with self.subTest(game_name): + multiworld = setup_solo_multiworld(world_type) + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + for key, data in multiworld.worlds[1].fill_slot_data().items(): + self.assertIsInstance(key, str, "keys in slot data must be a string") + self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.") From 6a2407468a08b79707e7ae9bfe8c26ae2e3d5c55 Mon Sep 17 00:00:00 2001 From: ArashiKurobara <131409871+ArashiKurobara@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:43:36 -0400 Subject: [PATCH 100/327] OoT: Update YAML Instructions (#1745) Existing setup guide hard-coded in a YAML from 0.1.7 --- worlds/oot/docs/setup_en.md | 358 +----------------------------------- 1 file changed, 10 insertions(+), 348 deletions(-) diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md index 612c5efd8f..72f15fa6c7 100644 --- a/worlds/oot/docs/setup_en.md +++ b/worlds/oot/docs/setup_en.md @@ -42,360 +42,22 @@ and select EmuHawk.exe. An alternative BizHawk setup guide as well as various pieces of troubleshooting advice can be found [here](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). -## Configuring your YAML file +## Create a Config (.yaml) File -### What is a YAML file and why do I need one? +### What is a config file and why do I need one? -Your YAML file contains a set of configuration options which provide the generator with information about how it should -generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy -an experience customized for their taste, and different players in the same multiworld can all have different options. +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) -### Where do I get a YAML file? +### Where do I get a config file? -A basic OoT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this -tutorial, if you want to see a complete list, download Archipelago from -the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in -the "Players" folder. +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Ocarina of Time Player Settings Page](/games/Ocarina%20of%20Time/player-settings) -```yaml -description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files -# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit -name: YourName -game: - Ocarina of Time: 1 -requires: - version: 0.1.7 # Version of Archipelago required for this yaml to work as expected. -# Shared Options supported by all games: -accessibility: - items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations - locations: 50 # Guarantees you will be able to access all locations, and therefore all items - none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items -progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere - 0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items. - 25: 0 - 50: 50 # Make it likely you have stuff to do. - 99: 0 # Get important items early, and stay at the front of the progression. -Ocarina of Time: - logic_rules: # Set the logic used for the generator. - glitchless: 50 - glitched: 0 - no_logic: 0 - logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song. - false: 50 - true: 0 - open_forest: # Set the state of Kokiri Forest and the path to Deku Tree. - open: 50 - closed_deku: 0 - closed: 0 - open_kakariko: # Set the state of the Kakariko Village gate. - open: 50 - zelda: 0 - closed: 0 - open_door_of_time: # Open the Door of Time by default, without the Song of Time. - false: 0 - true: 50 - zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain. - open: 0 - adult: 0 - closed: 50 - gerudo_fortress: # Set the requirements for access to Gerudo Fortress. - normal: 0 - fast: 50 - open: 0 - bridge: # Set the requirements for the Rainbow Bridge. - open: 0 - vanilla: 0 - stones: 0 - medallions: 50 - dungeons: 0 - tokens: 0 - trials: # Set the number of required trials in Ganon's Castle. - # you can add additional values between minimum and maximum - 0: 50 # minimum value - 6: 0 # maximum value - random: 0 - random-low: 0 - random-high: 0 - starting_age: # Choose which age Link will start as. - child: 50 - adult: 0 - triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game. - false: 50 - true: 0 - triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting. - # you can add additional values between minimum and maximum - 1: 0 # minimum value - 50: 0 # maximum value - random: 0 - random-low: 0 - random-high: 0 - 20: 50 - bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling. - false: 50 - true: 0 - bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 3: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - bridge_medallions: # Set the number of medallions required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 6: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 9: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 100: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses. - remove: 0 - startwith: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_smallkeys: # Control where to shuffle dungeon small keys. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_hideoutkeys: # Control where to shuffle the Gerudo Fortress small keys. - vanilla: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key. - remove: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - on_lacs: 0 - enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is. - false: 50 - true: 0 - lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time. - vanilla: 50 - stones: 0 - medallions: 0 - dungeons: 0 - tokens: 0 - lacs_stones: # Set the number of Spiritual Stones required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 3: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - lacs_medallions: # Set the number of medallions required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 6: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - lacs_rewards: # Set the number of dungeon rewards required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 9: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS. - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 100: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - shuffle_song_items: # Set where songs can appear. - song: 50 - dungeon: 0 - any: 0 - shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops. - 0: 0 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random_value: 0 - off: 50 - tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool. - off: 50 - dungeons: 0 - overworld: 0 - all: 0 - shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices. - off: 50 - low: 0 - regular: 0 - random_prices: 0 - shuffle_cows: # Cows give items when Epona's Song is played. - false: 50 - true: 0 - shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool. - false: 50 - true: 0 - shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool. - false: 50 - true: 0 - shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle. - false: 50 - true: 0 - shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool. - false: 50 - true: 0 - shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees. - false: 50 - true: 0 - shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman. - false: 50 - true: 0 - skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed. - false: 50 - true: 0 - no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights. - false: 0 - true: 50 - no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda. - false: 0 - true: 50 - no_epona_race: # Epona can always be summoned with Epona's Song. - false: 0 - true: 50 - skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt. - false: 0 - true: 50 - complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop. - false: 50 - true: 0 - useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched. - false: 50 - true: 0 - fast_chests: # All chest animations are fast. If disabled, major items have a slow animation. - false: 0 - true: 50 - free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song. - false: 50 - true: 0 - fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask. - false: 50 - true: 0 - chicken_count: # Controls the number of Cuccos for Anju to give an item as child. - \# you can add additional values between minimum and maximum - 0: 0 # minimum value - 7: 50 # maximum value - random: 0 - random-low: 0 - random-high: 0 - hints: # Gossip Stones can give hints about item locations. - none: 0 - mask: 0 - agony: 0 - always: 50 - hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc. - balanced: 50 - ddr: 0 - league: 0 - mw2: 0 - scrubs: 0 - strong: 0 - tournament: 0 - useless: 0 - very_strong: 0 - text_shuffle: # Randomizes text in the game for comedic effect. - none: 50 - except_hints: 0 - complete: 0 - damage_multiplier: # Controls the amount of damage Link takes. - half: 0 - normal: 50 - double: 0 - quadruple: 0 - ohko: 0 - no_collectible_hearts: # Hearts will not drop from enemies or objects. - false: 50 - true: 0 - starting_tod: # Change the starting time of day. - default: 50 - sunrise: 0 - morning: 0 - noon: 0 - afternoon: 0 - sunset: 0 - evening: 0 - midnight: 0 - witching_hour: 0 - start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts. - false: 50 - true: 0 - start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet. - false: 50 - true: 0 - item_pool_value: # Changes the number of items available in the game. - plentiful: 0 - balanced: 50 - scarce: 0 - minimal: 0 - junk_ice_traps: # Adds ice traps to the item pool. - off: 0 - normal: 50 - on: 0 - mayhem: 0 - onslaught: 0 - ice_trap_appearance: # Changes the appearance of ice traps as freestanding items. - major_only: 50 - junk_only: 0 - anything: 0 - logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 50 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 0 - logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 0 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 50 +### Verifying your config file -``` +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) ## Joining a MultiWorld Game From fc2855ca6d0e63467b2237dcce44c237c7dfef76 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 27 Oct 2023 18:09:12 +0200 Subject: [PATCH 101/327] Stardew Valley: speed up rules creation by 4% (#2371) * Stardew Valley: speed up rules creation by 4% No class should ever inherit from And, Or, False_ or True_ and isinstance is not free. Sadly there is no cheap way to forbid inheritance, but it was tested using metaclass. * Stardew Valley: save calls to type() Local variable is a bit faster than fetching type again * Stardew Valley: save calls to True_() and False_(), also use 'in' operator * Stardew Valley: optimize And and Or simplification * Stardew Valley: optimize logic constructors --- worlds/stardew_valley/stardew_rule.py | 111 ++++++++++++++------------ 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py index d0fa9858cc..5455a40e7a 100644 --- a/worlds/stardew_valley/stardew_rule.py +++ b/worlds/stardew_valley/stardew_rule.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Iterable, Dict, List, Union, FrozenSet +from typing import Iterable, Dict, List, Union, FrozenSet, Set from BaseClasses import CollectionState, ItemClassification from .items import item_table @@ -14,13 +14,13 @@ class StardewRule: raise NotImplementedError def __or__(self, other) -> StardewRule: - if isinstance(other, Or): + if type(other) is Or: return Or(self, *other.rules) return Or(self, other) def __and__(self, other) -> StardewRule: - if isinstance(other, And): + if type(other) is And: return And(other.rules.union({self})) return And(self, other) @@ -80,28 +80,36 @@ class False_(StardewRule): # noqa return 999999999 +false_ = False_() +true_ = True_() +assert false_ is False_() +assert true_ is True_() + + class Or(StardewRule): rules: FrozenSet[StardewRule] def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() + rules_list: Set[StardewRule] + if isinstance(rule, Iterable): - rules_list.update(rule) + rules_list = {*rule} else: - rules_list.add(rule) + rules_list = {rule} if rules is not None: rules_list.update(rules) assert rules_list, "Can't create a Or conditions without rules" - new_rules = set() - for rule in rules_list: - if isinstance(rule, Or): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules + if any(type(rule) is Or for rule in rules_list): + new_rules: Set[StardewRule] = set() + for rule in rules_list: + if type(rule) is Or: + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules self.rules = frozenset(rules_list) @@ -112,11 +120,11 @@ class Or(StardewRule): return f"({' | '.join(repr(rule) for rule in self.rules)})" def __or__(self, other): - if isinstance(other, True_): + if other is true_: return other - if isinstance(other, False_): + if other is false_: return self - if isinstance(other, Or): + if type(other) is Or: return Or(self.rules.union(other.rules)) return Or(self.rules.union({other})) @@ -131,17 +139,17 @@ class Or(StardewRule): return min(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: - if any(isinstance(rule, True_) for rule in self.rules): - return True_() + if true_ in self.rules: + return true_ - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not False_()} + simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules} + if simplified is not false_] if not simplified_rules: - return False_() + return false_ if len(simplified_rules) == 1: - return next(iter(simplified_rules)) + return simplified_rules[0] return Or(simplified_rules) @@ -150,25 +158,26 @@ class And(StardewRule): rules: FrozenSet[StardewRule] def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() + rules_list: Set[StardewRule] + if isinstance(rule, Iterable): - rules_list.update(rule) + rules_list = {*rule} else: - rules_list.add(rule) + rules_list = {rule} if rules is not None: rules_list.update(rules) - if len(rules_list) < 1: - rules_list.add(True_()) - - new_rules = set() - for rule in rules_list: - if isinstance(rule, And): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules + if not rules_list: + rules_list.add(true_) + elif any(type(rule) is And for rule in rules_list): + new_rules: Set[StardewRule] = set() + for rule in rules_list: + if type(rule) is And: + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules self.rules = frozenset(rules_list) @@ -179,11 +188,11 @@ class And(StardewRule): return f"({' & '.join(repr(rule) for rule in self.rules)})" def __and__(self, other): - if isinstance(other, True_): + if other is true_: return self - if isinstance(other, False_): + if other is false_: return other - if isinstance(other, And): + if type(other) is And: return And(self.rules.union(other.rules)) return And(self.rules.union({other})) @@ -198,17 +207,17 @@ class And(StardewRule): return max(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: - if any(isinstance(rule, False_) for rule in self.rules): - return False_() + if false_ in self.rules: + return false_ - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not True_()} + simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules} + if simplified is not true_] if not simplified_rules: - return True_() + return true_ if len(simplified_rules) == 1: - return next(iter(simplified_rules)) + return simplified_rules[0] return And(simplified_rules) @@ -218,11 +227,12 @@ class Count(StardewRule): rules: List[StardewRule] def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = [] + rules_list: List[StardewRule] + if isinstance(rule, Iterable): - rules_list.extend(rule) + rules_list = [*rule] else: - rules_list.append(rule) + rules_list = [rule] if rules is not None: rules_list.extend(rules) @@ -260,11 +270,12 @@ class TotalReceived(StardewRule): player: int def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): - items_list = [] + items_list: List[str] + if isinstance(items, Iterable): - items_list.extend(items) + items_list = [*items] else: - items_list.append(items) + items_list = [items] assert items_list, "Can't create a Total Received conditions without items" for item in items_list: From c470849cee8726fceaba333707f193dbf1225291 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 27 Oct 2023 19:10:16 +0200 Subject: [PATCH 102/327] Core: remove custom_data (#2380) --- BaseClasses.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0bd61f68f3..d35739c324 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -181,7 +181,6 @@ class MultiWorld(): set_player_attr('plando_connections', []) set_player_attr('game', "A Link to the Past") set_player_attr('completion_condition', lambda state: True) - self.custom_data = {} self.worlds = {} self.per_slot_randoms = {} self.plando_options = PlandoOptions.none @@ -199,7 +198,6 @@ class MultiWorld(): new_id: int = self.players + len(self.groups) + 1 self.game[new_id] = game - self.custom_data[new_id] = {} self.player_types[new_id] = NetUtils.SlotType.group self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] @@ -227,7 +225,6 @@ class MultiWorld(): def set_options(self, args: Namespace) -> None: for player in self.player_ids: - self.custom_data[player] = {} world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] From e3112e5d51abff42e2eae0e1102d2f60fcebae9f Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:18:33 +0200 Subject: [PATCH 103/327] Stardew Valley: Cut tests by 3 minutes (#2375) * Stardew Valley: Test: unify mods * Stardew Valley: Test: don't use SVTestBase where setUp is unused * Stardew Valley: Test: remove duplicate backpack test * Stardew Valley: Test: remove 2,3,4 heart tests assume the math is correct with just 2 points on the curve * Stardew Valley: Test: reduce duplicate test/gen runs * Stardew Valley: Test: Change 'long' tests to not use TestBase TestBase' setUp is not being used in the changed TestCases * Stardew Valley: Test: Use subtests and inheritance for backpacks * Stardew Valley: Test: add flag to skip some of the extensive tests by default --- worlds/stardew_valley/mods/mod_data.py | 8 + worlds/stardew_valley/test/TestBackpack.py | 49 ++++--- worlds/stardew_valley/test/TestGeneration.py | 39 +++-- worlds/stardew_valley/test/TestItems.py | 6 +- worlds/stardew_valley/test/TestOptions.py | 35 ++--- worlds/stardew_valley/test/TestRegions.py | 4 +- worlds/stardew_valley/test/__init__.py | 137 ++++++++++-------- .../test/checks/world_checks.py | 10 +- .../stardew_valley/test/long/TestModsLong.py | 16 +- .../test/long/TestOptionsLong.py | 7 +- .../test/long/TestRandomWorlds.py | 8 +- .../test/mods/TestBiggerBackpack.py | 51 +++---- worlds/stardew_valley/test/mods/TestMods.py | 25 ++-- 13 files changed, 200 insertions(+), 195 deletions(-) diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index 81c4989411..30fe96c9d9 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -21,3 +21,11 @@ class ModNames: ayeisha = "Ayeisha - The Postal Worker (Custom NPC)" riley = "Custom NPC - Riley" skull_cavern_elevator = "Skull Cavern Elevator" + + +all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py index f26a7c1f03..378c90e40a 100644 --- a/worlds/stardew_valley/test/TestBackpack.py +++ b/worlds/stardew_valley/test/TestBackpack.py @@ -5,40 +5,41 @@ from .. import options class TestBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla} - def test_no_backpack_in_pool(self): - item_names = {item.name for item in self.multiworld.get_items()} - self.assertNotIn("Progressive Backpack", item_names) + def test_no_backpack(self): + with self.subTest("no items"): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_names) - def test_no_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Large Pack", location_names) - self.assertNotIn("Deluxe Pack", location_names) + with self.subTest("no locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Large Pack", location_names) + self.assertNotIn("Deluxe Pack", location_names) class TestBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - def test_backpack_is_in_pool_2_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 2) + def test_backpack(self): + with self.subTest(check="has items"): + item_names = [item.name for item in self.multiworld.get_items()] + self.assertEqual(item_names.count("Progressive Backpack"), 2) - def test_2_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) + with self.subTest(check="has locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Large Pack", location_names) + self.assertIn("Deluxe Pack", location_names) -class TestBackpackEarlyProgressive(SVTestBase): +class TestBackpackEarlyProgressive(TestBackpackProgressive): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive} - def test_backpack_is_in_pool_2_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 2) + @property + def run_default_tests(self) -> bool: + # EarlyProgressive is default + return False - def test_2_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) + def test_backpack(self): + super().test_backpack() - def test_progressive_backpack_is_in_early_pool(self): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + with self.subTest(check="is early"): + self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 0142ad0079..46c6685ad5 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,5 +1,8 @@ +import typing + from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \ + allsanity_options_without_mods, minimal_locations_maximal_items from .. import locations, items, location_table, options from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name from ..items import items_by_group, Group @@ -7,11 +10,11 @@ from ..locations import LocationTags from ..mods.mod_data import ModNames -def get_real_locations(tester: SVTestBase, multiworld: MultiWorld): +def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): return [location for location in multiworld.get_locations(tester.player) if not location.event] -def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld): +def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): return [location.name for location in multiworld.get_locations(tester.player) if not location.event] @@ -115,21 +118,6 @@ class TestNoGingerIslandItemGeneration(SVTestBase): self.assertTrue(count == 0 or count == 2) -class TestGivenProgressiveBackpack(SVTestBase): - options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - - def test_when_generate_world_then_two_progressive_backpack_are_added(self): - self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2) - - def test_when_generate_world_then_backpack_locations_are_added(self): - created_locations = {location.name for location in self.multiworld.get_locations(1)} - backpacks_exist = [location.name in created_locations - for location in locations.locations_by_tag[LocationTags.BACKPACK] - if location.name != "Premium Pack"] - all_exist = all(backpacks_exist) - self.assertTrue(all_exist) - - class TestRemixedMineRewards(SVTestBase): def test_when_generate_world_then_one_reward_is_added_per_chest(self): # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool @@ -205,17 +193,17 @@ class TestLocationGeneration(SVTestBase): self.assertIn(location.name, location_table) -class TestLocationAndItemCount(SVTestBase): +class TestLocationAndItemCount(SVTestCase): def test_minimal_location_maximal_items_still_valid(self): - min_max_options = self.minimal_locations_maximal_items() + min_max_options = minimal_locations_maximal_items() multiworld = setup_solo_multiworld(min_max_options) valid_locations = get_real_locations(self, multiworld) self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool)) def test_allsanity_without_mods_has_at_least_locations(self): expected_locations = 994 - allsanity_options = self.allsanity_options_without_mods() + allsanity_options = allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -228,7 +216,7 @@ class TestLocationAndItemCount(SVTestBase): def test_allsanity_with_mods_has_at_least_locations(self): expected_locations = 1246 - allsanity_options = self.allsanity_options_with_mods() + allsanity_options = allsanity_options_with_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -245,6 +233,11 @@ class TestFriendsanityNone(SVTestBase): options.Friendsanity.internal_name: options.Friendsanity.option_none, } + @property + def run_default_tests(self) -> bool: + # None is default + return False + def test_no_friendsanity_items(self): for item in self.multiworld.itempool: self.assertFalse(item.name.endswith(" <3")) @@ -416,6 +409,7 @@ class TestFriendsanityAllNpcsWithMarriage(SVTestBase): self.assertLessEqual(int(hearts), 10) +""" # Assuming math is correct if we check 2 points class TestFriendsanityAllNpcsWithMarriageHeartSize2(SVTestBase): options = { options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, @@ -528,6 +522,7 @@ class TestFriendsanityAllNpcsWithMarriageHeartSize4(SVTestBase): self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14) else: self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10) +""" class TestFriendsanityAllNpcsWithMarriageHeartSize5(SVTestBase): diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 7f48f9347c..38f59c7490 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -6,12 +6,12 @@ import random from typing import Set from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods from .. import ItemData, StardewValleyWorld from ..items import Group, item_table -class TestItems(SVTestBase): +class TestItems(SVTestCase): def test_can_create_item_of_resource_pack(self): item_name = "Resource Pack: 500 Money" @@ -46,7 +46,7 @@ class TestItems(SVTestBase): def test_correct_number_of_stardrops(self): seed = random.randrange(sys.maxsize) - allsanity_options = self.allsanity_options_without_mods() + allsanity_options = allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options, seed=seed) stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name] self.assertEqual(len(stardrop_items), 5) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 712aa300d5..02b1ebf643 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,10 +1,11 @@ import itertools +import unittest from random import random from typing import Dict from BaseClasses import ItemClassification, MultiWorld from Options import SpecialRange -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations @@ -17,21 +18,21 @@ SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter} TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} -def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state)) -def basic_checks(tester: SVTestBase, multiworld: MultiWorld): +def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) assert_can_win(tester, multiworld) non_event_locations = [location for location in multiworld.get_locations() if not location.event] tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) -def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld): +def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld): ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]] ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]] for item in multiworld.get_items(): @@ -48,9 +49,9 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestBase): +class TestGenerateDynamicOptions(SVTestCase): def test_given_special_range_when_generate_then_basic_checks(self): - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange): continue @@ -62,7 +63,7 @@ class TestGenerateDynamicOptions(SVTestBase): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not option.options: continue @@ -73,7 +74,7 @@ class TestGenerateDynamicOptions(SVTestBase): basic_checks(self, multiworld) -class TestGoal(SVTestBase): +class TestGoal(SVTestCase): def test_given_goal_when_generate_then_victory_is_in_correct_location(self): for goal, location in [("community_center", GoalName.community_center), ("grandpa_evaluation", GoalName.grandpa_evaluation), @@ -90,7 +91,7 @@ class TestGoal(SVTestBase): self.assertEqual(victory.name, location) -class TestSeasonRandomization(SVTestBase): +class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} multi_world = setup_solo_multiworld(world_options) @@ -114,7 +115,7 @@ class TestSeasonRandomization(SVTestBase): self.assertEqual(items.count(Season.progressive), 3) -class TestToolProgression(SVTestBase): +class TestToolProgression(SVTestCase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} multi_world = setup_solo_multiworld(world_options) @@ -147,9 +148,9 @@ class TestToolProgression(SVTestBase): self.assertIn("Purchase Iridium Rod", locations) -class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): +class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_special_range_when_generate_exclude_ginger_island(self): - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue @@ -162,7 +163,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not option.options or option_name == ExcludeGingerIsland.internal_name: continue @@ -191,9 +192,9 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): basic_checks(self, multiworld) -class TestTraps(SVTestBase): +class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = self.allsanity_options_without_mods() + world_options = allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.option_no_traps}) multi_world = setup_solo_multiworld(world_options) @@ -209,7 +210,7 @@ class TestTraps(SVTestBase): for value in trap_option.options: if value == "no_traps": continue - world_options = self.allsanity_options_with_mods() + world_options = allsanity_options_with_mods() world_options.update({TrapItems.internal_name: trap_option.options[value]}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] @@ -219,7 +220,7 @@ class TestTraps(SVTestBase): self.assertIn(item, multiworld_items) -class TestSpecialOrders(SVTestBase): +class TestSpecialOrders(SVTestCase): def test_given_disabled_then_no_order_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} multi_world = setup_solo_multiworld(world_options) diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 2347ca33db..7ebbcece5c 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -2,7 +2,7 @@ import random import sys import unittest -from . import SVTestBase, setup_solo_multiworld +from . import SVTestCase, setup_solo_multiworld from .. import options, StardewValleyWorld, StardewValleyOptions from ..options import EntranceRandomization, ExcludeGingerIsland from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag @@ -88,7 +88,7 @@ class TestEntranceRando(unittest.TestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestEntranceClassifications(SVTestBase): +class TestEntranceClassifications(SVTestCase): def test_non_progression_are_all_accessible_with_empty_inventory(self): for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 53181154d3..b0c4ba2c7b 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,8 +1,10 @@ import os +import unittest from argparse import Namespace from typing import Dict, FrozenSet, Tuple, Any, ClassVar from BaseClasses import MultiWorld +from Utils import cache_argsless from test.TestBase import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from .. import StardewValleyWorld @@ -13,11 +15,17 @@ from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Frien BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods -class SVTestBase(WorldTestBase): +class SVTestCase(unittest.TestCase): + player: ClassVar[int] = 1 + """Set to False to not skip some 'extra' tests""" + skip_extra_tests: bool = True + """Set to False to run tests that take long""" + skip_long_tests: bool = True + + +class SVTestBase(WorldTestBase, SVTestCase): game = "Stardew Valley" world: StardewValleyWorld - player: ClassVar[int] = 1 - skip_long_tests: bool = True def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) @@ -34,66 +42,73 @@ class SVTestBase(WorldTestBase): should_run_default_tests = is_not_stardew_test and super().run_default_tests return should_run_default_tests - def minimal_locations_maximal_items(self): - min_max_options = { - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, - BackpackProgression.internal_name: BackpackProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_vanilla, - SkillProgression.internal_name: SkillProgression.option_vanilla, - BuildingProgression.internal_name: BuildingProgression.option_vanilla, - ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, - HelpWantedLocations.internal_name: 0, - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, - } - return min_max_options - def allsanity_options_without_mods(self): - allsanity = { - Goal.internal_name: Goal.option_perfection, - BundleRandomization.internal_name: BundleRandomization.option_shuffled, - BundlePrice.internal_name: BundlePrice.option_expensive, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - HelpWantedLocations.internal_name: 56, - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 1, - NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_nightmare, - } - return allsanity +@cache_argsless +def minimal_locations_maximal_items(): + min_max_options = { + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + BuildingProgression.internal_name: BuildingProgression.option_vanilla, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: 0, + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + } + return min_max_options + + +@cache_argsless +def allsanity_options_without_mods(): + allsanity = { + Goal.internal_name: Goal.option_perfection, + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + BundlePrice.internal_name: BundlePrice.option_expensive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: 56, + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 1, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_nightmare, + } + return allsanity + + +@cache_argsless +def allsanity_options_with_mods(): + allsanity = {} + allsanity.update(allsanity_options_without_mods()) + all_mods = ( + ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator + ) + allsanity.update({Mods.internal_name: all_mods}) + return allsanity - def allsanity_options_with_mods(self): - allsanity = {} - allsanity.update(self.allsanity_options_without_mods()) - all_mods = ( - ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator - ) - allsanity.update({Mods.internal_name: all_mods}) - return allsanity pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/checks/world_checks.py b/worlds/stardew_valley/test/checks/world_checks.py index 2cdb0534d4..9bd9fd614c 100644 --- a/worlds/stardew_valley/test/checks/world_checks.py +++ b/worlds/stardew_valley/test/checks/world_checks.py @@ -1,8 +1,8 @@ +import unittest from typing import List from BaseClasses import MultiWorld, ItemClassification from ... import StardewItem -from .. import SVTestBase def get_all_item_names(multiworld: MultiWorld) -> List[str]: @@ -13,21 +13,21 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]: return [location.name for location in multiworld.get_locations() if not location.event] -def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld): +def assert_victory_exists(tester: unittest.TestCase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) -def collect_all_then_assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state)) -def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) collect_all_then_assert_can_win(tester, multiworld) -def assert_same_number_items_locations(tester: SVTestBase, multiworld: MultiWorld): +def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld): non_event_locations = [location for location in multiworld.get_locations() if not location.event] tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index b3ec6f1420..36a59ae854 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -1,23 +1,17 @@ +import unittest from typing import List, Union from BaseClasses import MultiWorld -from worlds.stardew_valley.mods.mod_data import ModNames +from worlds.stardew_valley.mods.mod_data import all_mods from worlds.stardew_valley.test import setup_solo_multiworld -from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase +from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase from worlds.stardew_valley.items import item_table from worlds.stardew_valley.locations import location_table from worlds.stardew_valley.options import Mods from .option_names import options_to_include -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) - -def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -30,7 +24,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestBase): +class TestGenerateModsOptions(SVTestCase): def test_given_mod_pairs_when_generate_then_basic_checks(self): if self.skip_long_tests: diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index 23ac6125e6..3634dc5fd1 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,13 +1,14 @@ +import unittest from typing import Dict from BaseClasses import MultiWorld from Options import SpecialRange from .option_names import options_to_include from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations -from .. import setup_solo_multiworld, SVTestBase +from .. import setup_solo_multiworld, SVTestCase -def basic_checks(tester: SVTestBase, multiworld: MultiWorld): +def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): assert_can_win(tester, multiworld) assert_same_number_items_locations(tester, multiworld) @@ -20,7 +21,7 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestBase): +class TestGenerateDynamicOptions(SVTestCase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: return diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index 0145f471d1..e22c6c3564 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -4,7 +4,7 @@ import random from BaseClasses import MultiWorld from Options import SpecialRange, Range from .option_names import options_to_include -from .. import setup_solo_multiworld, SVTestBase +from .. import setup_solo_multiworld, SVTestCase from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \ assert_festivals_give_access_to_deluxe_scarecrow @@ -72,14 +72,14 @@ def generate_many_worlds(number_worlds: int, start_index: int) -> Dict[int, Mult return multiworlds -def check_every_multiworld_is_valid(tester: SVTestBase, multiworlds: Dict[int, MultiWorld]): +def check_every_multiworld_is_valid(tester: SVTestCase, multiworlds: Dict[int, MultiWorld]): for multiworld_id in multiworlds: multiworld = multiworlds[multiworld_id] with tester.subTest(f"Checking validity of world {multiworld_id}"): check_multiworld_is_valid(tester, multiworld_id, multiworld) -def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld: MultiWorld): +def check_multiworld_is_valid(tester: SVTestCase, multiworld_id: int, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) assert_same_number_items_locations(tester, multiworld) assert_goal_world_is_valid(tester, multiworld) @@ -88,7 +88,7 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld) -class TestGenerateManyWorlds(SVTestBase): +class TestGenerateManyWorlds(SVTestCase): def test_generate_many_worlds_then_check_results(self): if self.skip_long_tests: return diff --git a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py index 0265f61731..bc81f21963 100644 --- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py +++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py @@ -7,45 +7,40 @@ class TestBiggerBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.Mods.internal_name: ModNames.big_backpack} - def test_no_backpack_in_pool(self): - item_names = {item.name for item in self.multiworld.get_items()} - self.assertNotIn("Progressive Backpack", item_names) + def test_no_backpack(self): + with self.subTest(check="no items"): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_names) - def test_no_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Large Pack", location_names) - self.assertNotIn("Deluxe Pack", location_names) - self.assertNotIn("Premium Pack", location_names) + with self.subTest(check="no locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Large Pack", location_names) + self.assertNotIn("Deluxe Pack", location_names) + self.assertNotIn("Premium Pack", location_names) class TestBiggerBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack_is_in_pool_3_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 3) + def test_backpack(self): + with self.subTest(check="has items"): + item_names = [item.name for item in self.multiworld.get_items()] + self.assertEqual(item_names.count("Progressive Backpack"), 3) - def test_3_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) - self.assertIn("Premium Pack", location_names) + with self.subTest(check="has locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Large Pack", location_names) + self.assertIn("Deluxe Pack", location_names) + self.assertIn("Premium Pack", location_names) -class TestBiggerBackpackEarlyProgressive(SVTestBase): +class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack_is_in_pool_3_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 3) + def test_backpack(self): + super().test_backpack() - def test_3_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) - self.assertIn("Premium Pack", location_names) - - def test_progressive_backpack_is_in_early_pool(self): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + with self.subTest(check="is early"): + self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 02fd30a6b1..9bdabaf73f 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,24 +4,17 @@ import random import sys from BaseClasses import MultiWorld -from ...mods.mod_data import ModNames -from .. import setup_solo_multiworld -from ..TestOptions import basic_checks, SVTestBase +from ...mods.mod_data import all_mods +from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods +from ..TestOptions import basic_checks from ... import items, Group, ItemClassification from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions from ...items import item_table, items_by_group from ...locations import location_table from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) - -def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -34,7 +27,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestBase): +class TestGenerateModsOptions(SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in all_mods: @@ -50,6 +43,8 @@ class TestGenerateModsOptions(SVTestBase): multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod}) basic_checks(self, multiworld) check_stray_mod_items(mod, self, multiworld) + if self.skip_extra_tests: + return # assume the rest will work as well class TestBaseItemGeneration(SVTestBase): @@ -103,7 +98,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase): self.assertIn(progression_item.name, all_created_items) -class TestModEntranceRando(unittest.TestCase): +class TestModEntranceRando(SVTestCase): def test_mod_entrance_randomization(self): @@ -137,12 +132,12 @@ class TestModEntranceRando(unittest.TestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestModTraps(SVTestBase): +class TestModTraps(SVTestCase): def test_given_traps_when_generate_then_all_traps_in_pool(self): for value in TrapItems.options: if value == "no_traps": continue - world_options = self.allsanity_options_without_mods() + world_options = allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] From 20dd478fb5a3681fa4d70a2a043b26ae0bb32646 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 28 Oct 2023 03:13:08 +0200 Subject: [PATCH 104/327] OoT: create and copy less useless data state (#2379) --- BaseClasses.py | 4 ++++ worlds/oot/__init__.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d35739c324..735582e139 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -307,6 +307,10 @@ class MultiWorld(): def get_game_players(self, game_name: str) -> Tuple[int, ...]: return tuple(player for player in self.player_ids if self.game[player] == game_name) + @functools.lru_cache() + def get_game_groups(self, game_name: str) -> Tuple[int, ...]: + return tuple(group_id for group_id in self.groups if self.game[group_id] == game_name) + @functools.lru_cache() def get_game_worlds(self, game_name: str): return tuple(world for player, world in self.worlds.items() if diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 6af19683f4..2ac5416906 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -43,14 +43,14 @@ i_o_limiter = threading.Semaphore(2) class OOTCollectionState(metaclass=AutoLogicRegister): def init_mixin(self, parent: MultiWorld): - all_ids = parent.get_all_ids() - self.child_reachable_regions = {player: set() for player in all_ids} - self.adult_reachable_regions = {player: set() for player in all_ids} - self.child_blocked_connections = {player: set() for player in all_ids} - self.adult_blocked_connections = {player: set() for player in all_ids} - self.day_reachable_regions = {player: set() for player in all_ids} - self.dampe_reachable_regions = {player: set() for player in all_ids} - self.age = {player: None for player in all_ids} + oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game) + self.child_reachable_regions = {player: set() for player in oot_ids} + self.adult_reachable_regions = {player: set() for player in oot_ids} + self.child_blocked_connections = {player: set() for player in oot_ids} + self.adult_blocked_connections = {player: set() for player in oot_ids} + self.day_reachable_regions = {player: set() for player in oot_ids} + self.dampe_reachable_regions = {player: set() for player in oot_ids} + self.age = {player: None for player in oot_ids} def copy_mixin(self, ret) -> CollectionState: ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in From bdc15186e76fcf0c5c7f4c2134d2eb77919c48a2 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:40:06 -0400 Subject: [PATCH 105/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20cave=20surf?= =?UTF-8?q?=20bug=20(#2389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45570 -> 45893 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45511 -> 45875 bytes worlds/pokemon_rb/rom_addresses.py | 10 +++++----- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index b7bdda7fbbed37ff0fb9bff23d33460c38cdd1f3..eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1 100644 GIT binary patch literal 45893 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

    RP#AGhVah-Ib>@Av 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Τ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 literal 45511 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 diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 9c6621523c..97faf7bff2 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,10 +1,10 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c1, - "Option_Pitch_Black_Rock_Tunnel": 0x758, - "Option_Blind_Trainers": 0x30c3, - "Option_Trainersanity1": 0x3153, - "Option_Split_Card_Key": 0x3e0c, - "Option_Fix_Combat_Bugs": 0x3e0d, + "Option_Pitch_Black_Rock_Tunnel": 0x75c, + "Option_Blind_Trainers": 0x30c7, + "Option_Trainersanity1": 0x3157, + "Option_Split_Card_Key": 0x3e10, + "Option_Fix_Combat_Bugs": 0x3e11, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, From 7bddea3ee870fe6aa733e4b259348cad0a6f7244 Mon Sep 17 00:00:00 2001 From: kindasneaki Date: Sat, 28 Oct 2023 05:30:18 -0600 Subject: [PATCH 106/327] Hollow Knight: update item name groups (#2331) * add missing groups * remove set comprehensions * fix boss essence * reorganized them * combine boss essence on creation instead of update * rename to match option names * Add missing groups * add PoP totem --- worlds/hk/Items.py | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py index a9acbf48f3..def5c32981 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -19,18 +19,43 @@ lookup_type_to_names: Dict[str, Set[str]] = {} for item, item_data in item_table.items(): lookup_type_to_names.setdefault(item_data.type, set()).add(item) -item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel", - "Relic", "Root", "Map", "Stag", "Cocoon", - "Soul", "DreamWarrior", "DreamBoss")} - directionals = ('', 'Left_', 'Right_') - -item_name_groups.update({ +item_name_groups = ({ + "BossEssence": lookup_type_to_names["DreamWarrior"] | lookup_type_to_names["DreamBoss"], + "BossGeo": lookup_type_to_names["Boss_Geo"], + "CDash": {x + "Crystal_Heart" for x in directionals}, + "Charms": lookup_type_to_names["Charm"], + "CharmNotches": lookup_type_to_names["Notch"], + "Claw": {x + "Mantis_Claw" for x in directionals}, + "Cloak": {x + "Mothwing_Cloak" for x in directionals} | {"Shade_Cloak", "Split_Shade_Cloak"}, + "Dive": {"Desolate_Dive", "Descending_Dark"}, + "LifebloodCocoons": lookup_type_to_names["Cocoon"], "Dreamers": {"Herrah", "Monomon", "Lurien"}, - "Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'}, - "Claw": {x + 'Mantis_Claw' for x in directionals}, - "CDash": {x + 'Crystal_Heart' for x in directionals}, - "Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, + "Fireball": {"Vengeful_Spirit", "Shade_Soul"}, + "GeoChests": lookup_type_to_names["Geo"], + "GeoRocks": lookup_type_to_names["Rock"], + "GrimmkinFlames": lookup_type_to_names["Flame"], + "Grubs": lookup_type_to_names["Grub"], + "JournalEntries": lookup_type_to_names["Journal"], + "JunkPitChests": lookup_type_to_names["JunkPitChest"], + "Keys": lookup_type_to_names["Key"], + "LoreTablets": lookup_type_to_names["Lore"] | lookup_type_to_names["PalaceLore"], + "Maps": lookup_type_to_names["Map"], + "MaskShards": lookup_type_to_names["Mask"], + "Mimics": lookup_type_to_names["Mimic"], + "Nail": lookup_type_to_names["CursedNail"], + "PalaceJournal": {"Journal_Entry-Seal_of_Binding"}, + "PalaceLore": lookup_type_to_names["PalaceLore"], + "PalaceTotem": {"Soul_Totem-Palace", "Soul_Totem-Path_of_Pain"}, + "RancidEggs": lookup_type_to_names["Egg"], + "Relics": lookup_type_to_names["Relic"], + "Scream": {"Howling_Wraiths", "Abyss_Shriek"}, + "Skills": lookup_type_to_names["Skill"], + "SoulTotems": lookup_type_to_names["Soul"], + "Stags": lookup_type_to_names["Stag"], + "VesselFragments": lookup_type_to_names["Vessel"], + "WhisperingRoots": lookup_type_to_names["Root"], + "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, }) item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} From bf46e0e60fa844f37dd96b7d715f99324c952256 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 28 Oct 2023 19:32:12 +0200 Subject: [PATCH 107/327] Core: deprecate Utils.get_options and remove Utils.get_default_options (#2352) * Core: deprecate Utils.get_options and remove Utils.get_default_options * L2AC, Adventure: use settings instead of Utils.get_options --- Utils.py | 12 +++++------- test/general/test_host_yaml.py | 4 ++-- worlds/adventure/Rom.py | 6 ++---- worlds/lufia2ac/Rom.py | 5 ++--- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Utils.py b/Utils.py index 5fb037a173..88807ebec1 100644 --- a/Utils.py +++ b/Utils.py @@ -257,15 +257,13 @@ def get_public_ipv6() -> str: return ip -OptionsType = Settings # TODO: remove ~2 versions after 0.4.1 +OptionsType = Settings # TODO: remove when removing get_options -@cache_argsless -def get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1 - return Settings(None) - - -get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported +def get_options() -> Settings: + # TODO: switch to Utils.deprecate after 0.4.4 + warnings.warn("Utils.get_options() is deprecated. Use the settings API instead.", DeprecationWarning) + return get_settings() def persistent_store(category: str, key: typing.Any, value: typing.Any): diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 9408f95b16..79285d3a63 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -16,7 +16,7 @@ class TestIDs(unittest.TestCase): def test_utils_in_yaml(self) -> None: """Tests that the auto generated host.yaml has default settings in it""" - for option_key, option_set in Utils.get_default_options().items(): + for option_key, option_set in Settings(None).items(): with self.subTest(option_key): self.assertIn(option_key, self.yaml_options) for sub_option_key in option_set: @@ -24,7 +24,7 @@ class TestIDs(unittest.TestCase): def test_yaml_in_utils(self) -> None: """Tests that the auto generated host.yaml shows up in reference calls""" - utils_options = Utils.get_default_options() + utils_options = Settings(None) for option_key, option_set in self.yaml_options.items(): with self.subTest(option_key): self.assertIn(option_key, utils_options) diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py index 62c4019718..9f1ca3fe5e 100644 --- a/worlds/adventure/Rom.py +++ b/worlds/adventure/Rom.py @@ -6,9 +6,8 @@ from typing import Optional, Any import Utils from .Locations import AdventureLocation, LocationData -from Utils import OptionsType +from settings import get_settings from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer -from itertools import chain import bsdiff4 @@ -313,9 +312,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options: OptionsType = Utils.get_options() if not file_name: - file_name = options["adventure_options"]["rom_file"] + file_name = get_settings()["adventure_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index 1da8d235a6..446668d392 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -3,7 +3,7 @@ import os from typing import Optional import Utils -from Utils import OptionsType +from settings import get_settings from worlds.Files import APDeltaPatch L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d" @@ -35,9 +35,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options: OptionsType = Utils.get_options() if not file_name: - file_name = options["lufia2ac_options"]["rom_file"] + file_name = get_settings()["lufia2ac_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name From f5e9fc9b34891a2bf760c1f0cbfc49ce5abd9c40 Mon Sep 17 00:00:00 2001 From: eudaimonistic <94811100+eudaimonistic@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:18:11 -0400 Subject: [PATCH 108/327] Docs, WebHost: Update faq_en.md (#2313) * Update faq_en.md Reorganizing information and adding links to some of the various guides and website pages. Even just adding the Getting Started, Supported Games, and Server Commands links seems like a hefty upgrade. We have good resources, we should make them obvious. I think more can probably be done here, but I already shuffled this around a lot. * Reorganize information again, elaborate single player Sneaki's suggestion makes way more sense organizationally. Added more detail to the single player section to more clearly explain the easiest method. * Usage of multi-world Consistency Co-authored-by: kindasneaki * More multi-world More consistency Co-authored-by: kindasneaki * Revert to multiworld Makes more sense and is colloquially the preferred terminology. * Rework "leaving early" Changed the "What if a player needs to leave early" section into, "Does everyone need to be connected at the same time?" This allows the FAQ to explain briefly what a sync multiworld and an async multiworld is. This is probably good material for the Glossary, but it comes up so much in the Discord that we probably need to explain it here as briefly as possible. This paragraph lends itself to the question of what to do if a player must leave early anyway. * Grammatical, tensing, and voice updates for consistency with other pages I originally authored. --------- Co-authored-by: kindasneaki Co-authored-by: Chris Wilson --- WebHostLib/static/assets/faq/faq_en.md | 109 ++++++++++++++----------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index 74f423df1f..fb1ccd2d6f 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -2,13 +2,62 @@ ## What is a randomizer? -A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A -normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized +A randomizer is a modification of a game which reorganizes the items required to progress through that game. A +normal play-through might require you to use item A to unlock item B, then C, and so forth. In a randomized game, you might first find item C, then A, then B. -This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they -play a randomized game. Putting items in non-standard locations can require the player to think about the game world and -the items they encounter in new and interesting ways. +This transforms the game from a linear experience into a puzzle, presenting players with a new challenge each time they +play. Putting items in non-standard locations can require the player to think about the game world and the items they +encounter in new and interesting ways. + +## What is a multiworld? + +While a randomizer shuffles a game, a multiworld randomizer shuffles that game for multiple players. For example, in a +two player multiworld, players A and B each get their own randomized version of a game, called a world. In each +player's game, they may find items which belong to the other player. If player A finds an item which belongs to +player B, the item will be sent to player B's world over the internet. This creates a cooperative experience, requiring +players to rely upon each other to complete their game. + +## What does multi-game mean? + +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. +Here is a list of our [Supported Games](https://archipelago.gg/games). + +## Can I generate a single-player game with Archipelago? + +Yes. All of our supported games can be generated as single-player experiences both on the website and by installing +the Archipelago generator software. The fastest way to do this is on the website. Find the Supported Game you wish to +play, open the Settings Page, pick your settings, and click Generate Game. + +## How do I get started? + +We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the +software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for +including multiple games, and hosting multiworlds on the website for ease and convenience. + +If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join +our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer +any questions you might have. + +## What are some common terms I should know? + +As randomizers and multiworld randomizers have been around for a while now, there are quite a few common terms used +by the communities surrounding them. A list of Archipelago jargon and terms commonly used by the community can be +found in the [Glossary](/glossary/en). + +## Does everyone need to be connected at the same time? + +There are two different play-styles that are common for Archipelago multiworld sessions. These sessions can either +be considered synchronous (or "sync"), where everyone connects and plays at the same time, or asynchronous (or "async"), +where players connect and play at their own pace. The setup for both is identical. The difference in play-style is how +you and your friends choose to organize and play your multiworld. Most groups decide on the format before creating +their multiworld. + +If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items +in that game belonging to other players are sent out automatically. This allows other players to continue to play +uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en). ## What happens if an item is placed somewhere it is impossible to get? @@ -17,53 +66,15 @@ is to ensure items necessary to complete the game will be accessible to the play rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are comfortable exploiting certain glitches in the game. -## What is a multi-world? - -While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a -two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's -game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the -item will be sent to player B's world over the internet. - -This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete -their game. - -## What happens if a person has to leave early? - -If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the -items in that game which belong to other players are sent out automatically, so other players can continue to play. - -## What does multi-game mean? - -While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows -players to randomize any of a number of supported games, and send items between them. This allows players of different -games to interact with one another in a single multiplayer environment. - -## Can I generate a single-player game with Archipelago? - -Yes. All our supported games can be generated as single-player experiences, and so long as you download the software, -the website is not required to generate them. - -## How do I get started? - -If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join -our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer -any questions you might have. - -## What are some common terms I should know? - -As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms -and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common -to Archipelago and its specific systems please see the [Glossary](/glossary/en). - ## I want to add a game to the Archipelago randomizer. How do I do that? -The best way to get started is to take a look at our code on GitHub -at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). +The best way to get started is to take a look at our code on GitHub: +[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). -There you will find examples of games in the worlds folder -at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). +There, you will find examples of games in the `worlds` folder: +[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). -You may also find developer documentation in the docs folder -at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). +You may also find developer documentation in the `docs` folder: +[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord. From 4b95065c470cd662b36ef1e44031707838c2968d Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:49:07 -0400 Subject: [PATCH 109/327] TLOZ: Update setup doc to include what version of TLOZ is required (#2395) --- worlds/tloz/docs/multiworld_en.md | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index ae53d953b1..df857f16df 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -6,6 +6,7 @@ - Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) - The BizHawk emulator. Versions 2.3.1 and higher are supported. - [BizHawk at TASVideos](https://tasvideos.org/BizHawk) +- Your legally acquired US v1.0 PRG0 ROM file, probably named `Legend of Zelda, The (U) (PRG0) [!].nes` ## Optional Software From 235334676897403d3c976fc24b82e78686cdb99b Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:43:09 +0200 Subject: [PATCH 110/327] minecraft: avoid duplicate prefix in output file name (#2048) --- worlds/minecraft/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index fa992e1e11..187f1fdf19 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -173,7 +173,7 @@ class MinecraftWorld(World): def generate_output(self, output_directory: str) -> None: data = self._get_mc_data() - filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc" + filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) From 253f3e61f74b5866b5427df5673420a8d7bf8028 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Sat, 28 Oct 2023 21:44:16 +0200 Subject: [PATCH 111/327] sm64ex: All Bowser Stages Goal (#2112) --- worlds/sm64ex/Options.py | 7 +++++++ worlds/sm64ex/Rules.py | 7 ++++++- worlds/sm64ex/__init__.py | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index a603b61c58..8a10f3edea 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -88,6 +88,12 @@ class ExclamationBoxes(Choice): option_Off = 0 option_1Ups_Only = 1 +class CompletionType(Choice): + """Set goal for game completion""" + display_name = "Completion Goal" + option_Last_Bowser_Stage = 0 + option_All_Bowser_Stages = 1 + class ProgressiveKeys(DefaultOnToggle): """Keys will first grant you access to the Basement, then to the Secound Floor""" @@ -110,4 +116,5 @@ sm64_options: typing.Dict[str, type(Option)] = { "death_link": DeathLink, "BuddyChecks": BuddyChecks, "ExclamationBoxes": ExclamationBoxes, + "CompletionType" : CompletionType, } diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 7c50ba4708..27b5fc8f7e 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -124,4 +124,9 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) - world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) + if world.CompletionType[player] == "last_bowser_stage": + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) + elif world.CompletionType[player] == "all_bowser_stages": + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ + state.can_reach("Bowser in the Fire Sea", 'Region', player) and \ + state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 6a7a3bd272..3cc87708e7 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -154,6 +154,7 @@ class SM64World(World): "MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value, "StarsToFinish": self.multiworld.StarsToFinish[self.player].value, "DeathLink": self.multiworld.death_link[self.player].value, + "CompletionType" : self.multiworld.CompletionType[self.player].value, } def generate_output(self, output_directory: str): From e8a7200740019de07a535f23060195621cfada8b Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Sat, 28 Oct 2023 13:47:14 -0600 Subject: [PATCH 112/327] Blasphemous: Include ranged attack in logic for all difficulties (#2271) --- worlds/blasphemous/Options.py | 1 + worlds/blasphemous/Rules.py | 8 +++++--- worlds/blasphemous/docs/en_Blasphemous.md | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index ea304d22ed..127a1dc776 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -67,6 +67,7 @@ class StartingLocation(ChoiceIsRandom): class Ending(Choice): """Choose which ending is required to complete the game. + Talking to Tirso in Albero will tell you the selected ending for the current game. Ending A: Collect all thorn upgrades. Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.""" display_name = "Ending" diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 4218fa94cf..5d88292131 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -578,11 +578,12 @@ def rules(blasphemousworld): or state.has("Purified Hand of the Nun", player) or state.has("D01Z02S03[NW]", player) and ( - can_cross_gap(state, logic, player, 1) + can_cross_gap(state, logic, player, 2) or state.has("Lorquiana", player) or aubade(state, player) or state.has("Cantina of the Blue Rose", player) or charge_beam(state, player) + or state.has("Ranged Skill", player) ) )) set_rule(world.get_location("Albero: Lvdovico's 1st reward", player), @@ -702,10 +703,11 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player), lambda state: ( - can_cross_gap(state, logic, player, 1) + can_cross_gap(state, logic, player, 2) or aubade(state, player) or charge_beam(state, player) - or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", \ + "Cloistered Ruby", "Ranged Skill"}, player) or precise_skips_allowed(logic) )) # Doors diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md index 15223213ac..1ff7f5a903 100644 --- a/worlds/blasphemous/docs/en_Blasphemous.md +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -19,6 +19,7 @@ In addition, there are other changes to the game that make it better optimized f - The Apodictic Heart of Mea Culpa can be unequipped. - Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt. - If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them. +- Talking to Tirso in Albero will tell you the selected ending for the current game. ## What has been changed about the side quests? From acfc71b8c9c95d0775d67356f4db12fda19d5c10 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 28 Oct 2023 12:48:31 -0700 Subject: [PATCH 113/327] BizHawkClient: Add support for server passwords (#2306) --- worlds/_bizhawk/context.py | 63 ++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 5d865f3321..ccf747f15a 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo import asyncio +import enum import subprocess import traceback from typing import Any, Dict, Optional @@ -21,6 +22,13 @@ from .client import BizHawkClient, AutoBizHawkClientRegister EXPECTED_SCRIPT_VERSION = 1 +class AuthStatus(enum.IntEnum): + NOT_AUTHENTICATED = 0 + NEED_INFO = 1 + PENDING = 2 + AUTHENTICATED = 3 + + class BizHawkClientCommandProcessor(ClientCommandProcessor): def _cmd_bh(self): """Shows the current status of the client's connection to BizHawk""" @@ -35,6 +43,8 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor): class BizHawkClientContext(CommonContext): command_processor = BizHawkClientCommandProcessor + auth_status: AuthStatus + password_requested: bool client_handler: Optional[BizHawkClient] slot_data: Optional[Dict[str, Any]] = None rom_hash: Optional[str] = None @@ -45,6 +55,8 @@ class BizHawkClientContext(CommonContext): def __init__(self, server_address: Optional[str], password: Optional[str]): super().__init__(server_address, password) + self.auth_status = AuthStatus.NOT_AUTHENTICATED + self.password_requested = False self.client_handler = None self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 @@ -61,10 +73,41 @@ class BizHawkClientContext(CommonContext): def on_package(self, cmd, args): if cmd == "Connected": self.slot_data = args.get("slot_data", None) + self.auth_status = AuthStatus.AUTHENTICATED if self.client_handler is not None: self.client_handler.on_package(self, cmd, args) + async def server_auth(self, password_requested: bool = False): + self.password_requested = password_requested + + if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED: + logger.info("Awaiting connection to BizHawk before authenticating") + return + + if self.client_handler is None: + return + + # Ask handler to set auth + if self.auth is None: + self.auth_status = AuthStatus.NEED_INFO + await self.client_handler.set_auth(self) + + # Handler didn't set auth, ask user for slot name + if self.auth is None: + await self.get_username() + + if password_requested and not self.password: + self.auth_status = AuthStatus.NEED_INFO + await super(BizHawkClientContext, self).server_auth(password_requested) + + await self.send_connect() + self.auth_status = AuthStatus.PENDING + + async def disconnect(self, allow_autoreconnect: bool = False): + self.auth_status = AuthStatus.NOT_AUTHENTICATED + await super().disconnect(allow_autoreconnect) + async def _game_watcher(ctx: BizHawkClientContext): showed_connecting_message = False @@ -109,12 +152,13 @@ async def _game_watcher(ctx: BizHawkClientContext): rom_hash = await get_hash(ctx.bizhawk_ctx) if ctx.rom_hash is not None and ctx.rom_hash != rom_hash: - if ctx.server is not None: + if ctx.server is not None and not ctx.server.socket.closed: logger.info(f"ROM changed. Disconnecting from server.") - await ctx.disconnect(True) ctx.auth = None ctx.username = None + ctx.client_handler = None + await ctx.disconnect(False) ctx.rom_hash = rom_hash if ctx.client_handler is None: @@ -136,15 +180,14 @@ async def _game_watcher(ctx: BizHawkClientContext): except NotConnectedError: continue - # Get slot name and send `Connect` - if ctx.server is not None and ctx.username is None: - await ctx.client_handler.set_auth(ctx) - - if ctx.auth is None: - await ctx.get_username() - - await ctx.send_connect() + # Server auth + if ctx.server is not None and not ctx.server.socket.closed: + if ctx.auth_status == AuthStatus.NOT_AUTHENTICATED: + Utils.async_start(ctx.server_auth(ctx.password_requested)) + else: + ctx.auth_status = AuthStatus.NOT_AUTHENTICATED + # Call the handler's game watcher await ctx.client_handler.game_watcher(ctx) From b874febb1e1c6f930309e9fd2802fb6ee7bb83f4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 28 Oct 2023 16:27:57 -0400 Subject: [PATCH 114/327] Noita: Extra Life change (#2247) * Item rate update, also removed unnecessary reverse region connections * Converted sets into lists, removed empties --- worlds/noita/Items.py | 16 ++++----- worlds/noita/Regions.py | 74 ++++++++++++++--------------------------- 2 files changed, 33 insertions(+), 57 deletions(-) diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index ca53c96233..217f546f29 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/Items.py @@ -84,8 +84,8 @@ item_table: Dict[str, ItemData] = { "Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful), "Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful), "Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful), - "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful), - "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful), + "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), + "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), @@ -95,15 +95,15 @@ item_table: Dict[str, ItemData] = { "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), - "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 2), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), "Random Potion": ItemData(110023, "Items", ItemClassification.filler), "Secret Potion": ItemData(110024, "Items", ItemClassification.filler), "Powder Pouch": ItemData(110025, "Items", ItemClassification.filler), "Chaos Die": ItemData(110026, "Items", ItemClassification.filler), "Greed Die": ItemData(110027, "Items", ItemClassification.filler), - "Kammi": ItemData(110028, "Items", ItemClassification.filler), - "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler), + "Kammi": ItemData(110028, "Items", ItemClassification.filler, 1), + "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1), "Sädekivi": ItemData(110030, "Items", ItemClassification.filler), "Broken Wand": ItemData(110031, "Items", ItemClassification.filler), @@ -122,8 +122,8 @@ filler_weights: Dict[str, int] = { "Wand (Tier 4)": 6, "Wand (Tier 5)": 5, "Wand (Tier 6)": 4, - "Extra Life Perk": 10, - "Random Potion": 9, + "Extra Life Perk": 3, + "Random Potion": 10, "Secret Potion": 10, "Powder Pouch": 10, "Chaos Die": 4, @@ -131,7 +131,7 @@ filler_weights: Dict[str, int] = { "Kammi": 4, "Refreshing Gourd": 4, "Sädekivi": 3, - "Broken Wand": 10, + "Broken Wand": 8, } diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py index a239b437d7..561d483b48 100644 --- a/worlds/noita/Regions.py +++ b/worlds/noita/Regions.py @@ -1,5 +1,5 @@ # Regions are areas in your game that you travel to. -from typing import Dict, Set +from typing import Dict, Set, List from BaseClasses import Entrance, MultiWorld, Region from . import Locations @@ -79,70 +79,46 @@ def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> N # - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game) # - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable # - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1 -noita_connections: Dict[str, Set[str]] = { - "Menu": {"Forest"}, - "Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"}, - "Snowy Wasteland": {"Forest"}, - "Frozen Vault": {"The Vault"}, - "Lake": {"The Laboratory"}, - "Desert": {"Forest"}, - "Floating Island": {"Forest"}, - "Pyramid": {"Hiisi Base"}, - "Overgrown Cavern": {"Sandcave", "Undeground Jungle"}, - "Sandcave": {"Overgrown Cavern"}, +noita_connections: Dict[str, List[str]] = { + "Menu": ["Forest"], + "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"], + "Frozen Vault": ["The Vault"], + "Overgrown Cavern": ["Sandcave"], ### - "Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"}, - "Collapsed Mines": {"Mines", "Dark Cave"}, - "Lava Lake": {"Mines", "Abyss Orb Room"}, - "Abyss Orb Room": {"Lava Lake"}, - "Below Lava Lake": {"Snowy Depths"}, - "Dark Cave": {"Collapsed Mines"}, - "Ancient Laboratory": {"Coal Pits"}, + "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"], + "Lava Lake": ["Abyss Orb Room"], ### - "Coal Pits Holy Mountain": {"Coal Pits"}, - "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"}, - "Fungal Caverns": {"Coal Pits"}, + "Coal Pits Holy Mountain": ["Coal Pits"], + "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"], ### - "Snowy Depths Holy Mountain": {"Snowy Depths"}, - "Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"}, - "Magical Temple": {"Snowy Depths"}, + "Snowy Depths Holy Mountain": ["Snowy Depths"], + "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"], ### - "Hiisi Base Holy Mountain": {"Hiisi Base"}, - "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"}, - "Secret Shop": {"Hiisi Base"}, + "Hiisi Base Holy Mountain": ["Hiisi Base"], + "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"], ### - "Underground Jungle Holy Mountain": {"Underground Jungle"}, - "Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", - "Lukki Lair"}, - "Dragoncave": {"Underground Jungle"}, - "Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"}, - "Snow Chasm": {}, + "Underground Jungle Holy Mountain": ["Underground Jungle"], + "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"], ### - "Vault Holy Mountain": {"The Vault"}, - "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"}, + "Vault Holy Mountain": ["The Vault"], + "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"], ### - "Temple of the Art Holy Mountain": {"Temple of the Art"}, - "Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower", - "Wizards' Den"}, - "Wizards' Den": {"Temple of the Art", "Powerplant"}, - "Powerplant": {"Wizards' Den", "Deep Underground"}, - "The Tower": {"Forest"}, - "Deep Underground": {}, + "Temple of the Art Holy Mountain": ["Temple of the Art"], + "Temple of the Art": ["Laboratory Holy Mountain", "The Tower", "Wizards' Den"], + "Wizards' Den": ["Powerplant"], + "Powerplant": ["Deep Underground"], ### - "Laboratory Holy Mountain": {"The Laboratory"}, - "The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)", "Lake"}, - "Friend Cave": {}, - "The Work": {}, - "The Work (Hell)": {}, + "Laboratory Holy Mountain": ["The Laboratory"], + "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"], ### } -noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) +noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values())) From ff65de146487c1398b9b0b56f867fd01ca91dd59 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 28 Oct 2023 18:32:03 -0400 Subject: [PATCH 115/327] Pokemon R/B: Reenable Rock tunnel location access rules (#2396) --- worlds/pokemon_rb/rules.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 0855e7a108..21dceb75e8 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -103,25 +103,25 @@ def set_rules(multiworld, player): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), # # Rock Tunnel - # "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), # Pokédex check "Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), From d9b076a687b5b5a48b08d088a034ace9346d1188 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:20:28 +0100 Subject: [PATCH 116/327] Stardew Valley: simplify in-place (#2393) this allows skipping multiple simplifications of the same object, e.g. item_rules also update the logic simplification tests to be a proper unittest.TestCase --- worlds/stardew_valley/stardew_rule.py | 16 +++- .../test/TestLogicSimplification.py | 91 ++++++++++--------- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py index 5455a40e7a..9c96de00d3 100644 --- a/worlds/stardew_valley/stardew_rule.py +++ b/worlds/stardew_valley/stardew_rule.py @@ -88,6 +88,7 @@ assert true_ is True_() class Or(StardewRule): rules: FrozenSet[StardewRule] + _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -112,6 +113,7 @@ class Or(StardewRule): rules_list = new_rules self.rules = frozenset(rules_list) + self._simplified = False def __call__(self, state: CollectionState) -> bool: return any(rule(state) for rule in self.rules) @@ -139,6 +141,8 @@ class Or(StardewRule): return min(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: + if self._simplified: + return self if true_ in self.rules: return true_ @@ -151,11 +155,14 @@ class Or(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - return Or(simplified_rules) + self.rules = frozenset(simplified_rules) + self._simplified = True + return self class And(StardewRule): rules: FrozenSet[StardewRule] + _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -180,6 +187,7 @@ class And(StardewRule): rules_list = new_rules self.rules = frozenset(rules_list) + self._simplified = False def __call__(self, state: CollectionState) -> bool: return all(rule(state) for rule in self.rules) @@ -207,6 +215,8 @@ class And(StardewRule): return max(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: + if self._simplified: + return self if false_ in self.rules: return false_ @@ -219,7 +229,9 @@ class And(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - return And(simplified_rules) + self.rules = frozenset(simplified_rules) + self._simplified = True + return self class Count(StardewRule): diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 33b2428098..3f02643b83 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,56 +1,57 @@ +import unittest from .. import True_ from ..logic import Received, Has, False_, And, Or -def test_simplify_true_in_and(): - rules = { - "Wood": True_(), - "Rock": True_(), - } - summer = Received("Summer", 0, 1) - assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer +class TestSimplification(unittest.TestCase): + def test_simplify_true_in_and(self): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + self.assertEqual((Has("Wood", rules) & summer & Has("Rock", rules)).simplify(), + summer) + def test_simplify_false_in_or(self): + rules = { + "Wood": False_(), + "Rock": False_(), + } + summer = Received("Summer", 0, 1) + self.assertEqual((Has("Wood", rules) | summer | Has("Rock", rules)).simplify(), + summer) -def test_simplify_false_in_or(): - rules = { - "Wood": False_(), - "Rock": False_(), - } - summer = Received("Summer", 0, 1) - assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer + def test_simplify_and_in_and(self): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Winter', 0, 1), Received('Spring', 0, 1))) + self.assertEqual(rule.simplify(), + And(Received('Summer', 0, 1), Received('Fall', 0, 1), + Received('Winter', 0, 1), Received('Spring', 0, 1))) + def test_simplify_duplicated_and(self): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) + self.assertEqual(rule.simplify(), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) -def test_simplify_and_in_and(): - rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), - And(Received('Winter', 0, 1), Received('Spring', 0, 1))) - assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), - Received('Spring', 0, 1)) + def test_simplify_or_in_or(self): + rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) + self.assertEqual(rule.simplify(), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1))) + def test_simplify_duplicated_or(self): + rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) + self.assertEqual(rule.simplify(), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) -def test_simplify_duplicated_and(): - rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), - And(Received('Summer', 0, 1), Received('Fall', 0, 1))) - assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1)) + def test_simplify_true_in_or(self): + rule = Or(True_(), Received('Summer', 0, 1)) + self.assertEqual(rule.simplify(), True_()) - -def test_simplify_or_in_or(): - rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), - Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) - assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), - Received('Spring', 0, 1)) - - -def test_simplify_duplicated_or(): - rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), - Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) - assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1)) - - -def test_simplify_true_in_or(): - rule = Or(True_(), Received('Summer', 0, 1)) - assert rule.simplify() == True_() - - -def test_simplify_false_in_and(): - rule = And(False_(), Received('Summer', 0, 1)) - assert rule.simplify() == False_() + def test_simplify_false_in_and(self): + rule = And(False_(), Received('Summer', 0, 1)) + self.assertEqual(rule.simplify(), False_()) From 3e0d1d4e1c6f34605aba1e36480f341e812fe26c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 29 Oct 2023 19:47:37 +0100 Subject: [PATCH 117/327] Core: change Region caching to on_change from on-miss-strategy (#2366) --- BaseClasses.py | 185 ++++++++++++++-------- Main.py | 7 +- Utils.py | 15 ++ test/bases.py | 2 +- test/general/test_locations.py | 3 - worlds/alttp/ItemPool.py | 1 - worlds/alttp/Rom.py | 6 +- worlds/alttp/Rules.py | 28 ++-- worlds/alttp/Shops.py | 3 - worlds/alttp/__init__.py | 41 +++-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/checksfinder/__init__.py | 4 +- worlds/ladx/Locations.py | 2 +- worlds/ladx/__init__.py | 9 +- worlds/meritous/Regions.py | 4 +- worlds/oot/__init__.py | 94 ++++++----- worlds/pokemon_rb/__init__.py | 5 - worlds/pokemon_rb/rom.py | 6 +- worlds/ror2/Rules.py | 3 +- worlds/sm/__init__.py | 6 +- worlds/soe/__init__.py | 2 +- worlds/undertale/__init__.py | 2 +- worlds/witness/hints.py | 4 +- worlds/zillion/__init__.py | 33 ++-- 24 files changed, 265 insertions(+), 202 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 735582e139..973a8d50f0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,14 +1,15 @@ from __future__ import annotations import copy +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 ChainMap, Counter, deque -from collections.abc import Collection +from collections import Counter, deque +from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ Type, ClassVar @@ -47,7 +48,6 @@ class ThreadBarrierProxy: class MultiWorld(): debug_types = False player_name: Dict[int, str] - _region_cache: Dict[int, Dict[str, Region]] difficulty_requirements: dict required_medallions: dict dark_room_logic: Dict[int, str] @@ -57,7 +57,7 @@ class MultiWorld(): plando_connections: List worlds: Dict[int, auto_world] groups: Dict[int, Group] - regions: List[Region] + regions: RegionManager itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -92,6 +92,34 @@ class MultiWorld(): def __getitem__(self, player) -> bool: return self.rule(player) + class RegionManager: + region_cache: Dict[int, Dict[str, Region]] + entrance_cache: Dict[int, Dict[str, Entrance]] + location_cache: Dict[int, Dict[str, Location]] + + def __init__(self, players: int): + self.region_cache = {player: {} for player in range(1, players+1)} + self.entrance_cache = {player: {} for player in range(1, players+1)} + self.location_cache = {player: {} for player in range(1, players+1)} + + def __iadd__(self, other: Iterable[Region]): + self.extend(other) + return self + + def append(self, region: Region): + self.region_cache[region.player][region.name] = region + + def extend(self, regions: Iterable[Region]): + for region in regions: + self.region_cache[region.player][region.name] = region + + def __iter__(self) -> Iterator[Region]: + for regions in self.region_cache.values(): + yield from regions.values() + + def __len__(self): + return sum(len(regions) for regions in self.region_cache.values()) + def __init__(self, players: int): # world-local random state is saved for multiple generations running concurrently self.random = ThreadBarrierProxy(random.Random()) @@ -100,16 +128,12 @@ class MultiWorld(): self.glitch_triforce = False self.algorithm = 'balanced' self.groups = {} - self.regions = [] + self.regions = self.RegionManager(players) self.shops = [] self.itempool = [] self.seed = None self.seed_name: str = "Unavailable" self.precollected_items = {player: [] for player in self.player_ids} - self._cached_entrances = None - self._cached_locations = None - self._entrance_cache = {} - self._location_cache: Dict[Tuple[str, int], Location] = {} self.required_locations = [] self.light_world_light_cone = False self.dark_world_light_cone = False @@ -137,7 +161,6 @@ class MultiWorld(): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val - set_player_attr('_region_cache', {}) set_player_attr('shuffle', "vanilla") set_player_attr('logic', "noglitches") set_player_attr('mode', 'open') @@ -199,7 +222,6 @@ class MultiWorld(): self.game[new_id] = game self.player_types[new_id] = NetUtils.SlotType.group - self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) @@ -333,41 +355,17 @@ class MultiWorld(): def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} - def _recache(self): - """Rebuild world cache""" - self._cached_locations = None - for region in self.regions: - player = region.player - self._region_cache[player][region.name] = region - for exit in region.exits: - self._entrance_cache[exit.name, player] = exit - - for r_location in region.locations: - self._location_cache[r_location.name, player] = r_location - def get_regions(self, player: Optional[int] = None) -> Collection[Region]: - return self.regions if player is None else self._region_cache[player].values() + return self.regions if player is None else self.regions.region_cache[player].values() - def get_region(self, regionname: str, player: int) -> Region: - try: - return self._region_cache[player][regionname] - except KeyError: - self._recache() - return self._region_cache[player][regionname] + def get_region(self, region_name: str, player: int) -> Region: + return self.regions.region_cache[player][region_name] - def get_entrance(self, entrance: str, player: int) -> Entrance: - try: - return self._entrance_cache[entrance, player] - except KeyError: - self._recache() - return self._entrance_cache[entrance, player] + def get_entrance(self, entrance_name: str, player: int) -> Entrance: + return self.regions.entrance_cache[player][entrance_name] - def get_location(self, location: str, player: int) -> Location: - try: - return self._location_cache[location, player] - except KeyError: - self._recache() - return self._location_cache[location, player] + 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: cached = getattr(self, "_all_state", None) @@ -428,28 +426,22 @@ class MultiWorld(): logging.debug('Placed %s at %s', item, location) - def get_entrances(self) -> List[Entrance]: - if self._cached_entrances is None: - self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances] - return self._cached_entrances - - def clear_entrance_cache(self): - self._cached_entrances = None + def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]: + if player is not None: + return self.regions.entrance_cache[player].values() + return Utils.RepeatableChain(tuple(self.regions.entrance_cache[player].values() + for player in self.regions.entrance_cache)) def register_indirect_condition(self, region: Region, entrance: Entrance): """Report that access to this Region can result in unlocking this Entrance, state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic.""" self.indirect_connections.setdefault(region, set()).add(entrance) - def get_locations(self, player: Optional[int] = None) -> List[Location]: - if self._cached_locations is None: - self._cached_locations = [location for region in self.regions for location in region.locations] + def get_locations(self, player: Optional[int] = None) -> Iterable[Location]: if player is not None: - return [location for location in self._cached_locations if location.player == player] - return self._cached_locations - - def clear_location_cache(self): - self._cached_locations = None + return self.regions.location_cache[player].values() + return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values() + for player in self.regions.location_cache)) def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]: return [location for location in self.get_locations(player) if location.item is None] @@ -471,16 +463,17 @@ class MultiWorld(): valid_locations = [location.name for location in self.get_unfilled_locations(player)] else: valid_locations = location_names + relevant_cache = self.regions.location_cache[player] for location_name in valid_locations: - location = self._location_cache.get((location_name, player), None) - if location is not None and location.item is None: + location = relevant_cache.get(location_name, None) + if location and location.item is None: yield location def unlocks_new_location(self, item: Item) -> bool: temp_state = self.state.copy() temp_state.collect(item, True) - for location in self.get_unfilled_locations(): + for location in self.get_unfilled_locations(item.player): if temp_state.can_reach(location) and not self.state.can_reach(location): return True @@ -820,15 +813,83 @@ class Region: locations: List[Location] entrance_type: ClassVar[Type[Entrance]] = Entrance + class Register(MutableSequence): + region_manager: MultiWorld.RegionManager + + def __init__(self, region_manager: MultiWorld.RegionManager): + self._list = [] + self.region_manager = region_manager + + def __getitem__(self, index: int) -> Location: + return self._list.__getitem__(index) + + def __setitem__(self, index: int, value: Location) -> None: + raise NotImplementedError() + + def __len__(self) -> int: + return self._list.__len__() + + # This seems to not be needed, but that's a bit suspicious. + # def __del__(self): + # self.clear() + + def copy(self): + return self._list.copy() + + class LocationRegister(Register): + def __delitem__(self, index: int) -> None: + location: Location = self._list.__getitem__(index) + self._list.__delitem__(index) + del(self.region_manager.location_cache[location.player][location.name]) + + def insert(self, index: int, value: Location) -> None: + self._list.insert(index, value) + self.region_manager.location_cache[value.player][value.name] = value + + class EntranceRegister(Register): + def __delitem__(self, index: int) -> None: + entrance: Entrance = self._list.__getitem__(index) + self._list.__delitem__(index) + del(self.region_manager.entrance_cache[entrance.player][entrance.name]) + + def insert(self, index: int, value: Entrance) -> None: + self._list.insert(index, value) + self.region_manager.entrance_cache[value.player][value.name] = value + + _locations: LocationRegister[Location] + _exits: EntranceRegister[Entrance] + def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name self.entrances = [] - self.exits = [] - self.locations = [] + self._exits = self.EntranceRegister(multiworld.regions) + self._locations = self.LocationRegister(multiworld.regions) self.multiworld = multiworld self._hint_text = hint self.player = player + def get_locations(self): + return self._locations + + def set_locations(self, new): + if new is self._locations: + return + self._locations.clear() + self._locations.extend(new) + + locations = property(get_locations, set_locations) + + def get_exits(self): + return self._exits + + def set_exits(self, new): + if new is self._exits: + return + self._exits.clear() + self._exits.extend(new) + + exits = property(get_exits, set_exits) + def can_reach(self, state: CollectionState) -> bool: if state.stale[self.player]: state.update_reachable_regions(self.player) diff --git a/Main.py b/Main.py index 0995d2091f..691b88b137 100644 --- a/Main.py +++ b/Main.py @@ -122,10 +122,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info('Creating Items.') AutoWorld.call_all(world, "create_items") - # All worlds should have finished creating all regions, locations, and entrances. - # Recache to ensure that they are all visible for locality rules. - world._recache() - logger.info('Calculating Access Rules.') for player in world.player_ids: @@ -233,7 +229,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No region = Region("Menu", group_id, world, "ItemLink") world.regions.append(region) - locations = region.locations = [] + locations = region.locations for item in world.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: @@ -267,7 +263,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.itempool.extend(items_to_add[:itemcount - len(world.itempool)]) if any(world.item_links.values()): - world._recache() world._all_state = None logger.info("Running Item Plando") diff --git a/Utils.py b/Utils.py index 88807ebec1..4cf8ca2200 100644 --- a/Utils.py +++ b/Utils.py @@ -5,6 +5,7 @@ import json import typing import builtins import os +import itertools import subprocess import sys import pickle @@ -903,3 +904,17 @@ def visualize_regions(root_region: Region, file_name: str, *, with open(file_name, "wt", encoding="utf-8") as f: f.write("\n".join(uml)) + + +class RepeatableChain: + def __init__(self, iterable: typing.Iterable): + self.iterable = iterable + + def __iter__(self): + return itertools.chain.from_iterable(self.iterable) + + def __bool__(self): + return any(sub_iterable for sub_iterable in self.iterable) + + def __len__(self): + return sum(len(iterable) for iterable in self.iterable) diff --git a/test/bases.py b/test/bases.py index 5fe4df2014..9911a45bc5 100644 --- a/test/bases.py +++ b/test/bases.py @@ -284,7 +284,7 @@ class WorldTestBase(unittest.TestCase): # basically a shortened reimplementation of this method from core, in order to force the check is done def fulfills_accessibility() -> bool: - locations = self.multiworld.get_locations(1).copy() + locations = list(self.multiworld.get_locations(1)) state = CollectionState(self.multiworld) while locations: sphere: typing.List[Location] = [] diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 2e609a756f..63b3b0f364 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -36,7 +36,6 @@ class TestBase(unittest.TestCase): for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game_name=game_name): multiworld = setup_solo_multiworld(world_type, gen_steps) - multiworld._recache() region_count = len(multiworld.get_regions()) location_count = len(multiworld.get_locations()) @@ -46,14 +45,12 @@ class TestBase(unittest.TestCase): self.assertEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during rule creation") - multiworld._recache() call_all(multiworld, "generate_basic") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during generate_basic") self.assertGreaterEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during generate_basic") - multiworld._recache() call_all(multiworld, "pre_fill") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during pre_fill") diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 806a420f41..88a2d899fc 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -293,7 +293,6 @@ def generate_itempool(world): loc.access_rule = lambda state: has_triforce_pieces(state, player) region.locations.append(loc) - multiworld.clear_location_cache() multiworld.push_item(loc, ItemFactory('Triforce', player), False) loc.event = True diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 47cea8c20e..e1ae0cc6e6 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # patch items - for location in world.get_locations(): - if location.player != player or location.address is None or location.shop_slot is not None: + for location in world.get_locations(player): + if location.address is None or location.shop_slot is not None: continue itemid = location.item.code if location.item is not None else 0x5A @@ -2247,7 +2247,7 @@ def write_strings(rom, world, player): tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' hint_locations = HintLocations.copy() local_random.shuffle(hint_locations) - all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] + all_entrances = list(world.get_entrances(player)) local_random.shuffle(all_entrances) # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 1fddecd8f4..469f4f82ee 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -197,8 +197,13 @@ def global_rules(world, player): # determines which S&Q locations are available - hide from paths since it isn't an in-game location for exit in world.get_region('Menu', player).exits: exit.hide_path = True - - set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) + try: + old_man_sq = world.get_entrance('Old Man S&Q', player) + except KeyError: + pass # it doesn't exist, should be dungeon-only unittests + else: + old_man = world.get_location("Old Man", player) + set_rule(old_man_sq, lambda state: old_man.can_reach(state)) set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) @@ -1526,16 +1531,16 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # Helper functions to determine if the moon pearl is required if inverted: def is_bunny(region): - return region.is_light_world + return region and region.is_light_world def is_link(region): - return region.is_dark_world + return region and region.is_dark_world else: def is_bunny(region): - return region.is_dark_world + return region and region.is_dark_world def is_link(region): - return region.is_light_world + return region and region.is_light_world def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or @@ -1603,21 +1608,20 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): return options_to_access_rule(possible_options) # Add requirements for bunny-impassible caves if link is a bunny in them - for region in [world.get_region(name, player) for name in bunny_impassable_caves]: - + for region in (world.get_region(name, player) for name in bunny_impassable_caves): if not is_bunny(region): continue rule = get_rule_to_add(region) - for exit in region.exits: - add_rule(exit, rule) + for region_exit in region.exits: + add_rule(region_exit, rule) paradox_shop = world.get_region('Light World Death Mountain Shop', player) if is_bunny(paradox_shop): add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop)) # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival - for entrance in world.get_entrances(): - if entrance.player == player and is_bunny(entrance.connected_region): + for entrance in world.get_entrances(player): + if is_bunny(entrance.connected_region): if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] : if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index f17eb1eadb..c0f2e2236e 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -348,7 +348,6 @@ def create_shops(world, player: int): loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) loc.shop_slot_disabled = True shop.region.locations.append(loc) - world.clear_location_cache() class ShopData(NamedTuple): @@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player): if shop.type == ShopType.TakeAny: loc.shop_slot_disabled = True shop.region.locations.append(loc) - world.clear_location_cache() - loc.shop_slot = i diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 65e36da3bd..3af55b768f 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -585,27 +585,26 @@ class ALTTPWorld(World): for player in checks_in_area: checks_in_area[player]["Total"] = 0 - - for location in multiworld.get_locations(): - if location.game == cls.game and type(location.address) is int: - main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance) - if location.parent_region.dungeon: - dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', - 'Inverted Ganons Tower': 'Ganons Tower'} \ - .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) - checks_in_area[location.player][dungeonname].append(location.address) - elif location.parent_region.type == LTTPRegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif location.parent_region.type == LTTPRegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - elif main_entrance.parent_region.type == LTTPRegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - else: - assert False, "Unknown Location area." - # TODO: remove Total as it's duplicated data and breaks consistent typing - checks_in_area[location.player]["Total"] += 1 + for location in multiworld.get_locations(player): + if location.game == cls.game and type(location.address) is int: + main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance) + if location.parent_region.dungeon: + dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', + 'Inverted Ganons Tower': 'Ganons Tower'} \ + .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) + checks_in_area[location.player][dungeonname].append(location.address) + elif location.parent_region.type == LTTPRegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif location.parent_region.type == LTTPRegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + elif main_entrance.parent_region.type == LTTPRegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + else: + assert False, "Unknown Location area." + # TODO: remove Total as it's duplicated data and breaks consistent typing + checks_in_area[location.player]["Total"] += 1 multidata["checks_in_area"].update(checks_in_area) diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 94c30c3493..8ca2791dcf 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -1,5 +1,5 @@ from BaseClasses import CollectionState, ItemClassification -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index feff148651..4978500da0 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -69,8 +69,8 @@ class ChecksFinderWorld(World): def create_regions(self): menu = Region("Menu", self.player, self.multiworld) board = Region("Board", self.player, self.multiworld) - board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) - for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] + board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) + for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] connection = Entrance(self.player, "New Board", menu) menu.exits.append(connection) diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 6c89db3891..1fd6772cdd 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -219,7 +219,7 @@ def create_regions_from_ladxr(player, multiworld, logic): r = LinksAwakeningRegion( name=name, ladxr_region=l, hint="", player=player, world=multiworld) - r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items] + r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items] regions[l] = r for ladxr_location in logic.location_list: diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 1d6c85dd64..d21190bb91 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -231,9 +231,7 @@ class LinksAwakeningWorld(World): # Find instrument, lock # TODO: we should be able to pinpoint the region we want, save a lookup table please found = False - for r in self.multiworld.get_regions(): - if r.player != self.player: - continue + for r in self.multiworld.get_regions(self.player): if r.dungeon_index != item.item_data.dungeon_index: continue for loc in r.locations: @@ -269,10 +267,7 @@ class LinksAwakeningWorld(World): event_location.place_locked_item(self.create_event("Can Play Trendy Game")) self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] - for r in self.multiworld.get_regions(): - if r.player != self.player: - continue - + for r in self.multiworld.get_regions(self.player): # Set aside dungeon locations if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations diff --git a/worlds/meritous/Regions.py b/worlds/meritous/Regions.py index 2c66a024ca..de34570d02 100644 --- a/worlds/meritous/Regions.py +++ b/worlds/meritous/Regions.py @@ -54,12 +54,12 @@ def create_regions(world: MultiWorld, player: int): world.regions.append(boss_region) region_final_boss = Region("Final Boss", player, world) - region_final_boss.locations = [MeritousLocation( + region_final_boss.locations += [MeritousLocation( player, "Wervyn Anixil", None, region_final_boss)] world.regions.append(region_final_boss) region_tfb = Region("True Final Boss", player, world) - region_tfb.locations = [MeritousLocation( + region_tfb.locations += [MeritousLocation( player, "Wervyn Anixil?", None, region_tfb)] world.regions.append(region_tfb) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 2ac5416906..f794171661 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -195,7 +195,8 @@ class OOTWorld(World): setattr(self, option_name, option_value) self.shop_prices = {} - self.regions = [] # internal cache of regions for this world, used later + self.regions = [] # internal caches of regions for this world, used later + self._regions_cache = {} self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -526,6 +527,10 @@ class OOTWorld(World): # We still need to fill the location even if ALR is off. logger.debug('Unreachable location: %s', new_location.name) new_location.player = self.player + # Change some attributes of Drop locations + if new_location.type == 'Drop': + new_location.name = new_region.name + ' ' + new_location.name + new_location.show_in_spoiler = False new_region.locations.append(new_location) if 'events' in region: for event, rule in region['events'].items(): @@ -555,7 +560,8 @@ class OOTWorld(World): self.multiworld.regions.append(new_region) self.regions.append(new_region) - self.multiworld._recache() + self._regions_cache[new_region.name] = new_region + # self.multiworld._recache() def set_scrub_prices(self): # Get Deku Scrub Locations @@ -622,7 +628,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] + boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards'])) boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names] placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None] @@ -636,7 +642,6 @@ class OOTWorld(World): item = prizepool.pop() loc = prize_locs.pop() loc.place_locked_item(item) - self.multiworld.itempool.remove(item) self.hinted_dungeon_reward_locations[item.name] = loc def create_item(self, name: str, allow_arbitrary_name: bool = False): @@ -671,7 +676,7 @@ class OOTWorld(World): self.multiworld.regions.append(menu) self.load_regions_from_json(overworld_data_path) self.load_regions_from_json(bosses_data_path) - start.connect(self.multiworld.get_region('Root', self.player)) + start.connect(self.get_region('Root')) create_dungeons(self) self.parser.create_delayed_rules() @@ -682,16 +687,11 @@ class OOTWorld(World): # Bind entrances to vanilla for region in self.regions: for exit in region.exits: - exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player)) + exit.connect(self.get_region(exit.vanilla_connected_region)) def create_items(self): - # Uniquely rename drop locations for each region and erase them from the spoiler - set_drop_location_names(self) # Generate itempool generate_itempool(self) - # Add dungeon rewards - rewardlist = sorted(list(self.item_name_groups['rewards'])) - self.itempool += map(self.create_item, rewardlist) junk_pool = get_junk_pool(self) removed_items = [] @@ -769,7 +769,7 @@ class OOTWorld(World): # Kill unreachable events that can't be gotten even with all items # Make sure to only kill actual internal events, not in-game "events" - all_state = self.multiworld.get_all_state(False) + all_state = self.multiworld.get_all_state(use_cache=True) all_locations = self.get_locations() reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if @@ -781,7 +781,6 @@ class OOTWorld(World): bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player) if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable: bigpoe.parent_region.locations.remove(bigpoe) - self.multiworld.clear_location_cache() # If fast scarecrow then we need to kill the Pierre location as it will be unreachable if self.free_scarecrow: @@ -997,6 +996,7 @@ class OOTWorld(World): fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, single_player_placement=False, lock=True, allow_excluded=True) + def generate_output(self, output_directory: str): if self.hints != 'none': self.hint_data_available.wait() @@ -1032,30 +1032,6 @@ class OOTWorld(World): player_name=self.multiworld.get_player_name(self.player)) apz5.write() - # Write entrances to spoiler log - all_entrances = self.get_shuffled_entrances() - all_entrances.sort(reverse=True, key=lambda x: x.name) - all_entrances.sort(reverse=True, key=lambda x: x.type) - if not self.decouple_entrances: - while all_entrances: - loadzone = all_entrances.pop() - if loadzone.type != 'Overworld': - if loadzone.primary: - entrance = loadzone - else: - entrance = loadzone.reverse - if entrance.reverse is not None: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) - else: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) - else: - reverse = loadzone.replaces.reverse - if reverse in all_entrances: - all_entrances.remove(reverse) - self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player) - else: - for entrance in all_entrances: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations. @classmethod @@ -1135,6 +1111,7 @@ class OOTWorld(World): for autoworld in multiworld.get_game_worlds("Ocarina of Time"): autoworld.hint_data_available.set() + def fill_slot_data(self): self.collectible_flags_available.wait() return { @@ -1142,6 +1119,7 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } + def modify_multidata(self, multidata: dict): # Replace connect name @@ -1156,6 +1134,7 @@ class OOTWorld(World): continue multidata["precollected_items"][self.player].remove(item_id) + def extend_hint_information(self, er_hint_data: dict): er_hint_data[self.player] = {} @@ -1202,6 +1181,7 @@ class OOTWorld(World): er_hint_data[self.player][location.address] = main_entrance.name logger.debug(f"Set {location.name} hint data to {main_entrance.name}") + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t]) spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n") @@ -1211,6 +1191,32 @@ class OOTWorld(World): for k, v in self.shop_prices.items(): spoiler_handle.write(f"{k}: {v} Rupees\n") + # Write entrances to spoiler log + all_entrances = self.get_shuffled_entrances() + all_entrances.sort(reverse=True, key=lambda x: x.name) + all_entrances.sort(reverse=True, key=lambda x: x.type) + if not self.decouple_entrances: + while all_entrances: + loadzone = all_entrances.pop() + if loadzone.type != 'Overworld': + if loadzone.primary: + entrance = loadzone + else: + entrance = loadzone.reverse + if entrance.reverse is not None: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) + else: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + else: + reverse = loadzone.replaces.reverse + if reverse in all_entrances: + all_entrances.remove(reverse) + self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player) + else: + for entrance in all_entrances: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + + # Key ring handling: # Key rings are multiple items glued together into one, so we need to give # the appropriate number of keys in the collection state when they are @@ -1242,9 +1248,8 @@ class OOTWorld(World): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and - (type == None or entrance.type == type) and - (not only_primary or entrance.primary))] + return [entrance for entrance in self.multiworld.get_entrances(self.player) if ( + (type == None or entrance.type == type) and (not only_primary or entrance.primary))] def get_shuffled_entrances(self, type=None, only_primary=False): return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if @@ -1258,8 +1263,13 @@ class OOTWorld(World): def get_location(self, location): return self.multiworld.get_location(location, self.player) - def get_region(self, region): - return self.multiworld.get_region(region, self.player) + def get_region(self, region_name): + try: + return self._regions_cache[region_name] + except KeyError: + ret = self.multiworld.get_region(region_name, self.player) + self._regions_cache[region_name] = ret + return ret def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 11aa737e0f..6b8008ea0f 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -445,13 +445,9 @@ class PokemonRedBlueWorld(World): # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not # fail. Re-use test_state from previous final loop. evolutions_region = self.multiworld.get_region("Evolution", self.player) - clear_cache = False for location in evolutions_region.locations.copy(): if not test_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) - clear_cache = True - if clear_cache: - self.multiworld.clear_location_cache() if self.multiworld.old_man[self.player] == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 @@ -559,7 +555,6 @@ class PokemonRedBlueWorld(World): else: raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location") - self.multiworld._recache() if self.multiworld.door_shuffle[self.player] == "decoupled": swept_state = self.multiworld.state.copy() diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 4b191d9176..096ab8e0a1 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -546,10 +546,8 @@ def generate_output(self, output_directory: str): write_quizzes(self, data, random) - for location in self.multiworld.get_locations(): - if location.player != self.player: - continue - elif location.party_data: + for location in self.multiworld.get_locations(self.player): + if location.party_data: for party in location.party_data: if not isinstance(party["party_address"], list): addresses = [rom_addresses[party["party_address"]]] diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 7d94177417..65c04d06cb 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -96,8 +96,7 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: # a long enough run to have enough director credits for scavengers and # help prevent being stuck in the same stages until that point.) - for location in multiworld.get_locations(): - if location.player != player: continue # ignore all checks that don't belong to this player + for location in multiworld.get_locations(player): if "Scavenger" in location.name: add_rule(location, lambda state: state.has("Stage_5", player)) # Regions diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index f208e600b9..4b4002c1c8 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -294,7 +294,7 @@ class SMWorld(World): for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions: src_region = self.multiworld.get_region(src.Name, self.player) dest_region = self.multiworld.get_region(dest.Name, self.player) - if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache): + if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]: src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region)) srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player) srcDestEntrance.connect(dest_region) @@ -563,8 +563,8 @@ class SMWorld(World): multiWorldItems: List[ByteEdit] = [] idx = 0 vanillaItemTypesCount = 21 - for itemLoc in self.multiworld.get_locations(): - if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class: + for itemLoc in self.multiworld.get_locations(self.player): + if "Boss" not in locationsDict[itemLoc.name].Class: SMZ3NameToSMType = { "ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb", "Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster", diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 9a8f38cdac..d02a8d02ee 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -417,7 +417,7 @@ class SoEWorld(World): flags += option.to_flag() with open(placement_file, "wb") as f: # generate placement file - for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()): + for location in self.multiworld.get_locations(self.player): item = location.item assert item is not None, "Can't handle unfilled location" if item.code is None or location.address is None: diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 5e36344703..9e784a4a59 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -193,7 +193,7 @@ class UndertaleWorld(World): def create_regions(self): def UndertaleRegion(region_name: str, exits=[]): ret = Region(region_name, self.player, self.multiworld) - ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) + ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) for loc_name, loc_data in advancement_table.items() if loc_data.region == region_name and (loc_name not in exclusion_table["NoStats"] or diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 4fd0edc429..8a9dab54bc 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -228,8 +228,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): if item.player == player and item.code and item.advancement } loc_in_this_world = { - location.name for location in multiworld.get_locations() - if location.player == player and location.address + location.name for location in multiworld.get_locations(player) + if location.address } always_locations = [ diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 1e79f4f133..a5e1bfe1ad 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -329,23 +329,22 @@ class ZillionWorld(World): empty = zz_items[4] multi_item = empty # a different patcher method differentiates empty from ap multi item multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) - for loc in self.multiworld.get_locations(): - if loc.player == self.player: - z_loc = cast(ZillionLocation, loc) - # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) - if z_loc.item is None: - self.logger.warn("generate_output location has no item - is that ok?") - z_loc.zz_loc.item = empty - elif z_loc.item.player == self.player: - z_item = cast(ZillionItem, z_loc.item) - z_loc.zz_loc.item = z_item.zz_item - else: # another player's item - # print(f"put multi item in {z_loc.zz_loc.name}") - z_loc.zz_loc.item = multi_item - multi_items[z_loc.zz_loc.name] = ( - z_loc.item.name, - self.multiworld.get_player_name(z_loc.item.player) - ) + for loc in self.multiworld.get_locations(self.player): + z_loc = cast(ZillionLocation, loc) + # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) + if z_loc.item is None: + self.logger.warn("generate_output location has no item - is that ok?") + z_loc.zz_loc.item = empty + elif z_loc.item.player == self.player: + z_item = cast(ZillionItem, z_loc.item) + z_loc.zz_loc.item = z_item.zz_item + else: # another player's item + # print(f"put multi item in {z_loc.zz_loc.name}") + z_loc.zz_loc.item = multi_item + multi_items[z_loc.zz_loc.name] = ( + z_loc.item.name, + self.multiworld.get_player_name(z_loc.item.player) + ) # debug_zz_loc_ids.sort() # for name, id_ in debug_zz_loc_ids.items(): # print(id_) From 9c80a7c4ec62b24c30fad8b28c386390b8c686d2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 29 Oct 2023 19:53:57 +0100 Subject: [PATCH 118/327] HK: skip for loop (#2390) --- worlds/hk/Rules.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index 4fe4160b4c..2dc512eca7 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -1,5 +1,4 @@ from ..generic.Rules import set_rule, add_rule -from BaseClasses import MultiWorld from ..AutoWorld import World from .GeneratedRules import set_generated_rules from typing import NamedTuple @@ -39,14 +38,12 @@ def hk_set_rule(hk_world: World, location: str, rule): def set_rules(hk_world: World): player = hk_world.player - world = hk_world.multiworld set_generated_rules(hk_world, hk_set_rule) # Shop costs - for region in world.get_regions(player): - for location in region.locations: - if location.costs: - for term, amount in location.costs.items(): - if term == "GEO": # No geo logic! - continue - add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + for location in hk_world.multiworld.get_locations(player): + if location.costs: + for term, amount in location.costs.items(): + if term == "GEO": # No geo logic! + continue + add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) From 36f95b0683568732efc6ad98141265efde42dad2 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 29 Oct 2023 15:02:53 -0400 Subject: [PATCH 119/327] Noita: Fix rare item fill failure for single-player games (#2387) --- worlds/noita/Items.py | 76 ++++++++++++++++++++++++------------------- worlds/noita/Rules.py | 13 ++++---- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index 217f546f29..c859a80394 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/Items.py @@ -44,20 +44,18 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]: return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else [] -def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: - filler_pool = filler_weights.copy() +def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]: + filler_pool = weights.copy() if multiworld.bad_effects[player].value == 0: del filler_pool["Trap"] - return multiworld.random.choices( - population=list(filler_pool.keys()), - weights=list(filler_pool.values()), - k=random_count - ) + return multiworld.random.choices(population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=count) def create_all_items(multiworld: MultiWorld, player: int) -> None: - sum_locations = len(multiworld.get_unfilled_locations(player)) + locations_to_fill = len(multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() @@ -66,9 +64,18 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None: + create_kantele(multiworld.victory_condition[player]) ) - random_count = sum_locations - len(itempool) - itempool += create_random_items(multiworld, player, random_count) + # if there's not enough shop-allowed items in the pool, we can encounter gen issues + # 39 is the number of shop-valid items we need to guarantee + if len(itempool) < 39: + itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool)) + # this is so that it passes tests and gens if you have minimal locations and only one player + if multiworld.players == 1: + for location in multiworld.get_unfilled_locations(player): + if "Shop Item" in location.name: + location.item = create_item(player, itempool.pop()) + locations_to_fill = len(multiworld.get_unfilled_locations(player)) + itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) multiworld.itempool += [create_item(player, name) for name in itempool] @@ -95,7 +102,7 @@ item_table: Dict[str, ItemData] = { "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), - "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 2), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), "Random Potion": ItemData(110023, "Items", ItemClassification.filler), "Secret Potion": ItemData(110024, "Items", ItemClassification.filler), @@ -106,32 +113,35 @@ item_table: Dict[str, ItemData] = { "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1), "Sädekivi": ItemData(110030, "Items", ItemClassification.filler), "Broken Wand": ItemData(110031, "Items", ItemClassification.filler), +} +shop_only_filler_weights: Dict[str, int] = { + "Trap": 15, + "Extra Max HP": 25, + "Spell Refresher": 20, + "Wand (Tier 1)": 10, + "Wand (Tier 2)": 8, + "Wand (Tier 3)": 7, + "Wand (Tier 4)": 6, + "Wand (Tier 5)": 5, + "Wand (Tier 6)": 4, + "Extra Life Perk": 10, } filler_weights: Dict[str, int] = { - "Trap": 15, - "Extra Max HP": 25, - "Spell Refresher": 20, - "Potion": 40, - "Gold (200)": 15, - "Gold (1000)": 6, - "Wand (Tier 1)": 10, - "Wand (Tier 2)": 8, - "Wand (Tier 3)": 7, - "Wand (Tier 4)": 6, - "Wand (Tier 5)": 5, - "Wand (Tier 6)": 4, - "Extra Life Perk": 3, - "Random Potion": 10, - "Secret Potion": 10, - "Powder Pouch": 10, - "Chaos Die": 4, - "Greed Die": 4, - "Kammi": 4, - "Refreshing Gourd": 4, - "Sädekivi": 3, - "Broken Wand": 8, + **shop_only_filler_weights, + "Gold (200)": 15, + "Gold (1000)": 6, + "Potion": 40, + "Random Potion": 9, + "Secret Potion": 10, + "Powder Pouch": 10, + "Chaos Die": 4, + "Greed Die": 4, + "Kammi": 4, + "Refreshing Gourd": 4, + "Sädekivi": 3, + "Broken Wand": 10, } diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 3eb6be5a7c..808dd3a200 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -44,12 +44,10 @@ wand_tiers: List[str] = [ "Wand (Tier 6)", # Temple of the Art ] - items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", "Powder Pouch"] - perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) @@ -155,11 +153,12 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: def create_all_rules(multiworld: MultiWorld, player: int) -> None: - ban_items_from_shops(multiworld, player) - ban_early_high_tier_wands(multiworld, player) - lock_holy_mountains_into_spheres(multiworld, player) - holy_mountain_unlock_conditions(multiworld, player) - biome_unlock_conditions(multiworld, player) + if multiworld.players > 1: + ban_items_from_shops(multiworld, player) + ban_early_high_tier_wands(multiworld, player) + lock_holy_mountains_into_spheres(multiworld, player) + holy_mountain_unlock_conditions(multiworld, player) + biome_unlock_conditions(multiworld, player) victory_unlock_conditions(multiworld, player) # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) From d5745d40513cc520b223003fe4de8555ff4c9460 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Mon, 30 Oct 2023 10:21:29 +1000 Subject: [PATCH 120/327] Muse Dash: Adds the new songs in the Happy Otaku Pack Vol.18 update. (#2398) --- worlds/musedash/MuseDashData.txt | 11 +++++++++-- worlds/musedash/__init__.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index bd07fef7af..5b3ef40e54 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -404,7 +404,7 @@ trippers feeling!|8-4|Give Up TREATMENT Vol.3|True|5|7|9|11 Lilith ambivalence lovers|8-5|Give Up TREATMENT Vol.3|False|5|8|10| Brave My Soul|7-0|Give Up TREATMENT Vol.2|False|4|6|8| Halcyon|7-1|Give Up TREATMENT Vol.2|False|4|7|10| -Crimson Nightingle|7-2|Give Up TREATMENT Vol.2|True|4|7|10| +Crimson Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10| Invader|7-3|Give Up TREATMENT Vol.2|True|3|7|11| Lyrith|7-4|Give Up TREATMENT Vol.2|False|5|7|10| GOODBOUNCE|7-5|Give Up TREATMENT Vol.2|False|4|6|9| @@ -488,4 +488,11 @@ Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10| The Vampire|66-6|Miku in Museland|False|4|6|9| Future Eve|66-7|Miku in Museland|False|4|8|11| Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10| -Shun-ran|66-9|Miku in Museland|False|4|7|9| \ No newline at end of file +Shun-ran|66-9|Miku in Museland|False|4|7|9| +NICE TYPE feat. monii|43-41|MD Plus Project|True|3|6|8| +Rainy Angel|67-0|Happy Otaku Pack Vol.18|True|4|6|9|11 +Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10| +RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10| +Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10| +OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10| +Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 63ce123c93..bfe321b64a 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -49,7 +49,7 @@ class MuseDashWorld(World): game = "Muse Dash" options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions topology_present = False - data_version = 10 + data_version = 11 web = MuseDashWebWorld() # Necessary Data From f81e72686a7ea46451ecf4a61ff4562020e8e059 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 30 Oct 2023 01:22:00 +0100 Subject: [PATCH 121/327] Core: log fill progress (#2382) * Core: log fill progress * Add names to common fill steps * Update Fill.py Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> * cleanup default name --------- Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- Fill.py | 45 +++++++++++++++++++++++++++++++--------- worlds/alttp/Dungeons.py | 3 ++- worlds/alttp/__init__.py | 3 ++- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Fill.py b/Fill.py index 9d5dc0b457..c9660ab708 100644 --- a/Fill.py +++ b/Fill.py @@ -15,6 +15,10 @@ class FillError(RuntimeError): pass +def _log_fill_progress(name: str, placed: int, total_items: int) -> None: + logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.") + + def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState: new_state = base_state.copy() for item in itempool: @@ -26,7 +30,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: 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) -> None: + allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: """ :param world: Multiworld to be filled. :param base_state: State assumed before fill. @@ -38,16 +42,20 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: :param on_place: callback that is called when a placement happens :param allow_partial: only place what is possible. Remaining items will be in the item_pool list. :param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations + :param name: name of this fill step for progress logging purposes """ unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] cleanup_required = False - swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} for item in item_pool: reachable_items.setdefault(item.player, deque()).append(item) + # for progress logging + total = min(len(item_pool), len(locations)) + placed = 0 + while any(reachable_items.values()) and locations: # grab one item per player items_to_place = [items.pop() @@ -152,9 +160,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement + placed += 1 + if not placed % 1000: + _log_fill_progress(name, placed, total) if on_place: on_place(spot_to_fill) + if total > 1000: + _log_fill_progress(name, placed, total) + if cleanup_required: # validate all placements and remove invalid ones state = sweep_from_pool(base_state, []) @@ -198,6 +212,8 @@ def remaining_fill(world: MultiWorld, 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 while locations and itempool: item_to_place = itempool.pop() spot_to_fill: typing.Optional[Location] = None @@ -247,6 +263,12 @@ def remaining_fill(world: MultiWorld, world.push_item(spot_to_fill, item_to_place, False) placements.append(spot_to_fill) + placed += 1 + if not placed % 1000: + _log_fill_progress("Remaining", placed, total) + + if total > 1000: + _log_fill_progress("Remaining", placed, total) if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them @@ -282,7 +304,7 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) - fill_restrictive(world, state, locations, pool) + fill_restrictive(world, state, locations, pool, name="Accessibility Corrections") def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): @@ -352,23 +374,25 @@ def distribute_early_items(world: MultiWorld, player_local = early_local_rest_items[player] fill_restrictive(world, base_state, [loc for loc in early_locations if loc.player == player], - player_local, lock=True, allow_partial=True) + player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_rest_items.extend(early_local_rest_items[player]) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True) + fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True, + name="Early Items") early_locations += early_priority_locations for player in world.player_ids: player_local = early_local_prog_items[player] fill_restrictive(world, base_state, [loc for loc in early_locations if loc.player == player], - player_local, lock=True, allow_partial=True) + player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_prog_items.extend(player_local) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True) + fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True, + name="Early Progression") unplaced_early_items = early_rest_items + early_prog_items if unplaced_early_items: logging.warning("Ran out of early locations for early items. Failed to place " @@ -422,13 +446,14 @@ def distribute_items_restrictive(world: MultiWorld) -> None: if prioritylocations: # "priority fill" - fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, + name="Priority") accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: - # "progression fill" - fill_restrictive(world, world.state, defaultlocations, progitempool) + # "advancement/progression fill" + fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression") if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 630d61e019..a68acf7288 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True) + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + name="LttP Dungeon Items") dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3af55b768f..2666641542 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -470,7 +470,8 @@ class ALTTPWorld(World): prizepool = unplaced_prizes.copy() prize_locs = empty_crystal_locations.copy() world.random.shuffle(prize_locs) - fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True) + fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True, + name="LttP Dungeon Prizes") except FillError as e: lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt) From db978aa48a8914ab911a0af4904c48f53a066d50 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:05:49 -0600 Subject: [PATCH 122/327] OoT Time Optimization (#2401) - Entrance randomizer no longer grows with multiworld - Improved ER success rate again by prioritizing Temple of Time even more - Prefill is faster, has slightly reduced failure rate when map/compass are in dungeon but previous items in any_dungeon (which consumed all available locations), no longer removes items from the main itempool; itemlinked prefill items removed to accomodate improvements - Now triggers only one recache after `generate_basic` instead of one per oot world - Avoids recaches during `create_regions` - All ER temp entrances have unique names (so the entrance cache does not break) --- worlds/oot/Entrance.py | 10 +- worlds/oot/EntranceShuffle.py | 52 ++++---- worlds/oot/Rules.py | 18 ++- worlds/oot/__init__.py | 238 +++++++++++++++++++--------------- 4 files changed, 172 insertions(+), 146 deletions(-) diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index e480c957a6..6c4b6428f5 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -1,6 +1,4 @@ - from BaseClasses import Entrance -from .Regions import TimeOfDay class OOTEntrance(Entrance): game: str = 'Ocarina of Time' @@ -29,16 +27,16 @@ class OOTEntrance(Entrance): self.connected_region = None return previously_connected - def get_new_target(self): + def get_new_target(self, pool_type): root = self.multiworld.get_region('Root Exits', self.player) - target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) + target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root) target_entrance.connect(self.connected_region) target_entrance.replaces = self root.exits.append(target_entrance) return target_entrance - def assume_reachable(self): + def assume_reachable(self, pool_type): if self.assumed == None: - self.assumed = self.get_new_target() + self.assumed = self.get_new_target(pool_type) self.disconnect() return self.assumed diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 3c1b2d78c6..bbdc30490c 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -2,6 +2,7 @@ from itertools import chain import logging from worlds.generic.Rules import set_rule, add_rule +from BaseClasses import CollectionState from .Hints import get_hint_area, HintAreaNotFound from .Regions import TimeOfDay @@ -25,12 +26,12 @@ def set_all_entrances_data(world, player): return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool, ootworld): +def assume_entrance_pool(entrance_pool, ootworld, pool_type): assumed_pool = [] for entrance in entrance_pool: - assumed_forward = entrance.assume_reachable() + assumed_forward = entrance.assume_reachable(pool_type) if entrance.reverse != None and not ootworld.decouple_entrances: - assumed_return = entrance.reverse.assume_reachable() + assumed_return = entrance.reverse.assume_reachable(pool_type) if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)): if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \ (entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances): @@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld): return assumed_pool -def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()): +def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()): one_way_entrances = [] for pool_type in types_to_include: one_way_entrances += world.get_shufflable_entrances(type=pool_type) valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances)) if target_region_names: - return [entrance.get_new_target() for entrance in valid_one_way_entrances + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances if entrance.connected_region.name in target_region_names] - return [entrance.get_new_target() for entrance in valid_one_way_entrances] + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances] # Abbreviations @@ -423,14 +424,14 @@ multi_interior_regions = { } interior_entrance_bias = { - 'Kakariko Village -> Kak Potion Shop Front': 4, - 'Kak Backyard -> Kak Potion Shop Back': 4, - 'Kakariko Village -> Kak Impas House': 3, - 'Kak Impas Ledge -> Kak Impas House Back': 3, - 'Goron City -> GC Shop': 2, - 'Zoras Domain -> ZD Shop': 2, + 'ToT Entrance -> Temple of Time': 4, + 'Kakariko Village -> Kak Potion Shop Front': 3, + 'Kak Backyard -> Kak Potion Shop Back': 3, + 'Kakariko Village -> Kak Impas House': 2, + 'Kak Impas Ledge -> Kak Impas House Back': 2, 'Market Entrance -> Market Guard House': 2, - 'ToT Entrance -> Temple of Time': 1, + 'Goron City -> GC Shop': 1, + 'Zoras Domain -> ZD Shop': 1, } @@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld): player = ootworld.player # Gather locations to keep reachable for validation - all_state = world.get_all_state(use_cache=True) + all_state = ootworld.get_state_with_complete_itempool() + all_state.sweep_for_events(locations=ootworld.get_locations()) locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances @@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld): for pool_type, entrance_pool in one_way_entrance_pools.items(): if pool_type == 'OwlDrop': valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) for target in one_way_target_entrance_pools[pool_type]: set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player)) elif pool_type in {'Spawn', 'WarpSong'}: valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types) # Ensure that the last entrance doesn't assume the rest of the targets are reachable for target in one_way_target_entrance_pools[pool_type]: add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))()) @@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld): target_entrance_pools = {} for pool_type, entrance_pool in entrance_pools.items(): - target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) + target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type) # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() - none_state = all_state.copy() - for item_tuple in none_state.prog_items: - if item_tuple[1] == player: - none_state.prog_items[item_tuple] = 0 + none_state = CollectionState(ootworld.multiworld) # Plando entrances if world.plando_connections[player]: @@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}') logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable - new_all_state = world.get_all_state(use_cache=False) + new_all_state = ootworld.get_state_with_complete_itempool() if not world.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world @@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') -def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): +def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances): - world = ootworld.multiworld player = ootworld.player # Disconnect all root assumed entrances and save original connections @@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran if entrance.connected_region: original_connected_regions[entrance] = entrance.disconnect() - all_state = world.get_all_state(use_cache=False) + all_state = ootworld.get_state_with_complete_itempool() restrictive_entrances = [] soft_entrances = [] @@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all all_state = all_state_orig.copy() none_state = none_state_orig.copy() - all_state.sweep_for_events() - none_state.sweep_for_events() + all_state.sweep_for_events(locations=ootworld.get_locations()) + none_state.sweep_for_events(locations=ootworld.get_locations()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index fa198e0ce1..3da3728c59 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -1,8 +1,12 @@ from collections import deque import logging +import typing from .Regions import TimeOfDay +from .DungeonList import dungeon_table +from .Hints import HintArea from .Items import oot_is_item_of_type +from .LocationList import dungeon_song_locations from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item @@ -150,11 +154,16 @@ def set_rules(ootworld): location = world.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) - if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items: + if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. location = world.get_location('Sheik in Ice Cavern', player) - add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) + add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) + + if ootworld.shuffle_child_trade == 'skip_child_zelda': + # Song from Impa must be local + location = world.get_location('Song from Impa', player) + add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) @@ -176,11 +185,6 @@ def create_shop_rule(location, parser): return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price)) -def limit_to_itemset(location, itemset): - old_rule = location.item_rule - location.item_rule = lambda item: item.name in itemset and old_rule(item) - - # This function should be run once after the shop items are placed in the world. # It should be run before other items are placed in the world so that logic has # the correct checks for them. This is safe to do since every shop is still diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index f794171661..865ad12545 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -170,15 +170,19 @@ class OOTWorld(World): location_name_groups = build_location_name_groups() + def __init__(self, world, player): self.hint_data_available = threading.Event() self.collectible_flags_available = threading.Event() super(OOTWorld, self).__init__(world, player) + @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): rom = Rom(file=get_options()['oot_options']['rom_file']) + + # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) @@ -194,9 +198,10 @@ class OOTWorld(World): option_value = result.current_key setattr(self, option_name, option_value) - self.shop_prices = {} self.regions = [] # internal caches of regions for this world, used later self._regions_cache = {} + + self.shop_prices = {} self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -490,6 +495,8 @@ class OOTWorld(World): # Farore's Wind skippable if not used for this logic trick in Water Temple self.nonadvancement_items.add('Farores Wind') + + # Reads a group of regions from the given JSON file. def load_regions_from_json(self, file_path): region_json = read_json(file_path) @@ -561,8 +568,9 @@ class OOTWorld(World): self.multiworld.regions.append(new_region) self.regions.append(new_region) self._regions_cache[new_region.name] = new_region - # self.multiworld._recache() + + # Sets deku scrub prices def set_scrub_prices(self): # Get Deku Scrub Locations scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}] @@ -591,6 +599,8 @@ class OOTWorld(World): if location.item is not None: location.item.price = price + + # Sets prices for shuffled shop locations def random_shop_prices(self): shop_item_indexes = ['7', '5', '8', '6'] self.shop_prices = {} @@ -616,6 +626,8 @@ class OOTWorld(World): elif self.shopsanity_prices == 'tycoons_wallet': self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + + # Fill boss prizes def fill_bosses(self, bossCount=9): boss_location_names = ( 'Queen Gohma', @@ -644,6 +656,44 @@ class OOTWorld(World): loc.place_locked_item(item) self.hinted_dungeon_reward_locations[item.name] = loc + + # Separate the result from generate_itempool into main and prefill pools + def divide_itempools(self): + prefill_item_types = set() + if self.shopsanity != 'off': + prefill_item_types.add('Shop') + if self.shuffle_song_items != 'any': + prefill_item_types.add('Song') + if self.shuffle_smallkeys != 'keysanity': + prefill_item_types.add('SmallKey') + if self.shuffle_bosskeys != 'keysanity': + prefill_item_types.add('BossKey') + if self.shuffle_hideoutkeys != 'keysanity': + prefill_item_types.add('HideoutSmallKey') + if self.shuffle_ganon_bosskey != 'keysanity': + prefill_item_types.add('GanonBossKey') + if self.shuffle_mapcompass != 'keysanity': + prefill_item_types.update({'Map', 'Compass'}) + + main_items = [] + prefill_items = [] + for item in self.itempool: + if item.type in prefill_item_types: + prefill_items.append(item) + else: + main_items.append(item) + return main_items, prefill_items + + + # only returns proper result after create_items and divide_itempools are run + def get_pre_fill_items(self): + return self.pre_fill_items + + + # Note on allow_arbitrary_name: + # OoT defines many helper items and event names that are treated indistinguishably from regular items, + # but are only defined in the logic files. This means we need to create items for any name. + # Allowing any item name to be created is dangerous in case of plando, so this is a middle ground. def create_item(self, name: str, allow_arbitrary_name: bool = False): if name in item_table: return OOTItem(name, self.player, item_table[name], False, @@ -663,7 +713,9 @@ class OOTWorld(World): location.internal = True return item - def create_regions(self): # create and link regions + + # Create regions, locations, and entrances + def create_regions(self): if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL world_type = 'World' else: @@ -689,6 +741,8 @@ class OOTWorld(World): for exit in region.exits: exit.connect(self.get_region(exit.vanilla_connected_region)) + + # Create items, starting item handling, boss prize fill (before entrance randomizer) def create_items(self): # Generate itempool generate_itempool(self) @@ -714,12 +768,16 @@ class OOTWorld(World): if self.start_with_rupees: self.starting_items['Rupees'] = 999 + # Divide itempool into prefill and main pools + self.itempool, self.pre_fill_items = self.divide_itempools() + self.multiworld.itempool += self.itempool self.remove_from_start_inventory.extend(removed_items) # Fill boss prizes. needs to happen before entrance shuffle self.fill_bosses() + def set_rules(self): # This has to run AFTER creating items but BEFORE set_entrances_based_rules if self.entrance_shuffle: @@ -757,6 +815,7 @@ class OOTWorld(World): set_rules(self) set_entrances_based_rules(self) + def generate_basic(self): # mostly killing locations that shouldn't exist by settings # Gather items for ice trap appearances @@ -769,7 +828,8 @@ class OOTWorld(World): # Kill unreachable events that can't be gotten even with all items # Make sure to only kill actual internal events, not in-game "events" - all_state = self.multiworld.get_all_state(use_cache=True) + all_state = self.get_state_with_complete_itempool() + all_state.sweep_for_events() all_locations = self.get_locations() reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if @@ -791,35 +851,63 @@ class OOTWorld(World): loc = self.multiworld.get_location("Deliver Rutos Letter", self.player) loc.parent_region.locations.remove(loc) + def pre_fill(self): + def prefill_state(base_state): + state = base_state.copy() + for item in self.get_pre_fill_items(): + self.collect(state, item) + state.sweep_for_events(self.get_locations()) + return state + + # Prefill shops, songs, and dungeon items + items = self.get_pre_fill_items() + locations = list(self.multiworld.get_unfilled_locations(self.player)) + self.multiworld.random.shuffle(locations) + + # Set up initial state + state = CollectionState(self.multiworld) + for item in self.itempool: + self.collect(state, item) + state.sweep_for_events(self.get_locations()) + # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - world_items = [item for item in self.multiworld.itempool if item.player == self.player] + type_to_setting = { + 'Map': 'shuffle_mapcompass', + 'Compass': 'shuffle_mapcompass', + 'SmallKey': 'shuffle_smallkeys', + 'BossKey': 'shuffle_bosskeys', + 'HideoutSmallKey': 'shuffle_hideoutkeys', + 'GanonBossKey': 'shuffle_ganon_bosskey', + } + special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1) + for fill_stage in special_fill_types: - stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items)) + stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items)) if not stage_items: continue if fill_stage in ['GanonBossKey', 'HideoutSmallKey']: locations = gather_locations(self.multiworld, fill_stage, self.player) if isinstance(locations, list): for item in stage_items: - self.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, + fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, single_player_placement=True, lock=True, allow_excluded=True) else: for dungeon_info in dungeon_table: dungeon_name = dungeon_info['name'] + dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) + if not dungeon_items: + continue locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) if isinstance(locations, list): - dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) - if not dungeon_items: - continue for item in dungeon_items: - self.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, + fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) # Place songs @@ -835,9 +923,9 @@ class OOTWorld(World): else: raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") - songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) + songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items)) for song in songs: - self.multiworld.itempool.remove(song) + self.pre_fill_items.remove(song) important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions) @@ -860,7 +948,7 @@ class OOTWorld(World): while tries: try: self.multiworld.random.shuffle(song_locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], + fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") except FillError as e: @@ -882,10 +970,8 @@ class OOTWorld(World): # Place shop items # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items if self.shopsanity != 'off': - shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop' - and item.advancement, self.multiworld.itempool)) - shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop' - and not item.advancement, self.multiworld.itempool)) + shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items)) + shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items)) shop_locations = list( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.multiworld.get_unfilled_locations(player=self.player))) @@ -895,30 +981,14 @@ class OOTWorld(World): 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement self.multiworld.random.shuffle(shop_locations) - for item in shop_prog + shop_junk: - self.multiworld.itempool.remove(item) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, + self.pre_fill_items = [] # all prefill should be done + fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, single_player_placement=True, lock=True, allow_excluded=True) fast_fill(self.multiworld, shop_junk, shop_locations) for loc in shop_locations: loc.locked = True set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled - # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it. - impa = self.multiworld.get_location("Song from Impa", self.player) - if self.shuffle_child_trade == 'skip_child_zelda': - if impa.item is None: - candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player) - if candidate_items: - item_to_place = self.multiworld.random.choice(candidate_items) - self.multiworld.itempool.remove(item_to_place) - else: - item_to_place = self.create_item("Recovery Heart") - impa.place_locked_item(item_to_place) - # Give items to startinventory - self.multiworld.push_precollected(impa.item) - self.multiworld.push_precollected(self.create_item("Zeldas Letter")) - # Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge # Check for dungeon ER later if self.logic_rules == 'glitchless': @@ -953,49 +1023,6 @@ class OOTWorld(World): or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None - # Handle item-linked dungeon items and songs - @classmethod - def stage_pre_fill(cls, multiworld: MultiWorld): - special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - for group_id, group in multiworld.groups.items(): - if group['game'] != cls.game: - continue - group_items = [item for item in multiworld.itempool if item.player == group_id] - for fill_stage in special_fill_types: - group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items)) - if not group_stage_items: - continue - if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']: - # No need to subdivide by dungeon name - locations = gather_locations(multiworld, fill_stage, group['players']) - if isinstance(locations, list): - for item in group_stage_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, - single_player_placement=False, lock=True, allow_excluded=True) - if fill_stage == 'Song': - # We don't want song locations to contain progression unless it's a song - # or it was marked as priority. - # We do this manually because we'd otherwise have to either - # iterate twice or do many function calls. - for loc in locations: - if loc.progress_type == LocationProgressType.DEFAULT: - loc.progress_type = LocationProgressType.EXCLUDED - add_item_rule(loc, lambda i: not (i.advancement or i.useful)) - else: - # Perform the fill task once per dungeon - for dungeon_info in dungeon_table: - dungeon_name = dungeon_info['name'] - locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name) - if isinstance(locations, list): - group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items)) - for item in group_dungeon_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, - single_player_placement=False, lock=True, allow_excluded=True) - def generate_output(self, output_directory: str): if self.hints != 'none': @@ -1134,6 +1161,15 @@ class OOTWorld(World): continue multidata["precollected_items"][self.player].remove(item_id) + # If skip child zelda, push item onto autotracker + if self.shuffle_child_trade == 'skip_child_zelda': + impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None) + zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None) + if impa_item_id: + multidata["precollected_items"][self.player].append(impa_item_id) + if zelda_item_id: + multidata["precollected_items"][self.player].append(zelda_item_id) + def extend_hint_information(self, er_hint_data: dict): @@ -1248,17 +1284,15 @@ class OOTWorld(World): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.multiworld.get_entrances(self.player) if ( - (type == None or entrance.type == type) and (not only_primary or entrance.primary))] + return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type) + and (not only_primary or entrance.primary))] def get_shuffled_entrances(self, type=None, only_primary=False): return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled] def get_locations(self): - for region in self.regions: - for loc in region.locations: - yield loc + return self.multiworld.get_locations(self.player) def get_location(self, location): return self.multiworld.get_location(location, self.player) @@ -1271,6 +1305,9 @@ class OOTWorld(World): self._regions_cache[region_name] = ret return ret + def get_entrances(self): + return self.multiworld.get_entrances(self.player) + def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1304,9 +1341,8 @@ class OOTWorld(World): # In particular, ensures that Time Travel needs to be found. def get_state_with_complete_itempool(self): all_state = CollectionState(self.multiworld) - for item in self.multiworld.itempool: - if item.player == self.player: - self.multiworld.worlds[item.player].collect(all_state, item) + for item in self.itempool + self.pre_fill_items: + self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), event=True) @@ -1346,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld, dungeon: str = '' ) -> Optional[List[OOTLocation]]: type_to_setting = { - 'Song': 'shuffle_song_items', 'Map': 'shuffle_mapcompass', 'Compass': 'shuffle_mapcompass', 'SmallKey': 'shuffle_smallkeys', @@ -1365,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld, players = {players} fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players} locations = [] - if item_type == 'Song': - if any(map(lambda v: v == 'any', fill_opts.values())): - return None - for player, option in fill_opts.items(): - if option == 'song': - condition = lambda location: location.type == 'Song' - elif option == 'dungeon': - condition = lambda location: location.name in dungeon_song_locations - locations += filter(condition, multiworld.get_unfilled_locations(player=player)) - else: - if any(map(lambda v: v == 'keysanity', fill_opts.values())): - return None - for player, option in fill_opts.items(): - condition = functools.partial(valid_dungeon_item_location, - multiworld.worlds[player], option, dungeon) - locations += filter(condition, multiworld.get_unfilled_locations(player=player)) + if any(map(lambda v: v == 'keysanity', fill_opts.values())): + return None + for player, option in fill_opts.items(): + condition = functools.partial(valid_dungeon_item_location, + multiworld.worlds[player], option, dungeon) + locations += filter(condition, multiworld.get_unfilled_locations(player=player)) return locations + From d743d10b2cf0d4e7b537065ce083fa80afe3605b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 30 Oct 2023 04:06:40 +0100 Subject: [PATCH 123/327] Core: log completion time if > 1.0 seconds per step (#2345) --- worlds/AutoWorld.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d4fe0f49a2..3e6e60c6f0 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -4,6 +4,7 @@ import hashlib import logging import pathlib import sys +import time from dataclasses import make_dataclass from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \ Union @@ -17,6 +18,8 @@ if TYPE_CHECKING: from . import GamesPackage from settings import Group +perf_logger = logging.getLogger("performance") + class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} @@ -103,10 +106,24 @@ class AutoLogicRegister(type): return new_class +def _timed_call(method: Callable[..., Any], *args: Any, + multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any: + start = time.perf_counter() + ret = method(*args) + taken = time.perf_counter() - start + if taken > 1.0: + if player and multiworld: + perf_logger.info(f"Took {taken} seconds in {method.__qualname__} for player {player}, " + f"named {multiworld.player_name[player]}.") + else: + perf_logger.info(f"Took {taken} seconds in {method.__qualname__}.") + return ret + + def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any: method = getattr(multiworld.worlds[player], method_name) try: - ret = method(*args) + ret = _timed_call(method, *args, multiworld=multiworld, player=player) except Exception as e: message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}." if sys.version_info >= (3, 11, 0): @@ -132,18 +149,15 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" " f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.") - for world_type in sorted(world_types, key=lambda world: world.__name__): - stage_callable = getattr(world_type, f"stage_{method_name}", None) - if stage_callable: - stage_callable(multiworld, *args) + call_stage(multiworld, method_name, *args) def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids} - for world_type in world_types: + for world_type in sorted(world_types, key=lambda world: world.__name__): stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: - stage_callable(multiworld, *args) + _timed_call(stage_callable, multiworld, *args) class WebWorld: From aa56383310fe574ca0cec33a056dcc1a6ad8b8b4 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:13:02 -0400 Subject: [PATCH 124/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20incompatibl?= =?UTF-8?q?e=20option=20combination=20(#2356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 6b8008ea0f..b2ee0702c9 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -463,13 +463,17 @@ class PokemonRedBlueWorld(World): locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - for loc in locs: + if not self.multiworld.key_items_only[self.player]: + rule = None if self.multiworld.fossil_check_item_types[self.player] == "key_items": - add_item_rule(loc, lambda i: i.advancement) + rule = lambda i: i.advancement elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": - add_item_rule(loc, lambda i: i.name in item_groups["Unique"]) + rule = lambda i: i.name in item_groups["Unique"] elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": - add_item_rule(loc, lambda i: not i.advancement) + rule = lambda i: not i.advancement + if rule: + for loc in locs: + add_item_rule(loc, rule) for mon in ([" ".join(self.multiworld.get_location( f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] From d4498948f226245ce08e8bc643f605b2b2b2e0ea Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 30 Oct 2023 15:14:14 -0500 Subject: [PATCH 125/327] Core: return the created entrance when connecting regions (#2406) --- BaseClasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 973a8d50f0..af1f218004 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -920,7 +920,7 @@ class Region: self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> None: + rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -931,6 +931,7 @@ class Region: if rule: exit_.access_rule = rule exit_.connect(connecting_region) + return exit_ def create_exit(self, name: str) -> Entrance: """ From 5f5c48e17b7eadfad57eda65c9f347ee45d8873a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 31 Oct 2023 02:08:56 +0100 Subject: [PATCH 126/327] Core: fix some memory leak sources without removing caching (#2400) * Core: fix some memory leak sources * Core: run gc before detecting memory leaks * Core: restore caching in BaseClasses.MultiWorld * SM: move spheres cache to MultiWorld._sm_spheres to avoid memory leak * Test: add tests for world memory leaks * Test: limit WorldTestBase leak-check to py>=3.11 --------- Co-authored-by: Fabian Dill --- BaseClasses.py | 6 ++-- Generate.py | 17 +++++++--- Utils.py | 27 +++++++++++++++ test/bases.py | 26 +++++++++++++++ test/general/test_memory.py | 16 +++++++++ test/utils/test_caches.py | 66 +++++++++++++++++++++++++++++++++++++ worlds/sm/__init__.py | 11 +++---- 7 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 test/general/test_memory.py create mode 100644 test/utils/test_caches.py diff --git a/BaseClasses.py b/BaseClasses.py index af1f218004..5dcc9daacd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -325,15 +325,15 @@ class MultiWorld(): def player_ids(self) -> Tuple[int, ...]: return tuple(range(1, self.players + 1)) - @functools.lru_cache() + @Utils.cache_self1 def get_game_players(self, game_name: str) -> Tuple[int, ...]: return tuple(player for player in self.player_ids if self.game[player] == game_name) - @functools.lru_cache() + @Utils.cache_self1 def get_game_groups(self, game_name: str) -> Tuple[int, ...]: return tuple(group_id for group_id in self.groups if self.game[group_id] == game_name) - @functools.lru_cache() + @Utils.cache_self1 def get_game_worlds(self, game_name: str): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) diff --git a/Generate.py b/Generate.py index 34a0084e8d..8113d8a0d7 100644 --- a/Generate.py +++ b/Generate.py @@ -7,8 +7,8 @@ import random import string import urllib.parse import urllib.request -from collections import ChainMap, Counter -from typing import Any, Callable, Dict, Tuple, Union +from collections import Counter +from typing import Any, Dict, Tuple, Union import ModuleUpdate @@ -225,7 +225,7 @@ def main(args=None, callback=ERmain): with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: yaml.dump(important, f) - callback(erargs, seed) + return callback(erargs, seed) def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -639,6 +639,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - main() + multiworld = main() + if __debug__: + import gc + import sys + import weakref + weak = weakref.ref(multiworld) + del multiworld + gc.collect() # need to collect to deref all hard references + assert not weak(), f"MultiWorld object was not de-allocated, it's referenced {sys.getrefcount(weak())} times." \ + " This would be a memory leak." # in case of error-free exit should not need confirmation atexit.unregister(confirmation) diff --git a/Utils.py b/Utils.py index 4cf8ca2200..114c2e8103 100644 --- a/Utils.py +++ b/Utils.py @@ -74,6 +74,8 @@ def snes_to_pc(value: int) -> int: RetType = typing.TypeVar("RetType") +S = typing.TypeVar("S") +T = typing.TypeVar("T") def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]: @@ -91,6 +93,31 @@ def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[] return _wrap +def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[S, T], RetType]: + """Specialized cache for self + 1 arg. Does not keep global ref to self and skips building a dict key tuple.""" + + assert function.__code__.co_argcount == 2, "Can only cache 2 argument functions with this cache." + + cache_name = f"__cache_{function.__name__}__" + + @functools.wraps(function) + def wrap(self: S, arg: T) -> RetType: + cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]], + getattr(self, cache_name, None)) + if cache is None: + res = function(self, arg) + setattr(self, cache_name, {arg: res}) + return res + try: + return cache[arg] + except KeyError: + res = function(self, arg) + cache[arg] = res + return res + + return wrap + + def is_frozen() -> bool: return typing.cast(bool, getattr(sys, 'frozen', False)) diff --git a/test/bases.py b/test/bases.py index 9911a45bc5..2054c2d187 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,3 +1,4 @@ +import sys import typing import unittest from argparse import Namespace @@ -107,11 +108,36 @@ class WorldTestBase(unittest.TestCase): game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" auto_construct: typing.ClassVar[bool] = True """ automatically set up a world for each test in this class """ + memory_leak_tested: typing.ClassVar[bool] = False + """ remember if memory leak test was already done for this class """ def setUp(self) -> None: if self.auto_construct: self.world_setup() + def tearDown(self) -> None: + if self.__class__.memory_leak_tested or not self.options or not self.constructed or \ + sys.version_info < (3, 11, 0): # the leak check in tearDown fails in py<3.11 for an unknown reason + # only run memory leak test once per class, only for constructed with non-default options + # default options will be tested in test/general + super().tearDown() + return + + import gc + import weakref + weak = weakref.ref(self.multiworld) + for attr_name in dir(self): # delete all direct references to MultiWorld and World + attr: object = typing.cast(object, getattr(self, attr_name)) + if type(attr) is MultiWorld or isinstance(attr, AutoWorld.World): + delattr(self, attr_name) + state_cache: typing.Optional[typing.Dict[typing.Any, typing.Any]] = getattr(self, "_state_cache", None) + if state_cache is not None: # in case of multiple inheritance with TestBase, we need to clear its cache + state_cache.clear() + gc.collect() + self.__class__.memory_leak_tested = True + self.assertFalse(weak(), f"World {getattr(self, 'game', '')} leaked MultiWorld object") + super().tearDown() + def world_setup(self, seed: typing.Optional[int] = None) -> None: if type(self) is WorldTestBase or \ (hasattr(WorldTestBase, self._testMethodName) diff --git a/test/general/test_memory.py b/test/general/test_memory.py new file mode 100644 index 0000000000..e352b9e875 --- /dev/null +++ b/test/general/test_memory.py @@ -0,0 +1,16 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister +from . import setup_solo_multiworld + + +class TestWorldMemory(unittest.TestCase): + def test_leak(self): + """Tests that worlds don't leak references to MultiWorld or themselves with default options.""" + import gc + import weakref + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + weak = weakref.ref(setup_solo_multiworld(world_type)) + gc.collect() + self.assertFalse(weak(), "World leaked a reference") diff --git a/test/utils/test_caches.py b/test/utils/test_caches.py new file mode 100644 index 0000000000..fc681611f0 --- /dev/null +++ b/test/utils/test_caches.py @@ -0,0 +1,66 @@ +# Tests for caches in Utils.py + +import unittest +from typing import Any + +from Utils import cache_argsless, cache_self1 + + +class TestCacheArgless(unittest.TestCase): + def test_cache(self) -> None: + @cache_argsless + def func_argless() -> object: + return object() + + self.assertTrue(func_argless() is func_argless()) + + if __debug__: # assert only available with __debug__ + def test_invalid_decorator(self) -> None: + with self.assertRaises(Exception): + @cache_argsless # type: ignore[arg-type] + def func_with_arg(_: Any) -> None: + pass + + +class TestCacheSelf1(unittest.TestCase): + def test_cache(self) -> None: + class Cls: + @cache_self1 + def func(self, _: Any) -> object: + return object() + + o1 = Cls() + o2 = Cls() + self.assertTrue(o1.func(1) is o1.func(1)) + self.assertFalse(o1.func(1) is o1.func(2)) + self.assertFalse(o1.func(1) is o2.func(1)) + + def test_gc(self) -> None: + # verify that we don't keep a global reference + import gc + import weakref + + class Cls: + @cache_self1 + def func(self, _: Any) -> object: + return object() + + o = Cls() + _ = o.func(o) # keep a hard ref to the result + r = weakref.ref(o) # keep weak ref to the cache + del o # remove hard ref to the cache + gc.collect() + self.assertFalse(r()) # weak ref should be dead now + + if __debug__: # assert only available with __debug__ + def test_no_self(self) -> None: + with self.assertRaises(Exception): + @cache_self1 # type: ignore[arg-type] + def func() -> Any: + pass + + def test_too_many_args(self) -> None: + with self.assertRaises(Exception): + @cache_self1 # type: ignore[arg-type] + def func(_1: Any, _2: Any, _3: Any) -> Any: + pass diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 4b4002c1c8..e85d79d3ee 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -112,15 +112,12 @@ class SMWorld(World): required_client_version = (0, 2, 6) itemManager: ItemManager - spheres = None Logic.factory('vanilla') def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() self.locations = {} - if SMWorld.spheres != None: - SMWorld.spheres = None super().__init__(world, player) @classmethod @@ -368,7 +365,7 @@ class SMWorld(World): locationsDict[first_local_collected_loc.name]), itemLoc.item.player, True) - for itemLoc in SMWorld.spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) + for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) ] # Having a sorted itemLocs from collection order is required for escapeTrigger when Tourian is Disabled. @@ -376,8 +373,10 @@ class SMWorld(World): # get_spheres could be cached in multiworld? # Another possible solution would be to have a globally accessible list of items in the order in which the get placed in push_item # and use the inversed starting from the first progression item. - if (SMWorld.spheres == None): - SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] + spheres: List[Location] = getattr(self.multiworld, "_sm_spheres", None) + if spheres is None: + spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] + setattr(self.multiworld, "_sm_spheres", spheres) self.itemLocs = [ ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type From d2c541c51c983286fee3f52210aab3f92df512e7 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 31 Oct 2023 05:11:18 -0500 Subject: [PATCH 127/327] SNIClient, ALttP: expose death_text to SNI client, add message to alttp (#1793) --- SNIClient.py | 4 ++-- worlds/alttp/Client.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 0909c61382..062d7a7cbe 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -207,12 +207,12 @@ class SNIContext(CommonContext): self.killing_player_task = asyncio.create_task(deathlink_kill_player(self)) super(SNIContext, self).on_deathlink(data) - async def handle_deathlink_state(self, currently_dead: bool) -> None: + async def handle_deathlink_state(self, currently_dead: bool, death_text: str = "") -> None: # in this state we only care about triggering a death send if self.death_state == DeathState.alive: if currently_dead: self.death_state = DeathState.dead - await self.send_death() + await self.send_death(death_text) # in this state we care about confirming a kill, to move state to dead elif self.death_state == DeathState.killing_player: # this is being handled in deathlink_kill_player(ctx) already diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 22ef2a39a8..edc68473b9 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -520,7 +520,8 @@ class ALTTPSNIClient(SNIClient): gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in DEATH_MODES - await ctx.handle_deathlink_state(currently_dead) + await ctx.handle_deathlink_state(currently_dead, + ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "") gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) From 3bff20a3cfc5e96cc589b02b29fa1dce3affb4bc Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Tue, 31 Oct 2023 14:20:07 -0700 Subject: [PATCH 128/327] WebHost: Round percentage of checks, fix possible 500 error (#2270) * WebHost: Round percentage of checks, fix possible 500 error * Round using str.format in the template How the percentage of checks done should be displayed is a display concern, so it makes sense to just always do it in the template. That way, along with using .format() instead of .round, means we always get exactly the same presentation regardless of whether it ends in .00 (which would not round to two decimal places), is an int (which `round(2)` wouldn't touch at all), etc. * Round percent_total_checks_done in lttp multitracker * Fix non-LttP games showing as 0% done in LttP MultiTracker --- WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 10 ++++++++-- WebHostLib/tracker.py | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html index 2b943a22b0..8eb471be39 100644 --- a/WebHostLib/templates/lttpMultiTracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -153,7 +153,7 @@ {%- endif -%} {% endif %} {%- endfor -%} - {{ percent_total_checks_done[team][player] }} + {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} {%- if activity_timers[(team, player)] -%} {{ activity_timers[(team, player)].total_seconds() }} {%- else -%} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 40d89eb4c6..1a3d353de1 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -55,7 +55,7 @@ {{ checks["Total"] }}/{{ locations[player] | length }} - {{ percent_total_checks_done[team][player] }} + {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} {%- if activity_timers[team, player] -%} {{ activity_timers[team, player].total_seconds() }} {%- else -%} @@ -72,7 +72,13 @@ All Games {{ completed_worlds }}/{{ players|length }} Complete {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} - {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} + + {% if total_locations[team] == 0 %} + 100 + {% else %} + {{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }} + {% endif %} + diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 0d9ead7951..55b98df59e 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1532,9 +1532,11 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s continue player_locations = locations[player] checks_done[team][player]["Total"] = len(locations_checked) - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / - len(player_locations) * 100) \ - if player_locations else 100 + percent_total_checks_done[team][player] = ( + checks_done[team][player]["Total"] / len(player_locations) * 100 + if player_locations + else 100 + ) activity_timers = {} now = datetime.datetime.utcnow() @@ -1690,10 +1692,13 @@ def get_LttP_multiworld_tracker(tracker: UUID): for recipient in recipients: attribute_item(team, recipient, item) checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] += 1 - percent_total_checks_done[team][player] = int( - checks_done[team][player]["Total"] / len(player_locations) * 100) if \ - player_locations else 100 + checks_done[team][player]["Total"] = len(locations_checked) + + percent_total_checks_done[team][player] = ( + checks_done[team][player]["Total"] / len(player_locations) * 100 + if player_locations + else 100 + ) for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: From 560c57fedd3cd7483891f27a1a6f7fc4c0630055 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:20:24 -0400 Subject: [PATCH 129/327] Docs, Various Games: Add Unique Local Commands to Game Page (#2285) * Add Unique Locals Commands to ChecksFinder * Add Unique Locals Commands to MMBN3 Game Page * Add Unique Locals Commands to Ocarina of Time Game Page * Add Unique Locals Commands to Undertale Game Page * Add Unique Locals Commands to Wargroove Game Page * Add Unique Locals Commands to The Legend of Zelda Game Page * Add Unique Locals Commands to Zillion Game Page * Amend Unique Locals Commands on Final Fantasy 1 Game Page * Add Unique Locals Commands to Pokemon R/B Game Page * Grammar fix for FF1 * Corrected sections names to match * Added commands to Starcraft 2 Wings of Liberty game page Co-authored-by: Bicoloursnake <60069210+bicoloursnake@users.noreply.github.com> --------- Co-authored-by: Bicoloursnake <60069210+bicoloursnake@users.noreply.github.com> --- worlds/checksfinder/docs/en_ChecksFinder.md | 13 ++++++++--- worlds/ff1/docs/en_Final Fantasy.md | 3 ++- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 7 ++++++ worlds/oot/docs/en_Ocarina of Time.md | 7 ++++++ .../docs/en_Pokemon Red and Blue.md | 6 +++++ .../docs/en_Starcraft 2 Wings of Liberty.md | 22 ++++++++++++++++++- worlds/tloz/docs/en_The Legend of Zelda.md | 14 +++++++++--- worlds/undertale/docs/en_Undertale.md | 19 ++++++++++++---- worlds/wargroove/docs/en_Wargroove.md | 9 +++++++- worlds/zillion/docs/en_Zillion.md | 10 ++++++++- 10 files changed, 96 insertions(+), 14 deletions(-) diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index bd82660b09..96fb0529df 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -14,11 +14,18 @@ many checks as you have gained items, plus five to start with being available. ## When the player receives an item, what happens? When the player receives an item in ChecksFinder, it either can make the future boards they play be bigger in width or -height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being -bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a number +height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being +bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a +number next to an icon, the number is how many you have gotten and the icon represents which item it is. ## What is the victory condition? Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map -Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. \ No newline at end of file +Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. + +## Unique Local Commands + +The following command is only available when using the ChecksFinderClient to play with Archipelago. + +- `/resync` Manually trigger a resync. diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 8962919743..59fa85d916 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -26,6 +26,7 @@ All local and remote items appear the same. Final Fantasy will say that you rece emulator will display what was found external to the in-game text box. ## Unique Local Commands -The following command is only available when using the FF1Client for the Final Fantasy Randomizer. +The following commands are only available when using the FF1Client for the Final Fantasy Randomizer. - `/nes` Shows the current status of the NES connection. +- `/toggle_msgs` Toggle displaying messages in EmuHawk diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md index 854034d5a8..7ffa4665fd 100644 --- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md +++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md @@ -72,3 +72,10 @@ what item and what player is receiving the item Whenever you have an item pending, the next time you are not in a battle, menu, or dialog box, you will receive a message on screen notifying you of the item and sender, and the item will be added directly to your inventory. + +## Unique Local Commands + +The following commands are only available when using the MMBN3Client to play with Archipelago. + +- `/gba` Check GBA Connection State +- `/debug` Toggle the Debug Text overlay in ROM diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md index b4610878b6..fa8e148957 100644 --- a/worlds/oot/docs/en_Ocarina of Time.md +++ b/worlds/oot/docs/en_Ocarina of Time.md @@ -31,3 +31,10 @@ Items belonging to other worlds are represented by the Zelda's Letter item. When the player receives an item, Link will hold the item above his head and display it to the world. It's good for business! + +## Unique Local Commands + +The following commands are only available when using the OoTClient to play with Archipelago. + +- `/n64` Check N64 Connection State +- `/deathlink` Toggle deathlink from client. Overrides default setting. diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index daefd6b2f7..086ec347f3 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -80,3 +80,9 @@ All items for other games will display simply as "AP ITEM," including those for A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is. If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to you until these have ended. + +## Unique Local Commands + +The following command is only available when using the PokemonClient to play with Archipelago. + +- `/gb` Check Gameboy Connection State diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md index f7c8519a2a..18bda64784 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -31,4 +31,24 @@ The goal is to beat the final mission: 'All In'. The config file determines whic By default, any of StarCraft 2's items (specified above) can be in another player's world. See the [Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) -for more information on how to change this. \ No newline at end of file +for more information on how to change this. + +## Unique Local Commands + +The following commands are only available when using the Starcraft 2 Client to play with Archipelago. + +- `/difficulty [difficulty]` Overrides the difficulty set for the world. + - Options: casual, normal, hard, brutal +- `/game_speed [game_speed]` Overrides the game speed for the world + - Options: default, slower, slow, normal, fast, faster +- `/color [color]` Changes your color (Currently has no effect) + - Options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, + lightgreen, darkgrey, pink, rainbow, random, default +- `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one + player can play the next mission in a chain the other player is doing. +- `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided +- `/available` Get what missions are currently available to play +- `/unfinished` Get what missions are currently available to play and have not had all locations checked +- `/set_path [path]` Menually set the SC2 install directory (if the automatic detection fails) +- `/download_data` Download the most recent release of the necassry files for playing SC2 with Archipelago. Will + overwrite existing files diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md index e443c9b953..7c2e6deda5 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -35,9 +35,17 @@ filler and useful items will cost less, and uncategorized items will be in the m ## Are there any other changes made? -- The map and compass for each dungeon start already acquired, and other items can be found in their place. +- The map and compass for each dungeon start already acquired, and other items can be found in their place. - The Recorder will warp you between all eight levels regardless of Triforce count - - It's possible for this to be your route to level 4! + - It's possible for this to be your route to level 4! - Pressing Select will cycle through your inventory. - Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. -- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file +- What slots from a Take Any Cave have been chosen are similarly tracked. +- + +## Local Unique Commands + +The following commands are only available when using the Zelda1Client to play with Archipelago. + +- `/nes` Check NES Connection State +- `/toggle_msgs` Toggle displaying messages in EmuHawk diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 3905d3bc3e..87011ee16b 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -42,11 +42,22 @@ In the Pacifist run, you are not required to go to the Ruins to spare Toriel. Th Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab. -Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight -Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`, +Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight +Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`, and `Mettaton Plush`. -The Riverperson will only take you to locations you have seen them at, meaning they will only take you to +The Riverperson will only take you to locations you have seen them at, meaning they will only take you to Waterfall if you have seen them at Waterfall at least once. -If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas. \ No newline at end of file +If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas. + +## Unique Local Commands + +The following commands are only available when using the UndertaleClient to play with Archipelago. + +- `/resync` Manually trigger a resync. +- `/patch` Patch the game. +- `/savepath` Redirect to proper save data folder. (Use before connecting!) +- `/auto_patch` Patch the game automatically. +- `/online` Makes you no longer able to see other Undertale players. +- `/deathlink` Toggles deathlink diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md index 18474a4269..f08902535d 100644 --- a/worlds/wargroove/docs/en_Wargroove.md +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -26,9 +26,16 @@ Any of the above items can be in another player's world. ## When the player receives an item, what happens? -When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action +When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action is taken in game. ## What is the goal of this game when randomized? The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`. + +## Unique Local Commands + +The following commands are only available when using the WargrooveClient to play with Archipelago. + +- `/resync` Manually trigger a resync. +- `/commander` Set the current commander to the given commander. diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md index b5d37cc202..06a11b7d79 100644 --- a/worlds/zillion/docs/en_Zillion.md +++ b/worlds/zillion/docs/en_Zillion.md @@ -67,8 +67,16 @@ Note that in "restrictive" mode, Champ is the only one that can get Zillion powe Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it. -When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected. +When you collect an item, you see the name of the player it goes to. You can see in the client log what item was +collected. ## When the player receives an item, what happens? The item collect sound is played. You can see in the client log what item was received. + +## Unique Local Commands + +The following commands are only available when using the ZillionClient to play with Archipelago. + +- `/sms` Tell the client that Zillion is running in RetroArch. +- `/map` Toggle view of the map tracker. From 5726d2f962ec992a28fd3128116a06f7dcafeaa4 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 31 Oct 2023 14:22:02 -0700 Subject: [PATCH 130/327] Fix weighted-settings page (#2408) The request for the JSON file that provides the setting data was missed during the rename in #2037, so prior to this the weighted settings page wasn't rendering at all. --- WebHostLib/static/assets/weighted-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index bdd121eff5..ca5431c331 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -43,7 +43,7 @@ const resetSettings = () => { }; const fetchSettingData = () => new Promise((resolve, reject) => { - fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => { + fetch(new Request(`${window.location.origin}/static/generated/weighted-options.json`)).then((response) => { try{ response.json().then((jsonObj) => resolve(jsonObj)); } catch(error){ reject(error); } }); From dc80f59165bff0199d8d79b2ffaecaa357c51c30 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 31 Oct 2023 14:25:07 -0700 Subject: [PATCH 131/327] WebHost: Expose name groups through the weighted-settings UI (#2327) * Factor out a common function for building lists * Expose name groups through the weighted-settings UI * Fix weighted-settings page The request for the JSON file that provides the setting data was missed during the rename in #2037, so prior to this the weighted settings page wasn't rendering at all. --- WebHostLib/options.py | 6 + WebHostLib/static/assets/weighted-options.js | 289 +++++------------- WebHostLib/static/styles/weighted-options.css | 6 + 3 files changed, 88 insertions(+), 213 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 785785cde0..1a2aab6d88 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -139,7 +139,13 @@ def create(): weighted_options["games"][game_name] = {} weighted_options["games"][game_name]["gameSettings"] = game_options weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names) + weighted_options["games"][game_name]["gameItemGroups"] = [ + group for group in world.item_name_groups.keys() if group != "Everything" + ] weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names) + weighted_options["games"][game_name]["gameLocationGroups"] = [ + group for group in world.location_name_groups.keys() if group != "Everywhere" + ] with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: json.dump(weighted_options, f, indent=2, separators=(',', ': ')) diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index ca5431c331..3811bd42ba 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -428,13 +428,13 @@ class GameSettings { const weightedSettingsDiv = this.#buildWeightedSettingsDiv(); gameDiv.appendChild(weightedSettingsDiv); - const itemPoolDiv = this.#buildItemsDiv(); + const itemPoolDiv = this.#buildItemPoolDiv(); gameDiv.appendChild(itemPoolDiv); const hintsDiv = this.#buildHintsDiv(); gameDiv.appendChild(hintsDiv); - const locationsDiv = this.#buildLocationsDiv(); + const locationsDiv = this.#buildPriorityExclusionDiv(); gameDiv.appendChild(locationsDiv); collapseButton.addEventListener('click', () => { @@ -734,107 +734,17 @@ class GameSettings { break; case 'items-list': - const itemsList = document.createElement('div'); - itemsList.classList.add('simple-list'); - - Object.values(this.data.gameItems).forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); - - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${this.name}-${settingName}-${item}`) - - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('id', `${this.name}-${settingName}-${item}`); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('data-game', this.name); - itemCheckbox.setAttribute('data-setting', settingName); - itemCheckbox.setAttribute('data-option', item.toString()); - itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(item)) { - itemCheckbox.setAttribute('checked', '1'); - } - - const itemName = document.createElement('span'); - itemName.innerText = item.toString(); - - itemLabel.appendChild(itemCheckbox); - itemLabel.appendChild(itemName); - - itemRow.appendChild(itemLabel); - itemsList.appendChild((itemRow)); - }); - + const itemsList = this.#buildItemsDiv(settingName); settingWrapper.appendChild(itemsList); break; case 'locations-list': - const locationsList = document.createElement('div'); - locationsList.classList.add('simple-list'); - - Object.values(this.data.gameLocations).forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-${settingName}-${location}`) - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('id', `${this.name}-${settingName}-${location}`); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', settingName); - locationCheckbox.setAttribute('data-option', location.toString()); - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - - const locationName = document.createElement('span'); - locationName.innerText = location.toString(); - - locationLabel.appendChild(locationCheckbox); - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - locationsList.appendChild((locationRow)); - }); - + const locationsList = this.#buildLocationsDiv(settingName); settingWrapper.appendChild(locationsList); break; case 'custom-list': - const customList = document.createElement('div'); - customList.classList.add('simple-list'); - - Object.values(this.data.gameSettings[settingName].options).forEach((listItem) => { - const customListRow = document.createElement('div'); - customListRow.classList.add('list-row'); - - const customItemLabel = document.createElement('label'); - customItemLabel.setAttribute('for', `${this.name}-${settingName}-${listItem}`) - - const customItemCheckbox = document.createElement('input'); - customItemCheckbox.setAttribute('id', `${this.name}-${settingName}-${listItem}`); - customItemCheckbox.setAttribute('type', 'checkbox'); - customItemCheckbox.setAttribute('data-game', this.name); - customItemCheckbox.setAttribute('data-setting', settingName); - customItemCheckbox.setAttribute('data-option', listItem.toString()); - customItemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(listItem)) { - customItemCheckbox.setAttribute('checked', '1'); - } - - const customItemName = document.createElement('span'); - customItemName.innerText = listItem.toString(); - - customItemLabel.appendChild(customItemCheckbox); - customItemLabel.appendChild(customItemName); - - customListRow.appendChild(customItemLabel); - customList.appendChild((customListRow)); - }); - + const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options); settingWrapper.appendChild(customList); break; @@ -849,7 +759,7 @@ class GameSettings { return settingsWrapper; } - #buildItemsDiv() { + #buildItemPoolDiv() { const itemsDiv = document.createElement('div'); itemsDiv.classList.add('items-div'); @@ -1058,35 +968,7 @@ class GameSettings { itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; - const itemHintsDiv = document.createElement('div'); - itemHintsDiv.classList.add('simple-list'); - this.data.gameItems.forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); - - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${this.name}-start_hints-${item}`); - - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('id', `${this.name}-start_hints-${item}`); - itemCheckbox.setAttribute('data-game', this.name); - itemCheckbox.setAttribute('data-setting', 'start_hints'); - itemCheckbox.setAttribute('data-option', item); - if (this.current.start_hints.includes(item)) { - itemCheckbox.setAttribute('checked', 'true'); - } - itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - itemLabel.appendChild(itemCheckbox); - - const itemName = document.createElement('span'); - itemName.innerText = item; - itemLabel.appendChild(itemName); - - itemRow.appendChild(itemLabel); - itemHintsDiv.appendChild(itemRow); - }); - + const itemHintsDiv = this.#buildItemsDiv('start_hints'); itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); @@ -1095,35 +977,7 @@ class GameSettings { locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; - const locationHintsDiv = document.createElement('div'); - locationHintsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-start_location_hints-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-start_location_hints-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'start_location_hints'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.start_location_hints.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - locationHintsDiv.appendChild(locationRow); - }); - + const locationHintsDiv = this.#buildLocationsDiv('start_location_hints'); locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); @@ -1131,7 +985,7 @@ class GameSettings { return hintsDiv; } - #buildLocationsDiv() { + #buildPriorityExclusionDiv() { const locationsDiv = document.createElement('div'); locationsDiv.classList.add('locations-div'); const locationsHeader = document.createElement('h3'); @@ -1151,35 +1005,7 @@ class GameSettings { priorityLocationsWrapper.classList.add('locations-wrapper'); priorityLocationsWrapper.innerText = 'Priority Locations'; - const priorityLocationsDiv = document.createElement('div'); - priorityLocationsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-priority_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-priority_locations-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'priority_locations'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.priority_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - priorityLocationsDiv.appendChild(locationRow); - }); - + const priorityLocationsDiv = this.#buildLocationsDiv('priority_locations'); priorityLocationsWrapper.appendChild(priorityLocationsDiv); locationsContainer.appendChild(priorityLocationsWrapper); @@ -1188,35 +1014,7 @@ class GameSettings { excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; - const excludeLocationsDiv = document.createElement('div'); - excludeLocationsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-exclude_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-exclude_locations-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'exclude_locations'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.exclude_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - excludeLocationsDiv.appendChild(locationRow); - }); - + const excludeLocationsDiv = this.#buildLocationsDiv('exclude_locations'); excludeLocationsWrapper.appendChild(excludeLocationsDiv); locationsContainer.appendChild(excludeLocationsWrapper); @@ -1224,6 +1022,71 @@ class GameSettings { return locationsDiv; } + // Builds a div for a setting whose value is a list of locations. + #buildLocationsDiv(setting) { + return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups); + } + + // Builds a div for a setting whose value is a list of items. + #buildItemsDiv(setting) { + return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups); + } + + // Builds a div for a setting named `setting` with a list value that can + // contain `items`. + // + // The `groups` option can be a list of additional options for this list + // (usually `item_name_groups` or `location_name_groups`) that are displayed + // in a special section at the top of the list. + #buildListDiv(setting, items, groups = []) { + const div = document.createElement('div'); + div.classList.add('simple-list'); + + groups.forEach((group) => { + const row = this.#addListRow(setting, group); + div.appendChild(row); + }); + + if (groups.length > 0) { + div.appendChild(document.createElement('hr')); + } + + items.forEach((item) => { + const row = this.#addListRow(setting, item); + div.appendChild(row); + }); + + return div; + } + + // Builds and returns a row for a list of checkboxes. + #addListRow(setting, item) { + const row = document.createElement('div'); + row.classList.add('list-row'); + + const label = document.createElement('label'); + label.setAttribute('for', `${this.name}-${setting}-${item}`); + + const checkbox = document.createElement('input'); + checkbox.setAttribute('type', 'checkbox'); + checkbox.setAttribute('id', `${this.name}-${setting}-${item}`); + checkbox.setAttribute('data-game', this.name); + checkbox.setAttribute('data-setting', setting); + checkbox.setAttribute('data-option', item); + if (this.current[setting].includes(item)) { + checkbox.setAttribute('checked', '1'); + } + checkbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + label.appendChild(checkbox); + + const name = document.createElement('span'); + name.innerText = item; + label.appendChild(name); + + row.appendChild(label); + return row; + } + #updateRangeSetting(evt) { const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); diff --git a/WebHostLib/static/styles/weighted-options.css b/WebHostLib/static/styles/weighted-options.css index cc5231634e..8a66ca2370 100644 --- a/WebHostLib/static/styles/weighted-options.css +++ b/WebHostLib/static/styles/weighted-options.css @@ -292,6 +292,12 @@ html{ margin-right: 0.5rem; } +#weighted-settings .simple-list hr{ + width: calc(100% - 2px); + margin: 2px auto; + border-bottom: 1px solid rgb(255 255 255 / 0.6); +} + #weighted-settings .invisible{ display: none; } From d7ec722aba08690b4e4a9361923640740ec467bd Mon Sep 17 00:00:00 2001 From: kindasneaki Date: Tue, 31 Oct 2023 15:34:24 -0600 Subject: [PATCH 132/327] RoR2: update options (#2391) --- worlds/ror2/Options.py | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 79739e85ef..0ed0a87b17 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -16,7 +16,7 @@ class Goal(Choice): display_name = "Game Mode" option_classic = 0 option_explore = 1 - default = 0 + default = 1 class TotalLocations(Range): @@ -48,7 +48,8 @@ class ScavengersPerEnvironment(Range): display_name = "Scavenger per Environment" range_start = 0 range_end = 1 - default = 1 + default = 0 + class ScannersPerEnvironment(Range): """Explore Mode: The number of scanners locations per environment.""" @@ -57,6 +58,7 @@ class ScannersPerEnvironment(Range): range_end = 1 default = 1 + class AltarsPerEnvironment(Range): """Explore Mode: The number of altars locations per environment.""" display_name = "Newts Per Environment" @@ -64,6 +66,7 @@ class AltarsPerEnvironment(Range): range_end = 2 default = 1 + class TotalRevivals(Range): """Total Percentage of `Dio's Best Friend` item put in the item pool.""" display_name = "Total Revives as percentage" @@ -83,6 +86,7 @@ class ItemPickupStep(Range): range_end = 5 default = 1 + class ShrineUseStep(Range): """ Explore Mode: @@ -131,7 +135,6 @@ class DLC_SOTV(Toggle): display_name = "Enable DLC - SOTV" - class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -274,25 +277,8 @@ class ItemWeights(Choice): option_void = 9 - - -# define a class for the weights of the generated item pool. @dataclass -class ROR2Weights: - green_scrap: GreenScrap - red_scrap: RedScrap - yellow_scrap: YellowScrap - white_scrap: WhiteScrap - common_item: CommonItem - uncommon_item: UncommonItem - legendary_item: LegendaryItem - boss_item: BossItem - lunar_item: LunarItem - void_item: VoidItem - equipment: Equipment - -@dataclass -class ROR2Options(PerGameCommonOptions, ROR2Weights): +class ROR2Options(PerGameCommonOptions): goal: Goal total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment @@ -310,4 +296,16 @@ class ROR2Options(PerGameCommonOptions, ROR2Weights): shrine_use_step: ShrineUseStep enable_lunar: AllowLunarItems item_weights: ItemWeights - item_pool_presets: ItemPoolPresetToggle \ No newline at end of file + item_pool_presets: ItemPoolPresetToggle + # define the weights of the generated item pool. + green_scrap: GreenScrap + red_scrap: RedScrap + yellow_scrap: YellowScrap + white_scrap: WhiteScrap + common_item: CommonItem + uncommon_item: UncommonItem + legendary_item: LegendaryItem + boss_item: BossItem + lunar_item: LunarItem + void_item: VoidItem + equipment: Equipment From f701b813080a1bba8bae8e6be7a07fcaa24dce95 Mon Sep 17 00:00:00 2001 From: dennisw100 <100dennisw@gmail.com> Date: Wed, 1 Nov 2023 22:08:04 +0100 Subject: [PATCH 133/327] Docs: Terraria Setup Guide added information about the Upgraded Research Mod (#2338) Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Seldom <38388947+Seldom-SE@users.noreply.github.com> --- worlds/terraria/docs/setup_en.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md index 84744a4a33..b69af591fa 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -31,6 +31,8 @@ highly recommended to use utility mods and features to speed up gameplay, such a - (Can be used to break progression) - Reduced Grinding - Upgraded Research + - (WARNING: Do not use without Journey mode) + - (NOTE: If items you pick up aren't showing up in your inventory, check your research menu. This mod automatically researches certain items.) ## Configuring your YAML File From 19dc0720bae80070c2b6f6a7cfaa19b1797776df Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Wed, 1 Nov 2023 23:39:29 -0600 Subject: [PATCH 134/327] OoT: fix enhanced_map_compass generation failure (#2411) --- worlds/oot/Patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index f83b34183c..0f1d3f4dcb 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2182,7 +2182,7 @@ def patch_rom(world, rom): 'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3), } for dungeon in world.dungeon_mq: - if dungeon in ['Gerudo Training Ground', 'Ganons Castle']: + if dungeon in ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']: pass elif dungeon in ['Bottom of the Well', 'Ice Cavern']: dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon] From 5669579374bcd547d07c614ad0396bd16bfd5e41 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 2 Nov 2023 00:41:20 -0500 Subject: [PATCH 135/327] Core: make state.prog_items a `Dict[int, Counter[str]]` (#2407) --- BaseClasses.py | 22 +++++++++++----------- test/general/test_fill.py | 4 ++-- worlds/AutoWorld.py | 8 ++++---- worlds/alttp/UnderworldGlitchRules.py | 2 +- worlds/alttp/__init__.py | 2 +- worlds/archipidle/Rules.py | 7 +------ worlds/dlcquest/Rules.py | 4 ++-- worlds/dlcquest/__init__.py | 4 ++-- worlds/hk/__init__.py | 16 ++++++++-------- worlds/ladx/Locations.py | 4 ++-- worlds/ladx/__init__.py | 4 ++-- worlds/messenger/__init__.py | 2 +- worlds/oot/__init__.py | 8 ++++---- worlds/smz3/__init__.py | 8 ++++---- worlds/stardew_valley/test/TestRules.py | 2 +- 15 files changed, 46 insertions(+), 51 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5dcc9daacd..a70dd70a92 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -605,7 +605,7 @@ PathValue = Tuple[str, Optional["PathValue"]] class CollectionState(): - prog_items: typing.Counter[Tuple[str, int]] + prog_items: Dict[int, Counter[str]] multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] @@ -617,7 +617,7 @@ class CollectionState(): additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] def __init__(self, parent: MultiWorld): - self.prog_items = Counter() + self.prog_items = {player: Counter() for player in parent.player_ids} self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} @@ -665,7 +665,7 @@ class CollectionState(): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = self.prog_items.copy() + ret.prog_items = copy.deepcopy(self.prog_items) ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in self.reachable_regions} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in @@ -709,23 +709,23 @@ class CollectionState(): self.collect(event.item, True, event) def has(self, item: str, player: int, count: int = 1) -> bool: - return self.prog_items[item, player] >= count + return self.prog_items[player][item] >= count def has_all(self, items: Set[str], player: int) -> bool: """Returns True if each item name of items is in state at least once.""" - return all(self.prog_items[item, player] for item in items) + return all(self.prog_items[player][item] for item in items) def has_any(self, items: Set[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[item, player] for item in items) + return any(self.prog_items[player][item] for item in items) def count(self, item: str, player: int) -> int: - return self.prog_items[item, player] + return self.prog_items[player][item] def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: found: int = 0 for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: - found += self.prog_items[item_name, player] + found += self.prog_items[player][item_name] if found >= count: return True return False @@ -733,11 +733,11 @@ class CollectionState(): def count_group(self, item_name_group: str, player: int) -> int: found: int = 0 for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: - found += self.prog_items[item_name, player] + found += self.prog_items[player][item_name] return found def item_count(self, item: str, player: int) -> int: - return self.prog_items[item, player] + return self.prog_items[player][item] def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: @@ -746,7 +746,7 @@ class CollectionState(): changed = self.multiworld.worlds[item.player].collect(self, item) if not changed and event: - self.prog_items[item.name, item.player] += 1 + self.prog_items[item.player][item.name] += 1 changed = True self.stale[item.player] = True diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 4e8cc2edb7..1e469ef04d 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -455,8 +455,8 @@ class TestFillRestrictive(unittest.TestCase): location.place_locked_item(item) multi_world.state.sweep_for_events() multi_world.state.sweep_for_events() - self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed") - self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times") + self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") + self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times") def test_correct_item_instance_removed_from_pool(self): """Test that a placed item gets removed from the submitted pool""" diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 3e6e60c6f0..d05797cf9e 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -414,16 +414,16 @@ class World(metaclass=AutoWorldRegister): def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: - state.prog_items[name, self.player] += 1 + state.prog_items[self.player][name] += 1 return True return False def remove(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item, True) if name: - state.prog_items[name, self.player] -= 1 - if state.prog_items[name, self.player] < 1: - del (state.prog_items[name, self.player]) + state.prog_items[self.player][name] -= 1 + if state.prog_items[self.player][name] < 1: + del (state.prog_items[self.player][name]) return True return False diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 4b6bc54111..a6aefc7412 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -31,7 +31,7 @@ def fake_pearl_state(state, player): if state.has('Moon Pearl', player): return state fake_state = state.copy() - fake_state.prog_items['Moon Pearl', player] += 1 + fake_state.prog_items[player]['Moon Pearl'] += 1 return fake_state diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 2666641542..d89e65c59d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -830,4 +830,4 @@ class ALttPLogic(LogicMixin): return True if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: return can_buy_unlimited(self, 'Small Key (Universal)', player) - return self.prog_items[item, player] >= count + return self.prog_items[player][item] >= count diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index cdd48e7604..3bf4bad475 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -5,12 +5,7 @@ from ..generic.Rules import set_rule class ArchipIDLELogic(LogicMixin): def _archipidle_location_is_accessible(self, player_id, items_required): - items_received = 0 - for item in self.prog_items: - if item[1] == player_id: - items_received += 1 - - return items_received >= items_required + return sum(self.prog_items[player_id].values()) >= items_required def set_rules(world: MultiWorld, player: int): diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index a11e5c504e..5792d9c3ab 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -12,11 +12,11 @@ def create_event(player, event: str) -> DLCQuestItem: def has_enough_coin(player: int, coin: int): - return lambda state: state.prog_items[" coins", player] >= coin + return lambda state: state.prog_items[player][" coins"] >= coin def has_enough_coin_freemium(player: int, coin: int): - return lambda state: state.prog_items[" coins freemium", player] >= coin + return lambda state: state.prog_items[player][" coins freemium"] >= coin def set_rules(world, player, World_Options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 54d27f7b65..e4e0a29274 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -92,7 +92,7 @@ class DLCqworld(World): if change: suffix = item.coin_suffix if suffix: - state.prog_items[suffix, self.player] += item.coins + state.prog_items[self.player][suffix] += item.coins return change def remove(self, state: CollectionState, item: DLCQuestItem) -> bool: @@ -100,5 +100,5 @@ class DLCqworld(World): if change: suffix = item.coin_suffix if suffix: - state.prog_items[suffix, self.player] -= item.coins + state.prog_items[self.player][suffix] -= item.coins return change diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1a9d4b5d61..c16a108cd1 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -517,12 +517,12 @@ class HKWorld(World): change = super(HKWorld, self).collect(state, item) if change: for effect_name, effect_value in item_effects.get(item.name, {}).items(): - state.prog_items[effect_name, item.player] += effect_value + state.prog_items[item.player][effect_name] += effect_value if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: - if state.prog_items.get(('RIGHTDASH', item.player), 0) and \ - state.prog_items.get(('LEFTDASH', item.player), 0): - (state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \ - ([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2) + if state.prog_items[item.player].get('RIGHTDASH', 0) and \ + state.prog_items[item.player].get('LEFTDASH', 0): + (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \ + ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2) return change def remove(self, state, item: HKItem) -> bool: @@ -530,9 +530,9 @@ class HKWorld(World): if change: for effect_name, effect_value in item_effects.get(item.name, {}).items(): - if state.prog_items[effect_name, item.player] == effect_value: - del state.prog_items[effect_name, item.player] - state.prog_items[effect_name, item.player] -= effect_value + if state.prog_items[item.player][effect_name] == effect_value: + del state.prog_items[item.player][effect_name] + state.prog_items[item.player][effect_name] -= effect_value return change diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 1fd6772cdd..c7b127ef2b 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -124,13 +124,13 @@ class GameStateAdapater: # Don't allow any money usage if you can't get back wasted rupees if item == "RUPEES": if can_farm_rupees(self.state, self.player): - return self.state.prog_items["RUPEES", self.player] + return self.state.prog_items[self.player]["RUPEES"] return 0 elif item.endswith("_USED"): return 0 else: item = ladxr_item_to_la_item_name[item] - return self.state.prog_items.get((item, self.player), default) + return self.state.prog_items[self.player].get(item, default) class LinksAwakeningEntrance(Entrance): diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index d21190bb91..eaaea5be2f 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -513,7 +513,7 @@ class LinksAwakeningWorld(World): change = super().collect(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items["RUPEES", item.player] += rupees + state.prog_items[item.player]["RUPEES"] += rupees return change @@ -521,6 +521,6 @@ class LinksAwakeningWorld(World): change = super().remove(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items["RUPEES", item.player] -= rupees + state.prog_items[item.player]["RUPEES"] -= rupees return change diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 0771989ffc..3fe13a3cb4 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -188,6 +188,6 @@ class MessengerWorld(World): shard_count = int(item.name.strip("Time Shard ()")) if remove: shard_count = -shard_count - state.prog_items["Shards", self.player] += shard_count + state.prog_items[self.player]["Shards"] += shard_count return super().collect_item(state, item, remove) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 865ad12545..9466e7c098 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1260,16 +1260,16 @@ class OOTWorld(World): def collect(self, state: CollectionState, item: OOTItem) -> bool: if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') - state.prog_items[alt_item_name, self.player] += count + state.prog_items[self.player][alt_item_name] += count return True return super().collect(state, item) def remove(self, state: CollectionState, item: OOTItem) -> bool: if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') - state.prog_items[alt_item_name, self.player] -= count - if state.prog_items[alt_item_name, self.player] < 1: - del (state.prog_items[alt_item_name, self.player]) + state.prog_items[self.player][alt_item_name] -= count + if state.prog_items[self.player][alt_item_name] < 1: + del (state.prog_items[self.player][alt_item_name]) return True return super().remove(state, item) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index e2eb2ac80a..2cc2ac97d9 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -470,7 +470,7 @@ class SMZ3World(World): def collect(self, state: CollectionState, item: Item) -> bool: state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: - state.prog_items[item.name, item.player] += 1 + state.prog_items[item.player][item.name] += 1 return True # indicate that a logical state change has occured return False @@ -478,9 +478,9 @@ class SMZ3World(World): name = self.collect_item(state, item, True) if name: state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) - state.prog_items[name, item.player] -= 1 - if state.prog_items[name, item.player] < 1: - del (state.prog_items[name, item.player]) + state.prog_items[item.player][item.name] -= 1 + if state.prog_items[item.player][item.name] < 1: + del (state.prog_items[item.player][item.name]) return True return False diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 0847d8a63b..72337812cd 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -24,7 +24,7 @@ class TestProgressiveToolsLogic(SVTestBase): def setUp(self): super().setUp() - self.multiworld.state.prog_items = Counter() + self.multiworld.state.prog_items = {1: Counter()} def test_sturgeon(self): self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state)) From ec70cfc7985551bcf547a582009a4e051ca8092d Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:02:38 -0600 Subject: [PATCH 136/327] OoT: fix incorrect calls to sweep_for_events (#2417) --- worlds/oot/Rules.py | 3 ++- worlds/oot/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 3da3728c59..529411f6fc 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -227,7 +227,8 @@ def set_shop_rules(ootworld): # The goal is to automatically set item rules based on age requirements in case entrances were shuffled def set_entrances_based_rules(ootworld): - all_state = ootworld.multiworld.get_all_state(False) + all_state = ootworld.get_state_with_complete_itempool() + all_state.sweep_for_events(locations=ootworld.get_locations()) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): # If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 9466e7c098..e9c889d6f6 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -829,8 +829,8 @@ class OOTWorld(World): # Kill unreachable events that can't be gotten even with all items # Make sure to only kill actual internal events, not in-game "events" all_state = self.get_state_with_complete_itempool() - all_state.sweep_for_events() all_locations = self.get_locations() + all_state.sweep_for_events(locations=all_locations) reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if (loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable] @@ -858,7 +858,7 @@ class OOTWorld(World): state = base_state.copy() for item in self.get_pre_fill_items(): self.collect(state, item) - state.sweep_for_events(self.get_locations()) + state.sweep_for_events(locations=self.get_locations()) return state # Prefill shops, songs, and dungeon items @@ -870,7 +870,7 @@ class OOTWorld(World): state = CollectionState(self.multiworld) for item in self.itempool: self.collect(state, item) - state.sweep_for_events(self.get_locations()) + state.sweep_for_events(locations=self.get_locations()) # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] From 880326c9a58676ab9e763254f1520005a74ce072 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:08:36 +0100 Subject: [PATCH 137/327] SM: fix missed SMWorld.spheres in #2400 (#2419) --- worlds/sm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index e85d79d3ee..3e9015eab7 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -389,7 +389,7 @@ class SMWorld(World): escapeTrigger = None if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"]: #used to simulate received items - first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player) + first_local_collected_loc = next(itemLoc for itemLoc in spheres if itemLoc.player == self.player) playerItemsItemLocs = get_player_ItemLocation(False) playerProgItemsItemLocs = get_player_ItemLocation(True) From d2e9bfb196b4253c38d7f66d8ad0fb0abd30e06a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 4 Nov 2023 10:26:51 +0100 Subject: [PATCH 138/327] AppImage: allow loading apworlds from ~/Archipelago and copy scripts (#2358) also fixes some mypy and flake8 violations in worlds/__init__.py --- Utils.py | 10 +++++++--- worlds/__init__.py | 42 +++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/Utils.py b/Utils.py index 114c2e8103..bb68602cce 100644 --- a/Utils.py +++ b/Utils.py @@ -174,12 +174,16 @@ def user_path(*path: str) -> str: if user_path.cached_path != local_path(): import filecmp if not os.path.exists(user_path("manifest.json")) or \ + not os.path.exists(local_path("manifest.json")) or \ not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True): import shutil - for dn in ("Players", "data/sprites"): + for dn in ("Players", "data/sprites", "data/lua"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - for fn in ("manifest.json",): - shutil.copy2(local_path(fn), user_path(fn)) + if not os.path.exists(local_path("manifest.json")): + warnings.warn(f"Upgrading {user_path()} from something that is not a proper install") + else: + shutil.copy2(local_path("manifest.json"), user_path("manifest.json")) + os.makedirs(user_path("worlds"), exist_ok=True) return os.path.join(user_path.cached_path, *path) diff --git a/worlds/__init__.py b/worlds/__init__.py index c6208fa9a1..40e0b20f19 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -5,19 +5,20 @@ import typing import warnings import zipimport -folder = os.path.dirname(__file__) +from Utils import user_path, local_path -__all__ = { +local_folder = os.path.dirname(__file__) +user_folder = user_path("worlds") if user_path() != local_path() else None + +__all__ = ( "lookup_any_item_id_to_name", "lookup_any_location_id_to_name", "network_data_package", "AutoWorldRegister", "world_sources", - "folder", -} - -if typing.TYPE_CHECKING: - from .AutoWorld import World + "local_folder", + "user_folder", +) class GamesData(typing.TypedDict): @@ -41,13 +42,13 @@ class WorldSource(typing.NamedTuple): is_zip: bool = False relative: bool = True # relative to regular world import folder - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})" @property def resolved_path(self) -> str: if self.relative: - return os.path.join(folder, self.path) + return os.path.join(local_folder, self.path) return self.path def load(self) -> bool: @@ -56,6 +57,7 @@ class WorldSource(typing.NamedTuple): importer = zipimport.zipimporter(self.resolved_path) if hasattr(importer, "find_spec"): # new in Python 3.10 spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) + assert spec, f"{self.path} is not a loadable module" mod = importlib.util.module_from_spec(spec) else: # TODO: remove with 3.8 support mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) @@ -72,7 +74,7 @@ class WorldSource(typing.NamedTuple): importlib.import_module(f".{self.path}", "worlds") return True - except Exception as e: + except Exception: # A single world failing can still mean enough is working for the user, log and carry on import traceback import io @@ -87,14 +89,16 @@ class WorldSource(typing.NamedTuple): # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] -file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly -for file in os.scandir(folder): - # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "." - if not file.name.startswith(("_", ".")): - if file.is_dir(): - world_sources.append(WorldSource(file.name)) - elif file.is_file() and file.name.endswith(".apworld"): - world_sources.append(WorldSource(file.name, is_zip=True)) +for folder in (folder for folder in (user_folder, local_folder) if folder): + relative = folder == local_folder + for entry in os.scandir(folder): + # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "." + if not entry.name.startswith(("_", ".")): + file_name = entry.name if relative else os.path.join(folder, entry.name) + if entry.is_dir(): + world_sources.append(WorldSource(file_name, relative=relative)) + elif entry.is_file() and entry.name.endswith(".apworld"): + world_sources.append(WorldSource(file_name, is_zip=True, relative=relative)) # import all submodules to trigger AutoWorldRegister world_sources.sort() @@ -105,7 +109,7 @@ lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games: typing.Dict[str, GamesPackage] = {} -from .AutoWorld import AutoWorldRegister +from .AutoWorld import AutoWorldRegister # noqa: E402 # Build the data package for each game. for world_name, world in AutoWorldRegister.world_types.items(): From e1f1bf83c246ad9f0a189a9a382644594a9bd67b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 5 Nov 2023 06:15:39 +0100 Subject: [PATCH 139/327] Core: Running item Plando dot (#2405) --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 691b88b137..7b42a89d12 100644 --- a/Main.py +++ b/Main.py @@ -265,7 +265,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No if any(world.item_links.values()): world._all_state = None - logger.info("Running Item Plando") + logger.info("Running Item Plando.") distribute_planned(world) From 84fb2f58faebb975a28a8e561fe836056b908a23 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Mon, 6 Nov 2023 00:01:49 -0500 Subject: [PATCH 140/327] DLC Quest Stardew: bug (#2423) --- worlds/dlcquest/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index e4e0a29274..c22b7cd984 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -3,7 +3,7 @@ from typing import Union from BaseClasses import Tutorial, CollectionState from worlds.AutoWorld import WebWorld, World from . import Options -from .Items import DLCQuestItem, ItemData, create_items, item_table +from .Items import DLCQuestItem, ItemData, create_items, item_table, items_by_group, Group from .Locations import DLCQuestLocation, location_table from .Options import DLCQuestOptions from .Regions import create_regions @@ -60,7 +60,9 @@ class DLCqworld(World): created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random) self.multiworld.itempool += created_items - self.multiworld.early_items[self.player]["Movement Pack"] = 1 + + if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both: + self.multiworld.early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: @@ -77,6 +79,10 @@ class DLCqworld(World): return DLCQuestItem(item.name, item.classification, item.code, self.player) + def get_filler_item_name(self) -> str: + trap = self.multiworld.random.choice(items_by_group[Group.Trap]) + return trap.name + def fill_slot_data(self): options_dict = self.options.as_dict( "death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle" From c984b48149f6933c5b99dcda34957bb767131d7e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:39:36 +0100 Subject: [PATCH 141/327] The Witness: Fix Town Tower 4th Door Logic (#2421) --- worlds/witness/settings/Disable_Unrandomized.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index f7a0fcb7cb..3cd7ec1fb5 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -9,7 +9,7 @@ Requirement Changes: 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 0x28B39 - True - Reflection 0x17CAB - True - True -0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7 +0x2779A - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: 0x03505 (Tutorial Gate Close) @@ -125,4 +125,4 @@ Precompleted Locations: 0x035F5 0x000D3 0x33A20 -0x03BE2 \ No newline at end of file +0x03BE2 From 5a7d69c8b42b03e401390b4d2df32a4961146d66 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Tue, 7 Nov 2023 18:31:06 +0100 Subject: [PATCH 142/327] ChecksFinder: Tweak link in ChecksFinder (#2353) Co-authored-by: Ludovic Marechal Co-authored-by: Marech Co-authored-by: Fabian Dill Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/checksfinder/__init__.py | 4 ++-- worlds/checksfinder/docs/{checksfinder_en.md => setup_en.md} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename worlds/checksfinder/docs/{checksfinder_en.md => setup_en.md} (100%) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 4978500da0..621e8f5c37 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -14,8 +14,8 @@ class ChecksFinderWeb(WebWorld): "A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers " "single-player, multiworld, and related software.", "English", - "checksfinder_en.md", - "checksfinder/en", + "setup_en.md", + "setup/en", ["Mewlif"] )] diff --git a/worlds/checksfinder/docs/checksfinder_en.md b/worlds/checksfinder/docs/setup_en.md similarity index 100% rename from worlds/checksfinder/docs/checksfinder_en.md rename to worlds/checksfinder/docs/setup_en.md From 72cb8b7d6080a088a3f129f1f3e7cf09e4949daf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 7 Nov 2023 21:02:28 +0100 Subject: [PATCH 143/327] Factorio: inflate location pool (#2422) --- worlds/factorio/Locations.py | 9 ++------- worlds/factorio/__init__.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py index f9db5f4a2b..52f0954cba 100644 --- a/worlds/factorio/Locations.py +++ b/worlds/factorio/Locations.py @@ -3,18 +3,13 @@ from typing import Dict, List from .Technologies import factorio_base_id from .Options import MaxSciencePack -boundary: int = 0xff -total_locations: int = 0xff - -assert total_locations <= boundary - def make_pools() -> Dict[str, List[str]]: pools: Dict[str, List[str]] = {} for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): - max_needed: int = 0xff + max_needed: int = 999 prefix: str = f"AP-{i}-" - pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + pools[pack] = [prefix + str(x).upper().zfill(3) for x in range(1, max_needed + 1)] return pools diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 8308bb2d65..eb078720c6 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -541,7 +541,7 @@ class FactorioScienceLocation(FactorioLocation): super(FactorioScienceLocation, self).__init__(player, name, address, parent) # "AP-{Complexity}-{Cost}" self.complexity = int(self.name[3]) - 1 - self.rel_cost = int(self.name[5:], 16) + self.rel_cost = int(self.name[5:]) self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): From 779a31265052d6e660e4cc165e3ab808bc92630a Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:41:13 -0500 Subject: [PATCH 144/327] Docs, Undertale: Added Suggestions Missed in #2285 (#2435) Co-authored-by: jonloveslegos <68133186+jonloveslegos@users.noreply.github.com> Co-authored-by: kindasneaki Co-authored-by: ScootyPuffJr1 <77215594+scootypuffjr1@users.noreply.github.com> --- UndertaleClient.py | 6 +++--- worlds/undertale/docs/en_Undertale.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 62fbe128bd..e1538ce81d 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -27,14 +27,14 @@ class UndertaleCommandProcessor(ClientCommandProcessor): self.ctx.syncing = True def _cmd_patch(self): - """Patch the game.""" + """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") def _cmd_savepath(self, directory: str): - """Redirect to proper save data folder. (Use before connecting!)""" + """Redirect to proper save data folder. This is necessary for Linux users to use before connecting.""" if isinstance(self.ctx, UndertaleContext): self.ctx.save_game_folder = directory self.output("Changed to the following directory: " + self.ctx.save_game_folder) @@ -67,7 +67,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor): self.output("Patching successful!") def _cmd_online(self): - """Makes you no longer able to see other Undertale players.""" + """Toggles seeing other Undertale players.""" if isinstance(self.ctx, UndertaleContext): self.ctx.update_online_mode(not ("Online" in self.ctx.tags)) if "Online" in self.ctx.tags: diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 87011ee16b..7ff5d55eda 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -56,8 +56,8 @@ If you press `W` while in the save menu, you will teleport back to the flower ro The following commands are only available when using the UndertaleClient to play with Archipelago. - `/resync` Manually trigger a resync. -- `/patch` Patch the game. -- `/savepath` Redirect to proper save data folder. (Use before connecting!) +- `/savepath` Redirect to proper save data folder. This is necessary for Linux users to use before connecting. - `/auto_patch` Patch the game automatically. -- `/online` Makes you no longer able to see other Undertale players. +- `/patch` Patch the game. Only use this command if `/auto_patch` fails. +- `/online` Toggles seeing other Undertale players. - `/deathlink` Toggles deathlink From ced35c5b78a5d2d23fce8c7938dbe95fe3a0f07d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:51:35 -0600 Subject: [PATCH 145/327] CommonClient: Add a hints tab (#2392) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- CommonClient.py | 7 +- data/client.kv | 75 +++++++++++++- kvui.py | 263 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 262 insertions(+), 83 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index a5e9b4553a..0952b08a58 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -758,6 +758,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} ctx.hint_points = args.get("hint_points", 0) ctx.consume_players_package(args["players"]) + ctx.stored_data_notification_keys.add(f"_read_hints_{ctx.team}_{ctx.slot}") msgs = [] if ctx.locations_checked: msgs.append({"cmd": "LocationChecks", @@ -836,10 +837,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict): elif cmd == "Retrieved": ctx.stored_data.update(args["keys"]) + if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" in args["keys"]: + ctx.ui.update_hints() elif cmd == "SetReply": ctx.stored_data[args["key"]] = args["value"] - if args["key"].startswith("EnergyLink"): + if ctx.ui and f"_read_hints_{ctx.team}_{ctx.slot}" == args["key"]: + ctx.ui.update_hints() + elif args["key"].startswith("EnergyLink"): ctx.current_energy_link_value = args["value"] if ctx.ui: ctx.ui.set_new_energy_link_value() diff --git a/data/client.kv b/data/client.kv index f0e3616900..3b48d216dd 100644 --- a/data/client.kv +++ b/data/client.kv @@ -17,6 +17,12 @@ color: "FFFFFF" : tab_width: root.width / app.tab_count +: + text_size: self.width, None + size_hint_y: None + height: self.texture_size[1] + font_size: dp(20) + markup: True : canvas.before: Color: @@ -24,11 +30,6 @@ Rectangle: size: self.size pos: self.pos - text_size: self.width, None - size_hint_y: None - height: self.texture_size[1] - font_size: dp(20) - markup: True : messages: 1000 # amount of messages stored in client logs. cols: 1 @@ -44,6 +45,70 @@ height: self.minimum_height orientation: 'vertical' spacing: dp(3) +: + canvas.before: + Color: + rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1) + Rectangle: + size: self.size + pos: self.pos + height: self.minimum_height + receiving_text: "Receiving Player" + item_text: "Item" + finding_text: "Finding Player" + location_text: "Location" + entrance_text: "Entrance" + found_text: "Found?" + TooltipLabel: + id: receiving + text: root.receiving_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: item + text: root.item_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: finding + text: root.finding_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: location + text: root.location_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: entrance + text: root.entrance_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} + TooltipLabel: + id: found + text: root.found_text + halign: 'center' + valign: 'center' + pos_hint: {"center_y": 0.5} +: + cols: 1 + viewclass: 'HintLabel' + scroll_y: self.height + scroll_type: ["content", "bars"] + bar_width: dp(12) + effect_cls: "ScrollEffect" + SelectableRecycleBoxLayout: + default_size: None, dp(20) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + spacing: dp(3) : text: "Server:" size_hint_x: None diff --git a/kvui.py b/kvui.py index 71bf80c86d..22e179d5be 100644 --- a/kvui.py +++ b/kvui.py @@ -5,12 +5,13 @@ import typing if sys.platform == "win32": import ctypes + # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's try: ctypes.windll.shcore.SetProcessDpiAwareness(0) except FileNotFoundError: # shcore may not be found on <= Windows 7 - pass # TODO: remove silent except when Python 3.8 is phased out. + pass # TODO: remove silent except when Python 3.8 is phased out. os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" @@ -18,14 +19,15 @@ os.environ["KIVY_NO_ARGS"] = "1" os.environ["KIVY_LOG_ENABLE"] = "0" import Utils + if Utils.is_frozen(): os.environ["KIVY_DATA_DIR"] = Utils.local_path("data") from kivy.config import Config Config.set("input", "mouse", "mouse,disable_multitouch") -Config.set('kivy', 'exit_on_escape', '0') -Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers +Config.set("kivy", "exit_on_escape", "0") +Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers from kivy.app import App from kivy.core.window import Window @@ -58,7 +60,6 @@ from kivy.uix.popup import Popup fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25) - from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType from Utils import async_start @@ -77,8 +78,8 @@ class HoverBehavior(object): border_point = ObjectProperty(None) def __init__(self, **kwargs): - self.register_event_type('on_enter') - self.register_event_type('on_leave') + self.register_event_type("on_enter") + self.register_event_type("on_leave") Window.bind(mouse_pos=self.on_mouse_pos) Window.bind(on_cursor_leave=self.on_cursor_leave) super(HoverBehavior, self).__init__(**kwargs) @@ -106,7 +107,7 @@ class HoverBehavior(object): self.dispatch("on_leave") -Factory.register('HoverBehavior', HoverBehavior) +Factory.register("HoverBehavior", HoverBehavior) class ToolTip(Label): @@ -121,6 +122,60 @@ class HovererableLabel(HoverBehavior, Label): pass +class TooltipLabel(HovererableLabel): + tooltip = None + + def create_tooltip(self, text, x, y): + text = text.replace("
    ", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]") + if self.tooltip: + # update + self.tooltip.children[0].text = text + else: + self.tooltip = FloatLayout() + tooltip_label = ToolTip(text=text) + self.tooltip.add_widget(tooltip_label) + fade_in_animation.start(self.tooltip) + App.get_running_app().root.add_widget(self.tooltip) + + # handle left-side boundary to not render off-screen + x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2) + + # position float layout + self.tooltip.x = x - self.tooltip.width / 2 + self.tooltip.y = y - self.tooltip.height / 2 + 48 + + def remove_tooltip(self): + if self.tooltip: + App.get_running_app().root.remove_widget(self.tooltip) + self.tooltip = None + + def on_mouse_pos(self, window, pos): + if not self.get_root_window(): + return # Abort if not displayed + super().on_mouse_pos(window, pos) + if self.refs and self.hovered: + + tx, ty = self.to_widget(*pos, relative=True) + # Why TF is Y flipped *within* the texture? + ty = self.texture_size[1] - ty + hit = False + for uid, zones in self.refs.items(): + for zone in zones: + x, y, w, h = zone + if x <= tx <= w and y <= ty <= h: + self.create_tooltip(uid.split("|", 1)[1], *pos) + hit = True + break + if not hit: + self.remove_tooltip() + + def on_enter(self): + pass + + def on_leave(self): + self.remove_tooltip() + + class ServerLabel(HovererableLabel): def __init__(self, *args, **kwargs): super(HovererableLabel, self).__init__(*args, **kwargs) @@ -189,11 +244,10 @@ class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, """ Adds selection and focus behaviour to the view. """ -class SelectableLabel(RecycleDataViewBehavior, HovererableLabel): +class SelectableLabel(RecycleDataViewBehavior, TooltipLabel): """ Add selection support to the Label """ index = None selected = BooleanProperty(False) - tooltip = None def refresh_view_attrs(self, rv, index, data): """ Catch and handle the view changes """ @@ -201,56 +255,6 @@ class SelectableLabel(RecycleDataViewBehavior, HovererableLabel): return super(SelectableLabel, self).refresh_view_attrs( rv, index, data) - def create_tooltip(self, text, x, y): - text = text.replace("
    ", "\n").replace('&', '&').replace('&bl;', '[').replace('&br;', ']') - if self.tooltip: - # update - self.tooltip.children[0].text = text - else: - self.tooltip = FloatLayout() - tooltip_label = ToolTip(text=text) - self.tooltip.add_widget(tooltip_label) - fade_in_animation.start(self.tooltip) - App.get_running_app().root.add_widget(self.tooltip) - - # handle left-side boundary to not render off-screen - x = max(x, 3+self.tooltip.children[0].texture_size[0] / 2) - - # position float layout - self.tooltip.x = x - self.tooltip.width / 2 - self.tooltip.y = y - self.tooltip.height / 2 + 48 - - def remove_tooltip(self): - if self.tooltip: - App.get_running_app().root.remove_widget(self.tooltip) - self.tooltip = None - - def on_mouse_pos(self, window, pos): - if not self.get_root_window(): - return # Abort if not displayed - super().on_mouse_pos(window, pos) - if self.refs and self.hovered: - - tx, ty = self.to_widget(*pos, relative=True) - # Why TF is Y flipped *within* the texture? - ty = self.texture_size[1] - ty - hit = False - for uid, zones in self.refs.items(): - for zone in zones: - x, y, w, h = zone - if x <= tx <= w and y <= ty <= h: - self.create_tooltip(uid.split("|", 1)[1], *pos) - hit = True - break - if not hit: - self.remove_tooltip() - - def on_enter(self): - pass - - def on_leave(self): - self.remove_tooltip() - def on_touch_down(self, touch): """ Add selection on touch down """ if super(SelectableLabel, self).on_touch_down(touch): @@ -274,7 +278,7 @@ class SelectableLabel(RecycleDataViewBehavior, HovererableLabel): elif not cmdinput.text and text.startswith("Missing: "): cmdinput.text = text.replace("Missing: ", "!hint_location ") - Clipboard.copy(text.replace('&', '&').replace('&bl;', '[').replace('&br;', ']')) + Clipboard.copy(text.replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) return self.parent.select_with_touch(self.index, touch) def apply_selection(self, rv, index, is_selected): @@ -282,9 +286,68 @@ class SelectableLabel(RecycleDataViewBehavior, HovererableLabel): self.selected = is_selected +class HintLabel(RecycleDataViewBehavior, BoxLayout): + selected = BooleanProperty(False) + striped = BooleanProperty(False) + index = None + no_select = [] + + def __init__(self): + super(HintLabel, self).__init__() + self.receiving_text = "" + self.item_text = "" + self.finding_text = "" + self.location_text = "" + self.entrance_text = "" + self.found_text = "" + for child in self.children: + child.bind(texture_size=self.set_height) + + def set_height(self, instance, value): + self.height = max([child.texture_size[1] for child in self.children]) + + def refresh_view_attrs(self, rv, index, data): + self.index = index + if "select" in data and not data["select"] and index not in self.no_select: + self.no_select.append(index) + self.striped = data["striped"] + self.receiving_text = data["receiving"]["text"] + self.item_text = data["item"]["text"] + self.finding_text = data["finding"]["text"] + self.location_text = data["location"]["text"] + self.entrance_text = data["entrance"]["text"] + self.found_text = data["found"]["text"] + self.height = self.minimum_height + return super(HintLabel, self).refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): + """ Add selection on touch down """ + if super(HintLabel, self).on_touch_down(touch): + return True + if self.index not in self.no_select: + if self.collide_point(*touch.pos): + if self.selected: + self.parent.clear_selection() + else: + text = "".join([self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ", + self.finding_text, "\'s World", (" at " + self.entrance_text) + if self.entrance_text != "Vanilla" + else "", ". (", self.found_text.lower(), ")"]) + temp = MarkupLabel(text).markup + text = "".join( + part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]"))) + Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]")) + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + """ Respond to the selection of items in the view. """ + if self.index not in self.no_select: + self.selected = is_selected + + class ConnectBarTextInput(TextInput): def insert_text(self, substring, from_undo=False): - s = substring.replace('\n', '').replace('\r', '') + s = substring.replace("\n", "").replace("\r", "") return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo) @@ -302,7 +365,7 @@ class MessageBox(Popup): def __init__(self, title, text, error=False, **kwargs): label = MessageBox.MessageBoxLabel(text=text) separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.] - super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width)+40), + super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40), separator_color=separator_color, **kwargs) self.height += max(0, label.height - 18) @@ -358,11 +421,14 @@ class GameManager(App): # top part server_label = ServerLabel() self.connect_layout.add_widget(server_label) - self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None, + self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", + size_hint_y=None, height=dp(30), multiline=False, write_tab=False) + def connect_bar_validate(sender): if not self.ctx.server: self.connect_button_action(sender) + self.server_connect_bar.bind(on_text_validate=connect_bar_validate) self.connect_layout.add_widget(self.server_connect_bar) self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None) @@ -383,20 +449,22 @@ class GameManager(App): bridge_logger = logging.getLogger(logger_name) panel = TabbedPanelItem(text=display_name) self.log_panels[display_name] = panel.content = UILog(bridge_logger) - self.tabs.add_widget(panel) + if len(self.logging_pairs) > 1: + # show Archipelago tab if other logging is present + self.tabs.add_widget(panel) + + hint_panel = TabbedPanelItem(text="Hints") + self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) + self.tabs.add_widget(hint_panel) + + if len(self.logging_pairs) == 1: + self.tabs.default_tab_text = "Archipelago" self.main_area_container = GridLayout(size_hint_y=1, rows=1) self.main_area_container.add_widget(self.tabs) self.grid.add_widget(self.main_area_container) - if len(self.logging_pairs) == 1: - # Hide Tab selection if only one tab - self.tabs.clear_tabs() - self.tabs.do_default_tab = False - self.tabs.current_tab.height = 0 - self.tabs.tab_height = 0 - # bottom part bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None) @@ -422,7 +490,7 @@ class GameManager(App): return self.container def update_texts(self, dt): - if hasattr(self.tabs.content.children[0], 'fix_heights'): + if hasattr(self.tabs.content.children[0], "fix_heights"): self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream if self.ctx.server: self.title = self.base_title + " " + Utils.__version__ + \ @@ -499,6 +567,10 @@ class GameManager(App): if hasattr(self, "energy_link_label"): self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J" + def update_hints(self): + hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"] + self.log_panels["Hints"].refresh_hints(hints) + # default F1 keybind, opens a settings menu, that seems to break the layout engine once closed def open_settings(self, *largs): pass @@ -513,12 +585,12 @@ class LogtoUI(logging.Handler): def format_compact(record: logging.LogRecord) -> str: if isinstance(record.msg, Exception): return str(record.msg) - return (f'{record.exc_info[1]}\n' if record.exc_info else '') + str(record.msg).split("\n")[0] + return (f"{record.exc_info[1]}\n" if record.exc_info else "") + str(record.msg).split("\n")[0] def handle(self, record: logging.LogRecord) -> None: - if getattr(record, 'skip_gui', False): + if getattr(record, "skip_gui", False): pass # skip output - elif getattr(record, 'compact_gui', False): + elif getattr(record, "compact_gui", False): self.on_log(self.format_compact(record)) else: self.on_log(self.format(record)) @@ -552,6 +624,44 @@ class UILog(RecycleView): element.height = element.texture_size[1] +class HintLog(RecycleView): + header = { + "receiving": {"text": "[u]Receiving Player[/u]"}, + "item": {"text": "[u]Item[/u]"}, + "finding": {"text": "[u]Finding Player[/u]"}, + "location": {"text": "[u]Location[/u]"}, + "entrance": {"text": "[u]Entrance[/u]"}, + "found": {"text": "[u]Status[/u]"}, + "striped": True, + "select": False, + } + + def __init__(self, parser): + super(HintLog, self).__init__() + self.data = [self.header] + self.parser = parser + + def refresh_hints(self, hints): + self.data = [self.header] + striped = False + for hint in hints: + self.data.append({ + "striped": striped, + "receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})}, + "item": {"text": self.parser.handle_node( + {"type": "item_id", "text": hint["item"], "flags": hint["item_flags"]})}, + "finding": {"text": self.parser.handle_node({"type": "player_id", "text": hint["finding_player"]})}, + "location": {"text": self.parser.handle_node({"type": "location_id", "text": hint["location"]})}, + "entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text", + "color": "blue", "text": hint["entrance"] + if hint["entrance"] else "Vanilla"})}, + "found": { + "text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red", + "text": "Found" if hint["found"] else "Not Found"})}, + }) + striped = not striped + + class E(ExceptionHandler): logger = logging.getLogger("Client") @@ -599,7 +709,7 @@ class KivyJSONtoTextParser(JSONtoTextParser): f"Type: {SlotType(slot_info.type).name}" if slot_info.group_members: text += f"
    Members:
    " + \ - '
    '.join(self.ctx.player_names[player] for player in slot_info.group_members) + "
    ".join(self.ctx.player_names[player] for player in slot_info.group_members) node.setdefault("refs", []).append(text) return super(KivyJSONtoTextParser, self)._handle_player_id(node) @@ -627,4 +737,3 @@ user_file = Utils.user_path("data", "user.kv") if os.path.exists(user_file): logging.info("Loading user.kv into builder.") Builder.load_file(user_file) - From 03e1c45d71ebea385db84de14e513799b8c1670c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 8 Nov 2023 02:15:06 -0600 Subject: [PATCH 146/327] Tests: log the seed fo slot_data failures (#2402) --- test/general/test_implemented.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index b60bcee467..624be71018 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -40,8 +40,8 @@ class TestImplemented(unittest.TestCase): # has an await for generate_output which isn't being called if game_name in {"Ocarina of Time", "Zillion"}: continue - with self.subTest(game_name): - multiworld = setup_solo_multiworld(world_type) + multiworld = setup_solo_multiworld(world_type) + with self.subTest(game=game_name, seed=multiworld.seed): distribute_items_restrictive(multiworld) call_all(multiworld, "post_fill") for key, data in multiworld.worlds[1].fill_slot_data().items(): From 504d09daf6e4422ca1c332fe921707b1108e5d55 Mon Sep 17 00:00:00 2001 From: Mewlif <68133186+jonloveslegos@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:50:29 -0500 Subject: [PATCH 147/327] Undertale: Logic fixes (#2436) --- worlds/undertale/Regions.py | 4 +++- worlds/undertale/Rules.py | 18 +++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/worlds/undertale/Regions.py b/worlds/undertale/Regions.py index ec13b249fa..138a684653 100644 --- a/worlds/undertale/Regions.py +++ b/worlds/undertale/Regions.py @@ -24,6 +24,7 @@ undertale_regions = [ ("True Lab", []), ("Core", ["Core Exit"]), ("New Home", ["New Home Exit"]), + ("Last Corridor", ["Last Corridor Exit"]), ("Barrier", []), ] @@ -40,7 +41,8 @@ mandatory_connections = [ ("News Show Entrance", "News Show"), ("Lab Elevator", "True Lab"), ("Core Exit", "New Home"), - ("New Home Exit", "Barrier"), + ("New Home Exit", "Last Corridor"), + ("Last Corridor Exit", "Barrier"), ("Snowdin Hub", "Snowdin Forest"), ("Waterfall Hub", "Waterfall"), ("Hotland Hub", "Hotland"), diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py index 648152c504..897484b050 100644 --- a/worlds/undertale/Rules.py +++ b/worlds/undertale/Rules.py @@ -81,23 +81,27 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance("New Home Exit", player), lambda state: (state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player])) + state.has("Key Piece", player, state.multiworld.key_pieces[player].value)) if _undertale_is_route(multiworld.state, player, 1): set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Complete Skeleton")) set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player)) set_rule(multiworld.get_entrance("Lab Elevator", player), - lambda state: state.has("Alphys Date", player) and _undertale_has_plot(state, player, "DT Extractor")) + lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and + ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or + state.has("Key Piece", player, state.multiworld.key_pieces[player].value))) set_rule(multiworld.get_location("Alphys Date", player), - lambda state: state.has("Undyne Letter EX", player) and state.has("Undyne Date", player)) + lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player) + and state.has("Undyne Date", player)) set_rule(multiworld.get_location("Papyrus Plot", player), lambda state: state.can_reach("Snowdin Town", "Region", player)) set_rule(multiworld.get_location("Undyne Plot", player), lambda state: state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("True Lab Plot", player), lambda state: state.can_reach("New Home", "Region", player) - and state.can_reach("Letter Quest", "Location", player)) + and state.can_reach("Letter Quest", "Location", player) + and state.can_reach("Alphys Date", "Location", player)) set_rule(multiworld.get_location("Chisps Machine", player), lambda state: state.can_reach("True Lab", "Region", player)) set_rule(multiworld.get_location("Dog Sale 1", player), @@ -113,7 +117,7 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("Hush Trade", player), lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1)) set_rule(multiworld.get_location("Letter Quest", player), - lambda state: state.can_reach("New Home Exit", "Entrance", player) and state.has("Undyne Date", player)) + lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player)) if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3): set_rule(multiworld.get_location("Nicecream Punch Card", player), lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player)) @@ -126,7 +130,7 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("Apron Hidden", player), lambda state: state.can_reach("Cooking Show", "Region", player)) if _undertale_is_route(multiworld.state, player, 2) and \ - (multiworld.rando_love[player] or multiworld.rando_stats[player]): + (bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)): maxlv = 1 exp = 190 curarea = "Old Home" @@ -304,7 +308,7 @@ def set_rules(multiworld: MultiWorld, player: int): # Sets rules on completion condition def set_completion_rules(multiworld: MultiWorld, player: int): - completion_requirements = lambda state: state.can_reach("New Home Exit", "Entrance", player) + completion_requirements = lambda state: state.can_reach("Barrier", "Region", player) if _undertale_is_route(multiworld.state, player, 1): completion_requirements = lambda state: state.can_reach("True Lab", "Region", player) From 154e17f4ff161e833816c857939cc7115c20ae10 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Wed, 8 Nov 2023 19:00:55 +0100 Subject: [PATCH 148/327] SC2: 0.4.3 bugfixes (#2273) Co-authored-by: Matthew --- worlds/sc2wol/Client.py | 9 +++++++-- worlds/sc2wol/Locations.py | 8 ++++---- worlds/sc2wol/Options.py | 8 ++++---- worlds/sc2wol/PoolFilter.py | 35 +++++++++++++++++++++++------------ worlds/sc2wol/Starcraft2.kv | 2 +- worlds/sc2wol/__init__.py | 4 ++-- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/worlds/sc2wol/Client.py b/worlds/sc2wol/Client.py index a9bb826b74..3dbd2047de 100644 --- a/worlds/sc2wol/Client.py +++ b/worlds/sc2wol/Client.py @@ -9,6 +9,7 @@ import multiprocessing import os.path import re import sys +import tempfile import typing import queue import zipfile @@ -286,6 +287,8 @@ class SC2Context(CommonContext): await super(SC2Context, self).server_auth(password_requested) await self.get_username() await self.send_connect() + if self.ui: + self.ui.first_check = True def on_package(self, cmd: str, args: dict): if cmd in {"Connected"}: @@ -1166,10 +1169,12 @@ def download_latest_release_zip(owner: str, repo: str, api_version: str, metadat r2 = requests.get(download_url, headers=headers) if r2.status_code == 200 and zipfile.is_zipfile(io.BytesIO(r2.content)): - with open(f"{repo}.zip", "wb") as fh: + tempdir = tempfile.gettempdir() + file = tempdir + os.sep + f"{repo}.zip" + with open(file, "wb") as fh: fh.write(r2.content) sc2_logger.info(f"Successfully downloaded {repo}.zip.") - return f"{repo}.zip", latest_metadata + return file, latest_metadata else: sc2_logger.warning(f"Status code: {r2.status_code}") sc2_logger.warning("Download failed.") diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index ae31fa8eaa..fba7051337 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -68,10 +68,10 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu lambda state: state._sc2wol_has_common_unit(multiworld, player) and (logic_level > 0 and state._sc2wol_has_anti_air(multiworld, player) or state._sc2wol_has_competent_anti_air(multiworld, player))), - LocationData("Evacuation", "Evacuation: First Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS), - LocationData("Evacuation", "Evacuation: Second Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS, + LocationData("Evacuation", "Evacuation: North Chrysalis", SC2WOL_LOC_ID_OFFSET + 401, LocationType.BONUS), + LocationData("Evacuation", "Evacuation: West Chrysalis", SC2WOL_LOC_ID_OFFSET + 402, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), - LocationData("Evacuation", "Evacuation: Third Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS, + LocationData("Evacuation", "Evacuation: East Chrysalis", SC2WOL_LOC_ID_OFFSET + 403, LocationType.BONUS, lambda state: state._sc2wol_has_common_unit(multiworld, player)), LocationData("Evacuation", "Evacuation: Reach Hanson", SC2WOL_LOC_ID_OFFSET + 404, LocationType.MISSION_PROGRESS), LocationData("Evacuation", "Evacuation: Secret Resource Stash", SC2WOL_LOC_ID_OFFSET + 405, LocationType.BONUS), @@ -419,7 +419,7 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), LocationData("A Sinister Turn", "A Sinister Turn: Northeast Base", SC2WOL_LOC_ID_OFFSET + 2304, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), - LocationData("A Sinister Turn", "A Sinister Turn: Southeast Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS, + LocationData("A Sinister Turn", "A Sinister Turn: Southwest Base", SC2WOL_LOC_ID_OFFSET + 2305, LocationType.MISSION_PROGRESS, lambda state: state._sc2wol_has_protoss_medium_units(multiworld, player)), LocationData("A Sinister Turn", "A Sinister Turn: Maar", SC2WOL_LOC_ID_OFFSET + 2306, LocationType.MISSION_PROGRESS, lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(multiworld, player)), diff --git a/worlds/sc2wol/Options.py b/worlds/sc2wol/Options.py index 13b01c42a2..e4b6a74066 100644 --- a/worlds/sc2wol/Options.py +++ b/worlds/sc2wol/Options.py @@ -41,6 +41,10 @@ class FinalMap(Choice): Vanilla mission order always ends with All in mission! + Warning: Using All-in with a short mission order (7 or fewer missions) is not recommended, + as there might not be enough locations to place all the required items, + any excess required items will be placed into the player's starting inventory! + This option is short-lived. It may be changed in the future """ display_name = "Final Map" @@ -265,7 +269,6 @@ class MissionProgressLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Mission Progress Locations" @@ -282,7 +285,6 @@ class BonusLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Bonus Locations" @@ -300,7 +302,6 @@ class ChallengeLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Challenge Locations" @@ -317,7 +318,6 @@ class OptionalBossLocations(LocationInclusion): Nothing: No rewards for this type of tasks, effectively disabling such locations Note: Individual locations subject to plando are always enabled, so the plando can be placed properly. - Warning: The generation may fail if too many locations are excluded by this way. See also: Excluded Locations, Item Plando (https://archipelago.gg/tutorial/Archipelago/plando/en#item-plando) """ display_name = "Optional Boss Locations" diff --git a/worlds/sc2wol/PoolFilter.py b/worlds/sc2wol/PoolFilter.py index 4a19e2dbb3..23422a3d1e 100644 --- a/worlds/sc2wol/PoolFilter.py +++ b/worlds/sc2wol/PoolFilter.py @@ -1,6 +1,7 @@ from typing import Callable, Dict, List, Set from BaseClasses import MultiWorld, ItemClassification, Item, Location -from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items +from .Items import get_full_item_list, spider_mine_sources, second_pass_placeable_items, filler_items, \ + progressive_if_nco from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\ mission_orders, MissionInfo, alt_final_mission_locations, MissionPools from .Options import get_option_value, MissionOrder, FinalMap, MissionProgressLocations, LocationInclusion @@ -15,7 +16,7 @@ UPGRADABLE_ITEMS = [ ] BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"} -FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine"} +FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator", "Widow Mine", "Cyclone"} STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven", "Liberator", "Valkyrie"} PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"} @@ -93,7 +94,10 @@ def get_item_upgrades(inventory: List[Item], parent_item: Item or str): ] -def get_item_quantity(item): +def get_item_quantity(item: Item, multiworld: MultiWorld, player: int): + if (not get_option_value(multiworld, player, "nco_items")) \ + and item.name in progressive_if_nco: + return 1 return get_full_item_list()[item.name].quantity @@ -138,13 +142,13 @@ class ValidInventory: if not all(requirement(self) for requirement in requirements): # If item cannot be removed, lock or revert self.logical_inventory.add(item.name) - for _ in range(get_item_quantity(item)): + for _ in range(get_item_quantity(item, self.multiworld, self.player)): locked_items.append(copy_item(item)) return False return True - + # Limit the maximum number of upgrades - maxUpgrad = get_option_value(self.multiworld, self.player, + maxUpgrad = get_option_value(self.multiworld, self.player, "max_number_of_upgrades") if maxUpgrad != -1: unit_avail_upgrades = {} @@ -197,15 +201,16 @@ class ValidInventory: # Don't process general upgrades, they may have been pre-locked per-level for item in items_to_lock: if item in inventory: + item_quantity = inventory.count(item) # Unit upgrades, lock all levels - for _ in range(inventory.count(item)): + for _ in range(item_quantity): inventory.remove(item) if item not in locked_items: # Lock all the associated items if not already locked - for _ in range(get_item_quantity(item)): + for _ in range(item_quantity): locked_items.append(copy_item(item)) - if item in existing_items: - existing_items.remove(item) + if item in existing_items: + existing_items.remove(item) if self.min_units_per_structure > 0 and self.has_units_per_structure(): requirements.append(lambda state: state.has_units_per_structure()) @@ -216,7 +221,13 @@ class ValidInventory: while len(inventory) + len(locked_items) > inventory_size: if len(inventory) == 0: - raise Exception("Reduced item pool generation failed - not enough locations available to place items.") + # There are more items than locations and all of them are already locked due to YAML or logic. + # Random items from locked ones will go to starting items + self.multiworld.random.shuffle(locked_items) + while len(locked_items) > inventory_size: + item: Item = locked_items.pop() + self.multiworld.push_precollected(item) + break # Select random item from removable items item = self.multiworld.random.choice(inventory) # Cascade removals to associated items @@ -245,7 +256,7 @@ class ValidInventory: for _ in range(inventory.count(transient_item)): inventory.remove(transient_item) if transient_item not in locked_items: - for _ in range(get_item_quantity(transient_item)): + for _ in range(get_item_quantity(transient_item, self.multiworld, self.player)): locked_items.append(copy_item(transient_item)) if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing): self.logical_inventory.add(transient_item.name) diff --git a/worlds/sc2wol/Starcraft2.kv b/worlds/sc2wol/Starcraft2.kv index 9c52d64c47..f0785b89e4 100644 --- a/worlds/sc2wol/Starcraft2.kv +++ b/worlds/sc2wol/Starcraft2.kv @@ -11,6 +11,6 @@ markup: True halign: 'center' valign: 'middle' - padding_x: 5 + padding: [5,0,5,0] markup: True outline_width: 1 diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 93aebb7ad1..5c487f8fee 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -34,7 +34,7 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 4 + data_version = 5 item_name_to_id = {name: data.code for name, data in get_full_item_list().items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} @@ -46,7 +46,7 @@ class SC2WoLWorld(World): mission_req_table = {} final_mission_id: int victory_item: str - required_client_version = 0, 3, 6 + required_client_version = 0, 4, 3 def __init__(self, multiworld: MultiWorld, player: int): super(SC2WoLWorld, self).__init__(multiworld, player) From ea9c31392d822ddda9160d34e65d988c39c7055b Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 8 Nov 2023 18:35:12 -0500 Subject: [PATCH 149/327] Lingo: New game (#1806) Co-authored-by: Aaron Wagener Co-authored-by: Fabian Dill Co-authored-by: Phar --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/lingo/LL1.yaml | 7505 +++++++++++++++++++++++++ worlds/lingo/__init__.py | 112 + worlds/lingo/docs/en_Lingo.md | 42 + worlds/lingo/docs/setup_en.md | 45 + worlds/lingo/ids.yaml | 1449 +++++ worlds/lingo/items.py | 106 + worlds/lingo/locations.py | 80 + worlds/lingo/options.py | 126 + worlds/lingo/player_logic.py | 298 + worlds/lingo/regions.py | 84 + worlds/lingo/rules.py | 104 + worlds/lingo/static_logic.py | 544 ++ worlds/lingo/test/TestDoors.py | 89 + worlds/lingo/test/TestMastery.py | 39 + worlds/lingo/test/TestOptions.py | 31 + worlds/lingo/test/TestOrangeTower.py | 175 + worlds/lingo/test/TestProgressive.py | 191 + worlds/lingo/test/__init__.py | 13 + worlds/lingo/testing.py | 2 + worlds/lingo/utils/assign_ids.rb | 178 + worlds/lingo/utils/validate_config.rb | 329 ++ 23 files changed, 11546 insertions(+) create mode 100644 worlds/lingo/LL1.yaml create mode 100644 worlds/lingo/__init__.py create mode 100644 worlds/lingo/docs/en_Lingo.md create mode 100644 worlds/lingo/docs/setup_en.md create mode 100644 worlds/lingo/ids.yaml create mode 100644 worlds/lingo/items.py create mode 100644 worlds/lingo/locations.py create mode 100644 worlds/lingo/options.py create mode 100644 worlds/lingo/player_logic.py create mode 100644 worlds/lingo/regions.py create mode 100644 worlds/lingo/rules.py create mode 100644 worlds/lingo/static_logic.py create mode 100644 worlds/lingo/test/TestDoors.py create mode 100644 worlds/lingo/test/TestMastery.py create mode 100644 worlds/lingo/test/TestOptions.py create mode 100644 worlds/lingo/test/TestOrangeTower.py create mode 100644 worlds/lingo/test/TestProgressive.py create mode 100644 worlds/lingo/test/__init__.py create mode 100644 worlds/lingo/testing.py create mode 100644 worlds/lingo/utils/assign_ids.rb create mode 100644 worlds/lingo/utils/validate_config.rb diff --git a/README.md b/README.md index 54b659397f..bcbc885b46 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Currently, the following games are supported: * Muse Dash * DOOM 1993 * Terraria +* Lingo For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index e92bfa42b6..0afc565280 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -61,6 +61,9 @@ # Kingdom Hearts 2 /worlds/kh2/ @JaredWeakStrike +# Lingo +/worlds/lingo/ @hatkirby + # Links Awakening DX /worlds/ladx/ @zig-for diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml new file mode 100644 index 0000000000..7ae015dc64 --- /dev/null +++ b/worlds/lingo/LL1.yaml @@ -0,0 +1,7505 @@ +--- + # This file is an associative array where the keys are region names. Rooms + # have four properties: entrances, panels, doors, and paintings. + # + # entrances is an array of regions from which this room can be accessed. The + # key of each entry is the room that can access this one. The value is a list + # of OR'd requirements for being able to access this room from the other one, + # although the list can be elided if there is only one requirement, and the + # value True can be used if there are no requirements (i.e. you always have + # access to this room if you have access to the other). Each requirement + # describes a door that must be opened in order to access this room from the + # other. The door is described by both the door's name and the name of the + # room that the door is in. The room name may be omitted if the door is + # located in the current room. + # + # panels is an array of panels in the room. The key of the array is an + # arbitrary name for the panel. Panels can have the following fields: + # - id: The internal ID of the panel in the LINGO map + # - required_room: In addition to having access to this room, the player must + # also have access to this other room in order to solve this + # panel. + # - required_door: In addition to having access to this room, the player must + # also have this door opened in order to solve this panel. + # - required_panel: In addition to having access to this room, the player must + # also be able to access this other panel in order to solve + # this panel. + # - colors: A list of colors that are required to be unlocked in order + # to solve this panel + # - check: A location check will be created for this individual panel. + # - exclude_reduce: Panel checks are assumed to be INCLUDED when reduce checks + # is on. This option excludes the check anyway. + # - tag: Label that describes how panel randomization should be + # done. In reorder mode, panels with the same tag can be + # shuffled amongst themselves. "forbid" is a special value + # meaning that no randomization should be done. This field is + # mandatory. + # - link: Panels with the same link label are randomized as a group. + # - subtag: Used to identify the separate parts of a linked group. + # - copy_to_sign: When randomizing this panel, the hint should be copied to + # the specified sign(s). + # - achievement: The name of the achievement that is received upon solving + # this panel. + # - non_counting: If True, this panel does not contribute to the total needed + # to unlock Level 2. + # + # doors is an array of doors associated with this room. When door + # randomization is enabled, each of these is an item. The key is a name that + # will be displayed as part of the item's name. Doors can have the following + # fields: + # - id: A string or list of internal door IDs from the LINGO map. + # In door shuffle mode, collecting the item generated for + # this door will open the doors listed here. + # - painting_id: An internal ID of a painting that should be moved upon + # receiving this door. + # - panels: These are the panels that canonically open this door. If + # there is only one panel for the door, then that panel is a + # check. If there is more than one panel, then that entire + # set of panels must be solved for a check. Panels can + # either be a string (representing a panel in this room) or + # a dict containing "room" and "panel". + # - item_name: Overrides the name of the item generated for this door. + # If not specified, the item name will be generated from + # the room name and the door name. + # - location_name: Overrides the name of the location generated for this + # door. If not specified, the location name will be + # generated using the names of the panels. + # - skip_location: If true, no location is generated for this door. + # - skip_item: If true, no item is generated for this door. + # - group: When simple doors is used, all doors with the same group + # will be covered by a single item. + # - include_reduce: Door checks are assumed to be EXCLUDED when reduce checks + # is on. This option includes the check anyway. + # - junk_item: If on, the item for this door will be considered a junk + # item instead of a progression item. Only use this for + # doors that could never gate progression regardless of + # options and state. + # - event: Denotes that the door is event only. This is similar to + # setting both skip_location and skip_item. + # + # paintings is an array of paintings in the room. This is used for painting + # shuffling. + # - id: The internal painting ID from the LINGO map. + # - enter_only: If true, painting shuffling will not place a warp exit on + # this painting. + # - exit_only: If true, painting shuffling will not place a warp entrance + # on this painting. + # - orientation: One of north/south/east/west. This is the direction that + # the player is facing when they are interacting with it, + # not the orientation of the painting itself. "North" is + # the direction the player faces at a new game, with the + # positive X axis to the right. + # - required_door: This door must be open for the painting to be usable as an + # entrance. If required_door is set, enter_only must be + # True. + # - required: Marks a painting as being the only entrance for a room, + # and thus it is required to be an exit when randomized. + # Use "required_when_no_doors" instead if it would be + # possible to enter the room without the painting in door + # shuffle mode. + # - move: Denotes that the painting is able to move. + Starting Room: + entrances: + Menu: True + panels: + HI: + id: Entry Room/Panel_hi_hi + tag: midwhite + HIDDEN: + id: Entry Room/Panel_hidden_hidden + tag: midwhite + TYPE: + id: Entry Room/Panel_type_type + tag: midwhite + THIS: + id: Entry Room/Panel_this_this + tag: midwhite + WRITE: + id: Entry Room/Panel_write_write + tag: midwhite + SAME: + id: Entry Room/Panel_same_same + tag: midwhite + doors: + Main Door: + event: True + panels: + - HI + Back Right Door: + id: Entry Room Area Doors/Door_hidden_hidden + include_reduce: True + panels: + - HIDDEN + Rhyme Room Entrance: + id: + - Palindrome Room Area Doors/Door_level_level_2 + - Palindrome Room Area Doors/Door_racecar_racecar_2 + - Palindrome Room Area Doors/Door_solos_solos_2 + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + paintings: + - id: arrows_painting + exit_only: True + orientation: south + - id: arrows_painting2 + disable: True + move: True + - id: arrows_painting3 + disable: True + move: True + - id: garden_painting_tower2 + enter_only: True + orientation: north + move: True + required_door: + room: Hedge Maze + door: Painting Shortcut + - id: flower_painting_8 + enter_only: True + orientation: north + move: True + required_door: + room: Courtyard + door: Painting Shortcut + - id: symmetry_painting_a_starter + enter_only: True + orientation: west + move: True + required_door: + room: The Wondrous (Doorknob) + door: Painting Shortcut + - id: pencil_painting6 + enter_only: True + orientation: east + move: True + required_door: + room: Outside The Bold + door: Painting Shortcut + - id: blueman_painting_3 + enter_only: True + orientation: east + move: True + required_door: + room: Outside The Undeterred + door: Painting Shortcut + - id: eyes_yellow_painting2 + enter_only: True + orientation: west + move: True + required_door: + room: Outside The Agreeable + door: Painting Shortcut + Hidden Room: + entrances: + Starting Room: + room: Starting Room + door: Back Right Door + The Seeker: + door: Seeker Entrance + Dead End Area: + door: Dead End Door + Knight Night (Outer Ring): + door: Knight Night Entrance + panels: + DEAD END: + id: Appendix Room/Panel_deadend_deadened + check: True + exclude_reduce: True + tag: topwhite + OPEN: + id: Heteronym Room/Panel_entrance_entrance + tag: midwhite + LIES: + id: Appendix Room/Panel_lies_lies + tag: midwhite + doors: + Dead End Door: + id: Appendix Room Area Doors/Door_rat_tar_2 + skip_location: true + group: Dead End Area Access + panels: + - room: Hub Room + panel: RAT + Knight Night Entrance: + id: Appendix Room Area Doors/Door_rat_tar_4 + skip_location: true + panels: + - room: Hub Room + panel: RAT + Seeker Entrance: + id: Entry Room Area Doors/Door_entrance_entrance + item_name: The Seeker - Entrance + panels: + - OPEN + Rhyme Room Entrance: + id: + - Appendix Room Area Doors/Door_rat_tar_3 + - Double Room Area Doors/Door_room_entry_stairs + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + - room: Hub Room + panel: RAT + paintings: + - id: owl_painting + orientation: north + The Seeker: + entrances: + Hidden Room: + room: Hidden Room + door: Seeker Entrance + Pilgrim Room: + room: Pilgrim Room + door: Shortcut to The Seeker + panels: + Achievement: + id: Countdown Panels/Panel_seeker_seeker + required_room: Hidden Room + tag: forbid + check: True + achievement: The Seeker + BEAR: + id: Heteronym Room/Panel_bear_bear + tag: midwhite + MINE: + id: Heteronym Room/Panel_mine_mine + tag: double midwhite + subtag: left + link: exact MINE + MINE (2): + id: Heteronym Room/Panel_mine_mine_2 + tag: double midwhite + subtag: right + link: exact MINE + BOW: + id: Heteronym Room/Panel_bow_bow + tag: midwhite + DOES: + id: Heteronym Room/Panel_does_does + tag: midwhite + MOBILE: + id: Heteronym Room/Panel_mobile_mobile + tag: double midwhite + subtag: left + link: exact MOBILE + MOBILE (2): + id: Heteronym Room/Panel_mobile_mobile_2 + tag: double midwhite + subtag: right + link: exact MOBILE + DESERT: + id: Heteronym Room/Panel_desert_desert + tag: topmid white stack + subtag: mid + link: topmid DESERT + DESSERT: + id: Heteronym Room/Panel_desert_dessert + tag: topmid white stack + subtag: top + link: topmid DESERT + SOW: + id: Heteronym Room/Panel_sow_sow + tag: topmid white stack + subtag: mid + link: topmid SOW + SEW: + id: Heteronym Room/Panel_sow_so + tag: topmid white stack + subtag: top + link: topmid SOW + TO: + id: Heteronym Room/Panel_two_to + tag: double topwhite + subtag: left + link: hp TWO + TOO: + id: Heteronym Room/Panel_two_too + tag: double topwhite + subtag: right + link: hp TWO + WRITE: + id: Heteronym Room/Panel_write_right + tag: topwhite + EWE: + id: Heteronym Room/Panel_you_ewe + tag: topwhite + KNOT: + id: Heteronym Room/Panel_not_knot + tag: double topwhite + subtag: left + link: hp NOT + NAUGHT: + id: Heteronym Room/Panel_not_naught + tag: double topwhite + subtag: right + link: hp NOT + BEAR (2): + id: Heteronym Room/Panel_bear_bare + tag: topwhite + Second Room: + entrances: + Starting Room: + room: Starting Room + door: Main Door + Hub Room: + door: Exit Door + panels: + HI: + id: Entry Room/Panel_hi_high + tag: topwhite + LOW: + id: Entry Room/Panel_low_low + tag: forbid # This is a midwhite pretending to be a botwhite + ANOTHER TRY: + id: Entry Room/Panel_advance + tag: topwhite + LEVEL 2: + # We will set up special rules for this in code. + id: EndPanel/Panel_level_2 + tag: forbid + non_counting: True + check: True + required_panel: + - panel: ANOTHER TRY + doors: + Exit Door: + id: Entry Room Area Doors/Door_hi_high + location_name: Second Room - Good Luck + include_reduce: True + panels: + - HI + - LOW + Hub Room: + entrances: + Second Room: + room: Second Room + door: Exit Door + Dead End Area: + door: Near RAT Door + Crossroads: + door: Crossroads Entrance + The Tenacious: + door: Tenacious Entrance + Warts Straw Area: + door: Symmetry Door + Hedge Maze: + door: Shortcut to Hedge Maze + Orange Tower First Floor: + room: Orange Tower First Floor + door: Shortcut to Hub Room + Owl Hallway: + painting: True + Outside The Initiated: + room: Outside The Initiated + door: Shortcut to Hub Room + The Traveled: + door: Traveled Entrance + Roof: True # through the sunwarp + Outside The Undeterred: # (NOTE: used in hardcoded pilgrimage) + room: Outside The Undeterred + door: Green Painting + painting: True + panels: + ORDER: + id: Shuffle Room/Panel_order_chaos + colors: black + tag: botblack + SLAUGHTER: + id: Palindrome Room/Panel_slaughter_laughter + colors: red + tag: midred + NEAR: + id: Symmetry Room/Panel_near_far + colors: black + tag: botblack + FAR: + id: Symmetry Room/Panel_far_near + colors: black + tag: botblack + TRACE: + id: Maze Room/Panel_trace_trace + tag: midwhite + RAT: + id: Appendix Room/Panel_rat_tar + colors: black + check: True + exclude_reduce: True + tag: midblack + OPEN: + id: Synonym Room/Panel_open_open + tag: midwhite + FOUR: + id: Backside Room/Panel_four_four_3 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + LOST: + id: Shuffle Room/Panel_lost_found + colors: black + tag: botblack + FORWARD: + id: Entry Room/Panel_forward_forward + tag: midwhite + BETWEEN: + id: Entry Room/Panel_between_between + tag: midwhite + BACKWARD: + id: Entry Room/Panel_backward_backward + tag: midwhite + doors: + Crossroads Entrance: + id: Shuffle Room Area Doors/Door_chaos + panels: + - ORDER + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_slaughter_laughter + group: Entrances to The Tenacious + panels: + - SLAUGHTER + Symmetry Door: + id: + - Symmetry Room Area Doors/Door_near_far + - Symmetry Room Area Doors/Door_far_near + group: Symmetry Doors + panels: + - NEAR + - FAR + Shortcut to Hedge Maze: + id: Maze Area Doors/Door_trace_trace + group: Hedge Maze Doors + panels: + - TRACE + Near RAT Door: + id: Appendix Room Area Doors/Door_deadend_deadened + skip_location: True + group: Dead End Area Access + panels: + - room: Hidden Room + panel: DEAD END + Traveled Entrance: + id: Appendix Room Area Doors/Door_open_open + item_name: The Traveled - Entrance + group: Entrance to The Traveled + panels: + - OPEN + Lost Door: + id: Shuffle Room Area Doors/Door_lost_found + junk_item: True + panels: + - LOST + paintings: + - id: maze_painting + orientation: west + Dead End Area: + entrances: + Hidden Room: + room: Hidden Room + door: Dead End Door + Hub Room: + room: Hub Room + door: Near RAT Door + panels: + FOUR: + id: Backside Room/Panel_four_four_2 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + EIGHT: + id: Backside Room/Panel_eight_eight_8 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + paintings: + - id: smile_painting_6 + orientation: north + Pilgrim Antechamber: + # Let's not shuffle the paintings yet. + entrances: + # The pilgrimage is hardcoded in rules.py + Starting Room: + door: Sun Painting + panels: + HOT CRUST: + id: Lingo Room/Panel_shortcut + colors: yellow + tag: midyellow + PILGRIMAGE: + id: Lingo Room/Panel_pilgrim + colors: blue + tag: midblue + MASTERY: + id: Master Room/Panel_mastery_mastery14 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Sun Painting: + item_name: Pilgrim Room - Sun Painting + location_name: Pilgrim Room - HOT CRUST + painting_id: pilgrim_painting2 + panels: + - HOT CRUST + Exit: + event: True + panels: + - PILGRIMAGE + Pilgrim Room: + entrances: + The Seeker: + door: Shortcut to The Seeker + Pilgrim Antechamber: + room: Pilgrim Antechamber + door: Exit + panels: + THIS: + id: Lingo Room/Panel_lingo_9 + colors: gray + tag: forbid + TIME ROOM: + id: Lingo Room/Panel_lingo_1 + colors: purple + tag: toppurp + SCIENCE ROOM: + id: Lingo Room/Panel_lingo_2 + tag: botwhite + SHINY ROCK ROOM: + id: Lingo Room/Panel_lingo_3 + tag: botwhite + ANGRY POWER: + id: Lingo Room/Panel_lingo_4 + colors: + - purple + tag: forbid + MICRO LEGION: + id: Lingo Room/Panel_lingo_5 + colors: yellow + tag: midyellow + LOSERS RELAX: + id: Lingo Room/Panel_lingo_6 + colors: + - black + tag: forbid + "906234": + id: Lingo Room/Panel_lingo_7 + colors: + - orange + - blue + tag: forbid + MOOR EMORDNILAP: + id: Lingo Room/Panel_lingo_8 + colors: black + tag: midblack + HALL ROOMMATE: + id: Lingo Room/Panel_lingo_10 + colors: + - red + - blue + tag: forbid + ALL GREY: + id: Lingo Room/Panel_lingo_11 + colors: yellow + tag: midyellow + PLUNDER ISLAND: + id: Lingo Room/Panel_lingo_12 + colors: + - purple + - red + tag: forbid + FLOSS PATHS: + id: Lingo Room/Panel_lingo_13 + colors: + - purple + - brown + tag: forbid + doors: + Shortcut to The Seeker: + id: Master Room Doors/Door_pilgrim_shortcut + include_reduce: True + panels: + - THIS + Crossroads: + entrances: + Hub Room: True # The sunwarp means that we never need the ORDER door + Color Hallways: True + The Tenacious: + door: Tenacious Entrance + Orange Tower Fourth Floor: True # through IRK HORN + Amen Name Area: + room: Lost Area + door: Exit + Roof: True # through the sunwarp + panels: + DECAY: + id: Palindrome Room/Panel_decay_day + colors: red + tag: midred + NOPE: + id: Sun Room/Panel_nope_open + colors: yellow + tag: midyellow + EIGHT: + id: Backside Room/Panel_eight_eight_5 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + WE ROT: + id: Shuffle Room/Panel_tower + colors: yellow + tag: midyellow + WORDS: + id: Shuffle Room/Panel_words_sword + colors: yellow + tag: midyellow + SWORD: + id: Shuffle Room/Panel_sword_words + colors: yellow + tag: midyellow + TURN: + id: Shuffle Room/Panel_turn_runt + colors: yellow + tag: midyellow + BEND HI: + id: Shuffle Room/Panel_behind + colors: yellow + tag: midyellow + THE EYES: + id: Shuffle Room/Panel_eyes_see_shuffle + colors: yellow + check: True + exclude_reduce: True + required_door: + door: Hollow Hallway + tag: midyellow + CORNER: + id: Shuffle Room/Panel_corner_corner + required_door: + door: Hollow Hallway + tag: midwhite + HOLLOW: + id: Shuffle Room/Panel_hollow_hollow + required_door: + door: Hollow Hallway + tag: midwhite + SWAP: + id: Shuffle Room/Panel_swap_wasp + colors: yellow + tag: midyellow + GEL: + id: Shuffle Room/Panel_gel + colors: yellow + tag: topyellow + required_door: + door: Tower Entrance + THOUGH: + id: Shuffle Room/Panel_though + colors: yellow + tag: topyellow + required_door: + door: Tower Entrance + CROSSROADS: + id: Shuffle Room/Panel_crossroads_crossroads + tag: midwhite + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_decay_day + group: Entrances to The Tenacious + panels: + - DECAY + Discerning Entrance: + id: Shuffle Room Area Doors/Door_nope_open + item_name: The Discerning - Entrance + panels: + - NOPE + Tower Entrance: + id: + - Shuffle Room Area Doors/Door_tower + - Shuffle Room Area Doors/Door_tower2 + - Shuffle Room Area Doors/Door_tower3 + - Shuffle Room Area Doors/Door_tower4 + group: Crossroads - Tower Entrances + panels: + - WE ROT + Tower Back Entrance: + id: Shuffle Room Area Doors/Door_runt + location_name: Crossroads - TURN/RUNT + group: Crossroads - Tower Entrances + panels: + - TURN + - room: Orange Tower Fourth Floor + panel: RUNT + Words Sword Door: + id: + - Shuffle Room Area Doors/Door_words_shuffle_3 + - Shuffle Room Area Doors/Door_words_shuffle_4 + group: Crossroads Doors + panels: + - WORDS + - SWORD + Eye Wall: + id: Shuffle Room Area Doors/Door_behind + junk_item: True + group: Crossroads Doors + panels: + - BEND HI + Hollow Hallway: + id: Shuffle Room Area Doors/Door_crossroads6 + skip_location: True + group: Crossroads Doors + panels: + - BEND HI + Roof Access: + id: Tower Room Area Doors/Door_level_6_2 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST + paintings: + - id: eye_painting + disable: True + orientation: east + move: True + required_door: + door: Eye Wall + - id: smile_painting_4 + orientation: south + Lost Area: + entrances: + Outside The Agreeable: + door: Exit + Crossroads: + room: Crossroads + door: Words Sword Door + panels: + LOST (1): + id: Shuffle Room/Panel_lost_lots + colors: yellow + tag: midyellow + LOST (2): + id: Shuffle Room/Panel_lost_slot + colors: yellow + tag: midyellow + doors: + Exit: + id: + - Shuffle Room Area Doors/Door_lost_shuffle_1 + - Shuffle Room Area Doors/Door_lost_shuffle_2 + location_name: Crossroads - LOST Pair + panels: + - LOST (1) + - LOST (2) + Amen Name Area: + entrances: + Crossroads: + room: Lost Area + door: Exit + Suits Area: + door: Exit + panels: + AMEN: + id: Shuffle Room/Panel_amen_mean + colors: yellow + tag: double midyellow + subtag: left + link: ana MEAN + NAME: + id: Shuffle Room/Panel_name_mean + colors: yellow + tag: double midyellow + subtag: right + link: ana MEAN + NINE: + id: Backside Room/Panel_nine_nine_3 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + doors: + Exit: + id: Shuffle Room Area Doors/Door_mean + panels: + - AMEN + - NAME + Suits Area: + entrances: + Amen Name Area: + room: Amen Name Area + door: Exit + Roof: True + panels: + SPADES: + id: Cross Room/Panel_spades_spades + tag: midwhite + CLUBS: + id: Cross Room/Panel_clubs_clubs + tag: midwhite + HEARTS: + id: Cross Room/Panel_hearts_hearts + tag: midwhite + paintings: + - id: west_afar + orientation: south + The Tenacious: + entrances: + Hub Room: + - room: Hub Room + door: Tenacious Entrance + - door: Shortcut to Hub Room + Crossroads: + room: Crossroads + door: Tenacious Entrance + Outside The Agreeable: + room: Outside The Agreeable + door: Tenacious Entrance + Dread Hallway: + room: Dread Hallway + door: Tenacious Entrance + panels: + LEVEL (Black): + id: Palindrome Room/Panel_level_level + colors: black + tag: midblack + RACECAR (Black): + id: Palindrome Room/Panel_racecar_racecar + colors: black + tag: palindrome + copy_to_sign: sign4 + SOLOS (Black): + id: Palindrome Room/Panel_solos_solos + colors: black + tag: palindrome + copy_to_sign: + - sign5 + - sign6 + LEVEL (White): + id: Palindrome Room/Panel_level_level_2 + tag: midwhite + RACECAR (White): + id: Palindrome Room/Panel_racecar_racecar_2 + tag: midwhite + copy_to_sign: sign3 + SOLOS (White): + id: Palindrome Room/Panel_solos_solos_2 + tag: midwhite + copy_to_sign: + - sign1 + - sign2 + Achievement: + id: Countdown Panels/Panel_tenacious_tenacious + check: True + tag: forbid + required_panel: + - panel: LEVEL (Black) + - panel: RACECAR (Black) + - panel: SOLOS (Black) + - panel: LEVEL (White) + - panel: RACECAR (White) + - panel: SOLOS (White) + - room: Hub Room + panel: SLAUGHTER + - room: Crossroads + panel: DECAY + - room: Outside The Agreeable + panel: MASSACRED + - room: Dread Hallway + panel: DREAD + achievement: The Tenacious + doors: + Shortcut to Hub Room: + id: + - Palindrome Room Area Doors/Door_level_level_1 + - Palindrome Room Area Doors/Door_racecar_racecar_1 + - Palindrome Room Area Doors/Door_solos_solos_1 + location_name: The Tenacious - Palindromes + group: Entrances to The Tenacious + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) + White Palindromes: + location_name: The Tenacious - White Palindromes + skip_item: True + panels: + - LEVEL (White) + - RACECAR (White) + - SOLOS (White) + Warts Straw Area: + entrances: + Hub Room: + room: Hub Room + door: Symmetry Door + Leaf Feel Area: + door: Door + panels: + WARTS: + id: Symmetry Room/Panel_warts_straw + colors: black + tag: midblack + STRAW: + id: Symmetry Room/Panel_straw_warts + colors: black + tag: midblack + doors: + Door: + id: + - Symmetry Room Area Doors/Door_warts_straw + - Symmetry Room Area Doors/Door_straw_warts + group: Symmetry Doors + panels: + - WARTS + - STRAW + Leaf Feel Area: + entrances: + Warts Straw Area: + room: Warts Straw Area + door: Door + Outside The Agreeable: + door: Door + panels: + LEAF: + id: Symmetry Room/Panel_leaf_feel + colors: black + tag: topblack + FEEL: + id: Symmetry Room/Panel_feel_leaf + colors: black + tag: topblack + doors: + Door: + id: + - Symmetry Room Area Doors/Door_leaf_feel + - Symmetry Room Area Doors/Door_feel_leaf + group: Symmetry Doors + panels: + - LEAF + - FEEL + Outside The Agreeable: + # Let's ignore the blue warp thing for now because the lookout is a dead + # end. Later on it could be filler checks. + entrances: + # We don't have to list Lost Area because of Crossroads. + Crossroads: True + The Tenacious: + door: Tenacious Entrance + The Agreeable: + door: Agreeable Entrance + Dread Hallway: + door: Black Door + Leaf Feel Area: + room: Leaf Feel Area + door: Door + Starting Room: + door: Painting Shortcut + painting: True + Hallway Room (2): True + Hallway Room (3): True + Hallway Room (4): True + Hedge Maze: True # through the door to the sectioned-off part of the hedge maze + panels: + MASSACRED: + id: Palindrome Room/Panel_massacred_sacred + colors: red + tag: midred + BLACK: + id: Symmetry Room/Panel_black_white + colors: black + tag: botblack + CLOSE: + id: Antonym Room/Panel_close_open + colors: black + tag: botblack + LEFT: + id: Symmetry Room/Panel_left_right + colors: black + tag: botblack + LEFT (2): + id: Symmetry Room/Panel_left_wrong + colors: black + tag: bot black black + RIGHT: + id: Symmetry Room/Panel_right_left + colors: black + tag: botblack + PURPLE: + id: Color Arrow Room/Panel_purple_afar + tag: midwhite + required_door: + door: Purple Barrier + FIVE (1): + id: Backside Room/Panel_five_five_5 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + FIVE (2): + id: Backside Room/Panel_five_five_4 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + OUT: + id: Hallway Room/Panel_out_out + check: True + exclude_reduce: True + tag: midwhite + HIDE: + id: Maze Room/Panel_hide_seek_4 + colors: black + tag: botblack + DAZE: + id: Maze Room/Panel_daze_maze + colors: purple + tag: midpurp + WALL: + id: Hallway Room/Panel_castle_1 + colors: blue + tag: quad bot blue + link: qbb CASTLE + KEEP: + id: Hallway Room/Panel_castle_2 + colors: blue + tag: quad bot blue + link: qbb CASTLE + BAILEY: + id: Hallway Room/Panel_castle_3 + colors: blue + tag: quad bot blue + link: qbb CASTLE + TOWER: + id: Hallway Room/Panel_castle_4 + colors: blue + tag: quad bot blue + link: qbb CASTLE + NORTH: + id: Cross Room/Panel_north_missing + colors: green + tag: forbid + required_room: Outside The Bold + DIAMONDS: + id: Cross Room/Panel_diamonds_missing + colors: green + tag: forbid + required_room: Suits Area + FIRE: + id: Cross Room/Panel_fire_missing + colors: green + tag: forbid + required_room: Elements Area + WINTER: + id: Cross Room/Panel_winter_missing + colors: green + tag: forbid + required_room: Orange Tower Fifth Floor + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_massacred_sacred + group: Entrances to The Tenacious + panels: + - MASSACRED + Black Door: + id: Symmetry Room Area Doors/Door_black_white + group: Entrances to The Tenacious + panels: + - BLACK + Agreeable Entrance: + id: Symmetry Room Area Doors/Door_close_open + item_name: The Agreeable - Entrance + panels: + - CLOSE + Painting Shortcut: + item_name: Starting Room - Street Painting + painting_id: eyes_yellow_painting2 + panels: + - RIGHT + Purple Barrier: + id: Color Arrow Room Doors/Door_purple_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: PURPLE + Hallway Door: + id: Red Blue Purple Room Area Doors/Door_room_2 + group: Hallway Room Doors + location_name: Hallway Room - First Room + panels: + - WALL + - KEEP + - BAILEY + - TOWER + paintings: + - id: panda_painting + orientation: south + - id: eyes_yellow_painting + orientation: east + progression: + Progressive Hallway Room: + - Hallway Door + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + Dread Hallway: + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Black Door + The Tenacious: + door: Tenacious Entrance + panels: + DREAD: + id: Palindrome Room/Panel_dread_dead + colors: red + tag: midred + doors: + Tenacious Entrance: + id: Palindrome Room Area Doors/Door_dread_dead + group: Entrances to The Tenacious + panels: + - DREAD + The Agreeable: + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Agreeable Entrance + Hedge Maze: + door: Shortcut to Hedge Maze + panels: + Achievement: + id: Countdown Panels/Panel_disagreeable_agreeable + colors: black + tag: forbid + required_room: Outside The Agreeable + check: True + achievement: The Agreeable + BYE: + id: Antonym Room/Panel_bye_hi + colors: black + tag: botblack + RETOOL: + id: Antonym Room/Panel_retool_looter + colors: black + tag: midblack + DRAWER: + id: Antonym Room/Panel_drawer_reward + colors: black + tag: midblack + READ: + id: Antonym Room/Panel_read_write + colors: black + tag: botblack + DIFFERENT: + id: Antonym Room/Panel_different_same + colors: black + tag: botblack + LOW: + id: Antonym Room/Panel_low_high + colors: black + tag: botblack + ALIVE: + id: Antonym Room/Panel_alive_dead + colors: black + tag: botblack + THAT: + id: Antonym Room/Panel_that_this + colors: black + tag: botblack + STRESSED: + id: Antonym Room/Panel_stressed_desserts + colors: black + tag: midblack + STAR: + id: Antonym Room/Panel_star_rats + colors: black + tag: midblack + TAME: + id: Antonym Room/Panel_tame_mate + colors: black + tag: topblack + CAT: + id: Antonym Room/Panel_cat_tack + colors: black + tag: topblack + doors: + Shortcut to Hedge Maze: + id: Symmetry Room Area Doors/Door_bye_hi + group: Hedge Maze Doors + panels: + - BYE + Hedge Maze: + entrances: + Hub Room: + room: Hub Room + door: Shortcut to Hedge Maze + Color Hallways: True + The Agreeable: + room: The Agreeable + door: Shortcut to Hedge Maze + The Perceptive: True + The Observant: + door: Observant Entrance + Owl Hallway: + room: Owl Hallway + door: Shortcut to Hedge Maze + Roof: True + panels: + DOWN: + id: Maze Room/Panel_down_up + colors: black + tag: botblack + HIDE (1): + id: Maze Room/Panel_hide_seek + colors: black + tag: botblack + HIDE (2): + id: Maze Room/Panel_hide_seek_2 + colors: black + tag: botblack + HIDE (3): + id: Maze Room/Panel_hide_seek_3 + colors: black + tag: botblack + MASTERY (1): + id: Master Room/Panel_mastery_mastery5 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (2): + id: Master Room/Panel_mastery_mastery9 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + PATH (1): + id: Maze Room/Panel_path_lock + colors: green + tag: forbid + PATH (2): + id: Maze Room/Panel_path_knot + colors: green + tag: forbid + PATH (3): + id: Maze Room/Panel_path_lost + colors: green + tag: forbid + PATH (4): + id: Maze Room/Panel_path_open + colors: green + tag: forbid + PATH (5): + id: Maze Room/Panel_path_help + colors: green + tag: forbid + PATH (6): + id: Maze Room/Panel_path_hunt + colors: green + tag: forbid + PATH (7): + id: Maze Room/Panel_path_nest + colors: green + tag: forbid + PATH (8): + id: Maze Room/Panel_path_look + colors: green + tag: forbid + REFLOW: + id: Maze Room/Panel_reflow_flower + colors: yellow + tag: midyellow + LEAP: + id: Maze Room/Panel_leap_jump + tag: botwhite + doors: + Perceptive Entrance: + id: Maze Area Doors/Door_maze_maze + item_name: The Perceptive - Entrance + group: Hedge Maze Doors + panels: + - DOWN + Painting Shortcut: + painting_id: garden_painting_tower2 + item_name: Starting Room - Hedge Maze Painting + skip_location: True + panels: + - DOWN + Observant Entrance: + id: + - Maze Area Doors/Door_look_room_1 + - Maze Area Doors/Door_look_room_2 + - Maze Area Doors/Door_look_room_3 + skip_location: True + item_name: The Observant - Entrance + group: Observant Doors + panels: + - room: The Perceptive + panel: GAZE + Hide and Seek: + skip_item: True + location_name: Hedge Maze - Hide and Seek + include_reduce: True + panels: + - HIDE (1) + - HIDE (2) + - HIDE (3) + - room: Outside The Agreeable + panel: HIDE + The Perceptive: + entrances: + Starting Room: + room: Hedge Maze + door: Painting Shortcut + painting: True + Hedge Maze: + room: Hedge Maze + door: Perceptive Entrance + panels: + Achievement: + id: Countdown Panels/Panel_perceptive_perceptive + colors: green + tag: forbid + check: True + achievement: The Perceptive + GAZE: + id: Maze Room/Panel_look_look + check: True + exclude_reduce: True + tag: botwhite + paintings: + - id: garden_painting_tower + orientation: north + The Fearless (First Floor): + entrances: + The Perceptive: True + panels: + NAPS: + id: Naps Room/Panel_naps_span + colors: black + tag: midblack + TEAM: + id: Naps Room/Panel_team_meet + colors: black + tag: topblack + TEEM: + id: Naps Room/Panel_teem_meat + colors: black + tag: topblack + IMPATIENT: + id: Naps Room/Panel_impatient_doctor + colors: black + tag: bot black black + EAT: + id: Naps Room/Panel_eat_tea + colors: black + tag: topblack + doors: + Second Floor: + id: Naps Room Doors/Door_hider_5 + location_name: The Fearless - First Floor Puzzles + group: Fearless Doors + panels: + - NAPS + - TEAM + - TEEM + - IMPATIENT + - EAT + progression: + Progressive Fearless: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor + The Fearless (Second Floor): + entrances: + The Fearless (First Floor): + room: The Fearless (First Floor) + door: Second Floor + panels: + NONE: + id: Naps Room/Panel_one_many + colors: black + tag: bot black top white + SUM: + id: Naps Room/Panel_one_none + colors: black + tag: top white bot black + FUNNY: + id: Naps Room/Panel_funny_enough + colors: black + tag: topblack + MIGHT: + id: Naps Room/Panel_might_time + colors: black + tag: topblack + SAFE: + id: Naps Room/Panel_safe_face + colors: black + tag: topblack + SAME: + id: Naps Room/Panel_same_mace + colors: black + tag: topblack + CAME: + id: Naps Room/Panel_came_make + colors: black + tag: topblack + doors: + Third Floor: + id: + - Naps Room Doors/Door_hider_1b2 + - Naps Room Doors/Door_hider_new1 + location_name: The Fearless - Second Floor Puzzles + group: Fearless Doors + panels: + - NONE + - SUM + - FUNNY + - MIGHT + - SAFE + - SAME + - CAME + The Fearless: + entrances: + The Fearless (First Floor): + room: The Fearless (Second Floor) + door: Third Floor + panels: + Achievement: + id: Countdown Panels/Panel_fearless_fearless + colors: black + tag: forbid + check: True + achievement: The Fearless + EASY: + id: Naps Room/Panel_easy_soft + colors: black + tag: bot black black + SOMETIMES: + id: Naps Room/Panel_sometimes_always + colors: black + tag: bot black black + DARK: + id: Naps Room/Panel_dark_extinguish + colors: black + tag: bot black black + EVEN: + id: Naps Room/Panel_even_ordinary + colors: black + tag: bot black black + The Observant: + entrances: + Hedge Maze: + room: Hedge Maze + door: Observant Entrance + The Incomparable: True + panels: + Achievement: + id: Countdown Panels/Panel_observant_observant + colors: green + check: True + tag: forbid + required_door: + door: Stairs + achievement: The Observant + BACK: + id: Look Room/Panel_four_back + colors: green + tag: forbid + SIDE: + id: Look Room/Panel_four_side + colors: green + tag: forbid + BACKSIDE: + id: Backside Room/Panel_backside_2 + tag: midwhite + required_door: + door: Backside Door + STAIRS: + id: Look Room/Panel_six_stairs + colors: green + tag: forbid + WAYS: + id: Look Room/Panel_four_ways + colors: green + tag: forbid + "ON": + id: Look Room/Panel_two_on + colors: green + tag: forbid + UP: + id: Look Room/Panel_two_up + colors: green + tag: forbid + SWIMS: + id: Look Room/Panel_five_swims + colors: green + tag: forbid + UPSTAIRS: + id: Look Room/Panel_eight_upstairs + colors: green + tag: forbid + required_door: + door: Stairs + TOIL: + id: Look Room/Panel_blue_toil + colors: green + tag: forbid + required_door: + door: Stairs + STOP: + id: Look Room/Panel_four_stop + colors: green + tag: forbid + required_door: + door: Stairs + TOP: + id: Look Room/Panel_aqua_top + colors: green + tag: forbid + required_door: + door: Stairs + HI: + id: Look Room/Panel_blue_hi + colors: green + tag: forbid + required_door: + door: Stairs + HI (2): + id: Look Room/Panel_blue_hi2 + colors: green + tag: forbid + required_door: + door: Stairs + "31": + id: Look Room/Panel_numbers_31 + colors: green + tag: forbid + required_door: + door: Stairs + "52": + id: Look Room/Panel_numbers_52 + colors: green + tag: forbid + required_door: + door: Stairs + OIL: + id: Look Room/Panel_aqua_oil + colors: green + tag: forbid + required_door: + door: Stairs + BACKSIDE (GREEN): + id: Look Room/Panel_eight_backside + colors: green + tag: forbid + required_door: + door: Stairs + SIDEWAYS: + id: Look Room/Panel_eight_sideways + colors: green + tag: forbid + required_door: + door: Stairs + doors: + Backside Door: + id: Maze Area Doors/Door_backside + group: Backside Doors + panels: + - BACK + - SIDE + Stairs: + id: Maze Area Doors/Door_stairs + group: Observant Doors + panels: + - STAIRS + The Incomparable: + entrances: + The Observant: True # Assuming that access to The Observant includes access to the right entrance + Eight Room: True + Eight Alcove: + door: Eight Painting + panels: + Achievement: + id: Countdown Panels/Panel_incomparable_incomparable + colors: blue + check: True + tag: forbid + required_room: + - Elements Area + - Courtyard + - Eight Room + achievement: The Incomparable + A (One): + id: Strand Room/Panel_blank_a + colors: blue + tag: forbid + A (Two): + id: Strand Room/Panel_a_an + colors: blue + tag: forbid + A (Three): + id: Strand Room/Panel_a_and + colors: blue + tag: forbid + A (Four): + id: Strand Room/Panel_a_sand + colors: blue + tag: forbid + A (Five): + id: Strand Room/Panel_a_stand + colors: blue + tag: forbid + A (Six): + id: Strand Room/Panel_a_strand + colors: blue + tag: forbid + I (One): + id: Strand Room/Panel_blank_i + colors: blue + tag: forbid + I (Two): + id: Strand Room/Panel_i_in + colors: blue + tag: forbid + I (Three): + id: Strand Room/Panel_i_sin + colors: blue + tag: forbid + I (Four): + id: Strand Room/Panel_i_sing + colors: blue + tag: forbid + I (Five): + id: Strand Room/Panel_i_sting + colors: blue + tag: forbid + I (Six): + id: Strand Room/Panel_i_string + colors: blue + tag: forbid + I (Seven): + id: Strand Room/Panel_i_strings + colors: blue + tag: forbid + doors: + Eight Painting: + id: Red Blue Purple Room Area Doors/Door_a_strands + location_name: Giant Sevens + group: Observant Doors + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A + Eight Alcove: + entrances: + The Incomparable: + room: The Incomparable + door: Eight Painting + paintings: + - id: eight_painting2 + orientation: north + Eight Room: + entrances: + Eight Alcove: + painting: True + panels: + Eight Back: + id: Strand Room/Panel_i_starling + colors: blue + tag: forbid + Eight Front: + id: Strand Room/Panel_i_starting + colors: blue + tag: forbid + Nine: + id: Strand Room/Panel_i_startling + colors: blue + tag: forbid + paintings: + - id: eight_painting + orientation: south + exit_only: True + required: True + Orange Tower: + # This is a special, meta-ish room. + entrances: + Menu: True + doors: + Second Floor: + id: Tower Room Area Doors/Door_level_1 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + Third Floor: + id: Tower Room Area Doors/Door_level_2 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + Fourth Floor: + id: Tower Room Area Doors/Door_level_3 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + Fifth Floor: + id: Tower Room Area Doors/Door_level_4 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + Sixth Floor: + id: Tower Room Area Doors/Door_level_5 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + Seventh Floor: + id: Tower Room Area Doors/Door_level_6 + skip_location: True + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST + progression: + Progressive Orange Tower: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor + Orange Tower First Floor: + entrances: + Hub Room: + door: Shortcut to Hub Room + Outside The Wanderer: + room: Outside The Wanderer + door: Tower Entrance + Orange Tower Second Floor: + room: Orange Tower + door: Second Floor + Directional Gallery: + door: Salt Pepper Door + Roof: True # through the sunwarp + panels: + SECRET: + id: Shuffle Room/Panel_secret_secret + tag: midwhite + DADS + ALE: + id: Tower Room/Panel_dads_ale_dead_1 + colors: orange + check: True + tag: midorange + SALT: + id: Backside Room/Panel_salt_pepper + colors: black + tag: botblack + doors: + Shortcut to Hub Room: + id: Shuffle Room Area Doors/Door_secret_secret + group: Orange Tower First Floor - Shortcuts + panels: + - SECRET + Salt Pepper Door: + id: Count Up Room Area Doors/Door_salt_pepper + location_name: Orange Tower First Floor - Salt Pepper Door + group: Orange Tower First Floor - Shortcuts + panels: + - SALT + - room: Directional Gallery + panel: PEPPER + Orange Tower Second Floor: + entrances: + Orange Tower First Floor: + room: Orange Tower + door: Second Floor + Orange Tower Third Floor: + room: Orange Tower + door: Third Floor + Outside The Undeterred: True + Orange Tower Third Floor: + entrances: + Knight Night Exit: + room: Knight Night (Final) + door: Exit + Orange Tower Second Floor: + room: Orange Tower + door: Third Floor + Orange Tower Fourth Floor: + room: Orange Tower + door: Fourth Floor + Hot Crusts Area: True # sunwarp + Bearer Side Area: # This is complicated because of The Bearer's topology + room: Bearer Side Area + door: Shortcut to Tower + Rhyme Room (Smiley): + door: Rhyme Room Entrance + panels: + RED: + id: Color Arrow Room/Panel_red_afar + tag: midwhite + required_door: + door: Red Barrier + DEER + WREN: + id: Tower Room/Panel_deer_wren_rats_3 + colors: orange + check: True + tag: midorange + doors: + Red Barrier: + id: Color Arrow Room Doors/Door_red_6 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: RED + Rhyme Room Entrance: + id: Double Room Area Doors/Door_room_entry_stairs2 + skip_location: True + group: Rhyme Room Doors + panels: + - room: The Tenacious + panel: LEVEL (Black) + - room: The Tenacious + panel: RACECAR (Black) + - room: The Tenacious + panel: SOLOS (Black) + Orange Barrier: # see note in Outside The Initiated + id: + - Color Arrow Room Doors/Door_orange_hider_1 + - Color Arrow Room Doors/Door_orange_hider_2 + - Color Arrow Room Doors/Door_orange_hider_3 + location_name: Color Hunt - RED and YELLOW + group: Champion's Rest - Color Barriers + item_name: Champion's Rest - Orange Barrier + panels: + - RED + - room: Directional Gallery + panel: YELLOW + paintings: + - id: arrows_painting_6 + orientation: east + - id: flower_painting_5 + orientation: south + Orange Tower Fourth Floor: + entrances: + Orange Tower Third Floor: + room: Orange Tower + door: Fourth Floor + Orange Tower Fifth Floor: + room: Orange Tower + door: Fifth Floor + Hot Crusts Area: + door: Hot Crusts Door + Crossroads: + - room: Crossroads + door: Tower Entrance + - room: Crossroads + door: Tower Back Entrance + Courtyard: True + Roof: True # through the sunwarp + panels: + RUNT: + id: Shuffle Room/Panel_turn_runt2 + colors: yellow + tag: midyellow + RUNT (2): + id: Shuffle Room/Panel_runt3 + colors: + - yellow + - blue + tag: mid yellow blue + LEARNS + UNSEW: + id: Tower Room/Panel_learns_unsew_unrest_4 + colors: orange + check: True + tag: midorange + HOT CRUSTS: + id: Shuffle Room/Panel_shortcuts + colors: yellow + tag: midyellow + IRK HORN: + id: Shuffle Room/Panel_corner + colors: yellow + check: True + exclude_reduce: True + tag: topyellow + doors: + Hot Crusts Door: + id: Shuffle Room Area Doors/Door_hotcrust_shortcuts + panels: + - HOT CRUSTS + Hot Crusts Area: + entrances: + Orange Tower Fourth Floor: + room: Orange Tower Fourth Floor + door: Hot Crusts Door + Roof: True # through the sunwarp + panels: + EIGHT: + id: Backside Room/Panel_eight_eight_3 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + paintings: + - id: smile_painting_8 + orientation: north + Orange Tower Fifth Floor: + entrances: + Orange Tower Fourth Floor: + room: Orange Tower + door: Fifth Floor + Orange Tower Sixth Floor: + room: Orange Tower + door: Sixth Floor + Cellar: + room: Room Room + door: Shortcut to Fifth Floor + Welcome Back Area: + door: Welcome Back + Art Gallery: + room: Art Gallery + door: Exit + The Bearer: + room: Art Gallery + door: Exit + Outside The Initiated: + room: Art Gallery + door: Exit + panels: + SIZE (Small): + id: Entry Room/Panel_size_small + colors: gray + tag: forbid + SIZE (Big): + id: Entry Room/Panel_size_big + colors: gray + tag: forbid + DRAWL + RUNS: + id: Tower Room/Panel_drawl_runs_enter_5 + colors: orange + check: True + tag: midorange + NINE: + id: Backside Room/Panel_nine_nine_2 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + SUMMER: + id: Entry Room/Panel_summer_summer + tag: midwhite + AUTUMN: + id: Entry Room/Panel_autumn_autumn + tag: midwhite + SPRING: + id: Entry Room/Panel_spring_spring + tag: midwhite + PAINTING (1): + id: Panel Room/Panel_painting_flower + colors: green + tag: forbid + required_room: Cellar + PAINTING (2): + id: Panel Room/Panel_painting_eye + colors: green + tag: forbid + required_room: Cellar + PAINTING (3): + id: Panel Room/Panel_painting_snowman + colors: green + tag: forbid + required_room: Cellar + PAINTING (4): + id: Panel Room/Panel_painting_owl + colors: green + tag: forbid + required_room: Cellar + PAINTING (5): + id: Panel Room/Panel_painting_panda + colors: green + tag: forbid + required_room: Cellar + ROOM: + id: Panel Room/Panel_room_stairs + colors: gray + tag: forbid + required_room: Cellar + doors: + Welcome Back: + id: Entry Room Area Doors/Door_sizes + group: Welcome Back Doors + panels: + - SIZE (Small) + - SIZE (Big) + paintings: + - id: hi_solved_painting3 + orientation: south + - id: hi_solved_painting2 + orientation: south + - id: east_afar + orientation: north + Orange Tower Sixth Floor: + entrances: + Orange Tower Fifth Floor: + room: Orange Tower + door: Sixth Floor + The Scientific: + painting: True + paintings: + - id: arrows_painting_10 + orientation: east + - id: owl_painting_3 + orientation: north + - id: clock_painting + orientation: west + - id: scenery_painting_5d_2 + orientation: south + - id: symmetry_painting_b_7 + orientation: north + - id: panda_painting_2 + orientation: south + - id: pencil_painting + orientation: north + - id: colors_painting2 + orientation: south + - id: cherry_painting2 + orientation: east + - id: hi_solved_painting + orientation: west + Orange Tower Seventh Floor: + entrances: + Orange Tower Sixth Floor: + room: Orange Tower + door: Seventh Floor + panels: + THE END: + id: EndPanel/Panel_end_end + check: True + tag: forbid + non_counting: True + THE MASTER: + # We will set up special rules for this in code. + id: Countdown Panels/Panel_master_master + check: True + tag: forbid + MASTERY: + # This is the MASTERY on the other side of THE FEARLESS. It can only be + # accessed by jumping from the top of the tower. + id: Master Room/Panel_mastery_mastery8 + tag: midwhite + required_door: + door: Mastery + doors: + Mastery: + id: + - Master Room Doors/Door_tower_down + - Master Room Doors/Door_master_master + - Master Room Doors/Door_master_master_2 + - Master Room Doors/Door_master_master_3 + - Master Room Doors/Door_master_master_4 + - Master Room Doors/Door_master_master_5 + - Master Room Doors/Door_master_master_6 + - Master Room Doors/Door_master_master_10 + - Master Room Doors/Door_master_master_11 + - Master Room Doors/Door_master_master_12 + - Master Room Doors/Door_master_master_13 + - Master Room Doors/Door_master_master_14 + - Master Room Doors/Door_master_master_15 + - Master Room Doors/Door_master_down + - Master Room Doors/Door_master_down2 + skip_location: True + panels: + - THE MASTER + Mastery Panels: + skip_item: True + location_name: Mastery Panels + panels: + - room: Room Room + panel: MASTERY + - room: The Steady (Topaz) + panel: MASTERY + - room: Orange Tower Basement + panel: MASTERY + - room: Arrow Garden + panel: MASTERY + - room: Hedge Maze + panel: MASTERY (1) + - room: Roof + panel: MASTERY (1) + - room: Roof + panel: MASTERY (2) + - MASTERY + - room: Hedge Maze + panel: MASTERY (2) + - room: Roof + panel: MASTERY (3) + - room: Roof + panel: MASTERY (4) + - room: Roof + panel: MASTERY (5) + - room: Elements Area + panel: MASTERY + - room: Pilgrim Antechamber + panel: MASTERY + - room: Roof + panel: MASTERY (6) + paintings: + - id: map_painting2 + orientation: north + enter_only: True # otherwise you might just skip the whole game! + Roof: + entrances: + Orange Tower Seventh Floor: True + Crossroads: + room: Crossroads + door: Roof Access + panels: + MASTERY (1): + id: Master Room/Panel_mastery_mastery6 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (2): + id: Master Room/Panel_mastery_mastery7 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (3): + id: Master Room/Panel_mastery_mastery10 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (4): + id: Master Room/Panel_mastery_mastery11 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (5): + id: Master Room/Panel_mastery_mastery12 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + MASTERY (6): + id: Master Room/Panel_mastery_mastery15 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + STAIRCASE: + id: Open Areas/Panel_staircase + tag: midwhite + Orange Tower Basement: + entrances: + Orange Tower Sixth Floor: + room: Orange Tower Seventh Floor + door: Mastery + panels: + MASTERY: + id: Master Room/Panel_mastery_mastery3 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + THE LIBRARY: + id: EndPanel/Panel_library + check: True + tag: forbid + non_counting: True + paintings: + - id: arrows_painting_11 + orientation: east + Courtyard: + entrances: + Roof: True + Orange Tower Fourth Floor: True + Arrow Garden: + painting: True + Starting Room: + door: Painting Shortcut + painting: True + Yellow Backside Area: + room: First Second Third Fourth + door: Backside Door + The Colorful (White): True + panels: + I: + id: Strand Room/Panel_i_staring + colors: blue + tag: forbid + GREEN: + id: Color Arrow Room/Panel_green_afar + tag: midwhite + required_door: + door: Green Barrier + PINECONE: + id: Shuffle Room/Panel_pinecone_pine + colors: brown + tag: botbrown + ACORN: + id: Shuffle Room/Panel_acorn_oak + colors: brown + tag: botbrown + doors: + Painting Shortcut: + painting_id: flower_painting_8 + item_name: Starting Room - Flower Painting + skip_location: True + panels: + - room: First Second Third Fourth + panel: FIRST + - room: First Second Third Fourth + panel: SECOND + - room: First Second Third Fourth + panel: THIRD + - room: First Second Third Fourth + panel: FOURTH + Green Barrier: + id: Color Arrow Room Doors/Door_green_5 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: GREEN + paintings: + - id: flower_painting_7 + orientation: north + Yellow Backside Area: + entrances: + Courtyard: + room: First Second Third Fourth + door: Backside Door + Roof: True + panels: + BACKSIDE: + id: Backside Room/Panel_backside_3 + tag: midwhite + NINE: + id: Backside Room/Panel_nine_nine_8 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + paintings: + - id: blueman_painting + orientation: east + First Second Third Fourth: + # We are separating this door + its panels into its own room because they + # are accessible from two distinct regions (Courtyard and Yellow Backside + # Area). We need to do this because painting shuffle makes it possible to + # have access to Yellow Backside Area without having access to Courtyard, + # and we want it to still be in logic to solve these panels. + entrances: + Courtyard: True + Yellow Backside Area: True + panels: + FIRST: + id: Backside Room/Panel_first_first + tag: midwhite + SECOND: + id: Backside Room/Panel_second_second + tag: midwhite + THIRD: + id: Backside Room/Panel_third_third + tag: midwhite + FOURTH: + id: Backside Room/Panel_fourth_fourth + tag: midwhite + doors: + Backside Door: + id: Count Up Room Area Doors/Door_yellow_backside + group: Backside Doors + location_name: Courtyard - FIRST, SECOND, THIRD, FOURTH + item_name: Courtyard - Backside Door + panels: + - FIRST + - SECOND + - THIRD + - FOURTH + The Colorful (White): + entrances: + Courtyard: True + The Colorful (Black): + door: Progress Door + panels: + BEGIN: + id: Doorways Room/Panel_begin_start + tag: botwhite + doors: + Progress Door: + id: Doorway Room Doors/Door_white + item_name: The Colorful - White Door + group: Colorful Doors + location_name: The Colorful - White + panels: + - BEGIN + The Colorful (Black): + entrances: + The Colorful (White): + room: The Colorful (White) + door: Progress Door + The Colorful (Red): + door: Progress Door + panels: + FOUND: + id: Doorways Room/Panel_found_lost + colors: black + tag: botblack + doors: + Progress Door: + id: Doorway Room Doors/Door_black + item_name: The Colorful - Black Door + location_name: The Colorful - Black + group: Colorful Doors + panels: + - FOUND + The Colorful (Red): + entrances: + The Colorful (Black): + room: The Colorful (Black) + door: Progress Door + The Colorful (Yellow): + door: Progress Door + panels: + LOAF: + id: Doorways Room/Panel_loaf_crust + colors: red + tag: botred + doors: + Progress Door: + id: Doorway Room Doors/Door_red + item_name: The Colorful - Red Door + location_name: The Colorful - Red + group: Colorful Doors + panels: + - LOAF + The Colorful (Yellow): + entrances: + The Colorful (Red): + room: The Colorful (Red) + door: Progress Door + The Colorful (Blue): + door: Progress Door + panels: + CREAM: + id: Doorways Room/Panel_eggs_breakfast + colors: yellow + tag: botyellow + doors: + Progress Door: + id: Doorway Room Doors/Door_yellow + item_name: The Colorful - Yellow Door + location_name: The Colorful - Yellow + group: Colorful Doors + panels: + - CREAM + The Colorful (Blue): + entrances: + The Colorful (Yellow): + room: The Colorful (Yellow) + door: Progress Door + The Colorful (Purple): + door: Progress Door + panels: + SUN: + id: Doorways Room/Panel_sun_sky + colors: blue + tag: botblue + doors: + Progress Door: + id: Doorway Room Doors/Door_blue + item_name: The Colorful - Blue Door + location_name: The Colorful - Blue + group: Colorful Doors + panels: + - SUN + The Colorful (Purple): + entrances: + The Colorful (Blue): + room: The Colorful (Blue) + door: Progress Door + The Colorful (Orange): + door: Progress Door + panels: + SPOON: + id: Doorways Room/Panel_teacher_substitute + colors: purple + tag: botpurple + doors: + Progress Door: + id: Doorway Room Doors/Door_purple + item_name: The Colorful - Purple Door + location_name: The Colorful - Purple + group: Colorful Doors + panels: + - SPOON + The Colorful (Orange): + entrances: + The Colorful (Purple): + room: The Colorful (Purple) + door: Progress Door + The Colorful (Green): + door: Progress Door + panels: + LETTERS: + id: Doorways Room/Panel_walnuts_orange + colors: orange + tag: botorange + doors: + Progress Door: + id: Doorway Room Doors/Door_orange + item_name: The Colorful - Orange Door + location_name: The Colorful - Orange + group: Colorful Doors + panels: + - LETTERS + The Colorful (Green): + entrances: + The Colorful (Orange): + room: The Colorful (Orange) + door: Progress Door + The Colorful (Brown): + door: Progress Door + panels: + WALLS: + id: Doorways Room/Panel_path_i + colors: green + tag: forbid + doors: + Progress Door: + id: Doorway Room Doors/Door_green + item_name: The Colorful - Green Door + location_name: The Colorful - Green + group: Colorful Doors + panels: + - WALLS + The Colorful (Brown): + entrances: + The Colorful (Green): + room: The Colorful (Green) + door: Progress Door + The Colorful (Gray): + door: Progress Door + panels: + IRON: + id: Doorways Room/Panel_iron_rust + colors: brown + tag: botbrown + doors: + Progress Door: + id: Doorway Room Doors/Door_brown + item_name: The Colorful - Brown Door + location_name: The Colorful - Brown + group: Colorful Doors + panels: + - IRON + The Colorful (Gray): + entrances: + The Colorful (Brown): + room: The Colorful (Brown) + door: Progress Door + The Colorful: + door: Progress Door + panels: + OBSTACLE: + id: Doorways Room/Panel_obstacle_door + colors: gray + tag: forbid + doors: + Progress Door: + id: + - Doorway Room Doors/Door_gray + - Doorway Room Doors/Door_gray2 # See comment below + item_name: The Colorful - Gray Door + location_name: The Colorful - Gray + group: Colorful Doors + panels: + - OBSTACLE + The Colorful: + # The set of required_doors in the achievement panel should prevent + # generation from asking you to solve The Colorful before opening all of the + # doors. Access from the roof is included so that the painting here could be + # an entrance. The client will have to be hardcoded to not open the door to + # the achievement until all of the doors are open, whether by solving the + # panels or through receiving items. + entrances: + The Colorful (Gray): + room: The Colorful (Gray) + door: Progress Door + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_colorful_colorful + check: True + tag: forbid + required_door: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + achievement: The Colorful + paintings: + - id: arrows_painting_12 + orientation: north + Welcome Back Area: + entrances: + Starting Room: + door: Shortcut to Starting Room + Hub Room: True + Outside The Wondrous: True + Outside The Undeterred: True + Outside The Initiated: True + Outside The Agreeable: True + Outside The Wanderer: True + Eight Alcove: True + Orange Tower Fifth Floor: + room: Orange Tower Fifth Floor + door: Welcome Back + Challenge Room: + room: Challenge Room + door: Welcome Door + panels: + WELCOME BACK: + id: Entry Room/Panel_return_return + tag: midwhite + SECRET: + id: Entry Room/Panel_secret_secret + tag: midwhite + CLOCKWISE: + id: Shuffle Room/Panel_clockwise_counterclockwise + colors: black + check: True + exclude_reduce: True + tag: botblack + doors: + Shortcut to Starting Room: + id: Entry Room Area Doors/Door_return_return + group: Welcome Back Doors + include_reduce: True + panels: + - WELCOME BACK + Owl Hallway: + entrances: + Hidden Room: + painting: True + Hedge Maze: + door: Shortcut to Hedge Maze + Orange Tower Sixth Floor: + painting: True + panels: + STRAYS: + id: Maze Room/Panel_strays_maze + colors: purple + tag: toppurp + READS + RUST: + id: Tower Room/Panel_reads_rust_lawns_6 + colors: orange + check: True + tag: midorange + doors: + Shortcut to Hedge Maze: + id: Maze Area Doors/Door_strays_maze + group: Hedge Maze Doors + panels: + - STRAYS + paintings: + - id: arrows_painting_8 + orientation: south + - id: maze_painting_2 + orientation: north + - id: owl_painting_2 + orientation: south + required_when_no_doors: True + - id: clock_painting_4 + orientation: north + Outside The Initiated: + entrances: + Hub Room: + door: Shortcut to Hub Room + Knight Night Exit: + room: Knight Night (Final) + door: Exit + Orange Tower Third Floor: True # sunwarp + Orange Tower Fifth Floor: + room: Art Gallery + door: Exit + panels: + SEVEN (1): + id: Backside Room/Panel_seven_seven_5 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + SEVEN (2): + id: Backside Room/Panel_seven_seven_6 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight_7 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + NINE: + id: Backside Room/Panel_nine_nine_4 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + BLUE: + id: Color Arrow Room/Panel_blue_afar + tag: midwhite + required_door: + door: Blue Barrier + ORANGE: + id: Color Arrow Room/Panel_orange_afar + tag: midwhite + required_door: + door: Orange Barrier + UNCOVER: + id: Appendix Room/Panel_discover_recover + colors: purple + tag: midpurp + OXEN: + id: Rhyme Room/Panel_locked_knocked + colors: purple + tag: midpurp + BACKSIDE: + id: Backside Room/Panel_backside_1 + tag: midwhite + The Optimistic: + id: Countdown Panels/Panel_optimistic_optimistic + check: True + tag: forbid + required_door: + door: Backsides + achievement: The Optimistic + PAST: + id: Shuffle Room/Panel_past_present + colors: brown + tag: botbrown + FUTURE: + id: Shuffle Room/Panel_future_present + colors: + - brown + - black + tag: bot brown black + FUTURE (2): + id: Shuffle Room/Panel_future_past + colors: black + tag: botblack + PAST (2): + id: Shuffle Room/Panel_past_future + colors: black + tag: botblack + PRESENT: + id: Shuffle Room/Panel_past_past + colors: + - brown + - black + tag: bot brown black + SMILE: + id: Open Areas/Panel_smile_smile + tag: midwhite + ANGERED: + id: Open Areas/Panel_angered_enraged + colors: + - yellow + tag: syn anagram + copy_to_sign: sign18 + VOTE: + id: Open Areas/Panel_vote_veto + colors: + - yellow + - black + tag: ant anagram + copy_to_sign: sign17 + doors: + Shortcut to Hub Room: + id: Appendix Room Area Doors/Door_recover_discover + panels: + - UNCOVER + Blue Barrier: + id: Color Arrow Room Doors/Door_blue_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: BLUE + Orange Barrier: + id: Color Arrow Room Doors/Door_orange_3 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: ORANGE + Initiated Entrance: + id: Red Blue Purple Room Area Doors/Door_locked_knocked + item_name: The Initiated - Entrance + panels: + - OXEN + # These would be more appropriate in Champion's Rest, but as currently + # implemented, locations need to include at least one panel from the + # containing region. + Green Barrier: + id: Color Arrow Room Doors/Door_green_hider_1 + location_name: Color Hunt - BLUE and YELLOW + item_name: Champion's Rest - Green Barrier + group: Champion's Rest - Color Barriers + panels: + - BLUE + - room: Directional Gallery + panel: YELLOW + Purple Barrier: + id: + - Color Arrow Room Doors/Door_purple_hider_1 + - Color Arrow Room Doors/Door_purple_hider_2 + - Color Arrow Room Doors/Door_purple_hider_3 + location_name: Color Hunt - RED and BLUE + item_name: Champion's Rest - Purple Barrier + group: Champion's Rest - Color Barriers + panels: + - BLUE + - room: Orange Tower Third Floor + panel: RED + Entrance: + id: + - Color Arrow Room Doors/Door_all_hider_1 + - Color Arrow Room Doors/Door_all_hider_2 + - Color Arrow Room Doors/Door_all_hider_3 + location_name: Color Hunt - GREEN, ORANGE and PURPLE + item_name: Champion's Rest - Entrance + panels: + - ORANGE + - room: Courtyard + panel: GREEN + - room: Outside The Agreeable + panel: PURPLE + Backsides: + event: True + panels: + - room: The Observant + panel: BACKSIDE + - room: Yellow Backside Area + panel: BACKSIDE + - room: Directional Gallery + panel: BACKSIDE + - room: The Bearer + panel: BACKSIDE + paintings: + - id: clock_painting_5 + orientation: east + - id: smile_painting_1 + orientation: north + The Initiated: + entrances: + Outside The Initiated: + room: Outside The Initiated + door: Initiated Entrance + panels: + Achievement: + id: Countdown Panels/Panel_illuminated_initiated + colors: purple + tag: forbid + check: True + achievement: The Initiated + DAUGHTER: + id: Rhyme Room/Panel_daughter_laughter + colors: purple + tag: midpurp + START: + id: Rhyme Room/Panel_move_love + colors: purple + tag: double midpurp + subtag: left + link: change STARS + STARE: + id: Rhyme Room/Panel_stove_love + colors: purple + tag: double midpurp + subtag: right + link: change STARS + HYPE: + id: Rhyme Room/Panel_scope_type + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign16 + ABYSS: + id: Rhyme Room/Panel_abyss_this + colors: purple + tag: toppurp + SWEAT: + id: Rhyme Room/Panel_sweat_great + colors: purple + tag: double midpurp + subtag: left + link: change GREAT + BEAT: + id: Rhyme Room/Panel_beat_great + colors: purple + tag: double midpurp + subtag: right + link: change GREAT + ALUMNI: + id: Rhyme Room/Panel_alumni_hi + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign14 + PATS: + id: Rhyme Room/Panel_wrath_path + colors: purple + tag: midpurp and rhyme + copy_to_sign: sign15 + KNIGHT: + id: Rhyme Room/Panel_knight_write + colors: purple + tag: double toppurp + subtag: left + link: change WRITE + BYTE: + id: Rhyme Room/Panel_byte_write + colors: purple + tag: double toppurp + subtag: right + link: change WRITE + MAIM: + id: Rhyme Room/Panel_maim_same + colors: purple + tag: toppurp + MORGUE: + id: Rhyme Room/Panel_chair_bear + colors: purple + tag: purple rhyme change stack + subtag: top + link: prcs CYBORG + CHAIR: + id: Rhyme Room/Panel_bare_bear + colors: purple + tag: toppurp + HUMAN: + id: Rhyme Room/Panel_cost_most + colors: purple + tag: purple rhyme change stack + subtag: bot + link: prcs CYBORG + BED: + id: Rhyme Room/Panel_bed_dead + colors: purple + tag: toppurp + The Traveled: + entrances: + Hub Room: + room: Hub Room + door: Traveled Entrance + Color Hallways: + door: Color Hallways Entrance + panels: + Achievement: + id: Countdown Panels/Panel_traveled_traveled + required_room: Hub Room + tag: forbid + check: True + achievement: The Traveled + CLOSE: + id: Synonym Room/Panel_close_near + tag: botwhite + COMPOSE: + id: Synonym Room/Panel_compose_write + tag: double botwhite + subtag: left + link: syn WRITE + RECORD: + id: Synonym Room/Panel_record_write + tag: double botwhite + subtag: right + link: syn WRITE + CATEGORY: + id: Synonym Room/Panel_category_type + tag: botwhite + HELLO: + id: Synonym Room/Panel_hello_hi + tag: botwhite + DUPLICATE: + id: Synonym Room/Panel_duplicate_same + tag: double botwhite + subtag: left + link: syn SAME + IDENTICAL: + id: Synonym Room/Panel_identical_same + tag: double botwhite + subtag: right + link: syn SAME + DISTANT: + id: Synonym Room/Panel_distant_far + tag: botwhite + HAY: + id: Synonym Room/Panel_hay_straw + tag: botwhite + GIGGLE: + id: Synonym Room/Panel_giggle_laugh + tag: double botwhite + subtag: left + link: syn LAUGH + CHUCKLE: + id: Synonym Room/Panel_chuckle_laugh + tag: double botwhite + subtag: right + link: syn LAUGH + SNITCH: + id: Synonym Room/Panel_snitch_rat + tag: botwhite + CONCEALED: + id: Synonym Room/Panel_concealed_hidden + tag: botwhite + PLUNGE: + id: Synonym Room/Panel_plunge_fall + tag: double botwhite + subtag: left + link: syn FALL + AUTUMN: + id: Synonym Room/Panel_autumn_fall + tag: double botwhite + subtag: right + link: syn FALL + ROAD: + id: Synonym Room/Panel_growths_warts + tag: botwhite + FOUR: + id: Backside Room/Panel_four_four_4 + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fours + doors: + Color Hallways Entrance: + id: Appendix Room Area Doors/Door_hello_hi + group: Entrance to The Traveled + panels: + - HELLO + Color Hallways: + entrances: + The Traveled: + room: The Traveled + door: Color Hallways Entrance + Outside The Bold: True + Outside The Undeterred: True + Crossroads: True + Hedge Maze: True + Outside The Initiated: True # backside + Directional Gallery: True # backside + Yellow Backside Area: True + The Bearer: + room: The Bearer + door: Backside Door + The Observant: + room: The Observant + door: Backside Door + Outside The Bold: + entrances: + Color Hallways: True + Champion's Rest: + room: Champion's Rest + door: Shortcut to The Steady + The Bearer: + room: The Bearer + door: Shortcut to The Bold + Directional Gallery: + # There is a painting warp here from the Directional Gallery, but it + # only appears when the sixes are revealed. It could be its own item if + # we wanted. + room: Number Hunt + door: Sixes + painting: True + Starting Room: + door: Painting Shortcut + painting: True + Room Room: True # trapdoor + panels: + UNOPEN: + id: Truncate Room/Panel_unopened_open + colors: red + tag: midred + BEGIN: + id: Rock Room/Panel_begin_begin + tag: midwhite + SIX: + id: Backside Room/Panel_six_six_4 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + NINE: + id: Backside Room/Panel_nine_nine_5 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + LEFT: + id: Shuffle Room/Panel_left_left_2 + tag: midwhite + RIGHT: + id: Shuffle Room/Panel_right_right_2 + tag: midwhite + RISE (Horizon): + id: Open Areas/Panel_rise_horizon + colors: blue + tag: double topblue + subtag: left + link: expand HORIZON + RISE (Sunrise): + id: Open Areas/Panel_rise_sunrise + colors: blue + tag: double topblue + subtag: left + link: expand SUNRISE + ZEN: + id: Open Areas/Panel_son_horizon + colors: blue + tag: double topblue + subtag: right + link: expand HORIZON + SON: + id: Open Areas/Panel_son_sunrise + colors: blue + tag: double topblue + subtag: right + link: expand SUNRISE + STARGAZER: + id: Open Areas/Panel_stargazer_stargazer + tag: midwhite + required_door: + door: Stargazer Door + MOUTH: + id: Cross Room/Panel_mouth_south + colors: purple + tag: midpurp + YEAST: + id: Cross Room/Panel_yeast_east + colors: red + tag: midred + WET: + id: Cross Room/Panel_wet_west + colors: blue + tag: midblue + doors: + Bold Entrance: + id: Red Blue Purple Room Area Doors/Door_unopened_open + item_name: The Bold - Entrance + panels: + - UNOPEN + Painting Shortcut: + painting_id: pencil_painting6 + skip_location: True + item_name: Starting Room - Pencil Painting + panels: + - UNOPEN + Steady Entrance: + id: Rock Room Doors/Door_2 + item_name: The Steady - Entrance + panels: + - BEGIN + Lilac Entrance: + event: True + panels: + - room: The Steady (Rose) + panel: SOAR + Stargazer Door: + event: True + panels: + - RISE (Horizon) + - RISE (Sunrise) + - ZEN + - SON + paintings: + - id: pencil_painting2 + orientation: west + - id: north_missing2 + orientation: north + The Bold: + entrances: + Outside The Bold: + room: Outside The Bold + door: Bold Entrance + panels: + Achievement: + id: Countdown Panels/Panel_emboldened_bold + colors: red + tag: forbid + check: True + achievement: The Bold + FOOT: + id: Truncate Room/Panel_foot_toe + colors: red + tag: botred + NEEDLE: + id: Truncate Room/Panel_needle_eye + colors: red + tag: double botred + subtag: left + link: mero EYE + FACE: + id: Truncate Room/Panel_face_eye + colors: red + tag: double botred + subtag: right + link: mero EYE + SIGN: + id: Truncate Room/Panel_sign_sigh + colors: red + tag: topred + HEARTBREAK: + id: Truncate Room/Panel_heartbreak_brake + colors: red + tag: topred + UNDEAD: + id: Truncate Room/Panel_undead_dead + colors: red + tag: double midred + subtag: left + link: trunc DEAD + DEADLINE: + id: Truncate Room/Panel_deadline_dead + colors: red + tag: double midred + subtag: right + link: trunc DEAD + SUSHI: + id: Truncate Room/Panel_sushi_hi + colors: red + tag: midred + THISTLE: + id: Truncate Room/Panel_thistle_this + colors: red + tag: midred + LANDMASS: + id: Truncate Room/Panel_landmass_mass + colors: red + tag: double midred + subtag: left + link: trunc MASS + MASSACRED: + id: Truncate Room/Panel_massacred_mass + colors: red + tag: double midred + subtag: right + link: trunc MASS + AIRPLANE: + id: Truncate Room/Panel_airplane_plain + colors: red + tag: topred + NIGHTMARE: + id: Truncate Room/Panel_nightmare_knight + colors: red + tag: topred + MOUTH: + id: Truncate Room/Panel_mouth_teeth + colors: red + tag: double botred + subtag: left + link: mero TEETH + SAW: + id: Truncate Room/Panel_saw_teeth + colors: red + tag: double botred + subtag: right + link: mero TEETH + HAND: + id: Truncate Room/Panel_hand_finger + colors: red + tag: botred + Outside The Undeterred: + entrances: + Color Hallways: True + Orange Tower First Floor: True # sunwarp + Orange Tower Second Floor: True + The Artistic (Smiley): True + The Artistic (Panda): True + The Artistic (Apple): True + The Artistic (Lattice): True + Yellow Backside Area: + painting: True + Number Hunt: + door: Number Hunt + Directional Gallery: + room: Directional Gallery + door: Shortcut to The Undeterred + Starting Room: + door: Painting Shortcut + painting: True + panels: + HOLLOW: + id: Hallway Room/Panel_hollow_hollow + tag: midwhite + ART + ART: + id: Tower Room/Panel_art_art_eat_2 + colors: orange + check: True + tag: midorange + PEN: + id: Blue Room/Panel_pen_open + colors: blue + tag: midblue + HUSTLING: + id: Open Areas/Panel_hustling_sunlight + colors: yellow + tag: midyellow + SUNLIGHT: + id: Open Areas/Panel_sunlight_light + colors: red + tag: midred + required_panel: + panel: HUSTLING + LIGHT: + id: Open Areas/Panel_light_bright + colors: purple + tag: midpurp + required_panel: + panel: SUNLIGHT + BRIGHT: + id: Open Areas/Panel_bright_sunny + tag: botwhite + required_panel: + panel: LIGHT + SUNNY: + id: Open Areas/Panel_sunny_rainy + colors: black + tag: botblack + required_panel: + panel: BRIGHT + RAINY: + id: Open Areas/Panel_rainy_rainbow + colors: brown + tag: botbrown + required_panel: + panel: SUNNY + check: True + ZERO: + id: Backside Room/Panel_zero_zero + tag: midwhite + required_door: + room: Number Hunt + door: Zero Door + ONE: + id: Backside Room/Panel_one_one + tag: midwhite + TWO (1): + id: Backside Room/Panel_two_two + tag: midwhite + required_door: + door: Twos + TWO (2): + id: Backside Room/Panel_two_two_2 + tag: midwhite + required_door: + door: Twos + THREE (1): + id: Backside Room/Panel_three_three + tag: midwhite + required_door: + door: Threes + THREE (2): + id: Backside Room/Panel_three_three_2 + tag: midwhite + required_door: + door: Threes + THREE (3): + id: Backside Room/Panel_three_three_3 + tag: midwhite + required_door: + door: Threes + FOUR: + id: Backside Room/Panel_four_four + tag: midwhite + required_door: + door: Fours + doors: + Undeterred Entrance: + id: Red Blue Purple Room Area Doors/Door_pen_open + item_name: The Undeterred - Entrance + panels: + - PEN + Painting Shortcut: + painting_id: + - blueman_painting_3 + - arrows_painting3 + skip_location: True + item_name: Starting Room - Blue Painting + panels: + - PEN + Green Painting: + painting_id: maze_painting_3 + skip_location: True + panels: + - FOUR + Twos: + id: + - Count Up Room Area Doors/Door_two_hider + - Count Up Room Area Doors/Door_two_hider_2 + include_reduce: True + panels: + - ONE + Threes: + id: + - Count Up Room Area Doors/Door_three_hider + - Count Up Room Area Doors/Door_three_hider_2 + - Count Up Room Area Doors/Door_three_hider_3 + location_name: Twos + include_reduce: True + panels: + - TWO (1) + - TWO (2) + Number Hunt: + id: Count Up Room Area Doors/Door_three_unlocked + location_name: Threes + include_reduce: True + panels: + - THREE (1) + - THREE (2) + - THREE (3) + Fours: + id: + - Count Up Room Area Doors/Door_four_hider + - Count Up Room Area Doors/Door_four_hider_2 + - Count Up Room Area Doors/Door_four_hider_3 + - Count Up Room Area Doors/Door_four_hider_4 + skip_location: True + panels: + - THREE (1) + - THREE (2) + - THREE (3) + Fives: + id: + - Count Up Room Area Doors/Door_five_hider + - Count Up Room Area Doors/Door_five_hider_4 + - Count Up Room Area Doors/Door_five_hider_5 + location_name: Fours + item_name: Number Hunt - Fives + include_reduce: True + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR + Challenge Entrance: + id: Count Up Room Area Doors/Door_zero_unlocked + item_name: Number Hunt - Challenge Entrance + panels: + - ZERO + paintings: + - id: maze_painting_3 + enter_only: True + orientation: north + move: True + required_door: + door: Green Painting + - id: blueman_painting_2 + orientation: east + The Undeterred: + entrances: + Outside The Undeterred: + room: Outside The Undeterred + door: Undeterred Entrance + panels: + Achievement: + id: Countdown Panels/Panel_deterred_undeterred + colors: blue + tag: forbid + check: True + achievement: The Undeterred + BONE: + id: Blue Room/Panel_bone_skeleton + colors: blue + tag: botblue + EYE: + id: Blue Room/Panel_mouth_face + colors: blue + tag: double botblue + subtag: left + link: holo FACE + MOUTH: + id: Blue Room/Panel_eye_face + colors: blue + tag: double botblue + subtag: right + link: holo FACE + IRIS: + id: Blue Room/Panel_toucan_bird + colors: blue + tag: botblue + EYE (2): + id: Blue Room/Panel_two_toucan + colors: blue + tag: topblue + ICE: + id: Blue Room/Panel_ice_eyesight + colors: blue + tag: double topblue + subtag: left + link: hex EYESIGHT + HEIGHT: + id: Blue Room/Panel_height_eyesight + colors: blue + tag: double topblue + subtag: right + link: hex EYESIGHT + EYE (3): + id: Blue Room/Panel_eye_hi + colors: blue + tag: topblue + NOT: + id: Blue Room/Panel_not_notice + colors: blue + tag: midblue + JUST: + id: Blue Room/Panel_just_readjust + colors: blue + tag: double midblue + subtag: left + link: exp READJUST + READ: + id: Blue Room/Panel_read_readjust + colors: blue + tag: double midblue + subtag: right + link: exp READJUST + FATHER: + id: Blue Room/Panel_ate_primate + colors: blue + tag: midblue + FEATHER: + id: Blue Room/Panel_primate_mammal + colors: blue + tag: botblue + CONTINENT: + id: Blue Room/Panel_continent_planet + colors: blue + tag: double botblue + subtag: left + link: holo PLANET + OCEAN: + id: Blue Room/Panel_ocean_planet + colors: blue + tag: double botblue + subtag: right + link: holo PLANET + WALL: + id: Blue Room/Panel_wall_room + colors: blue + tag: botblue + Number Hunt: + # This works a little differently than in the base game. The door to the + # initial number in each set opens at the same time as the rest of the doors + # in that set. + entrances: + Outside The Undeterred: + room: Outside The Undeterred + door: Number Hunt + Directional Gallery: + door: Door to Directional Gallery + Challenge Room: + room: Outside The Undeterred + door: Challenge Entrance + panels: + FIVE: + id: Backside Room/Panel_five_five + tag: midwhite + required_door: + room: Outside The Undeterred + door: Fives + SIX: + id: Backside Room/Panel_six_six + tag: midwhite + required_door: + door: Sixes + SEVEN: + id: Backside Room/Panel_seven_seven + tag: midwhite + required_door: + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight + tag: midwhite + required_door: + door: Eights + NINE: + id: Backside Room/Panel_nine_nine + tag: midwhite + required_door: + door: Nines + doors: + Door to Directional Gallery: + id: Count Up Room Area Doors/Door_five_unlocked + group: Directional Gallery Doors + skip_location: True + panels: + - FIVE + Sixes: + id: + - Count Up Room Area Doors/Door_six_hider + - Count Up Room Area Doors/Door_six_hider_2 + - Count Up Room Area Doors/Door_six_hider_3 + - Count Up Room Area Doors/Door_six_hider_4 + - Count Up Room Area Doors/Door_six_hider_5 + - Count Up Room Area Doors/Door_six_hider_6 + painting_id: pencil_painting3 # See note in Outside The Bold + location_name: Fives + include_reduce: True + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + Sevens: + id: + - Count Up Room Area Doors/Door_seven_hider + - Count Up Room Area Doors/Door_seven_unlocked + - Count Up Room Area Doors/Door_seven_hider_2 + - Count Up Room Area Doors/Door_seven_hider_3 + - Count Up Room Area Doors/Door_seven_hider_4 + - Count Up Room Area Doors/Door_seven_hider_5 + - Count Up Room Area Doors/Door_seven_hider_6 + - Count Up Room Area Doors/Door_seven_hider_7 + location_name: Sixes + include_reduce: True + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + Eights: + id: + - Count Up Room Area Doors/Door_eight_hider + - Count Up Room Area Doors/Door_eight_unlocked + - Count Up Room Area Doors/Door_eight_hider_2 + - Count Up Room Area Doors/Door_eight_hider_3 + - Count Up Room Area Doors/Door_eight_hider_4 + - Count Up Room Area Doors/Door_eight_hider_5 + - Count Up Room Area Doors/Door_eight_hider_6 + - Count Up Room Area Doors/Door_eight_hider_7 + - Count Up Room Area Doors/Door_eight_hider_8 + location_name: Sevens + include_reduce: True + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + Nines: + id: + - Count Up Room Area Doors/Door_nine_hider + - Count Up Room Area Doors/Door_nine_hider_2 + - Count Up Room Area Doors/Door_nine_hider_3 + - Count Up Room Area Doors/Door_nine_hider_4 + - Count Up Room Area Doors/Door_nine_hider_5 + - Count Up Room Area Doors/Door_nine_hider_6 + - Count Up Room Area Doors/Door_nine_hider_7 + - Count Up Room Area Doors/Door_nine_hider_8 + - Count Up Room Area Doors/Door_nine_hider_9 + location_name: Eights + include_reduce: True + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + Zero Door: + # The black wall isn't a door, so we can't ever hide it. + id: Count Up Room Area Doors/Door_zero_hider_2 + location_name: Nines + item_name: Outside The Undeterred - Zero Door + include_reduce: True + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE + paintings: + - id: smile_painting_5 + enter_only: True + orientation: east + required_door: + door: Eights + Directional Gallery: + entrances: + Outside The Agreeable: True # sunwarp + Orange Tower First Floor: + room: Orange Tower First Floor + door: Salt Pepper Door + Outside The Undeterred: + door: Shortcut to The Undeterred + Number Hunt: + room: Number Hunt + door: Door to Directional Gallery + panels: + PEPPER: + id: Backside Room/Panel_pepper_salt + colors: black + tag: botblack + TURN: + id: Backside Room/Panel_turn_return + colors: blue + tag: midblue + LEARN: + id: Backside Room/Panel_learn_return + colors: purple + tag: midpurp + FIVE (1): + id: Backside Room/Panel_five_five_3 + tag: midwhite + required_panel: + panel: LIGHT + FIVE (2): + id: Backside Room/Panel_five_five_2 + tag: midwhite + required_panel: + panel: WARD + SIX (1): + id: Backside Room/Panel_six_six_3 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + SIX (2): + id: Backside Room/Panel_six_six_2 + tag: midwhite + required_door: + room: Number Hunt + door: Sixes + SEVEN: + id: Backside Room/Panel_seven_seven_2 + tag: midwhite + required_door: + room: Number Hunt + door: Sevens + EIGHT: + id: Backside Room/Panel_eight_eight_2 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + NINE: + id: Backside Room/Panel_nine_nine_6 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + BACKSIDE: + id: Backside Room/Panel_backside_4 + tag: midwhite + "834283054": + id: Tower Room/Panel_834283054_undaunted + colors: orange + check: True + exclude_reduce: True + tag: midorange + required_door: + room: Number Hunt + door: Sixes + PARANOID: + id: Backside Room/Panel_paranoid_paranoid + tag: midwhite + check: True + exclude_reduce: True + required_door: + room: Number Hunt + door: Sixes + YELLOW: + id: Color Arrow Room/Panel_yellow_afar + tag: midwhite + required_door: + door: Yellow Barrier + WADED + WEE: + id: Tower Room/Panel_waded_wee_warts_7 + colors: orange + check: True + exclude_reduce: True + tag: midorange + THE EYES: + id: Shuffle Room/Panel_theeyes_theeyes + tag: midwhite + LEFT: + id: Shuffle Room/Panel_left_left + tag: midwhite + RIGHT: + id: Shuffle Room/Panel_right_right + tag: midwhite + MIDDLE: + id: Shuffle Room/Panel_middle_middle + tag: midwhite + WARD: + id: Backside Room/Panel_ward_forward + colors: blue + tag: midblue + HIND: + id: Backside Room/Panel_hind_behind + colors: blue + tag: midblue + RIG: + id: Backside Room/Panel_rig_right + colors: blue + tag: midblue + WINDWARD: + id: Backside Room/Panel_windward_forward + colors: purple + tag: midpurp + LIGHT: + id: Backside Room/Panel_light_right + colors: purple + tag: midpurp + REWIND: + id: Backside Room/Panel_rewind_behind + colors: purple + tag: midpurp + doors: + Shortcut to The Undeterred: + id: Count Up Room Area Doors/Door_return_double + group: Directional Gallery Doors + panels: + - TURN + - LEARN + Yellow Barrier: + id: Color Arrow Room Doors/Door_yellow_4 + group: Color Hunt Barriers + skip_location: True + panels: + - room: Champion's Rest + panel: YELLOW + paintings: + - id: smile_painting_7 + orientation: south + - id: flower_painting_4 + orientation: south + - id: pencil_painting3 + enter_only: True + orientation: east + move: True + required_door: + room: Number Hunt + door: Sixes + - id: boxes_painting + orientation: south + - id: cherry_painting + orientation: east + Champion's Rest: + entrances: + Outside The Bold: + door: Shortcut to The Steady + Orange Tower Fourth Floor: True # sunwarp + Roof: True # through ceiling of sunwarp + panels: + EXIT: + id: Rock Room/Panel_red_red + tag: midwhite + HUES: + id: Color Arrow Room/Panel_hues_colors + tag: botwhite + RED: + id: Color Arrow Room/Panel_red_near + check: True + tag: midwhite + BLUE: + id: Color Arrow Room/Panel_blue_near + check: True + tag: midwhite + YELLOW: + id: Color Arrow Room/Panel_yellow_near + check: True + tag: midwhite + GREEN: + id: Color Arrow Room/Panel_green_near + check: True + tag: midwhite + required_door: + room: Outside The Initiated + door: Green Barrier + PURPLE: + id: Color Arrow Room/Panel_purple_near + check: True + tag: midwhite + required_door: + room: Outside The Initiated + door: Purple Barrier + ORANGE: + id: Color Arrow Room/Panel_orange_near + check: True + tag: midwhite + required_door: + room: Orange Tower Third Floor + door: Orange Barrier + YOU: + id: Color Arrow Room/Panel_you + required_door: + room: Outside The Initiated + door: Entrance + check: True + colors: gray + tag: forbid + ME: + id: Color Arrow Room/Panel_me + colors: gray + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET BLUE: + # Pretend this and the other two are white, because they are snipes. + # TODO: Extract them and randomize them? + id: Color Arrow Room/Panel_secret_blue + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET YELLOW: + id: Color Arrow Room/Panel_secret_yellow + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + SECRET RED: + id: Color Arrow Room/Panel_secret_red + tag: forbid + required_door: + room: Outside The Initiated + door: Entrance + doors: + Shortcut to The Steady: + id: Rock Room Doors/Door_hint + panels: + - EXIT + paintings: + - id: arrows_painting_7 + orientation: east + - id: fruitbowl_painting3 + orientation: west + enter_only: True + required_door: + room: Outside The Initiated + door: Entrance + - id: colors_painting + orientation: south + enter_only: True + required_door: + room: Outside The Initiated + door: Entrance + The Bearer: + entrances: + Outside The Bold: + door: Shortcut to The Bold + Orange Tower Fifth Floor: + room: Art Gallery + door: Exit + The Bearer (East): True + The Bearer (North): True + The Bearer (South): True + The Bearer (West): True + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_bearer_bearer + check: True + tag: forbid + required_panel: + - panel: PART + - panel: HEART + - room: Cross Tower (East) + panel: WINTER + - room: The Bearer (East) + panel: PEACE + - room: Cross Tower (North) + panel: NORTH + - room: The Bearer (North) + panel: SILENT (1) + - room: The Bearer (North) + panel: SILENT (2) + - room: The Bearer (North) + panel: SPACE + - room: The Bearer (North) + panel: WARTS + - room: Cross Tower (South) + panel: FIRE + - room: The Bearer (South) + panel: TENT + - room: The Bearer (South) + panel: BOWL + - room: Cross Tower (West) + panel: DIAMONDS + - room: The Bearer (West) + panel: SNOW + - room: The Bearer (West) + panel: SMILE + - room: Bearer Side Area + panel: SHORTCUT + - room: Bearer Side Area + panel: POTS + achievement: The Bearer + MIDDLE: + id: Shuffle Room/Panel_middle_middle_2 + tag: midwhite + FARTHER: + id: Backside Room/Panel_farther_far + colors: red + tag: midred + BACKSIDE: + id: Backside Room/Panel_backside_5 + tag: midwhite + required_door: + door: Backside Door + PART: + id: Cross Room/Panel_part_rap + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (East) + panel: PEACE + HEART: + id: Cross Room/Panel_heart_tar + colors: + - red + - yellow + tag: mid red yellow + doors: + Shortcut to The Bold: + id: Red Blue Purple Room Area Doors/Door_middle_middle + panels: + - MIDDLE + Backside Door: + id: Red Blue Purple Room Area Doors/Door_locked_knocked2 # yeah... + group: Backside Doors + panels: + - FARTHER + East Entrance: + event: True + panels: + - HEART + The Bearer (East): + entrances: + Cross Tower (East): True + Bearer Side Area: + door: Side Area Access + Roof: True + panels: + SIX: + id: Backside Room/Panel_six_six_5 + tag: midwhite + colors: + - red + - yellow + required_door: + room: Number Hunt + door: Sixes + PEACE: + id: Cross Room/Panel_peace_ape + colors: + - red + - yellow + tag: mid red yellow + doors: + North Entrance: + event: True + panels: + - room: The Bearer + panel: PART + Side Area Access: + event: True + panels: + - room: The Bearer (North) + panel: SPACE + The Bearer (North): + entrances: + Cross Tower (East): True + Roof: True + panels: + SILENT (1): + id: Cross Room/Panel_silent_list + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (West) + panel: SMILE + SILENT (2): + id: Cross Room/Panel_silent_list_2 + colors: + - red + - yellow + tag: mid yellow red + required_panel: + room: The Bearer (West) + panel: SMILE + SPACE: + id: Cross Room/Panel_space_cape + colors: + - red + - yellow + tag: mid red yellow + WARTS: + id: Cross Room/Panel_warts_star + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (West) + panel: SNOW + doors: + South Entrance: + event: True + panels: + - room: Bearer Side Area + panel: POTS + The Bearer (South): + entrances: + Cross Tower (North): True + Bearer Side Area: + door: Side Area Shortcut + Roof: True + panels: + SIX: + id: Backside Room/Panel_six_six_6 + tag: midwhite + colors: + - red + - yellow + required_door: + room: Number Hunt + door: Sixes + TENT: + id: Cross Room/Panel_tent_net + colors: + - red + - yellow + tag: mid red yellow + BOWL: + id: Cross Room/Panel_bowl_low + colors: + - red + - yellow + tag: mid red yellow + required_panel: + panel: TENT + doors: + Side Area Shortcut: + event: True + panels: + - room: The Bearer (North) + panel: SILENT (1) + The Bearer (West): + entrances: + Cross Tower (West): True + Bearer Side Area: + door: Side Area Shortcut + Roof: True + panels: + SNOW: + id: Cross Room/Panel_smile_lime + colors: + - red + - yellow + tag: mid yellow red + SMILE: + id: Cross Room/Panel_snow_won + colors: + - red + - yellow + tag: mid red yellow + required_panel: + room: The Bearer (North) + panel: WARTS + doors: + Side Area Shortcut: + event: True + panels: + - room: Cross Tower (East) + panel: WINTER + - room: Cross Tower (North) + panel: NORTH + - room: Cross Tower (South) + panel: FIRE + - room: Cross Tower (West) + panel: DIAMONDS + Bearer Side Area: + entrances: + The Bearer (East): + room: The Bearer (East) + door: Side Area Access + The Bearer (South): + room: The Bearer (South) + door: Side Area Shortcut + The Bearer (West): + room: The Bearer (West) + door: Side Area Shortcut + Orange Tower Third Floor: + door: Shortcut to Tower + Roof: True + panels: + SHORTCUT: + id: Cross Room/Panel_shortcut_shortcut + tag: midwhite + POTS: + id: Cross Room/Panel_pots_top + colors: + - red + - yellow + tag: mid yellow red + doors: + Shortcut to Tower: + id: Cross Room Doors/Door_shortcut + item_name: The Bearer - Shortcut to Tower + location_name: The Bearer - SHORTCUT + panels: + - SHORTCUT + West Entrance: + event: True + panels: + - room: The Bearer (South) + panel: BOWL + Cross Tower (East): + entrances: + The Bearer: + room: The Bearer + door: East Entrance + Roof: True + panels: + WINTER: + id: Cross Room/Panel_winter_winter + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: SPACE + required_room: Orange Tower Fifth Floor + Cross Tower (North): + entrances: + The Bearer (East): + room: The Bearer (East) + door: North Entrance + Roof: True + panels: + NORTH: + id: Cross Room/Panel_north_north + colors: blue + tag: forbid + required_panel: + room: The Bearer (West) + panel: SMILE + required_room: Outside The Bold + Cross Tower (South): + entrances: # No roof access + The Bearer (North): + room: The Bearer (North) + door: South Entrance + panels: + FIRE: + id: Cross Room/Panel_fire_fire + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: SILENT (1) + required_room: Elements Area + Cross Tower (West): + entrances: + Bearer Side Area: + room: Bearer Side Area + door: West Entrance + Roof: True + panels: + DIAMONDS: + id: Cross Room/Panel_diamonds_diamonds + colors: blue + tag: forbid + required_panel: + room: The Bearer (North) + panel: WARTS + required_room: Suits Area + The Steady (Rose): + entrances: + Outside The Bold: + room: Outside The Bold + door: Steady Entrance + The Steady (Lilac): + room: The Steady + door: Reveal + The Steady (Ruby): + door: Forward Exit + The Steady (Carnation): + door: Right Exit + panels: + SOAR: + id: Rock Room/Panel_soar_rose + colors: black + tag: topblack + doors: + Forward Exit: + event: True + panels: + - SOAR + Right Exit: + event: True + panels: + - room: The Steady (Lilac) + panel: LIE LACK + The Steady (Ruby): + entrances: + The Steady (Rose): + room: The Steady (Rose) + door: Forward Exit + The Steady (Amethyst): + room: The Steady + door: Reveal + The Steady (Cherry): + door: Forward Exit + The Steady (Amber): + door: Right Exit + panels: + BURY: + id: Rock Room/Panel_bury_ruby + colors: yellow + tag: midyellow + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Lime) + panel: LIMELIGHT + Right Exit: + event: True + panels: + - room: The Steady (Carnation) + panel: INCARNATION + The Steady (Carnation): + entrances: + The Steady (Rose): + room: The Steady (Rose) + door: Right Exit + Outside The Bold: + room: The Steady + door: Reveal + The Steady (Amber): + room: The Steady + door: Reveal + The Steady (Sunflower): + door: Right Exit + panels: + INCARNATION: + id: Rock Room/Panel_incarnation_carnation + colors: red + tag: midred + doors: + Right Exit: + event: True + panels: + - room: The Steady (Amethyst) + panel: PACIFIST + The Steady (Sunflower): + entrances: + The Steady (Carnation): + room: The Steady (Carnation) + door: Right Exit + The Steady (Topaz): + room: The Steady (Topaz) + door: Back Exit + panels: + SUN: + id: Rock Room/Panel_sun_sunflower + colors: blue + tag: midblue + doors: + Back Exit: + event: True + panels: + - SUN + The Steady (Plum): + entrances: + The Steady (Amethyst): + room: The Steady + door: Reveal + The Steady (Blueberry): + room: The Steady + door: Reveal + The Steady (Cherry): + room: The Steady (Cherry) + door: Left Exit + panels: + LUMP: + id: Rock Room/Panel_lump_plum + colors: yellow + tag: midyellow + The Steady (Lime): + entrances: + The Steady (Sunflower): True + The Steady (Emerald): + room: The Steady + door: Reveal + The Steady (Blueberry): + door: Right Exit + panels: + LIMELIGHT: + id: Rock Room/Panel_limelight_lime + colors: red + tag: midred + doors: + Right Exit: + event: True + panels: + - room: The Steady (Amber) + panel: ANTECHAMBER + paintings: + - id: pencil_painting5 + orientation: south + The Steady (Lemon): + entrances: + The Steady (Emerald): True + The Steady (Orange): + room: The Steady + door: Reveal + The Steady (Topaz): + door: Back Exit + panels: + MELON: + id: Rock Room/Panel_melon_lemon + colors: yellow + tag: midyellow + doors: + Back Exit: + event: True + panels: + - MELON + paintings: + - id: pencil_painting4 + orientation: south + The Steady (Topaz): + entrances: + The Steady (Lemon): + room: The Steady (Lemon) + door: Back Exit + The Steady (Amber): + room: The Steady + door: Reveal + The Steady (Sunflower): + door: Back Exit + panels: + TOP: + id: Rock Room/Panel_top_topaz + colors: blue + tag: midblue + MASTERY: + id: Master Room/Panel_mastery_mastery2 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Back Exit: + event: True + panels: + - TOP + The Steady (Orange): + entrances: + The Steady (Cherry): + room: The Steady + door: Reveal + The Steady (Lemon): + room: The Steady + door: Reveal + The Steady (Amber): + room: The Steady (Amber) + door: Forward Exit + panels: + BLUE: + id: Rock Room/Panel_blue_orange + colors: black + tag: botblack + The Steady (Sapphire): + entrances: + The Steady (Emerald): + door: Left Exit + The Steady (Blueberry): + room: The Steady + door: Reveal + The Steady (Amethyst): + room: The Steady (Amethyst) + door: Left Exit + panels: + SAP: + id: Rock Room/Panel_sap_sapphire + colors: blue + tag: midblue + doors: + Left Exit: + event: True + panels: + - room: The Steady (Plum) + panel: LUMP + - room: The Steady (Orange) + panel: BLUE + The Steady (Blueberry): + entrances: + The Steady (Lime): + room: The Steady (Lime) + door: Right Exit + The Steady (Sapphire): + room: The Steady + door: Reveal + The Steady (Plum): + room: The Steady + door: Reveal + panels: + BLUE: + id: Rock Room/Panel_blue_blueberry + colors: blue + tag: midblue + The Steady (Amber): + entrances: + The Steady (Ruby): + room: The Steady (Ruby) + door: Right Exit + The Steady (Carnation): + room: The Steady + door: Reveal + The Steady (Orange): + door: Forward Exit + The Steady (Topaz): + room: The Steady + door: Reveal + panels: + ANTECHAMBER: + id: Rock Room/Panel_antechamber_amber + colors: red + tag: midred + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Blueberry) + panel: BLUE + The Steady (Emerald): + entrances: + The Steady (Sapphire): + room: The Steady (Sapphire) + door: Left Exit + The Steady (Lime): + room: The Steady + door: Reveal + panels: + HERALD: + id: Rock Room/Panel_herald_emerald + colors: purple + tag: midpurp + The Steady (Amethyst): + entrances: + The Steady (Lilac): + room: The Steady (Lilac) + door: Forward Exit + The Steady (Sapphire): + door: Left Exit + The Steady (Plum): + room: The Steady + door: Reveal + The Steady (Ruby): + room: The Steady + door: Reveal + panels: + PACIFIST: + id: Rock Room/Panel_thistle_amethyst + colors: purple + tag: toppurp + doors: + Left Exit: + event: True + panels: + - room: The Steady (Sunflower) + panel: SUN + The Steady (Lilac): + entrances: + Outside The Bold: + room: Outside The Bold + door: Lilac Entrance + The Steady (Amethyst): + door: Forward Exit + The Steady (Rose): + room: The Steady + door: Reveal + panels: + LIE LACK: + id: Rock Room/Panel_lielack_lilac + tag: topwhite + doors: + Forward Exit: + event: True + panels: + - room: The Steady (Ruby) + panel: BURY + The Steady (Cherry): + entrances: + The Steady (Plum): + door: Left Exit + The Steady (Orange): + room: The Steady + door: Reveal + The Steady (Ruby): + room: The Steady (Ruby) + door: Forward Exit + panels: + HAIRY: + id: Rock Room/Panel_hairy_cherry + colors: blue + tag: topblue + doors: + Left Exit: + event: True + panels: + - room: The Steady (Sapphire) + panel: SAP + The Steady: + entrances: + The Steady (Sunflower): + room: The Steady (Sunflower) + door: Back Exit + panels: + Achievement: + id: Countdown Panels/Panel_steady_steady + required_panel: + - room: The Steady (Rose) + panel: SOAR + - room: The Steady (Carnation) + panel: INCARNATION + - room: The Steady (Sunflower) + panel: SUN + - room: The Steady (Ruby) + panel: BURY + - room: The Steady (Plum) + panel: LUMP + - room: The Steady (Lime) + panel: LIMELIGHT + - room: The Steady (Lemon) + panel: MELON + - room: The Steady (Topaz) + panel: TOP + - room: The Steady (Orange) + panel: BLUE + - room: The Steady (Sapphire) + panel: SAP + - room: The Steady (Blueberry) + panel: BLUE + - room: The Steady (Amber) + panel: ANTECHAMBER + - room: The Steady (Emerald) + panel: HERALD + - room: The Steady (Amethyst) + panel: PACIFIST + - room: The Steady (Lilac) + panel: LIE LACK + - room: The Steady (Cherry) + panel: HAIRY + tag: forbid + check: True + achievement: The Steady + doors: + Reveal: + event: True + panels: + - Achievement + Knight Night (Outer Ring): + entrances: + Hidden Room: + room: Hidden Room + door: Knight Night Entrance + Knight Night Exit: True + panels: + NIGHT: + id: Appendix Room/Panel_night_knight + colors: blue + tag: homophone midblue + copy_to_sign: sign7 + KNIGHT: + id: Appendix Room/Panel_knight_night + colors: red + tag: homophone midred + copy_to_sign: sign8 + BEE: + id: Appendix Room/Panel_bee_be + colors: red + tag: homophone midred + copy_to_sign: sign9 + NEW: + id: Appendix Room/Panel_new_knew + colors: blue + tag: homophone midblue + copy_to_sign: sign11 + FORE: + id: Appendix Room/Panel_fore_for + colors: red + tag: homophone midred + copy_to_sign: sign10 + TRUSTED (1): + id: Appendix Room/Panel_trusted_trust + colors: red + tag: midred + required_panel: + room: Knight Night (Right Lower Segment) + panel: BEFORE + TRUSTED (2): + id: Appendix Room/Panel_trusted_rusted + colors: red + tag: midred + required_panel: + room: Knight Night (Right Lower Segment) + panel: BEFORE + ENCRUSTED: + id: Appendix Room/Panel_encrusted_rust + colors: red + tag: midred + required_panel: + - panel: TRUSTED (1) + - panel: TRUSTED (2) + ADJUST (1): + id: Appendix Room/Panel_adjust_readjust + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Lower Segment) + panel: BE + ADJUST (2): + id: Appendix Room/Panel_adjust_adjusted + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Lower Segment) + panel: BE + RIGHT: + id: Appendix Room/Panel_right_right + tag: midwhite + required_panel: + room: Knight Night (Right Lower Segment) + panel: ADJUST + TRUST: + id: Appendix Room/Panel_trust_crust + colors: + - red + - blue + tag: mid red blue + required_panel: + - room: Knight Night (Right Lower Segment) + panel: ADJUST + - room: Knight Night (Right Lower Segment) + panel: LEFT + doors: + Fore Door: + event: True + panels: + - FORE + New Door: + event: True + panels: + - NEW + To End: + event: True + panels: + - RIGHT + - room: Knight Night (Right Lower Segment) + panel: LEFT + Knight Night (Right Upper Segment): + entrances: + Knight Night Exit: True + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: Fore Door + Knight Night (Right Lower Segment): + door: Segment Door + panels: + RUST (1): + id: Appendix Room/Panel_rust_trust + colors: blue + tag: midblue + required_panel: + room: Knight Night (Outer Ring) + panel: BEE + RUST (2): + id: Appendix Room/Panel_rust_crust + colors: blue + tag: midblue + required_panel: + room: Knight Night (Outer Ring) + panel: BEE + doors: + Segment Door: + event: True + panels: + - RUST (2) + - room: Knight Night (Right Lower Segment) + panel: BEFORE + Knight Night (Right Lower Segment): + entrances: + Knight Night Exit: True + Knight Night (Right Upper Segment): + room: Knight Night (Right Upper Segment) + door: Segment Door + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: New Door + panels: + ADJUST: + id: Appendix Room/Panel_adjust_readjusted + colors: blue + tag: midblue + required_panel: + - room: Knight Night (Outer Ring) + panel: ADJUST (1) + - room: Knight Night (Outer Ring) + panel: ADJUST (2) + BEFORE: + id: Appendix Room/Panel_before_fore + colors: red + tag: midred and phone + required_panel: + room: Knight Night (Right Upper Segment) + panel: RUST (1) + BE: + id: Appendix Room/Panel_be_before + colors: blue + tag: midblue and phone + required_panel: + room: Knight Night (Right Upper Segment) + panel: RUST (1) + LEFT: + id: Appendix Room/Panel_left_left + tag: midwhite + required_panel: + room: Knight Night (Outer Ring) + panel: ENCRUSTED + TRUST: + id: Appendix Room/Panel_trust_crust_2 + colors: purple + tag: midpurp + required_panel: + - room: Knight Night (Outer Ring) + panel: ENCRUSTED + - room: Knight Night (Outer Ring) + panel: RIGHT + Knight Night (Final): + entrances: + Knight Night Exit: True + Knight Night (Outer Ring): + room: Knight Night (Outer Ring) + door: To End + Knight Night (Right Upper Segment): + room: Knight Night (Outer Ring) + door: To End + panels: + TRUSTED: + id: Appendix Room/Panel_trusted_readjusted + colors: purple + tag: midpurp + doors: + Exit: + id: + - Appendix Room Area Doors/Door_trusted_readjusted + - Appendix Room Area Doors/Door_trusted_readjusted2 + - Appendix Room Area Doors/Door_trusted_readjusted3 + - Appendix Room Area Doors/Door_trusted_readjusted4 + - Appendix Room Area Doors/Door_trusted_readjusted5 + - Appendix Room Area Doors/Door_trusted_readjusted6 + - Appendix Room Area Doors/Door_trusted_readjusted7 + - Appendix Room Area Doors/Door_trusted_readjusted8 + - Appendix Room Area Doors/Door_trusted_readjusted9 + - Appendix Room Area Doors/Door_trusted_readjusted10 + - Appendix Room Area Doors/Door_trusted_readjusted11 + - Appendix Room Area Doors/Door_trusted_readjusted12 + - Appendix Room Area Doors/Door_trusted_readjusted13 + include_reduce: True + location_name: Knight Night Room - TRUSTED + item_name: Knight Night Room - Exit + panels: + - TRUSTED + Knight Night Exit: + entrances: + Knight Night (Outer Ring): + room: Knight Night (Final) + door: Exit + Orange Tower Third Floor: + room: Knight Night (Final) + door: Exit + Outside The Initiated: + room: Knight Night (Final) + door: Exit + panels: + SEVEN (1): + id: Backside Room/Panel_seven_seven_7 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + SEVEN (2): + id: Backside Room/Panel_seven_seven_3 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + SEVEN (3): + id: Backside Room/Panel_seven_seven_4 + tag: midwhite + required_door: + - room: Number Hunt + door: Sevens + DEAD END: + id: Appendix Room/Panel_deadend_deadend + tag: midwhite + WARNER: + id: Appendix Room/Panel_warner_corner + colors: purple + tag: toppurp + The Artistic (Smiley): + entrances: + Dead End Area: + painting: True + Crossroads: + painting: True + Hot Crusts Area: + painting: True + Outside The Initiated: + painting: True + Directional Gallery: + painting: True + Number Hunt: + room: Number Hunt + door: Eights + painting: True + Art Gallery: + painting: True + The Eyes They See: + painting: True + The Artistic (Panda): + door: Door to Panda + The Artistic (Apple): + room: The Artistic (Apple) + door: Door to Smiley + Elements Area: + room: Hallway Room (4) + door: Exit + panels: + Achievement: + id: Countdown Panels/Panel_artistic_artistic + colors: + - red + - black + - yellow + - blue + tag: forbid + required_room: + - The Artistic (Panda) + - The Artistic (Apple) + - The Artistic (Lattice) + check: True + achievement: The Artistic + FINE: + id: Ceiling Room/Panel_yellow_top_5 + colors: + - yellow + - blue + tag: yellow top blue bot + subtag: top + link: yxu KNIFE + BLADE: + id: Ceiling Room/Panel_blue_bot_5 + colors: + - blue + - yellow + tag: yellow top blue bot + subtag: bot + link: yxu KNIFE + RED: + id: Ceiling Room/Panel_blue_top_6 + colors: + - blue + - yellow + tag: blue top yellow mid + subtag: top + link: uyx BREAD + BEARD: + id: Ceiling Room/Panel_yellow_mid_6 + colors: + - yellow + - blue + tag: blue top yellow mid + subtag: mid + link: uyx BREAD + ICE: + id: Ceiling Room/Panel_blue_mid_7 + colors: + - blue + - yellow + tag: blue mid yellow bot + subtag: mid + link: xuy SPICE + ROOT: + id: Ceiling Room/Panel_yellow_bot_7 + colors: + - yellow + - blue + tag: blue mid yellow bot + subtag: bot + link: xuy SPICE + doors: + Door to Panda: + id: + - Ceiling Room Doors/Door_blue + - Ceiling Room Doors/Door_blue2 + location_name: The Artistic - Smiley and Panda + group: Artistic Doors + panels: + - FINE + - BLADE + - RED + - BEARD + - ICE + - ROOT + - room: The Artistic (Panda) + panel: EYE (Top) + - room: The Artistic (Panda) + panel: EYE (Bottom) + - room: The Artistic (Panda) + panel: LADYLIKE + - room: The Artistic (Panda) + panel: WATER + - room: The Artistic (Panda) + panel: OURS + - room: The Artistic (Panda) + panel: DAYS + - room: The Artistic (Panda) + panel: NIGHTTIME + - room: The Artistic (Panda) + panel: NIGHT + paintings: + - id: smile_painting_9 + orientation: north + exit_only: True + The Artistic (Panda): + entrances: + Orange Tower Sixth Floor: + painting: True + Outside The Agreeable: + painting: True + The Artistic (Smiley): + room: The Artistic (Smiley) + door: Door to Panda + The Artistic (Lattice): + door: Door to Lattice + panels: + EYE (Top): + id: Ceiling Room/Panel_blue_top_1 + colors: + - blue + - red + tag: blue top red bot + subtag: top + link: uxr IRIS + EYE (Bottom): + id: Ceiling Room/Panel_red_bot_1 + colors: + - red + - blue + tag: blue top red bot + subtag: bot + link: uxr IRIS + LADYLIKE: + id: Ceiling Room/Panel_red_mid_2 + colors: + - red + - blue + tag: red mid blue bot + subtag: mid + link: xru LAKE + WATER: + id: Ceiling Room/Panel_blue_bot_2 + colors: + - blue + - red + tag: red mid blue bot + subtag: bot + link: xru LAKE + OURS: + id: Ceiling Room/Panel_blue_mid_3 + colors: + - blue + - red + tag: blue mid red bot + subtag: mid + link: xur HOURS + DAYS: + id: Ceiling Room/Panel_red_bot_3 + colors: + - red + - blue + tag: blue mid red bot + subtag: bot + link: xur HOURS + NIGHTTIME: + id: Ceiling Room/Panel_red_top_4 + colors: + - red + - blue + tag: red top mid blue + subtag: top + link: rux KNIGHT + NIGHT: + id: Ceiling Room/Panel_blue_mid_4 + colors: + - blue + - red + tag: red top mid blue + subtag: mid + link: rux KNIGHT + doors: + Door to Lattice: + id: + - Ceiling Room Doors/Door_red + - Ceiling Room Doors/Door_red2 + location_name: The Artistic - Panda and Lattice + group: Artistic Doors + panels: + - EYE (Top) + - EYE (Bottom) + - LADYLIKE + - WATER + - OURS + - DAYS + - NIGHTTIME + - NIGHT + - room: The Artistic (Lattice) + panel: POSH + - room: The Artistic (Lattice) + panel: MALL + - room: The Artistic (Lattice) + panel: DEICIDE + - room: The Artistic (Lattice) + panel: WAVER + - room: The Artistic (Lattice) + panel: REPAID + - room: The Artistic (Lattice) + panel: BABY + - room: The Artistic (Lattice) + panel: LOBE + - room: The Artistic (Lattice) + panel: BOWELS + paintings: + - id: panda_painting_3 + exit_only: True + orientation: south + required_when_no_doors: True + The Artistic (Lattice): + entrances: + Directional Gallery: + painting: True + The Artistic (Panda): + room: The Artistic (Panda) + door: Door to Lattice + The Artistic (Apple): + door: Door to Apple + panels: + POSH: + id: Ceiling Room/Panel_black_top_12 + colors: + - black + - red + tag: black top red bot + subtag: top + link: bxr SHOP + MALL: + id: Ceiling Room/Panel_red_bot_12 + colors: + - red + - black + tag: black top red bot + subtag: bot + link: bxr SHOP + DEICIDE: + id: Ceiling Room/Panel_red_top_13 + colors: + - red + - black + tag: red top black bot + subtag: top + link: rxb DECIDE + WAVER: + id: Ceiling Room/Panel_black_bot_13 + colors: + - black + - red + tag: red top black bot + subtag: bot + link: rxb DECIDE + REPAID: + id: Ceiling Room/Panel_black_mid_14 + colors: + - black + - red + tag: black mid red bot + subtag: mid + link: xbr DIAPER + BABY: + id: Ceiling Room/Panel_red_bot_14 + colors: + - red + - black + tag: black mid red bot + subtag: bot + link: xbr DIAPER + LOBE: + id: Ceiling Room/Panel_black_top_15 + colors: + - black + - red + tag: black top red mid + subtag: top + link: brx BOWL + BOWELS: + id: Ceiling Room/Panel_red_mid_15 + colors: + - red + - black + tag: black top red mid + subtag: mid + link: brx BOWL + doors: + Door to Apple: + id: + - Ceiling Room Doors/Door_black + - Ceiling Room Doors/Door_black2 + location_name: The Artistic - Lattice and Apple + group: Artistic Doors + panels: + - POSH + - MALL + - DEICIDE + - WAVER + - REPAID + - BABY + - LOBE + - BOWELS + - room: The Artistic (Apple) + panel: SPRIG + - room: The Artistic (Apple) + panel: RELEASES + - room: The Artistic (Apple) + panel: MUCH + - room: The Artistic (Apple) + panel: FISH + - room: The Artistic (Apple) + panel: MASK + - room: The Artistic (Apple) + panel: HILL + - room: The Artistic (Apple) + panel: TINE + - room: The Artistic (Apple) + panel: THING + paintings: + - id: boxes_painting2 + orientation: south + exit_only: True + required_when_no_doors: True + The Artistic (Apple): + entrances: + Orange Tower Sixth Floor: + painting: True + Directional Gallery: + painting: True + The Artistic (Lattice): + room: The Artistic (Lattice) + door: Door to Apple + The Artistic (Smiley): + door: Door to Smiley + panels: + SPRIG: + id: Ceiling Room/Panel_yellow_mid_8 + colors: + - yellow + - black + tag: yellow mid black bot + subtag: mid + link: xyb GRIPS + RELEASES: + id: Ceiling Room/Panel_black_bot_8 + colors: + - black + - yellow + tag: yellow mid black bot + subtag: bot + link: xyb GRIPS + MUCH: + id: Ceiling Room/Panel_black_top_9 + colors: + - black + - yellow + tag: black top yellow bot + subtag: top + link: bxy CHUM + FISH: + id: Ceiling Room/Panel_yellow_bot_9 + colors: + - yellow + - black + tag: black top yellow bot + subtag: bot + link: bxy CHUM + MASK: + id: Ceiling Room/Panel_yellow_top_10 + colors: + - yellow + - black + tag: yellow top black bot + subtag: top + link: yxb CHASM + HILL: + id: Ceiling Room/Panel_black_bot_10 + colors: + - black + - yellow + tag: yellow top black bot + subtag: bot + link: yxb CHASM + TINE: + id: Ceiling Room/Panel_black_top_11 + colors: + - black + - yellow + tag: black top yellow mid + subtag: top + link: byx NIGHT + THING: + id: Ceiling Room/Panel_yellow_mid_11 + colors: + - yellow + - black + tag: black top yellow mid + subtag: mid + link: byx NIGHT + doors: + Door to Smiley: + id: + - Ceiling Room Doors/Door_yellow + - Ceiling Room Doors/Door_yellow2 + location_name: The Artistic - Apple and Smiley + group: Artistic Doors + panels: + - SPRIG + - RELEASES + - MUCH + - FISH + - MASK + - HILL + - TINE + - THING + - room: The Artistic (Smiley) + panel: FINE + - room: The Artistic (Smiley) + panel: BLADE + - room: The Artistic (Smiley) + panel: RED + - room: The Artistic (Smiley) + panel: BEARD + - room: The Artistic (Smiley) + panel: ICE + - room: The Artistic (Smiley) + panel: ROOT + paintings: + - id: cherry_painting3 + orientation: north + exit_only: True + required_when_no_doors: True + The Artistic (Hint Room): + entrances: + The Artistic (Lattice): + room: The Artistic (Lattice) + door: Door to Apple + panels: + THEME: + id: Ceiling Room/Panel_answer_1 + colors: red + tag: midred + PAINTS: + id: Ceiling Room/Panel_answer_2 + colors: yellow + tag: botyellow + I: + id: Ceiling Room/Panel_answer_3 + colors: blue + tag: midblue + KIT: + id: Ceiling Room/Panel_answer_4 + colors: black + tag: topblack + The Discerning: + entrances: + Crossroads: + room: Crossroads + door: Discerning Entrance + panels: + Achievement: + id: Countdown Panels/Panel_discerning_scramble + colors: yellow + tag: forbid + check: True + achievement: The Discerning + HITS: + id: Sun Room/Panel_hits_this + colors: yellow + tag: midyellow + WARRED: + id: Sun Room/Panel_warred_drawer + colors: yellow + tag: double midyellow + subtag: left + link: ana DRAWER + REDRAW: + id: Sun Room/Panel_redraw_drawer + colors: yellow + tag: double midyellow + subtag: right + link: ana DRAWER + ADDER: + id: Sun Room/Panel_adder_dread + colors: yellow + tag: midyellow + LAUGHTERS: + id: Sun Room/Panel_laughters_slaughter + colors: yellow + tag: midyellow + STONE: + id: Sun Room/Panel_stone_notes + colors: yellow + tag: double midyellow + subtag: left + link: ana NOTES + ONSET: + id: Sun Room/Panel_onset_notes + colors: yellow + tag: double midyellow + subtag: right + link: ana NOTES + RAT: + id: Sun Room/Panel_rat_art + colors: yellow + tag: midyellow + DUSTY: + id: Sun Room/Panel_dusty_study + colors: yellow + tag: midyellow + ARTS: + id: Sun Room/Panel_arts_star + colors: yellow + tag: double midyellow + subtag: left + link: ana STAR + TSAR: + id: Sun Room/Panel_tsar_star + colors: yellow + tag: double midyellow + subtag: right + link: ana STAR + STATE: + id: Sun Room/Panel_state_taste + colors: yellow + tag: midyellow + REACT: + id: Sun Room/Panel_react_trace + colors: yellow + tag: midyellow + DEAR: + id: Sun Room/Panel_dear_read + colors: yellow + tag: double midyellow + subtag: left + link: ana READ + DARE: + id: Sun Room/Panel_dare_read + colors: yellow + tag: double midyellow + subtag: right + link: ana READ + SEAM: + id: Sun Room/Panel_seam_same + colors: yellow + tag: midyellow + The Eyes They See: + entrances: + Crossroads: + room: Crossroads + door: Eye Wall + painting: True + Wondrous Lobby: + door: Exit + Directional Gallery: True + panels: + NEAR: + id: Shuffle Room/Panel_near_near + tag: midwhite + EIGHT: + id: Backside Room/Panel_eight_eight_4 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + doors: + Exit: + id: Count Up Room Area Doors/Door_near_near + group: Crossroads Doors + panels: + - NEAR + paintings: + - id: eye_painting_2 + orientation: west + - id: smile_painting_2 + orientation: north + Far Window: + entrances: + Crossroads: + room: Crossroads + door: Eye Wall + The Eyes They See: True + panels: + FAR: + id: Shuffle Room/Panel_far_far + tag: midwhite + Wondrous Lobby: + entrances: + Directional Gallery: True + The Eyes They See: + room: The Eyes They See + door: Exit + paintings: + - id: arrows_painting_5 + orientation: east + Outside The Wondrous: + entrances: + Wondrous Lobby: True + The Wondrous (Doorknob): + door: Wondrous Entrance + The Wondrous (Window): True + panels: + SHRINK: + id: Wonderland Room/Panel_shrink_shrink + tag: midwhite + doors: + Wondrous Entrance: + id: Red Blue Purple Room Area Doors/Door_wonderland + item_name: The Wondrous - Entrance + panels: + - SHRINK + The Wondrous (Doorknob): + entrances: + Outside The Wondrous: + room: Outside The Wondrous + door: Wondrous Entrance + Starting Room: + door: Painting Shortcut + painting: True + The Wondrous (Chandelier): + painting: True + The Wondrous (Table): True # There is a way that doesn't use the painting + doors: + Painting Shortcut: + painting_id: + - symmetry_painting_a_starter + - arrows_painting2 + skip_location: True + item_name: Starting Room - Symmetry Painting + panels: + - room: Outside The Wondrous + panel: SHRINK + paintings: + - id: symmetry_painting_a_1 + orientation: east + exit_only: True + - id: symmetry_painting_b_1 + orientation: south + The Wondrous (Bookcase): + entrances: + The Wondrous (Doorknob): True + panels: + CASE: + id: Wonderland Room/Panel_case_bookcase + colors: blue + tag: midblue + paintings: + - id: symmetry_painting_a_3 + orientation: west + exit_only: True + - id: symmetry_painting_b_3 + disable: True + The Wondrous (Chandelier): + entrances: + The Wondrous (Bookcase): True + panels: + CANDLE HEIR: + id: Wonderland Room/Panel_candleheir_chandelier + colors: yellow + tag: midyellow + paintings: + - id: symmetry_painting_a_5 + orientation: east + - id: symmetry_painting_a_5 + disable: True + The Wondrous (Window): + entrances: + The Wondrous (Bookcase): True + panels: + GLASS: + id: Wonderland Room/Panel_glass_window + colors: brown + tag: botbrown + paintings: + - id: symmetry_painting_b_4 + orientation: north + exit_only: True + - id: symmetry_painting_a_4 + disable: True + The Wondrous (Table): + entrances: + The Wondrous (Doorknob): + painting: True + The Wondrous: + painting: True + panels: + WOOD: + id: Wonderland Room/Panel_wood_table + colors: brown + tag: botbrown + BROOK NOD: + # This panel, while physically being in the first room, is facing upward + # and is only really solvable while standing on the windowsill, which is + # a location you can only get to from Table. + id: Wonderland Room/Panel_brooknod_doorknob + colors: yellow + tag: midyellow + paintings: + - id: symmetry_painting_a_2 + orientation: west + - id: symmetry_painting_b_2 + orientation: south + exit_only: True + required: True + The Wondrous: + entrances: + The Wondrous (Table): True + Arrow Garden: + door: Exit + panels: + FIREPLACE: + id: Wonderland Room/Panel_fireplace_fire + colors: red + tag: midred + Achievement: + id: Countdown Panels/Panel_wondrous_wondrous + required_panel: + - panel: FIREPLACE + - room: The Wondrous (Table) + panel: BROOK NOD + - room: The Wondrous (Bookcase) + panel: CASE + - room: The Wondrous (Chandelier) + panel: CANDLE HEIR + - room: The Wondrous (Window) + panel: GLASS + - room: The Wondrous (Table) + panel: WOOD + tag: forbid + achievement: The Wondrous + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_wonderland_exit + painting_id: arrows_painting_9 + include_reduce: True + panels: + - Achievement + paintings: + - id: arrows_painting_9 + enter_only: True + orientation: south + move: True + required_door: + door: Exit + - id: symmetry_painting_a_6 + orientation: west + exit_only: True + - id: symmetry_painting_b_6 + orientation: north + Arrow Garden: + entrances: + The Wondrous: + room: The Wondrous + door: Exit + Roof: True + panels: + MASTERY: + id: Master Room/Panel_mastery_mastery4 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + SHARP: + id: Open Areas/Panel_rainy_rainbow2 + tag: midwhite + paintings: + - id: flower_painting_6 + orientation: south + Hallway Room (2): + entrances: + Outside The Agreeable: + room: Outside The Agreeable + door: Hallway Door + Elements Area: True + panels: + WISE: + id: Hallway Room/Panel_counterclockwise_1 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + CLOCK: + id: Hallway Room/Panel_counterclockwise_2 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + ER: + id: Hallway Room/Panel_counterclockwise_3 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + COUNT: + id: Hallway Room/Panel_counterclockwise_4 + colors: blue + tag: quad mid blue + link: qmb COUNTERCLOCKWISE + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_room_3 + location_name: Hallway Room - Second Room + group: Hallway Room Doors + panels: + - WISE + - CLOCK + - ER + - COUNT + Hallway Room (3): + entrances: + Hallway Room (2): + room: Hallway Room (2) + door: Exit + # No entrance from Elements Area. The winding hallway does not connect. + panels: + TRANCE: + id: Hallway Room/Panel_transformation_1 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + FORM: + id: Hallway Room/Panel_transformation_2 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + A: + id: Hallway Room/Panel_transformation_3 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + SHUN: + id: Hallway Room/Panel_transformation_4 + colors: blue + tag: quad top blue + link: qtb TRANSFORMATION + doors: + Exit: + id: Red Blue Purple Room Area Doors/Door_room_4 + location_name: Hallway Room - Third Room + group: Hallway Room Doors + panels: + - TRANCE + - FORM + - A + - SHUN + Hallway Room (4): + entrances: + Hallway Room (3): + room: Hallway Room (3) + door: Exit + Elements Area: True + panels: + WHEEL: + id: Hallway Room/Panel_room_5 + colors: blue + tag: full stack blue + doors: + Exit: + id: + - Red Blue Purple Room Area Doors/Door_room_5 + - Red Blue Purple Room Area Doors/Door_room_6 # this is the connection to The Artistic + group: Hallway Room Doors + location_name: Hallway Room - Fourth Room + panels: + - WHEEL + include_reduce: True + Elements Area: + entrances: + Roof: True + Hallway Room (4): + room: Hallway Room (4) + door: Exit + The Artistic (Smiley): + room: Hallway Room (4) + door: Exit + panels: + A: + id: Strand Room/Panel_a_strands + colors: blue + tag: forbid + NINE: + id: Backside Room/Panel_nine_nine_7 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + UNDISTRACTED: + id: Open Areas/Panel_undistracted + check: True + exclude_reduce: True + tag: midwhite + MASTERY: + id: Master Room/Panel_mastery_mastery13 + tag: midwhite + required_door: + room: Orange Tower Seventh Floor + door: Mastery + EARTH: + id: Cross Room/Panel_earth_earth + tag: midwhite + WATER: + id: Cross Room/Panel_water_water + tag: midwhite + AIR: + id: Cross Room/Panel_air_air + tag: midwhite + paintings: + - id: south_afar + orientation: south + Outside The Wanderer: + entrances: + Orange Tower First Floor: + door: Tower Entrance + Rhyme Room (Cross): + room: Rhyme Room (Cross) + door: Exit + Roof: True + panels: + WANDERLUST: + id: Tower Room/Panel_wanderlust_1234567890 + colors: orange + tag: midorange + doors: + Wanderer Entrance: + id: Tower Room Area Doors/Door_wanderer_entrance + item_name: The Wanderer - Entrance + panels: + - WANDERLUST + Tower Entrance: + id: Tower Room Area Doors/Door_wanderlust_start + skip_location: True + panels: + - room: The Wanderer + panel: Achievement + The Wanderer: + entrances: + Outside The Wanderer: + room: Outside The Wanderer + door: Wanderer Entrance + panels: + Achievement: + id: Countdown Panels/Panel_1234567890_wanderlust + colors: orange + check: True + tag: forbid + achievement: The Wanderer + "7890": + id: Orange Room/Panel_lust + colors: orange + tag: midorange + "6524": + id: Orange Room/Panel_read + colors: orange + tag: midorange + "951": + id: Orange Room/Panel_sew + colors: orange + tag: midorange + "4524": + id: Orange Room/Panel_dead + colors: orange + tag: midorange + LEARN: + id: Orange Room/Panel_learn + colors: orange + tag: midorange + DUST: + id: Orange Room/Panel_dust + colors: orange + tag: midorange + STAR: + id: Orange Room/Panel_star + colors: orange + tag: midorange + WANDER: + id: Orange Room/Panel_wander + colors: orange + tag: midorange + Art Gallery: + entrances: + Orange Tower Third Floor: True + Art Gallery (Second Floor): True + Art Gallery (Third Floor): True + Art Gallery (Fourth Floor): True + Orange Tower Fifth Floor: + door: Exit + panels: + EIGHT: + id: Backside Room/Panel_eight_eight_6 + tag: midwhite + required_door: + room: Number Hunt + door: Eights + EON: + id: Painting Room/Panel_eon_one + colors: yellow + tag: midyellow + TRUSTWORTHY: + id: Painting Room/Panel_to_two + colors: red + tag: midred + FREE: + id: Painting Room/Panel_free_three + colors: purple + tag: midpurp + OUR: + id: Painting Room/Panel_our_four + colors: blue + tag: midblue + ONE ROAD MANY TURNS: + id: Painting Room/Panel_order_onepathmanyturns + tag: forbid + colors: + - yellow + - blue + - gray + - brown + - orange + required_door: + door: Fifth Floor + doors: + Second Floor: + painting_id: + - scenery_painting_2b + - scenery_painting_2c + skip_location: True + panels: + - EON + First Floor Puzzles: + skip_item: True + location_name: Art Gallery - First Floor Puzzles + panels: + - EON + - TRUSTWORTHY + - FREE + - OUR + Third Floor: + painting_id: + - scenery_painting_3b + - scenery_painting_3c + skip_location: True + panels: + - room: Art Gallery (Second Floor) + panel: PATH + Fourth Floor: + painting_id: + - scenery_painting_4b + - scenery_painting_4c + skip_location: True + panels: + - room: Art Gallery (Third Floor) + panel: ANY + Fifth Floor: + id: Tower Room Area Doors/Door_painting_backroom + painting_id: + - scenery_painting_5b + - scenery_painting_5c + skip_location: True + panels: + - room: Art Gallery (Fourth Floor) + panel: SEND - USE + Exit: + id: Tower Room Area Doors/Door_painting_exit + include_reduce: True + panels: + - ONE ROAD MANY TURNS + paintings: + - id: smile_painting_3 + orientation: west + - id: flower_painting_2 + orientation: east + - id: scenery_painting_0a + orientation: north + - id: map_painting + orientation: east + - id: fruitbowl_painting4 + orientation: south + progression: + Progressive Art Gallery: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Exit + Art Gallery (Second Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Second Floor + panels: + HOUSE: + id: Painting Room/Panel_house_neighborhood + colors: blue + tag: botblue + PATH: + id: Painting Room/Panel_path_road + colors: brown + tag: botbrown + PARK: + id: Painting Room/Panel_park_drive + colors: black + tag: botblack + CARRIAGE: + id: Painting Room/Panel_carriage_horse + colors: red + tag: botred + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Second Floor Puzzles + panels: + - HOUSE + - PATH + - PARK + - CARRIAGE + Art Gallery (Third Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Third Floor + panels: + AN: + id: Painting Room/Panel_an_many + colors: blue + tag: midblue + MAY: + id: Painting Room/Panel_may_many + colors: blue + tag: midblue + ANY: + id: Painting Room/Panel_any_many + colors: blue + tag: midblue + MAN: + id: Painting Room/Panel_man_many + colors: blue + tag: midblue + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Third Floor Puzzles + panels: + - AN + - MAY + - ANY + - MAN + Art Gallery (Fourth Floor): + entrances: + Art Gallery: + room: Art Gallery + door: Fourth Floor + panels: + URNS: + id: Painting Room/Panel_urns_turns + colors: blue + tag: midblue + LEARNS: + id: Painting Room/Panel_learns_turns + colors: purple + tag: midpurp + RUNTS: + id: Painting Room/Panel_runts_turns + colors: yellow + tag: midyellow + SEND - USE: + id: Painting Room/Panel_send_use_turns + colors: orange + tag: midorange + TRUST: + id: Painting Room/Panel_trust_06890 + colors: orange + tag: midorange + "062459": + id: Painting Room/Panel_06890_trust + colors: orange + tag: midorange + doors: + Puzzles: + skip_item: True + location_name: Art Gallery - Fourth Floor Puzzles + panels: + - URNS + - LEARNS + - RUNTS + - SEND - USE + - TRUST + - "062459" + Rhyme Room (Smiley): + entrances: + Orange Tower Third Floor: + room: Orange Tower Third Floor + door: Rhyme Room Entrance + Rhyme Room (Circle): + room: Rhyme Room (Circle) + door: Door to Smiley + Rhyme Room (Cross): True # one-way + panels: + LOANS: + id: Double Room/Panel_bones_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme BONES + SKELETON: + id: Double Room/Panel_bones_syn + tag: syn rhyme + subtag: bot + link: rhyme BONES + REPENTANCE: + id: Double Room/Panel_sentence_rhyme + colors: purple + tag: whole rhyme + subtag: top + link: rhyme SENTENCE + WORD: + id: Double Room/Panel_sentence_whole + colors: blue + tag: whole rhyme + subtag: bot + link: rhyme SENTENCE + SCHEME: + id: Double Room/Panel_dream_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme DREAM + FANTASY: + id: Double Room/Panel_dream_syn + tag: syn rhyme + subtag: bot + link: rhyme DREAM + HISTORY: + id: Double Room/Panel_mystery_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme MYSTERY + SECRET: + id: Double Room/Panel_mystery_syn + tag: syn rhyme + subtag: bot + link: rhyme MYSTERY + doors: + # This is complicated. I want the location in here to just be the four + # panels against the wall toward Target. But in vanilla, you also need to + # solve the panels in Circle that are against the Smiley wall. Logic needs + # to know this so that it can handle no door shuffle properly. So we split + # the item and location up. + Door to Target: + id: + - Double Room Area Doors/Door_room_3a + - Double Room Area Doors/Door_room_3bc + skip_location: True + group: Rhyme Room Doors + panels: + - SCHEME + - FANTASY + - HISTORY + - SECRET + - room: Rhyme Room (Circle) + panel: BIRD + - room: Rhyme Room (Circle) + panel: LETTER + - room: Rhyme Room (Circle) + panel: VIOLENT + - room: Rhyme Room (Circle) + panel: MUTE + Door to Target (Location): + location_name: Rhyme Room (Smiley) - Puzzles Toward Target + skip_item: True + panels: + - SCHEME + - FANTASY + - HISTORY + - SECRET + Rhyme Room (Cross): + entrances: + Rhyme Room (Target): # one-way + room: Rhyme Room (Target) + door: Door to Cross + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Cross + panels: + NINE: + id: Backside Room/Panel_nine_nine_9 + tag: midwhite + required_door: + room: Number Hunt + door: Nines + FERN: + id: Double Room/Panel_return_rhyme + colors: purple + tag: ant rhyme + subtag: top + link: rhyme RETURN + STAY: + id: Double Room/Panel_return_ant + colors: black + tag: ant rhyme + subtag: bot + link: rhyme RETURN + FRIEND: + id: Double Room/Panel_descend_rhyme + colors: purple + tag: ant rhyme + subtag: top + link: rhyme DESCEND + RISE: + id: Double Room/Panel_descend_ant + colors: black + tag: ant rhyme + subtag: bot + link: rhyme DESCEND + PLUMP: + id: Double Room/Panel_jump_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme JUMP + BOUNCE: + id: Double Room/Panel_jump_syn + tag: syn rhyme + subtag: bot + link: rhyme JUMP + SCRAWL: + id: Double Room/Panel_fall_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme FALL + PLUNGE: + id: Double Room/Panel_fall_syn + tag: syn rhyme + subtag: bot + link: rhyme FALL + LEAP: + id: Double Room/Panel_leap_leap + tag: midwhite + doors: + Exit: + id: Double Room Area Doors/Door_room_exit + location_name: Rhyme Room (Cross) - Exit Puzzles + group: Rhyme Room Doors + panels: + - PLUMP + - BOUNCE + - SCRAWL + - PLUNGE + Rhyme Room (Circle): + entrances: + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Circle + Hidden Room: + room: Hidden Room + door: Rhyme Room Entrance + Rhyme Room (Smiley): + door: Door to Smiley + panels: + BIRD: + id: Double Room/Panel_word_rhyme + colors: purple + tag: whole rhyme + subtag: top + link: rhyme WORD + LETTER: + id: Double Room/Panel_word_whole + colors: blue + tag: whole rhyme + subtag: bot + link: rhyme WORD + FORBIDDEN: + id: Double Room/Panel_hidden_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme HIDDEN + CONCEALED: + id: Double Room/Panel_hidden_syn + tag: syn rhyme + subtag: bot + link: rhyme HIDDEN + VIOLENT: + id: Double Room/Panel_silent_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme SILENT + MUTE: + id: Double Room/Panel_silent_syn + tag: syn rhyme + subtag: bot + link: rhyme SILENT + doors: + Door to Smiley: + id: + - Double Room Area Doors/Door_room_2b + - Double Room Area Doors/Door_room_3b + location_name: Rhyme Room - Circle/Smiley Wall + group: Rhyme Room Doors + panels: + - BIRD + - LETTER + - VIOLENT + - MUTE + - room: Rhyme Room (Smiley) + panel: LOANS + - room: Rhyme Room (Smiley) + panel: SKELETON + - room: Rhyme Room (Smiley) + panel: REPENTANCE + - room: Rhyme Room (Smiley) + panel: WORD + paintings: + - id: arrows_painting_3 + orientation: north + Rhyme Room (Looped Square): + entrances: + Starting Room: + room: Starting Room + door: Rhyme Room Entrance + Rhyme Room (Circle): + door: Door to Circle + Rhyme Room (Cross): + door: Door to Cross + Rhyme Room (Target): + door: Door to Target + panels: + WALKED: + id: Double Room/Panel_blocked_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme BLOCKED + OBSTRUCTED: + id: Double Room/Panel_blocked_syn + tag: syn rhyme + subtag: bot + link: rhyme BLOCKED + SKIES: + id: Double Room/Panel_rise_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme RISE + SWELL: + id: Double Room/Panel_rise_syn + tag: syn rhyme + subtag: bot + link: rhyme RISE + PENNED: + id: Double Room/Panel_ascend_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme ASCEND + CLIMB: + id: Double Room/Panel_ascend_syn + tag: syn rhyme + subtag: bot + link: rhyme ASCEND + TROUBLE: + id: Double Room/Panel_double_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme DOUBLE + DUPLICATE: + id: Double Room/Panel_double_syn + tag: syn rhyme + subtag: bot + link: rhyme DOUBLE + doors: + Door to Circle: + id: + - Double Room Area Doors/Door_room_2a + - Double Room Area Doors/Door_room_1c + location_name: Rhyme Room - Circle/Looped Square Wall + group: Rhyme Room Doors + panels: + - WALKED + - OBSTRUCTED + - SKIES + - SWELL + - room: Rhyme Room (Circle) + panel: BIRD + - room: Rhyme Room (Circle) + panel: LETTER + - room: Rhyme Room (Circle) + panel: FORBIDDEN + - room: Rhyme Room (Circle) + panel: CONCEALED + Door to Cross: + id: + - Double Room Area Doors/Door_room_1a + - Double Room Area Doors/Door_room_5a + location_name: Rhyme Room - Cross/Looped Square Wall + group: Rhyme Room Doors + panels: + - SKIES + - SWELL + - PENNED + - CLIMB + - room: Rhyme Room (Cross) + panel: FERN + - room: Rhyme Room (Cross) + panel: STAY + - room: Rhyme Room (Cross) + panel: FRIEND + - room: Rhyme Room (Cross) + panel: RISE + Door to Target: + id: + - Double Room Area Doors/Door_room_1b + - Double Room Area Doors/Door_room_4b + location_name: Rhyme Room - Target/Looped Square Wall + group: Rhyme Room Doors + panels: + - PENNED + - CLIMB + - TROUBLE + - DUPLICATE + - room: Rhyme Room (Target) + panel: WILD + - room: Rhyme Room (Target) + panel: KID + - room: Rhyme Room (Target) + panel: PISTOL + - room: Rhyme Room (Target) + panel: QUARTZ + Rhyme Room (Target): + entrances: + Rhyme Room (Smiley): # one-way + room: Rhyme Room (Smiley) + door: Door to Target + Rhyme Room (Looped Square): + room: Rhyme Room (Looped Square) + door: Door to Target + panels: + WILD: + id: Double Room/Panel_child_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CHILD + KID: + id: Double Room/Panel_child_syn + tag: syn rhyme + subtag: bot + link: rhyme CHILD + PISTOL: + id: Double Room/Panel_crystal_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CRYSTAL + QUARTZ: + id: Double Room/Panel_crystal_syn + tag: syn rhyme + subtag: bot + link: rhyme CRYSTAL + INNOVATIVE (Top): + id: Double Room/Panel_creative_rhyme + colors: purple + tag: syn rhyme + subtag: top + link: rhyme CREATIVE + INNOVATIVE (Bottom): + id: Double Room/Panel_creative_syn + tag: syn rhyme + subtag: bot + link: rhyme CREATIVE + doors: + Door to Cross: + id: Double Room Area Doors/Door_room_4a + location_name: Rhyme Room (Target) - Puzzles Toward Cross + group: Rhyme Room Doors + panels: + - PISTOL + - QUARTZ + - INNOVATIVE (Top) + - INNOVATIVE (Bottom) + paintings: + - id: arrows_painting_4 + orientation: north + Room Room: + # This is a bit of a weird room. You can't really get to it from the roof. + # And even if you were to go through the shortcut on the fifth floor into + # the basement and up the stairs, you'd be blocked by the backsides of the + # ROOM panels, which isn't ideal. So we will, at least for now, say that + # this room is vanilla. + # + # For pretty much the same reason, I don't want to shuffle the paintings in + # here. + entrances: + Orange Tower Fourth Floor: True + panels: + DOOR (1): + id: Panel Room/Panel_room_door_1 + colors: gray + tag: forbid + DOOR (2): + id: Panel Room/Panel_room_door_2 + colors: gray + tag: forbid + WINDOW: + id: Panel Room/Panel_room_window_1 + colors: gray + tag: forbid + STAIRS: + id: Panel Room/Panel_room_stairs_1 + colors: gray + tag: forbid + PAINTING: + id: Panel Room/Panel_room_painting_1 + colors: gray + tag: forbid + FLOOR (1): + id: Panel Room/Panel_room_floor_1 + colors: gray + tag: forbid + FLOOR (2): + id: Panel Room/Panel_room_floor_2 + colors: gray + tag: forbid + FLOOR (3): + id: Panel Room/Panel_room_floor_3 + colors: gray + tag: forbid + FLOOR (4): + id: Panel Room/Panel_room_floor_4 + colors: gray + tag: forbid + FLOOR (5): + id: Panel Room/Panel_room_floor_5 + colors: gray + tag: forbid + FLOOR (7): + id: Panel Room/Panel_room_floor_7 + colors: gray + tag: forbid + FLOOR (8): + id: Panel Room/Panel_room_floor_8 + colors: gray + tag: forbid + FLOOR (9): + id: Panel Room/Panel_room_floor_9 + colors: gray + tag: forbid + FLOOR (10): + id: Panel Room/Panel_room_floor_10 + colors: gray + tag: forbid + CEILING (1): + id: Panel Room/Panel_room_ceiling_1 + colors: gray + tag: forbid + CEILING (2): + id: Panel Room/Panel_room_ceiling_2 + colors: gray + tag: forbid + CEILING (3): + id: Panel Room/Panel_room_ceiling_3 + colors: gray + tag: forbid + CEILING (4): + id: Panel Room/Panel_room_ceiling_4 + colors: gray + tag: forbid + CEILING (5): + id: Panel Room/Panel_room_ceiling_5 + colors: gray + tag: forbid + WALL (1): + id: Panel Room/Panel_room_wall_1 + colors: gray + tag: forbid + WALL (2): + id: Panel Room/Panel_room_wall_2 + colors: gray + tag: forbid + WALL (3): + id: Panel Room/Panel_room_wall_3 + colors: gray + tag: forbid + WALL (4): + id: Panel Room/Panel_room_wall_4 + colors: gray + tag: forbid + WALL (5): + id: Panel Room/Panel_room_wall_5 + colors: gray + tag: forbid + WALL (6): + id: Panel Room/Panel_room_wall_6 + colors: gray + tag: forbid + WALL (7): + id: Panel Room/Panel_room_wall_7 + colors: gray + tag: forbid + WALL (8): + id: Panel Room/Panel_room_wall_8 + colors: gray + tag: forbid + WALL (9): + id: Panel Room/Panel_room_wall_9 + colors: gray + tag: forbid + WALL (10): + id: Panel Room/Panel_room_wall_10 + colors: gray + tag: forbid + WALL (11): + id: Panel Room/Panel_room_wall_11 + colors: gray + tag: forbid + WALL (12): + id: Panel Room/Panel_room_wall_12 + colors: gray + tag: forbid + WALL (13): + id: Panel Room/Panel_room_wall_13 + colors: gray + tag: forbid + WALL (14): + id: Panel Room/Panel_room_wall_14 + colors: gray + tag: forbid + WALL (15): + id: Panel Room/Panel_room_wall_15 + colors: gray + tag: forbid + WALL (16): + id: Panel Room/Panel_room_wall_16 + colors: gray + tag: forbid + WALL (17): + id: Panel Room/Panel_room_wall_17 + colors: gray + tag: forbid + WALL (18): + id: Panel Room/Panel_room_wall_18 + colors: gray + tag: forbid + WALL (19): + id: Panel Room/Panel_room_wall_19 + colors: gray + tag: forbid + WALL (20): + id: Panel Room/Panel_room_wall_20 + colors: gray + tag: forbid + WALL (21): + id: Panel Room/Panel_room_wall_21 + colors: gray + tag: forbid + BROOMED: + id: Panel Room/Panel_broomed_bedroom + colors: yellow + tag: midyellow + required_door: + door: Excavation + LAYS: + id: Panel Room/Panel_lays_maze + colors: purple + tag: toppurp + required_panel: + panel: BROOMED + BASE: + id: Panel Room/Panel_base_basement + colors: blue + tag: midblue + required_panel: + panel: LAYS + MASTERY: + id: Master Room/Panel_mastery_mastery + tag: midwhite + colors: gray + required_door: + room: Orange Tower Seventh Floor + door: Mastery + doors: + Excavation: + event: True + panels: + - WALL (1) + Shortcut to Fifth Floor: + id: + - Tower Room Area Doors/Door_panel_basement + - Tower Room Area Doors/Door_panel_basement2 + panels: + - BASE + Cellar: + entrances: + Room Room: + room: Room Room + door: Excavation + Orange Tower Fifth Floor: + room: Room Room + door: Shortcut to Fifth Floor + Outside The Wise: + entrances: + Orange Tower Sixth Floor: + painting: True + Outside The Initiated: + painting: True + panels: + KITTEN: + id: Clock Room/Panel_kitten_cat + colors: brown + tag: botbrown + CAT: + id: Clock Room/Panel_cat_kitten + tag: bot brown black + colors: + - brown + - black + doors: + Wise Entrance: + id: Clock Room Area Doors/Door_time_start + item_name: The Wise - Entrance + panels: + - KITTEN + - CAT + paintings: + - id: arrows_painting_2 + orientation: east + - id: clock_painting_2 + orientation: east + exit_only: True + required: True + The Wise: + entrances: + Outside The Wise: + room: Outside The Wise + door: Wise Entrance + panels: + Achievement: + id: Countdown Panels/Panel_intelligent_wise + colors: + - brown + - black + tag: forbid + check: True + achievement: The Wise + PUPPY: + id: Clock Room/Panel_puppy_dog + colors: brown + tag: botbrown + ADULT: + id: Clock Room/Panel_adult_child + colors: + - brown + - black + tag: bot brown black + BREAD: + id: Clock Room/Panel_bread_mold + colors: brown + tag: botbrown + DINOSAUR: + id: Clock Room/Panel_dinosaur_fossil + colors: brown + tag: botbrown + OAK: + id: Clock Room/Panel_oak_acorn + colors: + - brown + - black + tag: bot brown black + CORPSE: + id: Clock Room/Panel_corpse_skeleton + colors: brown + tag: botbrown + BEFORE: + id: Clock Room/Panel_before_ere + colors: + - brown + - black + tag: mid brown black + YOUR: + id: Clock Room/Panel_your_thy + colors: + - brown + - black + tag: mid brown black + BETWIXT: + id: Clock Room/Panel_betwixt_between + colors: brown + tag: midbrown + NIGH: + id: Clock Room/Panel_nigh_near + colors: brown + tag: midbrown + CONNEXION: + id: Clock Room/Panel_connexion_connection + colors: brown + tag: midbrown + THOU: + id: Clock Room/Panel_thou_you + colors: brown + tag: midbrown + paintings: + - id: clock_painting_3 + orientation: east + The Red: + entrances: + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_grandfathered_red + colors: red + tag: forbid + check: True + achievement: The Red + PANDEMIC (1): + id: Hangry Room/Panel_red_top_1 + colors: red + tag: topred + TRINITY: + id: Hangry Room/Panel_red_top_2 + colors: red + tag: topred + CHEMISTRY: + id: Hangry Room/Panel_red_top_3 + colors: red + tag: topred + FLUMMOXED: + id: Hangry Room/Panel_red_top_4 + colors: red + tag: topred + PANDEMIC (2): + id: Hangry Room/Panel_red_mid_1 + colors: red + tag: midred + COUNTERCLOCKWISE: + id: Hangry Room/Panel_red_mid_2 + colors: red + tag: red top red mid black bot + FEARLESS: + id: Hangry Room/Panel_red_mid_3 + colors: red + tag: midred + DEFORESTATION: + id: Hangry Room/Panel_red_mid_4 + colors: red + tag: red mid bot + subtag: mid + link: rmb FORE + CRAFTSMANSHIP: + id: Hangry Room/Panel_red_mid_5 + colors: red + tag: red mid bot + subtag: mid + link: rmb AFT + CAMEL: + id: Hangry Room/Panel_red_bot_1 + colors: red + tag: botred + LION: + id: Hangry Room/Panel_red_bot_2 + colors: red + tag: botred + TIGER: + id: Hangry Room/Panel_red_bot_3 + colors: red + tag: botred + SHIP (1): + id: Hangry Room/Panel_red_bot_4 + colors: red + tag: red mid bot + subtag: bot + link: rmb FORE + SHIP (2): + id: Hangry Room/Panel_red_bot_5 + colors: red + tag: red mid bot + subtag: bot + link: rmb AFT + GIRAFFE: + id: Hangry Room/Panel_red_bot_6 + colors: red + tag: botred + The Ecstatic: + entrances: + Roof: True + panels: + Achievement: + id: Countdown Panels/Panel_ecstatic_ecstatic + colors: yellow + tag: forbid + check: True + achievement: The Ecstatic + FORM (1): + id: Smiley Room/Panel_soundgram_1 + colors: yellow + tag: yellow top bot + subtag: bottom + link: ytb FORM + WIND: + id: Smiley Room/Panel_soundgram_2 + colors: yellow + tag: botyellow + EGGS: + id: Smiley Room/Panel_scrambled_1 + colors: yellow + tag: botyellow + VEGETABLES: + id: Smiley Room/Panel_scrambled_2 + colors: yellow + tag: botyellow + WATER: + id: Smiley Room/Panel_anagram_6_1 + colors: yellow + tag: botyellow + FRUITS: + id: Smiley Room/Panel_anagram_6_2 + colors: yellow + tag: botyellow + LEAVES: + id: Smiley Room/Panel_anagram_7_1 + colors: yellow + tag: topyellow + VINES: + id: Smiley Room/Panel_anagram_7_2 + colors: yellow + tag: topyellow + ICE: + id: Smiley Room/Panel_anagram_7_3 + colors: yellow + tag: topyellow + STYLE: + id: Smiley Room/Panel_anagram_7_4 + colors: yellow + tag: topyellow + FIR: + id: Smiley Room/Panel_anagram_8_1 + colors: yellow + tag: topyellow + REEF: + id: Smiley Room/Panel_anagram_8_2 + colors: yellow + tag: topyellow + ROTS: + id: Smiley Room/Panel_anagram_8_3 + colors: yellow + tag: topyellow + FORM (2): + id: Smiley Room/Panel_anagram_9_1 + colors: yellow + tag: yellow top bot + subtag: top + link: ytb FORM + Outside The Scientific: + entrances: + Roof: True + The Scientific: + door: Scientific Entrance + panels: + OPEN: + id: Chemistry Room/Panel_open + tag: midwhite + CLOSE: + id: Chemistry Room/Panel_close + colors: black + tag: botblack + AHEAD: + id: Chemistry Room/Panel_ahead + colors: black + tag: botblack + doors: + Scientific Entrance: + id: Red Blue Purple Room Area Doors/Door_chemistry_lab + item_name: The Scientific - Entrance + panels: + - OPEN + The Scientific: + entrances: + Outside The Scientific: + room: Outside The Scientific + door: Scientific Entrance + panels: + Achievement: + id: Countdown Panels/Panel_scientific_scientific + colors: + - yellow + - red + - blue + - brown + - black + - purple + tag: forbid + check: True + achievement: The Scientific + HYDROGEN (1): + id: Chemistry Room/Panel_blue_bot_3 + colors: blue + tag: tri botblue + link: tbb WATER + OXYGEN: + id: Chemistry Room/Panel_blue_bot_2 + colors: blue + tag: tri botblue + link: tbb WATER + HYDROGEN (2): + id: Chemistry Room/Panel_blue_bot_4 + colors: blue + tag: tri botblue + link: tbb WATER + SUGAR (1): + id: Chemistry Room/Panel_sugar_1 + colors: red + tag: botred + SUGAR (2): + id: Chemistry Room/Panel_sugar_2 + colors: red + tag: botred + SUGAR (3): + id: Chemistry Room/Panel_sugar_3 + colors: red + tag: botred + CHLORINE: + id: Chemistry Room/Panel_blue_bot_5 + colors: blue + tag: double botblue + subtag: left + link: holo SALT + SODIUM: + id: Chemistry Room/Panel_blue_bot_6 + colors: blue + tag: double botblue + subtag: right + link: holo SALT + FOREST: + id: Chemistry Room/Panel_long_bot_1 + colors: + - red + - blue + tag: chain red bot blue top + POUND: + id: Chemistry Room/Panel_long_top_1 + colors: + - red + - blue + tag: chain blue mid red bot + ICE: + id: Chemistry Room/Panel_brown_bot_1 + colors: brown + tag: botbrown + FISSION: + id: Chemistry Room/Panel_black_bot_1 + colors: black + tag: botblack + FUSION: + id: Chemistry Room/Panel_black_bot_2 + colors: black + tag: botblack + MISS: + id: Chemistry Room/Panel_blue_top_1 + colors: blue + tag: double topblue + subtag: left + link: exp CHEMISTRY + TREE (1): + id: Chemistry Room/Panel_blue_top_2 + colors: blue + tag: double topblue + subtag: right + link: exp CHEMISTRY + BIOGRAPHY: + id: Chemistry Room/Panel_biology_9 + colors: purple + tag: midpurp + CACTUS: + id: Chemistry Room/Panel_biology_4 + colors: red + tag: double botred + subtag: right + link: mero SPINE + VERTEBRATE: + id: Chemistry Room/Panel_biology_8 + colors: red + tag: double botred + subtag: left + link: mero SPINE + ROSE: + id: Chemistry Room/Panel_biology_2 + colors: red + tag: botred + TREE (2): + id: Chemistry Room/Panel_biology_3 + colors: red + tag: botred + FRUIT: + id: Chemistry Room/Panel_biology_1 + colors: red + tag: botred + MAMMAL: + id: Chemistry Room/Panel_biology_5 + colors: red + tag: botred + BIRD: + id: Chemistry Room/Panel_biology_6 + colors: red + tag: botred + FISH: + id: Chemistry Room/Panel_biology_7 + colors: red + tag: botred + GRAVELY: + id: Chemistry Room/Panel_physics_9 + colors: purple + tag: double midpurp + subtag: left + link: change GRAVITY + BREVITY: + id: Chemistry Room/Panel_biology_10 + colors: purple + tag: double midpurp + subtag: right + link: change GRAVITY + PART: + id: Chemistry Room/Panel_physics_2 + colors: blue + tag: blue mid red bot + subtag: mid + link: xur PARTICLE + MATTER: + id: Chemistry Room/Panel_physics_1 + colors: red + tag: blue mid red bot + subtag: bot + link: xur PARTICLE + ELECTRIC: + id: Chemistry Room/Panel_physics_6 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr ELECTRON + ATOM (1): + id: Chemistry Room/Panel_physics_3 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr ELECTRON + NEUTRAL: + id: Chemistry Room/Panel_physics_7 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr NEUTRON + ATOM (2): + id: Chemistry Room/Panel_physics_4 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr NEUTRON + PROPEL: + id: Chemistry Room/Panel_physics_8 + colors: purple + tag: purple mid red bot + subtag: mid + link: xpr PROTON + ATOM (3): + id: Chemistry Room/Panel_physics_5 + colors: red + tag: purple mid red bot + subtag: bot + link: xpr PROTON + ORDER: + id: Chemistry Room/Panel_physics_11 + colors: brown + tag: botbrown + OPTICS: + id: Chemistry Room/Panel_physics_10 + colors: yellow + tag: midyellow + GRAPHITE: + id: Chemistry Room/Panel_yellow_bot_1 + colors: yellow + tag: botyellow + HOT RYE: + id: Chemistry Room/Panel_anagram_1 + colors: yellow + tag: midyellow + SIT SHY HOPE: + id: Chemistry Room/Panel_anagram_2 + colors: yellow + tag: midyellow + ME NEXT PIER: + id: Chemistry Room/Panel_anagram_3 + colors: yellow + tag: midyellow + RUT LESS: + id: Chemistry Room/Panel_anagram_4 + colors: yellow + tag: midyellow + SON COUNCIL: + id: Chemistry Room/Panel_anagram_5 + colors: yellow + tag: midyellow + doors: + Chemistry Puzzles: + skip_item: True + location_name: The Scientific - Chemistry Puzzles + panels: + - HYDROGEN (1) + - OXYGEN + - HYDROGEN (2) + - SUGAR (1) + - SUGAR (2) + - SUGAR (3) + - CHLORINE + - SODIUM + - FOREST + - POUND + - ICE + - FISSION + - FUSION + - MISS + - TREE (1) + Biology Puzzles: + skip_item: True + location_name: The Scientific - Biology Puzzles + panels: + - BIOGRAPHY + - CACTUS + - VERTEBRATE + - ROSE + - TREE (2) + - FRUIT + - MAMMAL + - BIRD + - FISH + Physics Puzzles: + skip_item: True + location_name: The Scientific - Physics Puzzles + panels: + - GRAVELY + - BREVITY + - PART + - MATTER + - ELECTRIC + - ATOM (1) + - NEUTRAL + - ATOM (2) + - PROPEL + - ATOM (3) + - ORDER + - OPTICS + paintings: + - id: hi_solved_painting4 + orientation: south + Challenge Room: + entrances: + Welcome Back Area: + door: Welcome Door + Number Hunt: + room: Outside The Undeterred + door: Challenge Entrance + panels: + WELCOME: + id: Challenge Room/Panel_welcome_welcome + tag: midwhite + CHALLENGE: + id: Challenge Room/Panel_challenge_challenge + tag: midwhite + Achievement: + id: Countdown Panels/Panel_challenged_unchallenged + check: True + colors: + - black + - gray + - red + - blue + - yellow + - purple + - brown + - orange + tag: forbid + achievement: The Unchallenged + OPEN: + id: Challenge Room/Panel_open_nepotism + colors: + - black + - blue + tag: chain mid black !!! blue + SINGED: + id: Challenge Room/Panel_singed_singsong + colors: + - red + - blue + tag: chain mid red blue + NEVER TRUSTED: + id: Challenge Room/Panel_nevertrusted_maladjusted + colors: purple + tag: midpurp + CORNER: + id: Challenge Room/Panel_corner_corn + colors: red + tag: midred + STRAWBERRIES: + id: Challenge Room/Panel_strawberries_mold + colors: brown + tag: double botbrown + subtag: left + link: time MOLD + GRUB: + id: Challenge Room/Panel_grub_burger + colors: + - black + - blue + tag: chain mid black blue + BREAD: + id: Challenge Room/Panel_bread_mold + colors: brown + tag: double botbrown + subtag: right + link: time MOLD + COLOR: + id: Challenge Room/Panel_color_gray + colors: gray + tag: forbid + WRITER: + id: Challenge Room/Panel_writer_songwriter + colors: blue + tag: midblue + "02759": + id: Challenge Room/Panel_tales_stale + colors: + - orange + - yellow + tag: chain mid orange yellow + REAL EYES: + id: Challenge Room/Panel_realeyes_realize + tag: topwhite + LOBS: + id: Challenge Room/Panel_lobs_lobster + colors: blue + tag: midblue + PEST ALLY: + id: Challenge Room/Panel_double_anagram_1 + colors: yellow + tag: midyellow + GENIAL HALO: + id: Challenge Room/Panel_double_anagram_2 + colors: yellow + tag: midyellow + DUCK LOGO: + id: Challenge Room/Panel_double_anagram_3 + colors: yellow + tag: midyellow + AVIAN GREEN: + id: Challenge Room/Panel_double_anagram_4 + colors: yellow + tag: midyellow + FEVER TEAR: + id: Challenge Room/Panel_double_anagram_5 + colors: yellow + tag: midyellow + FACTS: + id: Challenge Room/Panel_facts + colors: + - red + - blue + tag: forbid + FACTS (1): + id: Challenge Room/Panel_facts2 + colors: red + tag: forbid + FACTS (3): + id: Challenge Room/Panel_facts3 + tag: forbid + FACTS (4): + id: Challenge Room/Panel_facts4 + colors: blue + tag: forbid + FACTS (5): + id: Challenge Room/Panel_facts5 + colors: blue + tag: forbid + FACTS (6): + id: Challenge Room/Panel_facts6 + colors: blue + tag: forbid + LAPEL SHEEP: + id: Challenge Room/Panel_double_anagram_6 + colors: yellow + tag: midyellow + doors: + Welcome Door: + id: Entry Room Area Doors/Door_challenge_challenge + panels: + - WELCOME diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py new file mode 100644 index 0000000000..1f426c92f2 --- /dev/null +++ b/worlds/lingo/__init__.py @@ -0,0 +1,112 @@ +""" +Archipelago init file for Lingo +""" +from BaseClasses import Item, Tutorial +from worlds.AutoWorld import WebWorld, World +from .items import ALL_ITEM_TABLE, LingoItem +from .locations import ALL_LOCATION_TABLE +from .options import LingoOptions +from .player_logic import LingoPlayerLogic +from .regions import create_regions +from .static_logic import Room, RoomEntrance +from .testing import LingoTestOptions + + +class LingoWebWorld(WebWorld): + theme = "grass" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Lingo with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["hatkirby"] + )] + + +class LingoWorld(World): + """ + Lingo is a first person indie puzzle game in the vein of The Witness. You find yourself in a mazelike, non-Euclidean + world filled with 800 word puzzles that use a variety of different mechanics. + """ + game = "Lingo" + web = LingoWebWorld() + + base_id = 444400 + topology_present = True + data_version = 1 + + options_dataclass = LingoOptions + options: LingoOptions + + item_name_to_id = { + name: data.code for name, data in ALL_ITEM_TABLE.items() + } + location_name_to_id = { + name: data.code for name, data in ALL_LOCATION_TABLE.items() + } + + player_logic: LingoPlayerLogic + + def generate_early(self): + self.player_logic = LingoPlayerLogic(self) + + def create_regions(self): + create_regions(self, self.player_logic) + + def create_items(self): + pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] + + if self.player_logic.FORCED_GOOD_ITEM != "": + new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) + location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) + location_obj.place_locked_item(new_item) + + item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) + if item_difference: + trap_percentage = self.options.trap_percentage + traps = int(item_difference * trap_percentage / 100.0) + non_traps = item_difference - traps + + if non_traps: + skip_percentage = self.options.puzzle_skip_percentage + skips = int(non_traps * skip_percentage / 100.0) + non_skips = non_traps - skips + + filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"] + for i in range(0, non_skips): + pool.append(self.create_item(filler_list[i % len(filler_list)])) + + for i in range(0, skips): + pool.append(self.create_item("Puzzle Skip")) + + if traps: + traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"] + + for i in range(0, traps): + pool.append(self.create_item(traps_list[i % len(traps_list)])) + + self.multiworld.itempool += pool + + def create_item(self, name: str) -> Item: + item = ALL_ITEM_TABLE[name] + return LingoItem(name, item.classification, item.code, self.player) + + def set_rules(self): + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def fill_slot_data(self): + slot_options = [ + "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", + "mastery_achievements", "level_2_requirement", "location_checks", "early_color_hallways" + ] + + slot_data = { + "seed": self.random.randint(0, 1000000), + **self.options.as_dict(*slot_options), + } + + if self.options.shuffle_paintings: + slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING + + return slot_data diff --git a/worlds/lingo/docs/en_Lingo.md b/worlds/lingo/docs/en_Lingo.md new file mode 100644 index 0000000000..cff0581d9b --- /dev/null +++ b/worlds/lingo/docs/en_Lingo.md @@ -0,0 +1,42 @@ +# Lingo + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +There are a couple of modes of randomization currently available, and you can pick and choose which ones you would like +to use. + +* **Door shuffle**: There are many doors in the game, which are opened by completing a set of panels. With door shuffle + on, the doors become items and only open up once you receive the corresponding item. The panel sets that would + ordinarily open the doors become locations. + +* **Color shuffle**: There are ten different colors of puzzle in the game, each representing a different mechanic. With + color shuffle on, you would start with only access to white puzzles. Puzzles of other colors will require you to + receive an item in order to solve them (e.g. you can't solve any red puzzles until you receive the "Red" item). + +* **Panel shuffle**: Panel shuffling replaces the puzzles on each panel with different ones. So far, the only mode of + panel shuffling is "rearrange" mode, which simply shuffles the already-existing puzzles from the base game onto + different panels. + +* **Painting shuffle**: This randomizes the appearance of the paintings in the game, as well as which of them are warps, + and the locations that they warp you to. It is the equivalent of an entrance randomizer in another game. + +## What is a "check" in this game? + +Most panels / panel sets that open a door are now location checks, even if door shuffle is not enabled. Various other +puzzles are also location checks, including the achievement panels for each area. + +## What about wall snipes? + +"Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. This randomizer +does not change how wall snipes work, but it will never require the use of them. There are three puzzles from the base +game that you would ordinarily be expected to wall snipe. The randomizer moves these panels out of the wall or otherwise +reveals them so that a snipe is not necessary. + +Because of this, all wall snipes are considered out of logic. This includes sniping The Bearer's MIDDLE while standing +outside The Bold, sniping The Colorful without opening all of the color doors, and sniping WELCOME from next to WELCOME +BACK. diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md new file mode 100644 index 0000000000..97f3ce5940 --- /dev/null +++ b/worlds/lingo/docs/setup_en.md @@ -0,0 +1,45 @@ +# Lingo Randomizer Setup + +## Required Software + +- [Lingo](https://store.steampowered.com/app/1814170/Lingo/) +- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md) + +## Optional Software + +- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Lingo AP Tracker](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md) + +## Installation + +1. Download the Lingo Archipelago Randomizer from the above link. +2. Open up Lingo, go to settings, and click View Game Data. This should open up + a folder in Windows Explorer. +3. Unzip the contents of the randomizer into the "maps" folder. You may need to + create the "maps" folder if you have not played a custom Lingo map before. +4. Installation complete! You may have to click Return to go back to the main + menu and then click Settings again in order to get the randomizer to show up + in the level selection list. + +## Joining a Multiworld game + +1. Launch Lingo +2. Click on Settings, and then Level. Choose Archipelago from the list. +3. Start a new game. Leave the name field blank (anything you type in will be + ignored). +4. Enter the Archipelago address, slot name, and password into the fields. +5. Press Connect. +6. Enjoy! + +To continue an earlier game, you can perform the exact same steps as above. You +do not have to re-select Archipelago in the level selection screen if you were +using Archipelago the last time you launched the game. + +In order to play the base game again, simply return to the level selection +screen and choose Level 1 (or whatever else you want to play). The randomizer +will not affect gameplay unless you launch it by starting a new game while it is +selected in the level selection screen, so it is safe to play the game normally +while the client is installed. + +**Note**: Running the randomizer modifies the game's memory. If you want to play +the base game after playing the randomizer, you need to restart Lingo first. diff --git a/worlds/lingo/ids.yaml b/worlds/lingo/ids.yaml new file mode 100644 index 0000000000..f48858a285 --- /dev/null +++ b/worlds/lingo/ids.yaml @@ -0,0 +1,1449 @@ +--- +special_items: + Black: 444400 + Red: 444401 + Blue: 444402 + Yellow: 444403 + Green: 444404 + Orange: 444405 + Gray: 444406 + Brown: 444407 + Purple: 444408 + ":)": 444409 + The Feeling of Being Lost: 444575 + Wanderlust: 444576 + Empty White Hallways: 444577 + Slowness Trap: 444410 + Iceland Trap: 444411 + Atbash Trap: 444412 + Puzzle Skip: 444413 +panels: + Starting Room: + HI: 444400 + HIDDEN: 444401 + TYPE: 444402 + THIS: 444403 + WRITE: 444404 + SAME: 444405 + Hidden Room: + DEAD END: 444406 + OPEN: 444407 + LIES: 444408 + The Seeker: + Achievement: 444409 + BEAR: 444410 + MINE: 444411 + MINE (2): 444412 + BOW: 444413 + DOES: 444414 + MOBILE: 444415 + MOBILE (2): 444416 + DESERT: 444417 + DESSERT: 444418 + SOW: 444419 + SEW: 444420 + TO: 444421 + TOO: 444422 + WRITE: 444423 + EWE: 444424 + KNOT: 444425 + NAUGHT: 444426 + BEAR (2): 444427 + Second Room: + HI: 444428 + LOW: 444429 + ANOTHER TRY: 444430 + LEVEL 2: 444431 + Hub Room: + ORDER: 444432 + SLAUGHTER: 444433 + NEAR: 444434 + FAR: 444435 + TRACE: 444436 + RAT: 444437 + OPEN: 444438 + FOUR: 444439 + LOST: 444440 + FORWARD: 444441 + BETWEEN: 444442 + BACKWARD: 444443 + Dead End Area: + FOUR: 444444 + EIGHT: 444445 + Pilgrim Antechamber: + HOT CRUST: 444446 + PILGRIMAGE: 444447 + MASTERY: 444448 + Pilgrim Room: + THIS: 444449 + TIME ROOM: 444450 + SCIENCE ROOM: 444451 + SHINY ROCK ROOM: 444452 + ANGRY POWER: 444453 + MICRO LEGION: 444454 + LOSERS RELAX: 444455 + '906234': 444456 + MOOR EMORDNILAP: 444457 + HALL ROOMMATE: 444458 + ALL GREY: 444459 + PLUNDER ISLAND: 444460 + FLOSS PATHS: 444461 + Crossroads: + DECAY: 444462 + NOPE: 444463 + EIGHT: 444464 + WE ROT: 444465 + WORDS: 444466 + SWORD: 444467 + TURN: 444468 + BEND HI: 444469 + THE EYES: 444470 + CORNER: 444471 + HOLLOW: 444472 + SWAP: 444473 + GEL: 444474 + THOUGH: 444475 + CROSSROADS: 444476 + Lost Area: + LOST (1): 444477 + LOST (2): 444478 + Amen Name Area: + AMEN: 444479 + NAME: 444480 + NINE: 444481 + Suits Area: + SPADES: 444482 + CLUBS: 444483 + HEARTS: 444484 + The Tenacious: + LEVEL (Black): 444485 + RACECAR (Black): 444486 + SOLOS (Black): 444487 + LEVEL (White): 444488 + RACECAR (White): 444489 + SOLOS (White): 444490 + Achievement: 444491 + Warts Straw Area: + WARTS: 444492 + STRAW: 444493 + Leaf Feel Area: + LEAF: 444494 + FEEL: 444495 + Outside The Agreeable: + MASSACRED: 444496 + BLACK: 444497 + CLOSE: 444498 + LEFT: 444499 + LEFT (2): 444500 + RIGHT: 444501 + PURPLE: 444502 + FIVE (1): 444503 + FIVE (2): 444504 + OUT: 444505 + HIDE: 444506 + DAZE: 444507 + WALL: 444508 + KEEP: 444509 + BAILEY: 444510 + TOWER: 444511 + NORTH: 444512 + DIAMONDS: 444513 + FIRE: 444514 + WINTER: 444515 + Dread Hallway: + DREAD: 444516 + The Agreeable: + Achievement: 444517 + BYE: 444518 + RETOOL: 444519 + DRAWER: 444520 + READ: 444521 + DIFFERENT: 444522 + LOW: 444523 + ALIVE: 444524 + THAT: 444525 + STRESSED: 444526 + STAR: 444527 + TAME: 444528 + CAT: 444529 + Hedge Maze: + DOWN: 444530 + HIDE (1): 444531 + HIDE (2): 444532 + HIDE (3): 444533 + MASTERY (1): 444534 + MASTERY (2): 444535 + PATH (1): 444536 + PATH (2): 444537 + PATH (3): 444538 + PATH (4): 444539 + PATH (5): 444540 + PATH (6): 444541 + PATH (7): 444542 + PATH (8): 444543 + REFLOW: 444544 + LEAP: 444545 + The Perceptive: + Achievement: 444546 + GAZE: 444547 + The Fearless (First Floor): + NAPS: 444548 + TEAM: 444549 + TEEM: 444550 + IMPATIENT: 444551 + EAT: 444552 + The Fearless (Second Floor): + NONE: 444553 + SUM: 444554 + FUNNY: 444555 + MIGHT: 444556 + SAFE: 444557 + SAME: 444558 + CAME: 444559 + The Fearless: + Achievement: 444560 + EASY: 444561 + SOMETIMES: 444562 + DARK: 444563 + EVEN: 444564 + The Observant: + Achievement: 444565 + BACK: 444566 + SIDE: 444567 + BACKSIDE: 444568 + STAIRS: 444569 + WAYS: 444570 + 'ON': 444571 + UP: 444572 + SWIMS: 444573 + UPSTAIRS: 444574 + TOIL: 444575 + STOP: 444576 + TOP: 444577 + HI: 444578 + HI (2): 444579 + '31': 444580 + '52': 444581 + OIL: 444582 + BACKSIDE (GREEN): 444583 + SIDEWAYS: 444584 + The Incomparable: + Achievement: 444585 + A (One): 444586 + A (Two): 444587 + A (Three): 444588 + A (Four): 444589 + A (Five): 444590 + A (Six): 444591 + I (One): 444592 + I (Two): 444593 + I (Three): 444594 + I (Four): 444595 + I (Five): 444596 + I (Six): 444597 + I (Seven): 444598 + Eight Room: + Eight Back: 444599 + Eight Front: 444600 + Nine: 444601 + Orange Tower First Floor: + SECRET: 444602 + DADS + ALE: 444603 + SALT: 444604 + Orange Tower Third Floor: + RED: 444605 + DEER + WREN: 444606 + Orange Tower Fourth Floor: + RUNT: 444607 + RUNT (2): 444608 + LEARNS + UNSEW: 444609 + HOT CRUSTS: 444610 + IRK HORN: 444611 + Hot Crusts Area: + EIGHT: 444612 + Orange Tower Fifth Floor: + SIZE (Small): 444613 + SIZE (Big): 444614 + DRAWL + RUNS: 444615 + NINE: 444616 + SUMMER: 444617 + AUTUMN: 444618 + SPRING: 444619 + PAINTING (1): 445078 + PAINTING (2): 445079 + PAINTING (3): 445080 + PAINTING (4): 445081 + PAINTING (5): 445082 + ROOM: 445083 + Orange Tower Seventh Floor: + THE END: 444620 + THE MASTER: 444621 + MASTERY: 444622 + Roof: + MASTERY (1): 444623 + MASTERY (2): 444624 + MASTERY (3): 444625 + MASTERY (4): 444626 + MASTERY (5): 444627 + MASTERY (6): 444628 + STAIRCASE: 444629 + Orange Tower Basement: + MASTERY: 444630 + THE LIBRARY: 444631 + Courtyard: + I: 444632 + GREEN: 444633 + PINECONE: 444634 + ACORN: 444635 + Yellow Backside Area: + BACKSIDE: 444636 + NINE: 444637 + First Second Third Fourth: + FIRST: 444638 + SECOND: 444639 + THIRD: 444640 + FOURTH: 444641 + The Colorful (White): + BEGIN: 444642 + The Colorful (Black): + FOUND: 444643 + The Colorful (Red): + LOAF: 444644 + The Colorful (Yellow): + CREAM: 444645 + The Colorful (Blue): + SUN: 444646 + The Colorful (Purple): + SPOON: 444647 + The Colorful (Orange): + LETTERS: 444648 + The Colorful (Green): + WALLS: 444649 + The Colorful (Brown): + IRON: 444650 + The Colorful (Gray): + OBSTACLE: 444651 + The Colorful: + Achievement: 444652 + Welcome Back Area: + WELCOME BACK: 444653 + SECRET: 444654 + CLOCKWISE: 444655 + Owl Hallway: + STRAYS: 444656 + READS + RUST: 444657 + Outside The Initiated: + SEVEN (1): 444658 + SEVEN (2): 444659 + EIGHT: 444660 + NINE: 444661 + BLUE: 444662 + ORANGE: 444663 + UNCOVER: 444664 + OXEN: 444665 + BACKSIDE: 444666 + The Optimistic: 444667 + PAST: 444668 + FUTURE: 444669 + FUTURE (2): 444670 + PAST (2): 444671 + PRESENT: 444672 + SMILE: 444673 + ANGERED: 444674 + VOTE: 444675 + The Initiated: + Achievement: 444676 + DAUGHTER: 444677 + START: 444678 + STARE: 444679 + HYPE: 444680 + ABYSS: 444681 + SWEAT: 444682 + BEAT: 444683 + ALUMNI: 444684 + PATS: 444685 + KNIGHT: 444686 + BYTE: 444687 + MAIM: 444688 + MORGUE: 444689 + CHAIR: 444690 + HUMAN: 444691 + BED: 444692 + The Traveled: + Achievement: 444693 + CLOSE: 444694 + COMPOSE: 444695 + RECORD: 444696 + CATEGORY: 444697 + HELLO: 444698 + DUPLICATE: 444699 + IDENTICAL: 444700 + DISTANT: 444701 + HAY: 444702 + GIGGLE: 444703 + CHUCKLE: 444704 + SNITCH: 444705 + CONCEALED: 444706 + PLUNGE: 444707 + AUTUMN: 444708 + ROAD: 444709 + FOUR: 444710 + Outside The Bold: + UNOPEN: 444711 + BEGIN: 444712 + SIX: 444713 + NINE: 444714 + LEFT: 444715 + RIGHT: 444716 + RISE (Horizon): 444717 + RISE (Sunrise): 444718 + ZEN: 444719 + SON: 444720 + STARGAZER: 444721 + MOUTH: 444722 + YEAST: 444723 + WET: 444724 + The Bold: + Achievement: 444725 + FOOT: 444726 + NEEDLE: 444727 + FACE: 444728 + SIGN: 444729 + HEARTBREAK: 444730 + UNDEAD: 444731 + DEADLINE: 444732 + SUSHI: 444733 + THISTLE: 444734 + LANDMASS: 444735 + MASSACRED: 444736 + AIRPLANE: 444737 + NIGHTMARE: 444738 + MOUTH: 444739 + SAW: 444740 + HAND: 444741 + Outside The Undeterred: + HOLLOW: 444742 + ART + ART: 444743 + PEN: 444744 + HUSTLING: 444745 + SUNLIGHT: 444746 + LIGHT: 444747 + BRIGHT: 444748 + SUNNY: 444749 + RAINY: 444750 + ZERO: 444751 + ONE: 444752 + TWO (1): 444753 + TWO (2): 444754 + THREE (1): 444755 + THREE (2): 444756 + THREE (3): 444757 + FOUR: 444758 + The Undeterred: + Achievement: 444759 + BONE: 444760 + EYE: 444761 + MOUTH: 444762 + IRIS: 444763 + EYE (2): 444764 + ICE: 444765 + HEIGHT: 444766 + EYE (3): 444767 + NOT: 444768 + JUST: 444769 + READ: 444770 + FATHER: 444771 + FEATHER: 444772 + CONTINENT: 444773 + OCEAN: 444774 + WALL: 444775 + Number Hunt: + FIVE: 444776 + SIX: 444777 + SEVEN: 444778 + EIGHT: 444779 + NINE: 444780 + Directional Gallery: + PEPPER: 444781 + TURN: 444782 + LEARN: 444783 + FIVE (1): 444784 + FIVE (2): 444785 + SIX (1): 444786 + SIX (2): 444787 + SEVEN: 444788 + EIGHT: 444789 + NINE: 444790 + BACKSIDE: 444791 + '834283054': 444792 + PARANOID: 444793 + YELLOW: 444794 + WADED + WEE: 444795 + THE EYES: 444796 + LEFT: 444797 + RIGHT: 444798 + MIDDLE: 444799 + WARD: 444800 + HIND: 444801 + RIG: 444802 + WINDWARD: 444803 + LIGHT: 444804 + REWIND: 444805 + Champion's Rest: + EXIT: 444806 + HUES: 444807 + RED: 444808 + BLUE: 444809 + YELLOW: 444810 + GREEN: 444811 + PURPLE: 444812 + ORANGE: 444813 + YOU: 444814 + ME: 444815 + SECRET BLUE: 444816 + SECRET YELLOW: 444817 + SECRET RED: 444818 + The Bearer: + Achievement: 444819 + MIDDLE: 444820 + FARTHER: 444821 + BACKSIDE: 444822 + PART: 444823 + HEART: 444824 + The Bearer (East): + SIX: 444825 + PEACE: 444826 + The Bearer (North): + SILENT (1): 444827 + SILENT (2): 444828 + SPACE: 444829 + WARTS: 444830 + The Bearer (South): + SIX: 444831 + TENT: 444832 + BOWL: 444833 + The Bearer (West): + SNOW: 444834 + SMILE: 444835 + Bearer Side Area: + SHORTCUT: 444836 + POTS: 444837 + Cross Tower (East): + WINTER: 444838 + Cross Tower (North): + NORTH: 444839 + Cross Tower (South): + FIRE: 444840 + Cross Tower (West): + DIAMONDS: 444841 + The Steady (Rose): + SOAR: 444842 + The Steady (Ruby): + BURY: 444843 + The Steady (Carnation): + INCARNATION: 444844 + The Steady (Sunflower): + SUN: 444845 + The Steady (Plum): + LUMP: 444846 + The Steady (Lime): + LIMELIGHT: 444847 + The Steady (Lemon): + MELON: 444848 + The Steady (Topaz): + TOP: 444849 + MASTERY: 444850 + The Steady (Orange): + BLUE: 444851 + The Steady (Sapphire): + SAP: 444852 + The Steady (Blueberry): + BLUE: 444853 + The Steady (Amber): + ANTECHAMBER: 444854 + The Steady (Emerald): + HERALD: 444855 + The Steady (Amethyst): + PACIFIST: 444856 + The Steady (Lilac): + LIE LACK: 444857 + The Steady (Cherry): + HAIRY: 444858 + The Steady: + Achievement: 444859 + Knight Night (Outer Ring): + NIGHT: 444860 + KNIGHT: 444861 + BEE: 444862 + NEW: 444863 + FORE: 444864 + TRUSTED (1): 444865 + TRUSTED (2): 444866 + ENCRUSTED: 444867 + ADJUST (1): 444868 + ADJUST (2): 444869 + RIGHT: 444870 + TRUST: 444871 + Knight Night (Right Upper Segment): + RUST (1): 444872 + RUST (2): 444873 + Knight Night (Right Lower Segment): + ADJUST: 444874 + BEFORE: 444875 + BE: 444876 + LEFT: 444877 + TRUST: 444878 + Knight Night (Final): + TRUSTED: 444879 + Knight Night Exit: + SEVEN (1): 444880 + SEVEN (2): 444881 + SEVEN (3): 444882 + DEAD END: 444883 + WARNER: 444884 + The Artistic (Smiley): + Achievement: 444885 + FINE: 444886 + BLADE: 444887 + RED: 444888 + BEARD: 444889 + ICE: 444890 + ROOT: 444891 + The Artistic (Panda): + EYE (Top): 444892 + EYE (Bottom): 444893 + LADYLIKE: 444894 + WATER: 444895 + OURS: 444896 + DAYS: 444897 + NIGHTTIME: 444898 + NIGHT: 444899 + The Artistic (Lattice): + POSH: 444900 + MALL: 444901 + DEICIDE: 444902 + WAVER: 444903 + REPAID: 444904 + BABY: 444905 + LOBE: 444906 + BOWELS: 444907 + The Artistic (Apple): + SPRIG: 444908 + RELEASES: 444909 + MUCH: 444910 + FISH: 444911 + MASK: 444912 + HILL: 444913 + TINE: 444914 + THING: 444915 + The Artistic (Hint Room): + THEME: 444916 + PAINTS: 444917 + I: 444918 + KIT: 444919 + The Discerning: + Achievement: 444920 + HITS: 444921 + WARRED: 444922 + REDRAW: 444923 + ADDER: 444924 + LAUGHTERS: 444925 + STONE: 444926 + ONSET: 444927 + RAT: 444928 + DUSTY: 444929 + ARTS: 444930 + TSAR: 444931 + STATE: 444932 + REACT: 444933 + DEAR: 444934 + DARE: 444935 + SEAM: 444936 + The Eyes They See: + NEAR: 444937 + EIGHT: 444938 + Far Window: + FAR: 444939 + Outside The Wondrous: + SHRINK: 444940 + The Wondrous (Bookcase): + CASE: 444941 + The Wondrous (Chandelier): + CANDLE HEIR: 444942 + The Wondrous (Window): + GLASS: 444943 + The Wondrous (Table): + WOOD: 444944 + BROOK NOD: 444945 + The Wondrous: + FIREPLACE: 444946 + Achievement: 444947 + Arrow Garden: + MASTERY: 444948 + SHARP: 444949 + Hallway Room (2): + WISE: 444950 + CLOCK: 444951 + ER: 444952 + COUNT: 444953 + Hallway Room (3): + TRANCE: 444954 + FORM: 444955 + A: 444956 + SHUN: 444957 + Hallway Room (4): + WHEEL: 444958 + Elements Area: + A: 444959 + NINE: 444960 + UNDISTRACTED: 444961 + MASTERY: 444962 + EARTH: 444963 + WATER: 444964 + AIR: 444965 + Outside The Wanderer: + WANDERLUST: 444966 + The Wanderer: + Achievement: 444967 + '7890': 444968 + '6524': 444969 + '951': 444970 + '4524': 444971 + LEARN: 444972 + DUST: 444973 + STAR: 444974 + WANDER: 444975 + Art Gallery: + EIGHT: 444976 + EON: 444977 + TRUSTWORTHY: 444978 + FREE: 444979 + OUR: 444980 + ONE ROAD MANY TURNS: 444981 + Art Gallery (Second Floor): + HOUSE: 444982 + PATH: 444983 + PARK: 444984 + CARRIAGE: 444985 + Art Gallery (Third Floor): + AN: 444986 + MAY: 444987 + ANY: 444988 + MAN: 444989 + Art Gallery (Fourth Floor): + URNS: 444990 + LEARNS: 444991 + RUNTS: 444992 + SEND - USE: 444993 + TRUST: 444994 + '062459': 444995 + Rhyme Room (Smiley): + LOANS: 444996 + SKELETON: 444997 + REPENTANCE: 444998 + WORD: 444999 + SCHEME: 445000 + FANTASY: 445001 + HISTORY: 445002 + SECRET: 445003 + Rhyme Room (Cross): + NINE: 445004 + FERN: 445005 + STAY: 445006 + FRIEND: 445007 + RISE: 445008 + PLUMP: 445009 + BOUNCE: 445010 + SCRAWL: 445011 + PLUNGE: 445012 + LEAP: 445013 + Rhyme Room (Circle): + BIRD: 445014 + LETTER: 445015 + FORBIDDEN: 445016 + CONCEALED: 445017 + VIOLENT: 445018 + MUTE: 445019 + Rhyme Room (Looped Square): + WALKED: 445020 + OBSTRUCTED: 445021 + SKIES: 445022 + SWELL: 445023 + PENNED: 445024 + CLIMB: 445025 + TROUBLE: 445026 + DUPLICATE: 445027 + Rhyme Room (Target): + WILD: 445028 + KID: 445029 + PISTOL: 445030 + QUARTZ: 445031 + INNOVATIVE (Top): 445032 + INNOVATIVE (Bottom): 445033 + Room Room: + DOOR (1): 445034 + DOOR (2): 445035 + WINDOW: 445036 + STAIRS: 445037 + PAINTING: 445038 + FLOOR (1): 445039 + FLOOR (2): 445040 + FLOOR (3): 445041 + FLOOR (4): 445042 + FLOOR (5): 445043 + FLOOR (7): 445044 + FLOOR (8): 445045 + FLOOR (9): 445046 + FLOOR (10): 445047 + CEILING (1): 445048 + CEILING (2): 445049 + CEILING (3): 445050 + CEILING (4): 445051 + CEILING (5): 445052 + WALL (1): 445053 + WALL (2): 445054 + WALL (3): 445055 + WALL (4): 445056 + WALL (5): 445057 + WALL (6): 445058 + WALL (7): 445059 + WALL (8): 445060 + WALL (9): 445061 + WALL (10): 445062 + WALL (11): 445063 + WALL (12): 445064 + WALL (13): 445065 + WALL (14): 445066 + WALL (15): 445067 + WALL (16): 445068 + WALL (17): 445069 + WALL (18): 445070 + WALL (19): 445071 + WALL (20): 445072 + WALL (21): 445073 + BROOMED: 445074 + LAYS: 445075 + BASE: 445076 + MASTERY: 445077 + Outside The Wise: + KITTEN: 445084 + CAT: 445085 + The Wise: + Achievement: 445086 + PUPPY: 445087 + ADULT: 445088 + BREAD: 445089 + DINOSAUR: 445090 + OAK: 445091 + CORPSE: 445092 + BEFORE: 445093 + YOUR: 445094 + BETWIXT: 445095 + NIGH: 445096 + CONNEXION: 445097 + THOU: 445098 + The Red: + Achievement: 445099 + PANDEMIC (1): 445100 + TRINITY: 445101 + CHEMISTRY: 445102 + FLUMMOXED: 445103 + PANDEMIC (2): 445104 + COUNTERCLOCKWISE: 445105 + FEARLESS: 445106 + DEFORESTATION: 445107 + CRAFTSMANSHIP: 445108 + CAMEL: 445109 + LION: 445110 + TIGER: 445111 + SHIP (1): 445112 + SHIP (2): 445113 + GIRAFFE: 445114 + The Ecstatic: + Achievement: 445115 + FORM (1): 445116 + WIND: 445117 + EGGS: 445118 + VEGETABLES: 445119 + WATER: 445120 + FRUITS: 445121 + LEAVES: 445122 + VINES: 445123 + ICE: 445124 + STYLE: 445125 + FIR: 445126 + REEF: 445127 + ROTS: 445128 + FORM (2): 445129 + Outside The Scientific: + OPEN: 445130 + CLOSE: 445131 + AHEAD: 445132 + The Scientific: + Achievement: 445133 + HYDROGEN (1): 445134 + OXYGEN: 445135 + HYDROGEN (2): 445136 + SUGAR (1): 445137 + SUGAR (2): 445138 + SUGAR (3): 445139 + CHLORINE: 445140 + SODIUM: 445141 + FOREST: 445142 + POUND: 445143 + ICE: 445144 + FISSION: 445145 + FUSION: 445146 + MISS: 445147 + TREE (1): 445148 + BIOGRAPHY: 445149 + CACTUS: 445150 + VERTEBRATE: 445151 + ROSE: 445152 + TREE (2): 445153 + FRUIT: 445154 + MAMMAL: 445155 + BIRD: 445156 + FISH: 445157 + GRAVELY: 445158 + BREVITY: 445159 + PART: 445160 + MATTER: 445161 + ELECTRIC: 445162 + ATOM (1): 445163 + NEUTRAL: 445164 + ATOM (2): 445165 + PROPEL: 445166 + ATOM (3): 445167 + ORDER: 445168 + OPTICS: 445169 + GRAPHITE: 445170 + HOT RYE: 445171 + SIT SHY HOPE: 445172 + ME NEXT PIER: 445173 + RUT LESS: 445174 + SON COUNCIL: 445175 + Challenge Room: + WELCOME: 445176 + CHALLENGE: 445177 + Achievement: 445178 + OPEN: 445179 + SINGED: 445180 + NEVER TRUSTED: 445181 + CORNER: 445182 + STRAWBERRIES: 445183 + GRUB: 445184 + BREAD: 445185 + COLOR: 445186 + WRITER: 445187 + '02759': 445188 + REAL EYES: 445189 + LOBS: 445190 + PEST ALLY: 445191 + GENIAL HALO: 445192 + DUCK LOGO: 445193 + AVIAN GREEN: 445194 + FEVER TEAR: 445195 + FACTS: 445196 + FACTS (1): 445197 + FACTS (3): 445198 + FACTS (4): 445199 + FACTS (5): 445200 + FACTS (6): 445201 + LAPEL SHEEP: 445202 +doors: + Starting Room: + Back Right Door: + item: 444416 + location: 444401 + Rhyme Room Entrance: + item: 444417 + Hidden Room: + Dead End Door: + item: 444419 + Knight Night Entrance: + item: 444421 + Seeker Entrance: + item: 444422 + location: 444407 + Rhyme Room Entrance: + item: 444423 + Second Room: + Exit Door: + item: 444424 + location: 445203 + Hub Room: + Crossroads Entrance: + item: 444425 + location: 444432 + Tenacious Entrance: + item: 444426 + location: 444433 + Symmetry Door: + item: 444428 + location: 445204 + Shortcut to Hedge Maze: + item: 444430 + location: 444436 + Near RAT Door: + item: 444432 + Traveled Entrance: + item: 444433 + location: 444438 + Lost Door: + item: 444435 + location: 444440 + Pilgrim Antechamber: + Sun Painting: + item: 444436 + location: 445205 + Pilgrim Room: + Shortcut to The Seeker: + item: 444437 + location: 444449 + Crossroads: + Tenacious Entrance: + item: 444438 + location: 444462 + Discerning Entrance: + item: 444439 + location: 444463 + Tower Entrance: + item: 444440 + location: 444465 + Tower Back Entrance: + item: 444442 + location: 445206 + Words Sword Door: + item: 444443 + location: 445207 + Eye Wall: + item: 444445 + location: 444469 + Hollow Hallway: + item: 444446 + Roof Access: + item: 444447 + Lost Area: + Exit: + item: 444448 + location: 445208 + Amen Name Area: + Exit: + item: 444449 + location: 445209 + The Tenacious: + Shortcut to Hub Room: + item: 444450 + location: 445210 + White Palindromes: + location: 445211 + Warts Straw Area: + Door: + item: 444451 + location: 445212 + Leaf Feel Area: + Door: + item: 444452 + location: 445213 + Outside The Agreeable: + Tenacious Entrance: + item: 444453 + location: 444496 + Black Door: + item: 444454 + location: 444497 + Agreeable Entrance: + item: 444455 + location: 444498 + Painting Shortcut: + item: 444456 + location: 444501 + Purple Barrier: + item: 444457 + Hallway Door: + item: 444459 + location: 445214 + Dread Hallway: + Tenacious Entrance: + item: 444462 + location: 444516 + The Agreeable: + Shortcut to Hedge Maze: + item: 444463 + location: 444518 + Hedge Maze: + Perceptive Entrance: + item: 444464 + location: 444530 + Painting Shortcut: + item: 444465 + Observant Entrance: + item: 444466 + Hide and Seek: + location: 445215 + The Fearless (First Floor): + Second Floor: + item: 444468 + location: 445216 + The Fearless (Second Floor): + Third Floor: + item: 444471 + location: 445217 + The Observant: + Backside Door: + item: 444472 + location: 445218 + Stairs: + item: 444474 + location: 444569 + The Incomparable: + Eight Painting: + item: 444475 + location: 445219 + Orange Tower: + Second Floor: + item: 444476 + Third Floor: + item: 444477 + Fourth Floor: + item: 444478 + Fifth Floor: + item: 444479 + Sixth Floor: + item: 444480 + Seventh Floor: + item: 444481 + Orange Tower First Floor: + Shortcut to Hub Room: + item: 444483 + location: 444602 + Salt Pepper Door: + item: 444485 + location: 445220 + Orange Tower Third Floor: + Red Barrier: + item: 444486 + Rhyme Room Entrance: + item: 444487 + Orange Barrier: + item: 444488 + location: 445221 + Orange Tower Fourth Floor: + Hot Crusts Door: + item: 444490 + location: 444610 + Orange Tower Fifth Floor: + Welcome Back: + item: 444491 + location: 445222 + Orange Tower Seventh Floor: + Mastery: + item: 444493 + Mastery Panels: + location: 445223 + Courtyard: + Painting Shortcut: + item: 444494 + Green Barrier: + item: 444495 + First Second Third Fourth: + Backside Door: + item: 444496 + location: 445224 + The Colorful (White): + Progress Door: + item: 444497 + location: 445225 + The Colorful (Black): + Progress Door: + item: 444499 + location: 445226 + The Colorful (Red): + Progress Door: + item: 444500 + location: 445227 + The Colorful (Yellow): + Progress Door: + item: 444501 + location: 445228 + The Colorful (Blue): + Progress Door: + item: 444502 + location: 445229 + The Colorful (Purple): + Progress Door: + item: 444503 + location: 445230 + The Colorful (Orange): + Progress Door: + item: 444504 + location: 445231 + The Colorful (Green): + Progress Door: + item: 444505 + location: 445232 + The Colorful (Brown): + Progress Door: + item: 444506 + location: 445233 + The Colorful (Gray): + Progress Door: + item: 444507 + location: 445234 + Welcome Back Area: + Shortcut to Starting Room: + item: 444508 + location: 444653 + Owl Hallway: + Shortcut to Hedge Maze: + item: 444509 + location: 444656 + Outside The Initiated: + Shortcut to Hub Room: + item: 444510 + location: 444664 + Blue Barrier: + item: 444511 + Orange Barrier: + item: 444512 + Initiated Entrance: + item: 444513 + location: 444665 + Green Barrier: + item: 444514 + location: 445235 + Purple Barrier: + item: 444515 + location: 445236 + Entrance: + item: 444516 + location: 445237 + The Traveled: + Color Hallways Entrance: + item: 444517 + location: 444698 + Outside The Bold: + Bold Entrance: + item: 444518 + location: 444711 + Painting Shortcut: + item: 444519 + Steady Entrance: + item: 444520 + location: 444712 + Outside The Undeterred: + Undeterred Entrance: + item: 444521 + location: 444744 + Painting Shortcut: + item: 444522 + Green Painting: + item: 444523 + Twos: + item: 444524 + location: 444752 + Threes: + item: 444525 + location: 445238 + Number Hunt: + item: 444526 + location: 445239 + Fours: + item: 444527 + Fives: + item: 444528 + location: 445240 + Challenge Entrance: + item: 444529 + location: 444751 + Number Hunt: + Door to Directional Gallery: + item: 444530 + Sixes: + item: 444532 + location: 445241 + Sevens: + item: 444533 + location: 445242 + Eights: + item: 444534 + location: 445243 + Nines: + item: 444535 + location: 445244 + Zero Door: + item: 444536 + location: 445245 + Directional Gallery: + Shortcut to The Undeterred: + item: 444537 + location: 445246 + Yellow Barrier: + item: 444538 + Champion's Rest: + Shortcut to The Steady: + item: 444539 + location: 444806 + The Bearer: + Shortcut to The Bold: + item: 444540 + location: 444820 + Backside Door: + item: 444541 + location: 444821 + Bearer Side Area: + Shortcut to Tower: + item: 444542 + location: 445247 + Knight Night (Final): + Exit: + item: 444543 + location: 445248 + The Artistic (Smiley): + Door to Panda: + item: 444544 + location: 445249 + The Artistic (Panda): + Door to Lattice: + item: 444546 + location: 445250 + The Artistic (Lattice): + Door to Apple: + item: 444547 + location: 445251 + The Artistic (Apple): + Door to Smiley: + item: 444548 + location: 445252 + The Eyes They See: + Exit: + item: 444549 + location: 444937 + Outside The Wondrous: + Wondrous Entrance: + item: 444550 + location: 444940 + The Wondrous (Doorknob): + Painting Shortcut: + item: 444551 + The Wondrous: + Exit: + item: 444552 + location: 444947 + Hallway Room (2): + Exit: + item: 444553 + location: 445253 + Hallway Room (3): + Exit: + item: 444554 + location: 445254 + Hallway Room (4): + Exit: + item: 444555 + location: 445255 + Outside The Wanderer: + Wanderer Entrance: + item: 444556 + location: 444966 + Tower Entrance: + item: 444557 + Art Gallery: + Second Floor: + item: 444558 + First Floor Puzzles: + location: 445256 + Third Floor: + item: 444559 + Fourth Floor: + item: 444560 + Fifth Floor: + item: 444561 + Exit: + item: 444562 + location: 444981 + Art Gallery (Second Floor): + Puzzles: + location: 445257 + Art Gallery (Third Floor): + Puzzles: + location: 445258 + Art Gallery (Fourth Floor): + Puzzles: + location: 445259 + Rhyme Room (Smiley): + Door to Target: + item: 444564 + Door to Target (Location): + location: 445260 + Rhyme Room (Cross): + Exit: + item: 444565 + location: 445261 + Rhyme Room (Circle): + Door to Smiley: + item: 444566 + location: 445262 + Rhyme Room (Looped Square): + Door to Circle: + item: 444567 + location: 445263 + Door to Cross: + item: 444568 + location: 445264 + Door to Target: + item: 444569 + location: 445265 + Rhyme Room (Target): + Door to Cross: + item: 444570 + location: 445266 + Room Room: + Shortcut to Fifth Floor: + item: 444571 + location: 445076 + Outside The Wise: + Wise Entrance: + item: 444572 + location: 445267 + Outside The Scientific: + Scientific Entrance: + item: 444573 + location: 445130 + The Scientific: + Chemistry Puzzles: + location: 445268 + Biology Puzzles: + location: 445269 + Physics Puzzles: + location: 445270 + Challenge Room: + Welcome Door: + item: 444574 + location: 445176 +door_groups: + Rhyme Room Doors: 444418 + Dead End Area Access: 444420 + Entrances to The Tenacious: 444427 + Symmetry Doors: 444429 + Hedge Maze Doors: 444431 + Entrance to The Traveled: 444434 + Crossroads - Tower Entrances: 444441 + Crossroads Doors: 444444 + Color Hunt Barriers: 444458 + Hallway Room Doors: 444460 + Observant Doors: 444467 + Fearless Doors: 444469 + Backside Doors: 444473 + Orange Tower First Floor - Shortcuts: 444484 + Champion's Rest - Color Barriers: 444489 + Welcome Back Doors: 444492 + Colorful Doors: 444498 + Directional Gallery Doors: 444531 + Artistic Doors: 444545 +progression: + Progressive Hallway Room: 444461 + Progressive Fearless: 444470 + Progressive Orange Tower: 444482 + Progressive Art Gallery: 444563 diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py new file mode 100644 index 0000000000..af24570f27 --- /dev/null +++ b/worlds/lingo/items.py @@ -0,0 +1,106 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from BaseClasses import Item, ItemClassification +from .options import ShuffleDoors +from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \ + get_door_item_id, get_progressive_item_id, get_special_item_id + +if TYPE_CHECKING: + from . import LingoWorld + + +class ItemData(NamedTuple): + """ + ItemData for an item in Lingo + """ + code: int + classification: ItemClassification + mode: Optional[str] + door_ids: List[str] + painting_ids: List[str] + + def should_include(self, world: "LingoWorld") -> bool: + if self.mode == "colors": + return world.options.shuffle_colors > 0 + elif self.mode == "doors": + return world.options.shuffle_doors != ShuffleDoors.option_none + elif self.mode == "orange tower": + # door shuffle is on and tower isn't progressive + return world.options.shuffle_doors != ShuffleDoors.option_none \ + and not world.options.progressive_orange_tower + elif self.mode == "complex door": + return world.options.shuffle_doors == ShuffleDoors.option_complex + elif self.mode == "door group": + return world.options.shuffle_doors == ShuffleDoors.option_simple + elif self.mode == "special": + return False + else: + return True + + +class LingoItem(Item): + """ + Item from the game Lingo + """ + game: str = "Lingo" + + +ALL_ITEM_TABLE: Dict[str, ItemData] = {} + + +def load_item_data(): + global ALL_ITEM_TABLE + + for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]: + ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression, + "colors", [], []) + + door_groups: Dict[str, List[str]] = {} + for room_name, doors in DOORS_BY_ROOM.items(): + for door_name, door in doors.items(): + if door.skip_item is True or door.event is True: + continue + + if door.group is None: + door_mode = "doors" + else: + door_mode = "complex door" + door_groups.setdefault(door.group, []).extend(door.door_ids) + + if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]: + if room_name == "Orange Tower": + door_mode = "orange tower" + else: + door_mode = "special" + + ALL_ITEM_TABLE[door.item_name] = \ + ItemData(get_door_item_id(room_name, door_name), + ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode, + door.door_ids, door.painting_ids) + + for group, group_door_ids in door_groups.items(): + ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group), + ItemClassification.progression, "door group", group_door_ids, []) + + special_items: Dict[str, ItemClassification] = { + ":)": ItemClassification.filler, + "The Feeling of Being Lost": ItemClassification.filler, + "Wanderlust": ItemClassification.filler, + "Empty White Hallways": ItemClassification.filler, + "Slowness Trap": ItemClassification.trap, + "Iceland Trap": ItemClassification.trap, + "Atbash Trap": ItemClassification.trap, + "Puzzle Skip": ItemClassification.useful, + } + + for item_name, classification in special_items.items(): + ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification, + "special", [], []) + + for item_name in PROGRESSIVE_ITEMS: + ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name), + ItemClassification.progression, "special", [], []) + + +# Initialize the item data at module scope. +load_item_data() diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py new file mode 100644 index 0000000000..5903d603ec --- /dev/null +++ b/worlds/lingo/locations.py @@ -0,0 +1,80 @@ +from enum import Flag, auto +from typing import Dict, List, NamedTuple + +from BaseClasses import Location +from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, RoomAndPanel, get_door_location_id, get_panel_location_id + + +class LocationClassification(Flag): + normal = auto() + reduced = auto() + insanity = auto() + + +class LocationData(NamedTuple): + """ + LocationData for a location in Lingo + """ + code: int + room: str + panels: List[RoomAndPanel] + classification: LocationClassification + + def panel_ids(self): + ids = set() + for panel in self.panels: + effective_room = self.room if panel.room is None else panel.room + panel_data = PANELS_BY_ROOM[effective_room][panel.panel] + ids = ids | set(panel_data.internal_ids) + return ids + + +class LingoLocation(Location): + """ + Location from the game Lingo + """ + game: str = "Lingo" + + +ALL_LOCATION_TABLE: Dict[str, LocationData] = {} + + +def load_location_data(): + global ALL_LOCATION_TABLE + + for room_name, panels in PANELS_BY_ROOM.items(): + for panel_name, panel in panels.items(): + location_name = f"{room_name} - {panel_name}" + + classification = LocationClassification.insanity + if panel.check: + classification |= LocationClassification.normal + + if not panel.exclude_reduce: + classification |= LocationClassification.reduced + + ALL_LOCATION_TABLE[location_name] = \ + LocationData(get_panel_location_id(room_name, panel_name), room_name, + [RoomAndPanel(None, panel_name)], classification) + + for room_name, doors in DOORS_BY_ROOM.items(): + for door_name, door in doors.items(): + if door.skip_location or door.event or door.panels is None: + continue + + location_name = door.location_name + classification = LocationClassification.normal + if door.include_reduce: + classification |= LocationClassification.reduced + + if location_name in ALL_LOCATION_TABLE: + new_id = ALL_LOCATION_TABLE[location_name].code + classification |= ALL_LOCATION_TABLE[location_name].classification + else: + new_id = get_door_location_id(room_name, door_name) + + ALL_LOCATION_TABLE[location_name] = LocationData(new_id, room_name, door.panels, classification) + + +# Initialize location data on the module scope. +load_location_data() diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py new file mode 100644 index 0000000000..7dc6a1389c --- /dev/null +++ b/worlds/lingo/options.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass + +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions + + +class ShuffleDoors(Choice): + """If on, opening doors will require their respective "keys". + In "simple", doors are sorted into logical groups, which are all opened by receiving an item. + In "complex", the items are much more granular, and will usually only open a single door each.""" + display_name = "Shuffle Doors" + option_none = 0 + option_simple = 1 + option_complex = 2 + + +class ProgressiveOrangeTower(DefaultOnToggle): + """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. + If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. + If on, there are six progressive items, which open up the tower from the bottom floor upward. + """ + display_name = "Progressive Orange Tower" + + +class LocationChecks(Choice): + """On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for + achievement panels and a small handful of other panels. + On "reduced", many of the locations that are associated with opening doors are removed. + On "insanity", every individual panel in the game is a location check.""" + display_name = "Location Checks" + option_normal = 0 + option_reduced = 1 + option_insanity = 2 + + +class ShuffleColors(Toggle): + """If on, an item is added to the pool for every puzzle color (besides White). + You will need to unlock the requisite colors in order to be able to solve puzzles of that color.""" + display_name = "Shuffle Colors" + + +class ShufflePanels(Choice): + """If on, the puzzles on each panel are randomized. + On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas.""" + display_name = "Shuffle Panels" + option_none = 0 + option_rearrange = 1 + + +class ShufflePaintings(Toggle): + """If on, the destination, location, and appearance of the painting warps in the game will be randomized.""" + display_name = "Shuffle Paintings" + + +class VictoryCondition(Choice): + """Change the victory condition.""" + display_name = "Victory Condition" + option_the_end = 0 + option_the_master = 1 + option_level_2 = 2 + + +class MasteryAchievements(Range): + """The number of achievements required to unlock THE MASTER. + In the base game, 21 achievements are needed. + If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required. + If you include the custom achievement (The Wanderer), 24 would be required. + """ + display_name = "Mastery Achievements" + range_start = 1 + range_end = 24 + default = 21 + + +class Level2Requirement(Range): + """The number of panel solves required to unlock LEVEL 2. + In the base game, 223 are needed. + Note that this count includes ANOTHER TRY. + """ + display_name = "Level 2 Requirement" + range_start = 2 + range_end = 800 + default = 223 + + +class EarlyColorHallways(Toggle): + """When on, a painting warp to the color hallways area will appear in the starting room. + This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on.""" + display_name = "Early Color Hallways" + + +class TrapPercentage(Range): + """Replaces junk items with traps, at the specified rate.""" + display_name = "Trap Percentage" + range_start = 0 + range_end = 100 + default = 20 + + +class PuzzleSkipPercentage(Range): + """Replaces junk items with puzzle skips, at the specified rate.""" + display_name = "Puzzle Skip Percentage" + range_start = 0 + range_end = 100 + default = 20 + + +class DeathLink(Toggle): + """If on: Whenever another player on death link dies, you will be returned to the starting room.""" + display_name = "Death Link" + + +@dataclass +class LingoOptions(PerGameCommonOptions): + shuffle_doors: ShuffleDoors + progressive_orange_tower: ProgressiveOrangeTower + location_checks: LocationChecks + shuffle_colors: ShuffleColors + shuffle_panels: ShufflePanels + shuffle_paintings: ShufflePaintings + victory_condition: VictoryCondition + mastery_achievements: MasteryAchievements + level_2_requirement: Level2Requirement + early_color_hallways: EarlyColorHallways + trap_percentage: TrapPercentage + puzzle_skip_percentage: PuzzleSkipPercentage + death_link: DeathLink diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py new file mode 100644 index 0000000000..217ad91fcd --- /dev/null +++ b/worlds/lingo/player_logic.py @@ -0,0 +1,298 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from .items import ALL_ITEM_TABLE +from .locations import ALL_LOCATION_TABLE, LocationClassification +from .options import LocationChecks, ShuffleDoors, VictoryCondition +from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ + RoomAndPanel +from .testing import LingoTestOptions + +if TYPE_CHECKING: + from . import LingoWorld + + +class PlayerLocation(NamedTuple): + name: str + code: Optional[int] = None + panels: List[RoomAndPanel] = [] + + +class LingoPlayerLogic: + """ + Defines logic after a player's options have been applied + """ + + ITEM_BY_DOOR: Dict[str, Dict[str, str]] + + LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] + REAL_LOCATIONS: List[str] + + EVENT_LOC_TO_ITEM: Dict[str, str] + REAL_ITEMS: List[str] + + VICTORY_CONDITION: str + MASTERY_LOCATION: str + LEVEL_2_LOCATION: str + + PAINTING_MAPPING: Dict[str, str] + + FORCED_GOOD_ITEM: str + + def add_location(self, room: str, loc: PlayerLocation): + self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + + def set_door_item(self, room: str, door: str, item: str): + self.ITEM_BY_DOOR.setdefault(room, {})[door] = item + + def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): + if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: + if room_name == "Orange Tower" and not world.options.progressive_orange_tower: + self.set_door_item(room_name, door_data.name, door_data.item_name) + else: + progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + self.set_door_item(room_name, door_data.name, progressive_item_name) + self.REAL_ITEMS.append(progressive_item_name) + else: + self.set_door_item(room_name, door_data.name, door_data.item_name) + + def __init__(self, world: "LingoWorld"): + self.ITEM_BY_DOOR = {} + self.LOCATIONS_BY_ROOM = {} + self.REAL_LOCATIONS = [] + self.EVENT_LOC_TO_ITEM = {} + self.REAL_ITEMS = [] + self.VICTORY_CONDITION = "" + self.MASTERY_LOCATION = "" + self.LEVEL_2_LOCATION = "" + self.PAINTING_MAPPING = {} + self.FORCED_GOOD_ITEM = "" + + door_shuffle = world.options.shuffle_doors + color_shuffle = world.options.shuffle_colors + painting_shuffle = world.options.shuffle_paintings + location_checks = world.options.location_checks + victory_condition = world.options.victory_condition + early_color_hallways = world.options.early_color_hallways + + if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none: + raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " + "be enough locations for all of the door items.") + + # Create an event for every room that represents being able to reach that room. + for room_name in ROOMS.keys(): + roomloc_name = f"{room_name} (Reached)" + self.add_location(room_name, PlayerLocation(roomloc_name, None, [])) + self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name + + # Create an event for every door, representing whether that door has been opened. Also create event items for + # doors that are event-only. + for room_name, room_data in DOORS_BY_ROOM.items(): + for door_name, door_data in room_data.items(): + if door_shuffle == ShuffleDoors.option_none: + itemloc_name = f"{room_name} - {door_name} (Opened)" + self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) + self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name + self.set_door_item(room_name, door_name, itemloc_name) + else: + # This line is duplicated from StaticLingoItems + if door_data.skip_item is False and door_data.event is False: + if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: + # Grouped doors are handled differently if shuffle doors is on simple. + self.set_door_item(room_name, door_name, door_data.group) + else: + self.handle_non_grouped_door(room_name, door_data, world) + + if door_data.event: + self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) + self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" + self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") + + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also + # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. + for room_name, room_data in PANELS_BY_ROOM.items(): + for panel_name, panel_data in room_data.items(): + if panel_data.achievement: + event_name = room_name + " - " + panel_name + " (Achieved)" + self.add_location(room_name, PlayerLocation(event_name, None, + [RoomAndPanel(room_name, panel_name)])) + self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" + + if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: + event_name = room_name + " - " + panel_name + " (Counted)" + self.add_location(room_name, PlayerLocation(event_name, None, + [RoomAndPanel(room_name, panel_name)])) + self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + + # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need + # to prevent the actual victory condition from becoming a check. + self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" + self.LEVEL_2_LOCATION = "N/A" + + if victory_condition == VictoryCondition.option_the_end: + self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" + self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) + self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + elif victory_condition == VictoryCondition.option_the_master: + self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" + self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" + + self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) + self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + elif victory_condition == VictoryCondition.option_level_2: + self.VICTORY_CONDITION = "Second Room - LEVEL 2" + self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" + + self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, + [RoomAndPanel("Second Room", "LEVEL 2")])) + self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + + # Instantiate all real locations. + location_classification = LocationClassification.normal + if location_checks == LocationChecks.option_reduced: + location_classification = LocationClassification.reduced + elif location_checks == LocationChecks.option_insanity: + location_classification = LocationClassification.insanity + + for location_name, location_data in ALL_LOCATION_TABLE.items(): + if location_name != self.VICTORY_CONDITION: + if location_classification not in location_data.classification: + continue + + self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, + location_data.panels)) + self.REAL_LOCATIONS.append(location_name) + + # Instantiate all real items. + for name, item in ALL_ITEM_TABLE.items(): + if item.should_include(world): + self.REAL_ITEMS.append(name) + + # Create the paintings mapping, if painting shuffle is on. + if painting_shuffle: + # Shuffle paintings until we get something workable. + workable_paintings = False + for i in range(0, 20): + workable_paintings = self.randomize_paintings(world) + if workable_paintings: + break + + if not workable_paintings: + raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 " + "iterations. This is very unlikely to happen on its own, and probably indicates some " + "kind of logic error.") + + if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ + and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: + # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, + # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right + # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are + # no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and + # door shuffle is on simple, because otherwise there are no extra checks in there. + good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] + + if not color_shuffle: + good_item_options.append("Pilgrim Room - Sun Painting") + + if door_shuffle == ShuffleDoors.option_simple: + good_item_options += ["Welcome Back Doors"] + + if not color_shuffle: + good_item_options.append("Rhyme Room Doors") + else: + good_item_options += ["Welcome Back Area - Shortcut to Starting Room"] + + for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]: + if not painting_obj.enter_only or painting_obj.required_door is None: + continue + + # If painting shuffle is on, we only want to consider paintings that actually go somewhere. + if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): + continue + + pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] + good_item_options.append(pdoor.item_name) + + # Copied from The Witness -- remove any plandoed items from the possible good items set. + for v in world.multiworld.plando_items[world.player]: + if v.get("from_pool", True): + for item_key in {"item", "items"}: + if item_key in v: + if type(v[item_key]) is str: + if v[item_key] in good_item_options: + good_item_options.remove(v[item_key]) + elif type(v[item_key]) is dict: + for item, weight in v[item_key].items(): + if weight and item in good_item_options: + good_item_options.remove(item) + else: + # Other type of iterable + for item in v[item_key]: + if item in good_item_options: + good_item_options.remove(item) + + if len(good_item_options) > 0: + self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) + self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) + self.REAL_LOCATIONS.remove("Second Room - Good Luck") + + def randomize_paintings(self, world: "LingoWorld") -> bool: + self.PAINTING_MAPPING.clear() + + door_shuffle = world.options.shuffle_doors + + # Determine the set of exit paintings. All required-exit paintings are included, as are all + # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings. + chosen_exits = [] + if door_shuffle == ShuffleDoors.option_none: + chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items() + if painting.required_when_no_doors] + chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items() + if painting.exit_only and painting.required] + exitable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.enter_only and not painting.disable and not painting.required] + chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits)) + + # Determine the set of entrance paintings. + enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and painting_id not in chosen_exits] + chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES) + + # Create a mapping from entrances to exits. + for warp_exit in chosen_exits: + warp_enter = world.random.choice(chosen_entrances) + + # Check whether this is a warp from a required painting room to another (or the same) required painting + # room. This could cause a cycle that would make certain regions inaccessible. + warp_exit_room = PAINTINGS[warp_exit].room + warp_enter_room = PAINTINGS[warp_enter].room + + required_painting_rooms = REQUIRED_PAINTING_ROOMS + if door_shuffle == ShuffleDoors.option_none: + required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + + if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms: + # This shuffling is non-workable. Start over. + return False + + chosen_entrances.remove(warp_enter) + self.PAINTING_MAPPING[warp_enter] = warp_exit + + for warp_enter in chosen_entrances: + warp_exit = world.random.choice(chosen_exits) + self.PAINTING_MAPPING[warp_enter] = warp_exit + + # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). + # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the + # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall + # is forced to point to the vanilla exit. + if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): + self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" + + # Just for sanity's sake, ensure that all required painting rooms are accessed. + for painting_id, painting in PAINTINGS.items(): + if painting_id not in self.PAINTING_MAPPING.values() \ + and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)): + return False + + return True diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py new file mode 100644 index 0000000000..c75cf4956d --- /dev/null +++ b/worlds/lingo/regions.py @@ -0,0 +1,84 @@ +from typing import Dict, TYPE_CHECKING + +from BaseClasses import ItemClassification, Region +from .items import LingoItem +from .locations import LingoLocation +from .player_logic import LingoPlayerLogic +from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda +from .static_logic import ALL_ROOMS, PAINTINGS, Room + +if TYPE_CHECKING: + from . import LingoWorld + + +def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: + new_region = Region(room.name, world.player, world.multiworld) + for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): + new_location = LingoLocation(world.player, location.name, location.code, new_region) + new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) + new_region.locations.append(new_location) + if location.name in player_logic.EVENT_LOC_TO_ITEM: + event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] + event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) + new_location.place_locked_item(event_item) + + return new_region + + +def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: + target_region = regions["Pilgrim Antechamber"] + source_region = regions["Outside The Agreeable"] + source_region.connect( + target_region, + "Pilgrimage", + lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) + + +def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", + player_logic: LingoPlayerLogic) -> None: + source_painting = PAINTINGS[warp_enter] + target_painting = PAINTINGS[warp_exit] + + target_region = regions[target_painting.room] + source_region = regions[source_painting.room] + source_region.connect( + target_region, + f"{source_painting.room} to {target_painting.room} (Painting)", + lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, + player_logic)) + + +def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: + regions = { + "Menu": Region("Menu", world.player, world.multiworld) + } + + painting_shuffle = world.options.shuffle_paintings + early_color_hallways = world.options.early_color_hallways + + # Instantiate all rooms as regions with their locations first. + for room in ALL_ROOMS: + regions[room.name] = create_region(room, world, player_logic) + + # Connect all created regions now that they exist. + for room in ALL_ROOMS: + for entrance in room.entrances: + # Don't use the vanilla painting connections if we are shuffling paintings. + if entrance.painting and painting_shuffle: + continue + + regions[entrance.room].connect( + regions[room.name], + f"{entrance.room} to {room.name}", + lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, player_logic)) + + handle_pilgrim_room(regions, world, player_logic) + + if early_color_hallways: + regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") + + if painting_shuffle: + for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): + connect_painting(regions, warp_enter, warp_exit, world, player_logic) + + world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py new file mode 100644 index 0000000000..90c889b7f0 --- /dev/null +++ b/worlds/lingo/rules.py @@ -0,0 +1,104 @@ +from typing import TYPE_CHECKING + +from BaseClasses import CollectionState +from .options import VictoryCondition +from .player_logic import LingoPlayerLogic, PlayerLocation +from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor + +if TYPE_CHECKING: + from . import LingoWorld + + +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, + player_logic: LingoPlayerLogic): + if door is None: + return True + + return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) + + +def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): + fake_pilgrimage = [ + ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], + ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], + ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], + ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], + ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], + ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], + ["Outside The Agreeable", "Tenacious Entrance"] + ] + for entrance in fake_pilgrimage: + if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): + return False + + return True + + +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + for panel in location.panels: + panel_room = room_name if panel.room is None else panel.room + if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): + return False + + return True + + +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): + return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) + + +def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, + player_logic: LingoPlayerLogic): + """ + Determines whether a door can be opened + """ + item_name = player_logic.ITEM_BY_DOOR[room][door] + if item_name in PROGRESSIVE_ITEMS: + progression = PROGRESSION_BY_ROOM[room][door] + return state.has(item_name, player, progression.index) + + return state.has(item_name, player) + + +def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + """ + Determines whether a panel can be solved + """ + if start_room != room and not state.has(f"{room} (Reached)", world.player): + return False + + if room == "Second Room" and panel == "ANOTHER TRY" \ + and world.options.victory_condition == VictoryCondition.option_level_2 \ + and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): + return False + + panel_object = PANELS_BY_ROOM[room][panel] + for req_room in panel_object.required_rooms: + if not state.has(f"{req_room} (Reached)", world.player): + return False + + for req_door in panel_object.required_doors: + if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, + req_door.door, world.player, player_logic): + return False + + for req_panel in panel_object.required_panels: + if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, + req_panel.panel, world, player_logic): + return False + + if len(panel_object.colors) > 0 and world.options.shuffle_colors: + for color in panel_object.colors: + if not state.has(color.capitalize(), world.player): + return False + + return True + + +def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): + if location.name == player_logic.MASTERY_LOCATION: + return lambda state: lingo_can_use_mastery_location(state, world) + + return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py new file mode 100644 index 0000000000..d122169c5d --- /dev/null +++ b/worlds/lingo/static_logic.py @@ -0,0 +1,544 @@ +from typing import Dict, List, NamedTuple, Optional, Set + +import yaml + + +class RoomAndDoor(NamedTuple): + room: Optional[str] + door: str + + +class RoomAndPanel(NamedTuple): + room: Optional[str] + panel: str + + +class RoomEntrance(NamedTuple): + room: str # source room + door: Optional[RoomAndDoor] + painting: bool + + +class Room(NamedTuple): + name: str + entrances: List[RoomEntrance] + + +class Door(NamedTuple): + name: str + item_name: str + location_name: Optional[str] + panels: Optional[List[RoomAndPanel]] + skip_location: bool + skip_item: bool + door_ids: List[str] + painting_ids: List[str] + event: bool + group: Optional[str] + include_reduce: bool + junk_item: bool + + +class Panel(NamedTuple): + required_rooms: List[str] + required_doors: List[RoomAndDoor] + required_panels: List[RoomAndPanel] + colors: List[str] + check: bool + event: bool + internal_ids: List[str] + exclude_reduce: bool + achievement: bool + non_counting: bool + + +class Painting(NamedTuple): + id: str + room: str + enter_only: bool + exit_only: bool + orientation: str + required: bool + required_when_no_doors: bool + required_door: Optional[RoomAndDoor] + disable: bool + move: bool + + +class Progression(NamedTuple): + item_name: str + index: int + + +ROOMS: Dict[str, Room] = {} +PANELS: Dict[str, Panel] = {} +DOORS: Dict[str, Door] = {} +PAINTINGS: Dict[str, Painting] = {} + +ALL_ROOMS: List[Room] = [] +DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} +PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PAINTINGS_BY_ROOM: Dict[str, List[Painting]] = {} + +PROGRESSIVE_ITEMS: List[str] = [] +PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} + +PAINTING_ENTRANCES: int = 0 +PAINTING_EXIT_ROOMS: Set[str] = set() +PAINTING_EXITS: int = 0 +REQUIRED_PAINTING_ROOMS: List[str] = [] +REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = [] + +SPECIAL_ITEM_IDS: Dict[str, int] = {} +PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {} +DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} +DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} + + +def load_static_data(): + global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \ + DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS + + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + # Load in all item and location IDs. These are broken up into groups based on the type of item/location. + with files("worlds.lingo").joinpath("ids.yaml").open() as file: + config = yaml.load(file, Loader=yaml.Loader) + + if "special_items" in config: + for item_name, item_id in config["special_items"].items(): + SPECIAL_ITEM_IDS[item_name] = item_id + + if "panels" in config: + for room_name in config["panels"].keys(): + PANEL_LOCATION_IDS[room_name] = {} + + for panel_name, location_id in config["panels"][room_name].items(): + PANEL_LOCATION_IDS[room_name][panel_name] = location_id + + if "doors" in config: + for room_name in config["doors"].keys(): + DOOR_LOCATION_IDS[room_name] = {} + DOOR_ITEM_IDS[room_name] = {} + + for door_name, door_data in config["doors"][room_name].items(): + if "location" in door_data: + DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"] + + if "item" in door_data: + DOOR_ITEM_IDS[room_name][door_name] = door_data["item"] + + if "door_groups" in config: + for item_name, item_id in config["door_groups"].items(): + DOOR_GROUP_ITEM_IDS[item_name] = item_id + + if "progression" in config: + for item_name, item_id in config["progression"].items(): + PROGRESSIVE_ITEM_IDS[item_name] = item_id + + # Process the main world file. + with files("worlds.lingo").joinpath("LL1.yaml").open() as file: + config = yaml.load(file, Loader=yaml.Loader) + + for room_name, room_data in config.items(): + process_room(room_name, room_data) + + PAINTING_EXITS = len(PAINTING_EXIT_ROOMS) + + +def get_special_item_id(name: str): + if name not in SPECIAL_ITEM_IDS: + raise Exception(f"Item ID for special item {name} not found in ids.yaml.") + + return SPECIAL_ITEM_IDS[name] + + +def get_panel_location_id(room: str, name: str): + if room not in PANEL_LOCATION_IDS or name not in PANEL_LOCATION_IDS[room]: + raise Exception(f"Location ID for panel {room} - {name} not found in ids.yaml.") + + return PANEL_LOCATION_IDS[room][name] + + +def get_door_location_id(room: str, name: str): + if room not in DOOR_LOCATION_IDS or name not in DOOR_LOCATION_IDS[room]: + raise Exception(f"Location ID for door {room} - {name} not found in ids.yaml.") + + return DOOR_LOCATION_IDS[room][name] + + +def get_door_item_id(room: str, name: str): + if room not in DOOR_ITEM_IDS or name not in DOOR_ITEM_IDS[room]: + raise Exception(f"Item ID for door {room} - {name} not found in ids.yaml.") + + return DOOR_ITEM_IDS[room][name] + + +def get_door_group_item_id(name: str): + if name not in DOOR_GROUP_ITEM_IDS: + raise Exception(f"Item ID for door group {name} not found in ids.yaml.") + + return DOOR_GROUP_ITEM_IDS[name] + + +def get_progressive_item_id(name: str): + if name not in PROGRESSIVE_ITEM_IDS: + raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.") + + return PROGRESSIVE_ITEM_IDS[name] + + +def process_entrance(source_room, doors, room_obj): + global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS + + # If the value of an entrance is just True, that means that the entrance is always accessible. + if doors is True: + room_obj.entrances.append(RoomEntrance(source_room, None, False)) + elif isinstance(doors, dict): + # If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a + # painting-based entrance, or both. + if "painting" in doors and "door" not in doors: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, None, True)) + else: + if "painting" in doors and doors["painting"]: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor( + doors["room"] if "room" in doors else None, + doors["door"] + ), doors["painting"] if "painting" in doors else False)) + else: + # If the value of an entrance is a list, then there are multiple possible doors that can give access to the + # entrance. + for door in doors: + if "painting" in door and door["painting"]: + PAINTING_EXIT_ROOMS.add(room_obj.name) + PAINTING_ENTRANCES += 1 + + room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + ), door["painting"] if "painting" in door else False)) + + +def process_panel(room_name, panel_name, panel_data): + global PANELS, PANELS_BY_ROOM + + full_name = f"{room_name} - {panel_name}" + + # required_room can either be a single room or a list of rooms. + if "required_room" in panel_data: + if isinstance(panel_data["required_room"], list): + required_rooms = panel_data["required_room"] + else: + required_rooms = [panel_data["required_room"]] + else: + required_rooms = [] + + # required_door can either be a single door or a list of doors. For convenience, the room key for each door does not + # need to be specified if the door is in this room. + required_doors = list() + if "required_door" in panel_data: + if isinstance(panel_data["required_door"], dict): + door = panel_data["required_door"] + required_doors.append(RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + )) + else: + for door in panel_data["required_door"]: + required_doors.append(RoomAndDoor( + door["room"] if "room" in door else None, + door["door"] + )) + + # required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does + # not need to be specified if the panel is in this room. + required_panels = list() + if "required_panel" in panel_data: + if isinstance(panel_data["required_panel"], dict): + other_panel = panel_data["required_panel"] + required_panels.append(RoomAndPanel( + other_panel["room"] if "room" in other_panel else None, + other_panel["panel"] + )) + else: + for other_panel in panel_data["required_panel"]: + required_panels.append(RoomAndPanel( + other_panel["room"] if "room" in other_panel else None, + other_panel["panel"] + )) + + # colors can either be a single color or a list of colors. + if "colors" in panel_data: + if isinstance(panel_data["colors"], list): + colors = panel_data["colors"] + else: + colors = [panel_data["colors"]] + else: + colors = [] + + if "check" in panel_data: + check = panel_data["check"] + else: + check = False + + if "event" in panel_data: + event = panel_data["event"] + else: + event = False + + if "achievement" in panel_data: + achievement = True + else: + achievement = False + + if "exclude_reduce" in panel_data: + exclude_reduce = panel_data["exclude_reduce"] + else: + exclude_reduce = False + + if "non_counting" in panel_data: + non_counting = panel_data["non_counting"] + else: + non_counting = False + + if "id" in panel_data: + if isinstance(panel_data["id"], list): + internal_ids = panel_data["id"] + else: + internal_ids = [panel_data["id"]] + else: + internal_ids = [] + + panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, internal_ids, + exclude_reduce, achievement, non_counting) + PANELS[full_name] = panel_obj + PANELS_BY_ROOM[room_name][panel_name] = panel_obj + + +def process_door(room_name, door_name, door_data): + global DOORS, DOORS_BY_ROOM + + # The item name associated with a door can be explicitly specified in the configuration. If it is not, it is + # generated from the room and door name. + if "item_name" in door_data: + item_name = door_data["item_name"] + else: + item_name = f"{room_name} - {door_name}" + + if "skip_location" in door_data: + skip_location = door_data["skip_location"] + else: + skip_location = False + + if "skip_item" in door_data: + skip_item = door_data["skip_item"] + else: + skip_item = False + + if "event" in door_data: + event = door_data["event"] + else: + event = False + + if "include_reduce" in door_data: + include_reduce = door_data["include_reduce"] + else: + include_reduce = False + + if "junk_item" in door_data: + junk_item = door_data["junk_item"] + else: + junk_item = False + + if "group" in door_data: + group = door_data["group"] + else: + group = None + + # panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or + # a dictionary specifying a panel in a different room. + if "panels" in door_data: + panels = list() + for panel in door_data["panels"]: + if isinstance(panel, dict): + panels.append(RoomAndPanel(panel["room"], panel["panel"])) + else: + panels.append(RoomAndPanel(None, panel)) + else: + skip_location = True + panels = None + + # The location name associated with a door can be explicitly specified in the configuration. If it is not, then the + # name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite + # messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it + # would be better to specify a name. + if "location_name" in door_data: + location_name = door_data["location_name"] + elif skip_location is False: + panel_per_room = dict() + for panel in panels: + panel_room_name = room_name if panel.room is None else panel.room + panel_per_room.setdefault(panel_room_name, []).append(panel.panel) + + room_strs = list() + for door_room_str, door_panels_str in panel_per_room.items(): + room_strs.append(door_room_str + " - " + ", ".join(door_panels_str)) + + location_name = " and ".join(room_strs) + else: + location_name = None + + # The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should + # open more than one actual in-game door. + if "id" in door_data: + if isinstance(door_data["id"], list): + door_ids = door_data["id"] + else: + door_ids = [door_data["id"]] + else: + door_ids = [] + + # The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical + # door should move more than one actual in-game painting. + if "painting_id" in door_data: + if isinstance(door_data["painting_id"], list): + painting_ids = door_data["painting_id"] + else: + painting_ids = [door_data["painting_id"]] + else: + painting_ids = [] + + door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, door_ids, + painting_ids, event, group, include_reduce, junk_item) + + DOORS[door_obj.item_name] = door_obj + DOORS_BY_ROOM[room_name][door_name] = door_obj + + +def process_painting(room_name, painting_data): + global PAINTINGS, PAINTINGS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + + # Read in information about this painting and store it in an object. + painting_id = painting_data["id"] + + if "orientation" in painting_data: + orientation = painting_data["orientation"] + else: + orientation = "" + + if "disable" in painting_data: + disable_painting = painting_data["disable"] + else: + disable_painting = False + + if "required" in painting_data: + required_painting = painting_data["required"] + if required_painting: + REQUIRED_PAINTING_ROOMS.append(room_name) + else: + required_painting = False + + if "move" in painting_data: + move_painting = painting_data["move"] + else: + move_painting = False + + if "required_when_no_doors" in painting_data: + rwnd = painting_data["required_when_no_doors"] + if rwnd: + REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name) + else: + rwnd = False + + if "exit_only" in painting_data: + exit_only = painting_data["exit_only"] + else: + exit_only = False + + if "enter_only" in painting_data: + enter_only = painting_data["enter_only"] + else: + enter_only = False + + required_door = None + if "required_door" in painting_data: + door = painting_data["required_door"] + required_door = RoomAndDoor( + door["room"] if "room" in door else room_name, + door["door"] + ) + + painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation, + required_painting, rwnd, required_door, disable_painting, move_painting) + PAINTINGS[painting_id] = painting_obj + PAINTINGS_BY_ROOM[room_name].append(painting_obj) + + +def process_progression(room_name, progression_name, progression_doors): + global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM + + # Progressive items are configured as a list of doors. + PROGRESSIVE_ITEMS.append(progression_name) + + progression_index = 1 + for door in progression_doors: + if isinstance(door, Dict): + door_room = door["room"] + door_door = door["door"] + else: + door_room = room_name + door_door = door + + room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {}) + room_progressions[door_door] = Progression(progression_name, progression_index) + progression_index += 1 + + +def process_room(room_name, room_data): + global ROOMS, ALL_ROOMS + + room_obj = Room(room_name, []) + + if "entrances" in room_data: + for source_room, doors in room_data["entrances"].items(): + process_entrance(source_room, doors, room_obj) + + if "panels" in room_data: + PANELS_BY_ROOM[room_name] = dict() + + for panel_name, panel_data in room_data["panels"].items(): + process_panel(room_name, panel_name, panel_data) + + if "doors" in room_data: + DOORS_BY_ROOM[room_name] = dict() + + for door_name, door_data in room_data["doors"].items(): + process_door(room_name, door_name, door_data) + + if "paintings" in room_data: + PAINTINGS_BY_ROOM[room_name] = [] + + for painting_data in room_data["paintings"]: + process_painting(room_name, painting_data) + + if "progression" in room_data: + for progression_name, progression_doors in room_data["progression"].items(): + process_progression(room_name, progression_name, progression_doors) + + ROOMS[room_name] = room_obj + ALL_ROOMS.append(room_obj) + + +# Initialize the static data at module scope. +load_static_data() diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py new file mode 100644 index 0000000000..5dc989af59 --- /dev/null +++ b/worlds/lingo/test/TestDoors.py @@ -0,0 +1,89 @@ +from . import LingoTestBase + + +class TestRequiredRoomLogic(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_pilgrim_first(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Sun Painting") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Shortcut to The Seeker") + self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + def test_hidden_first(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Shortcut to The Seeker") + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Pilgrim Room - Sun Painting") + self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + +class TestRequiredDoorLogic(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_through_rhyme(self) -> None: + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Rhyme Room Entrance") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Rhyme Room (Looped Square) - Door to Circle") + self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + def test_through_hidden(self) -> None: + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Rhyme Room Entrance") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Starting Room - Back Right Door") + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + self.collect_by_name("Hidden Room - Rhyme Room Entrance") + self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) + + +class TestSimpleDoors(LingoTestBase): + options = { + "shuffle_doors": "simple" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + + self.collect_by_name("Rhyme Room Doors") + self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py new file mode 100644 index 0000000000..3fb3c95a02 --- /dev/null +++ b/worlds/lingo/test/TestMastery.py @@ -0,0 +1,39 @@ +from . import LingoTestBase + + +class TestMasteryWhenVictoryIsTheEnd(LingoTestBase): + options = { + "mastery_achievements": "22", + "victory_condition": "the_end", + "shuffle_colors": "true" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + self.assertTrue(self.can_reach_location("The End (Solved)")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER")) + + self.collect_by_name(["Green", "Brown", "Yellow"]) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER")) + + +class TestMasteryWhenVictoryIsTheMaster(LingoTestBase): + options = { + "mastery_achievements": "24", + "victory_condition": "the_master", + "shuffle_colors": "true" + } + + def test_requirement(self): + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE END")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) + + self.collect_by_name(["Green", "Gray", "Brown", "Yellow"]) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) \ No newline at end of file diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py new file mode 100644 index 0000000000..1769677862 --- /dev/null +++ b/worlds/lingo/test/TestOptions.py @@ -0,0 +1,31 @@ +from . import LingoTestBase + + +class TestMultiShuffleOptions(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "shuffle_paintings": "true", + "early_color_hallways": "true" + } + + +class TestPanelsanity(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "location_checks": "insanity", + "shuffle_colors": "true" + } + + +class TestAllPanelHunt(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "victory_condition": "level_2", + "level_2_requirement": "800", + "early_color_hallways": "true" + } diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py new file mode 100644 index 0000000000..7b0c3bb525 --- /dev/null +++ b/worlds/lingo/test/TestOrangeTower.py @@ -0,0 +1,175 @@ +from . import LingoTestBase + + +class TestProgressiveOrangeTower(LingoTestBase): + options = { + "shuffle_doors": "complex", + "progressive_orange_tower": "true" + } + + def test_from_welcome_back(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Welcome Back Area - Shortcut to Starting Room") + self.collect_by_name("Orange Tower Fifth Floor - Welcome Back") + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + progressive_tower = self.get_items_by_name("Progressive Orange Tower") + + self.collect(progressive_tower[0]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[1]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[2]) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[3]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[4]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[5]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + def test_from_hub_room(self) -> None: + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Second Room - Exit Door") + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect_by_name("Orange Tower First Floor - Shortcut to Hub Room") + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + progressive_tower = self.get_items_by_name("Progressive Orange Tower") + + self.collect(progressive_tower[0]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.remove(self.get_item_by_name("Orange Tower First Floor - Shortcut to Hub Room")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[1]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[2]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[3]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[4]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) + + self.collect(progressive_tower[5]) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py new file mode 100644 index 0000000000..026971c45d --- /dev/null +++ b/worlds/lingo/test/TestProgressive.py @@ -0,0 +1,191 @@ +from . import LingoTestBase + + +class TestComplexProgressiveHallwayRoom(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "The Tenacious - Shortcut to Hub Room", + "Outside The Agreeable - Tenacious Entrance"]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + progressive_hallway_room = self.get_items_by_name("Progressive Hallway Room") + + self.collect(progressive_hallway_room[0]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[1]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[2]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect(progressive_hallway_room[3]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + +class TestSimpleHallwayRoom(LingoTestBase): + options = { + "shuffle_doors": "simple" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "Entrances to The Tenacious"]) + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + self.collect_by_name("Hallway Room Doors") + self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player)) + + +class TestProgressiveArtGallery(LingoTestBase): + options = { + "shuffle_doors": "complex" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance", + "Orange Tower Fourth Floor - Hot Crusts Door"]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery") + + self.collect(progressive_gallery_room[0]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[1]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[2]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[3]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect(progressive_gallery_room[4]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + +class TestNoDoorsArtGallery(LingoTestBase): + options = { + "shuffle_doors": "none", + "shuffle_colors": "true" + } + + def test_item(self): + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Yellow") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Brown") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name("Blue") + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) + + self.collect_by_name(["Orange", "Gray"]) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) + self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player)) + self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS")) + self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player)) diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py new file mode 100644 index 0000000000..ffbf9032b6 --- /dev/null +++ b/worlds/lingo/test/__init__.py @@ -0,0 +1,13 @@ +from typing import ClassVar + +from test.bases import WorldTestBase +from .. import LingoTestOptions + + +class LingoTestBase(WorldTestBase): + game = "Lingo" + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs): + LingoTestOptions.disable_forced_good_item = True + super().world_setup(*args, **kwargs) diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py new file mode 100644 index 0000000000..22fafea0fc --- /dev/null +++ b/worlds/lingo/testing.py @@ -0,0 +1,2 @@ +class LingoTestOptions: + disable_forced_good_item: bool = False diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb new file mode 100644 index 0000000000..9e1ce67bd2 --- /dev/null +++ b/worlds/lingo/utils/assign_ids.rb @@ -0,0 +1,178 @@ +# This utility goes through the provided Lingo config and assigns item and +# location IDs to entities that require them (such as doors and panels). These +# IDs are output in a separate yaml file. If the output file already exists, +# then it will be updated with any newly assigned IDs rather than overwritten. +# In this event, all new IDs will be greater than any already existing IDs, +# even if there are gaps in the ID space; this is to prevent collision when IDs +# are retired. +# +# This utility should be run whenever logically new items or locations are +# required. If an item or location is created that is logically equivalent to +# one that used to exist, this utility should not be used, and instead the ID +# file should be manually edited so that the old ID can be reused. + +require 'set' +require 'yaml' + +configpath = ARGV[0] +outputpath = ARGV[1] + +next_item_id = 444400 +next_location_id = 444400 + +location_id_by_name = {} + +old_generated = YAML.load_file(outputpath) +File.write(outputpath + ".old", old_generated.to_yaml) + +if old_generated.include? "special_items" then + old_generated["special_items"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end +if old_generated.include? "special_locations" then + old_generated["special_locations"].each do |name, id| + if id >= next_location_id then + next_location_id = id + 1 + end + end +end +if old_generated.include? "panels" then + old_generated["panels"].each do |room, panels| + panels.each do |name, id| + if id >= next_location_id then + next_location_id = id + 1 + end + location_name = "#{room} - #{name}" + location_id_by_name[location_name] = id + end + end +end +if old_generated.include? "doors" then + old_generated["doors"].each do |room, doors| + doors.each do |name, ids| + if ids.include? "location" then + if ids["location"] >= next_location_id then + next_location_id = ids["location"] + 1 + end + end + if ids.include? "item" then + if ids["item"] >= next_item_id then + next_item_id = ids["item"] + 1 + end + end + end + end +end +if old_generated.include? "door_groups" then + old_generated["door_groups"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end +if old_generated.include? "progression" then + old_generated["progression"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end + +door_groups = Set[] + +config = YAML.load_file(configpath) +config.each do |room_name, room_data| + if room_data.include? "panels" + room_data["panels"].each do |panel_name, panel| + unless old_generated.include? "panels" and old_generated["panels"].include? room_name and old_generated["panels"][room_name].include? panel_name then + old_generated["panels"] ||= {} + old_generated["panels"][room_name] ||= {} + old_generated["panels"][room_name][panel_name] = next_location_id + + location_name = "#{room_name} - #{panel_name}" + location_id_by_name[location_name] = next_location_id + + next_location_id += 1 + end + end + end +end + +config.each do |room_name, room_data| + if room_data.include? "doors" + room_data["doors"].each do |door_name, door| + if door.include? "event" and door["event"] then + next + end + + unless door.include? "skip_item" and door["skip_item"] then + unless old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "item" then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["item"] = next_item_id + + next_item_id += 1 + end + + if door.include? "group" and not door_groups.include? door["group"] then + door_groups.add(door["group"]) + + unless old_generated.include? "door_groups" and old_generated["door_groups"].include? door["group"] then + old_generated["door_groups"] ||= {} + old_generated["door_groups"][door["group"]] = next_item_id + + next_item_id += 1 + end + end + end + + unless door.include? "skip_location" and door["skip_location"] then + location_name = "" + if door.include? "location_name" then + location_name = door["location_name"] + elsif door.include? "panels" then + location_name = door["panels"].map do |panel| + if panel.kind_of? Hash then + panel + else + {"room" => room_name, "panel" => panel} + end + end.sort_by {|panel| panel["room"]}.chunk {|panel| panel["room"]}.map do |room_panels| + room_panels[0] + " - " + room_panels[1].map{|panel| panel["panel"]}.join(", ") + end.join(" and ") + end + + if location_id_by_name.has_key? location_name then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["location"] = location_id_by_name[location_name] + elsif not (old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "location") then + old_generated["doors"] ||= {} + old_generated["doors"][room_name] ||= {} + old_generated["doors"][room_name][door_name] ||= {} + old_generated["doors"][room_name][door_name]["location"] = next_location_id + + next_location_id += 1 + end + end + end + end + + if room_data.include? "progression" + room_data["progression"].each do |progression_name, pdata| + unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then + old_generated["progression"] ||= {} + old_generated["progression"][progression_name] = next_item_id + + next_item_id += 1 + end + end + end +end + +File.write(outputpath, old_generated.to_yaml) diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb new file mode 100644 index 0000000000..ed2e9058f9 --- /dev/null +++ b/worlds/lingo/utils/validate_config.rb @@ -0,0 +1,329 @@ +# Script to validate a level config file. This checks that the names used within +# the file are consistent. It also checks that the panel and door IDs mentioned +# all exist in the map file. +# +# Usage: validate_config.rb [config file] [map file] + +require 'set' +require 'yaml' + +configpath = ARGV[0] +mappath = ARGV[1] + +panels = Set["Countdown Panels/Panel_1234567890_wanderlust"] +doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"] +paintings = Set[] + +File.readlines(mappath).each do |line| + line.match(/node name=\"(.*)\" parent=\"Panels\/(.*)\" instance/) do |m| + panels.add(m[2] + "/" + m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Doors\/(.*)\" instance/) do |m| + doors.add(m[2] + "/" + m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Decorations\/Paintings\" instance/) do |m| + paintings.add(m[1]) + end + line.match(/node name=\"(.*)\" parent=\"Decorations\/EndPanel\" instance/) do |m| + panels.add("EndPanel/" + m[1]) + end +end + +configured_rooms = Set["Menu"] +configured_doors = Set[] +configured_panels = Set[] + +mentioned_rooms = Set[] +mentioned_doors = Set[] +mentioned_panels = Set[] + +door_groups = {} + +directives = Set["entrances", "panels", "doors", "paintings", "progression"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"] +door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"] +painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move"] + +non_counting = 0 + +config = YAML.load_file(configpath) +config.each do |room_name, room| + configured_rooms.add(room_name) + + used_directives = Set[] + room.each_key do |key| + used_directives.add(key) + end + diff_directives = used_directives - directives + unless diff_directives.empty? then + puts("#{room_name} has the following invalid top-level directives: #{diff_directives.to_s}") + end + + (room["entrances"] || {}).each do |source_room, entrance| + mentioned_rooms.add(source_room) + + entrances = [] + if entrance.kind_of? Hash + if entrance.keys() != ["painting"] then + entrances = [entrance] + end + elsif entrance.kind_of? Array + entrances = entrance + end + + entrances.each do |e| + entrance_room = e.include?("room") ? e["room"] : room_name + mentioned_rooms.add(entrance_room) + mentioned_doors.add(entrance_room + " - " + e["door"]) + end + end + + (room["panels"] || {}).each do |panel_name, panel| + unless panel_name.kind_of? String then + puts "#{room_name} has an invalid panel name" + end + + configured_panels.add(room_name + " - " + panel_name) + + if panel.include?("id") + panel_ids = [] + if panel["id"].kind_of? Array + panel_ids = panel["id"] + else + panel_ids = [panel["id"]] + end + + panel_ids.each do |panel_id| + unless panels.include? panel_id then + puts "#{room_name} - #{panel_name} :::: Invalid Panel ID #{panel_id}" + end + end + else + puts "#{room_name} - #{panel_name} :::: Panel is missing an ID" + end + + if panel.include?("required_room") + required_rooms = [] + if panel["required_room"].kind_of? Array + required_rooms = panel["required_room"] + else + required_rooms = [panel["required_room"]] + end + + required_rooms.each do |required_room| + mentioned_rooms.add(required_room) + end + end + + if panel.include?("required_door") + required_doors = [] + if panel["required_door"].kind_of? Array + required_doors = panel["required_door"] + else + required_doors = [panel["required_door"]] + end + + required_doors.each do |required_door| + other_room = required_door.include?("room") ? required_door["room"] : room_name + mentioned_rooms.add(other_room) + mentioned_doors.add("#{other_room} - #{required_door["door"]}") + end + end + + if panel.include?("required_panel") + required_panels = [] + if panel["required_panel"].kind_of? Array + required_panels = panel["required_panel"] + else + required_panels = [panel["required_panel"]] + end + + required_panels.each do |required_panel| + other_room = required_panel.include?("room") ? required_panel["room"] : room_name + mentioned_rooms.add(other_room) + mentioned_panels.add("#{other_room} - #{required_panel["panel"]}") + end + end + + unless panel.include?("tag") then + puts "#{room_name} - #{panel_name} :::: Panel is missing a tag" + end + + if panel.include?("non_counting") then + non_counting += 1 + end + + bad_subdirectives = [] + panel.keys.each do |key| + unless panel_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["doors"] || {}).each do |door_name, door| + configured_doors.add("#{room_name} - #{door_name}") + + if door.include?("id") + door_ids = [] + if door["id"].kind_of? Array + door_ids = door["id"] + else + door_ids = [door["id"]] + end + + door_ids.each do |door_id| + unless doors.include? door_id then + puts "#{room_name} - #{door_name} :::: Invalid Door ID #{door_id}" + end + end + end + + if door.include?("painting_id") + painting_ids = [] + if door["painting_id"].kind_of? Array + painting_ids = door["painting_id"] + else + painting_ids = [door["painting_id"]] + end + + painting_ids.each do |painting_id| + unless paintings.include? painting_id then + puts "#{room_name} - #{door_name} :::: Invalid Painting ID #{painting_id}" + end + end + end + + if not door.include?("id") and not door.include?("painting_id") and not door["skip_item"] and not door["event"] then + puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors or paintings" + end + + if door.include?("panels") + door["panels"].each do |panel| + if panel.kind_of? Hash then + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{other_room} - #{panel["panel"]}") + else + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{room_name} - #{panel}") + end + end + elsif not door["skip_location"] + puts "#{room_name} - #{door_name} :::: Should be marked skip_location if there are no panels" + end + + if door.include?("group") + door_groups[door["group"]] ||= 0 + door_groups[door["group"]] += 1 + end + + bad_subdirectives = [] + door.keys.each do |key| + unless door_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["paintings"] || []).each do |painting| + if painting.include?("id") and painting["id"].kind_of? String then + unless paintings.include? painting["id"] then + puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}" + end + else + puts "#{room_name} :::: Painting is missing an ID" + end + + if painting["disable"] then + # We're good. + next + end + + if painting.include?("orientation") then + unless ["north", "south", "east", "west"].include? painting["orientation"] then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}" + end + else + puts "#{room_name} :::: Painting is missing an orientation" + end + + if painting.include?("required_door") + other_room = painting["required_door"].include?("room") ? painting["required_door"]["room"] : room_name + mentioned_doors.add("#{other_room} - #{painting["required_door"]["door"]}") + + unless painting["enter_only"] then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Should be marked enter_only if there is a required_door" + end + end + + bad_subdirectives = [] + painting.keys.each do |key| + unless painting_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{painting["id"] || "painting"} :::: Painting has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + end + + (room["progression"] || {}).each do |progression_name, door_list| + door_list.each do |door| + if door.kind_of? Hash then + mentioned_doors.add("#{door["room"]} - #{door["door"]}") + else + mentioned_doors.add("#{room_name} - #{door}") + end + end + end +end + +errored_rooms = mentioned_rooms - configured_rooms +unless errored_rooms.empty? then + puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s +end + +errored_panels = mentioned_panels - configured_panels +unless errored_panels.empty? then + puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s +end + +errored_doors = mentioned_doors - configured_doors +unless errored_doors.empty? then + puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s +end + +door_groups.each do |group,num| + if num == 1 then + puts "Door group \"#{group}\" only has one door in it" + end +end + +slashed_rooms = configured_rooms.select do |room| + room.include? "/" +end +unless slashed_rooms.empty? then + puts "The following rooms have slashes in their names: " + slashed_rooms.to_s +end + +slashed_panels = configured_panels.select do |panel| + panel.include? "/" +end +unless slashed_panels.empty? then + puts "The following panels have slashes in their names: " + slashed_panels.to_s +end + +slashed_doors = configured_doors.select do |door| + door.include? "/" +end +unless slashed_doors.empty? then + puts "The following doors have slashes in their names: " + slashed_doors.to_s +end + +puts "#{configured_panels.size} panels (#{non_counting} non counting)" From b5bd95771d7e89422205040a76f124660633c2e6 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:47:36 +0100 Subject: [PATCH 150/327] Raft: Use world.random instead of global random (#2439) --- worlds/raft/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index fec60c3bd5..8e4eda09e1 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -1,5 +1,4 @@ import typing -import random from .Locations import location_table, lookup_name_to_id as locations_lookup_name_to_id from .Items import (createResourcePackName, item_table, progressive_table, progressive_item_list, @@ -100,7 +99,7 @@ class RaftWorld(World): extraItemNamePool.append(item["name"]) if (len(extraItemNamePool) > 0): - for randomItem in random.choices(extraItemNamePool, k=extras): + for randomItem in self.random.choices(extraItemNamePool, k=extras): raft_item = self.create_item_replaceAsNecessary(randomItem) pool.append(raft_item) @@ -194,7 +193,7 @@ class RaftWorld(World): previousLocation = "RadioTower" while (len(availableLocationList) > 0): if (len(availableLocationList) > 1): - currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)] + currentLocation = availableLocationList[self.random.randint(0, len(availableLocationList) - 2)] else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) @@ -212,7 +211,7 @@ class RaftWorld(World): def setLocationItemFromRegion(self, region: str, itemName: str): itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - location = random.choice(list(loc for loc in location_table if loc["region"] == region)) + location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse) def fill_slot_data(self): From f444d570d3bee733972ad44e3b9fb1467f159b11 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 10 Nov 2023 14:07:56 -0500 Subject: [PATCH 151/327] Lingo: Fix edge case painting shuffle accessibility issues (#2441) * Lingo: Fix painting shuffle logic issue in The Wise * Lingo: More generic painting cycle prevention * Lingo: okay how about now * Lingo: Consider Owl Hallway blocked painting areas in vanilla doors * Lingo: so honestly I should've seen this one coming * Lingo: Refined req_blocked for vanilla doors * Lingo: Orange Tower Basement is also owl-blocked * Lingo: Rewrite randomize_paintings to eliminate rerolls Now, mapping is done in two phases, rather than assigning everything at once and then rerolling if the mapping is non-viable. --- worlds/lingo/LL1.yaml | 11 +++++++ worlds/lingo/player_logic.py | 62 +++++++++++++++++++----------------- worlds/lingo/static_logic.py | 15 ++++++++- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/LL1.yaml index 7ae015dc64..db1418f596 100644 --- a/worlds/lingo/LL1.yaml +++ b/worlds/lingo/LL1.yaml @@ -97,6 +97,11 @@ # Use "required_when_no_doors" instead if it would be # possible to enter the room without the painting in door # shuffle mode. + # - req_blocked: Marks that a painting cannot be an entrance leading to a + # required painting. Paintings within a room that has a + # required painting are automatically req blocked. + # Use "req_blocked_when_no_doors" instead if it would be + # fine in door shuffle mode. # - move: Denotes that the painting is able to move. Starting Room: entrances: @@ -2210,6 +2215,7 @@ - id: map_painting2 orientation: north enter_only: True # otherwise you might just skip the whole game! + req_blocked_when_no_doors: True # owl hallway in vanilla doors Roof: entrances: Orange Tower Seventh Floor: True @@ -2276,6 +2282,7 @@ paintings: - id: arrows_painting_11 orientation: east + req_blocked_when_no_doors: True # owl hallway in vanilla doors Courtyard: entrances: Roof: True @@ -5755,11 +5762,13 @@ move: True required_door: door: Exit + req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors - id: symmetry_painting_a_6 orientation: west exit_only: True - id: symmetry_painting_b_6 orientation: north + req_blocked_when_no_doors: True # the wondrous (table) in vanilla doors Arrow Garden: entrances: The Wondrous: @@ -6914,6 +6923,7 @@ paintings: - id: clock_painting_3 orientation: east + req_blocked: True # outside the wise (with or without door shuffle) The Red: entrances: Roof: True @@ -7362,6 +7372,7 @@ paintings: - id: hi_solved_painting4 orientation: south + req_blocked_when_no_doors: True # owl hallway in vanilla doors Challenge Room: entrances: Welcome Back Area: diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 217ad91fcd..66fe317d14 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -241,43 +241,46 @@ class LingoPlayerLogic: door_shuffle = world.options.shuffle_doors - # Determine the set of exit paintings. All required-exit paintings are included, as are all - # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings. - chosen_exits = [] + # First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to + # required paintings. + req_exits = [] + required_painting_rooms = REQUIRED_PAINTING_ROOMS if door_shuffle == ShuffleDoors.option_none: - chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items() - if painting.required_when_no_doors] - chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items() - if painting.exit_only and painting.required] + required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS + req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] + req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and not painting.req_blocked and + not painting.req_blocked_when_no_doors and painting.room not in required_painting_rooms] + else: + req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if not painting.exit_only and not painting.disable and not painting.req_blocked and + painting.room not in required_painting_rooms] + req_exits += [painting_id for painting_id, painting in PAINTINGS.items() + if painting.exit_only and painting.required] + req_entrances = world.random.sample(req_enterable, len(req_exits)) + + self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) + + # Next, determine the rest of the exit paintings. exitable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.enter_only and not painting.disable and not painting.required] - chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits)) + if not painting.enter_only and not painting.disable and painting_id not in req_exits and + painting_id not in req_entrances] + nonreq_exits = world.random.sample(exitable, PAINTING_EXITS - len(req_exits)) + chosen_exits = req_exits + nonreq_exits - # Determine the set of entrance paintings. + # Determine the rest of the entrance paintings. enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.exit_only and not painting.disable and painting_id not in chosen_exits] - chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES) + if not painting.exit_only and not painting.disable and painting_id not in chosen_exits and + painting_id not in req_entrances] + chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES - len(req_entrances)) - # Create a mapping from entrances to exits. - for warp_exit in chosen_exits: + # Assign one entrance to each non-required exit, to ensure that the total number of exits is achieved. + for warp_exit in nonreq_exits: warp_enter = world.random.choice(chosen_entrances) - - # Check whether this is a warp from a required painting room to another (or the same) required painting - # room. This could cause a cycle that would make certain regions inaccessible. - warp_exit_room = PAINTINGS[warp_exit].room - warp_enter_room = PAINTINGS[warp_enter].room - - required_painting_rooms = REQUIRED_PAINTING_ROOMS - if door_shuffle == ShuffleDoors.option_none: - required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS - - if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms: - # This shuffling is non-workable. Start over. - return False - chosen_entrances.remove(warp_enter) self.PAINTING_MAPPING[warp_enter] = warp_exit + # Assign each of the remaining entrances to any required or non-required exit. for warp_enter in chosen_entrances: warp_exit = world.random.choice(chosen_exits) self.PAINTING_MAPPING[warp_enter] = warp_exit @@ -292,7 +295,8 @@ class LingoPlayerLogic: # Just for sanity's sake, ensure that all required painting rooms are accessed. for painting_id, painting in PAINTINGS.items(): if painting_id not in self.PAINTING_MAPPING.values() \ - and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)): + and (painting.required or (painting.required_when_no_doors and + door_shuffle == ShuffleDoors.option_none)): return False return True diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index d122169c5d..f6690f93a4 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -63,6 +63,8 @@ class Painting(NamedTuple): required_door: Optional[RoomAndDoor] disable: bool move: bool + req_blocked: bool + req_blocked_when_no_doors: bool class Progression(NamedTuple): @@ -471,6 +473,16 @@ def process_painting(room_name, painting_data): else: enter_only = False + if "req_blocked" in painting_data: + req_blocked = painting_data["req_blocked"] + else: + req_blocked = False + + if "req_blocked_when_no_doors" in painting_data: + req_blocked_when_no_doors = painting_data["req_blocked_when_no_doors"] + else: + req_blocked_when_no_doors = False + required_door = None if "required_door" in painting_data: door = painting_data["required_door"] @@ -480,7 +492,8 @@ def process_painting(room_name, painting_data): ) painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation, - required_painting, rwnd, required_door, disable_painting, move_painting) + required_painting, rwnd, required_door, disable_painting, move_painting, req_blocked, + req_blocked_when_no_doors) PAINTINGS[painting_id] = painting_obj PAINTINGS_BY_ROOM[room_name].append(painting_obj) From 7af7ef2dc7ff9563aef5f2d01ab7851dc5bf3052 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 10 Nov 2023 14:19:05 -0500 Subject: [PATCH 152/327] Lingo: Removed "Reached" event items (#2442) --- worlds/lingo/player_logic.py | 6 ------ worlds/lingo/rules.py | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 66fe317d14..abb975e020 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -79,12 +79,6 @@ class LingoPlayerLogic: raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " "be enough locations for all of the door items.") - # Create an event for every room that represents being able to reach that room. - for room_name in ROOMS.keys(): - roomloc_name = f"{room_name} (Reached)" - self.add_location(room_name, PlayerLocation(roomloc_name, None, [])) - self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name - # Create an event for every door, representing whether that door has been opened. Also create event items for # doors that are event-only. for room_name, room_data in DOORS_BY_ROOM.items(): diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 90c889b7f0..d59b8a1ef7 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -66,7 +66,7 @@ def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, p """ Determines whether a panel can be solved """ - if start_room != room and not state.has(f"{room} (Reached)", world.player): + if start_room != room and not state.can_reach(room, "Region", world.player): return False if room == "Second Room" and panel == "ANOTHER TRY" \ @@ -76,7 +76,7 @@ def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, p panel_object = PANELS_BY_ROOM[room][panel] for req_room in panel_object.required_rooms: - if not state.has(f"{req_room} (Reached)", world.player): + if not state.can_reach(req_room, "Region", world.player): return False for req_door in panel_object.required_doors: From ac77666f2f3d031218347a7085f8d911e3a4adb5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 10 Nov 2023 22:02:34 +0100 Subject: [PATCH 153/327] Factorio: skip a bunch of file IO (#2444) In a lot of cases, Factorio would write data to file first, then attach that file into zip. It now directly attaches the data to the zip and encapsulation was used to allow earlier GC in places (rendered templates especially). --- worlds/factorio/Mod.py | 67 +++++++++++++++--------------- worlds/factorio/data/mod/info.json | 14 ------- 2 files changed, 34 insertions(+), 47 deletions(-) delete mode 100644 worlds/factorio/data/mod/info.json diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 270e7dacf0..c897e72dcd 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -5,7 +5,7 @@ import os import shutil import threading import zipfile -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple import jinja2 @@ -24,6 +24,7 @@ data_template: Optional[jinja2.Template] = None data_final_template: Optional[jinja2.Template] = None locale_template: Optional[jinja2.Template] = None control_template: Optional[jinja2.Template] = None +settings_template: Optional[jinja2.Template] = None template_load_lock = threading.Lock() @@ -62,15 +63,24 @@ recipe_time_ranges = { class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives + writing_tasks: List[Callable[[], Tuple[str, str]]] + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.writing_tasks = [] def write_contents(self, opened_zipfile: zipfile.ZipFile): # directory containing Factorio mod has to come first, or Factorio won't recognize this file as a mod. mod_dir = self.path[:-4] # cut off .zip for root, dirs, files in os.walk(mod_dir): for file in files: - opened_zipfile.write(os.path.join(root, file), - os.path.relpath(os.path.join(root, file), + filename = os.path.join(root, file) + opened_zipfile.write(filename, + os.path.relpath(filename, os.path.join(mod_dir, '..'))) + for task in self.writing_tasks: + target, content = task() + opened_zipfile.writestr(target, content) # now we can add extras. super(FactorioModFile, self).write_contents(opened_zipfile) @@ -98,6 +108,7 @@ def generate_mod(world: "Factorio", output_directory: str): locations = [(location, location.item) for location in world.science_locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" + versioned_mod_name = mod_name + "_" + Utils.__version__ random = multiworld.per_slot_randoms[player] @@ -153,48 +164,38 @@ def generate_mod(world: "Factorio", output_directory: str): template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) - control_code = control_template.render(**template_data) - data_template_code = data_template.render(**template_data) - data_final_fixes_code = data_final_template.render(**template_data) - settings_code = settings_template.render(**template_data) + mod_dir = os.path.join(output_directory, versioned_mod_name) - mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) - en_locale_dir = os.path.join(mod_dir, "locale", "en") - os.makedirs(en_locale_dir, exist_ok=True) + zf_path = os.path.join(mod_dir + ".zip") + mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) if world.zip_path: - # Maybe investigate read from zip, write to zip, without temp file? with zipfile.ZipFile(world.zip_path) as zf: for file in zf.infolist(): if not file.is_dir() and "/data/mod/" in file.filename: path_part = Utils.get_text_after(file.filename, "/data/mod/") - target = os.path.join(mod_dir, path_part) - os.makedirs(os.path.split(target)[0], exist_ok=True) - - with open(target, "wb") as f: - f.write(zf.read(file)) + mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): + (arcpath, content)) else: shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) - with open(os.path.join(mod_dir, "data.lua"), "wt") as f: - f.write(data_template_code) - with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: - f.write(data_final_fixes_code) - with open(os.path.join(mod_dir, "control.lua"), "wt") as f: - f.write(control_code) - with open(os.path.join(mod_dir, "settings.lua"), "wt") as f: - f.write(settings_code) - locale_content = locale_template.render(**template_data) - with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f: - f.write(locale_content) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua", + data_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/data-final-fixes.lua", + data_final_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/control.lua", + control_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/settings.lua", + settings_template.render(**template_data))) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/locale/en/locale.cfg", + locale_template.render(**template_data))) + info = base_info.copy() info["name"] = mod_name - with open(os.path.join(mod_dir, "info.json"), "wt") as f: - json.dump(info, f, indent=4) + mod.writing_tasks.append(lambda: (versioned_mod_name + "/info.json", + json.dumps(info, indent=4))) - # zip the result - zf_path = os.path.join(mod_dir + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + # write the mod file mod.write() - + # clean up shutil.rmtree(mod_dir) diff --git a/worlds/factorio/data/mod/info.json b/worlds/factorio/data/mod/info.json deleted file mode 100644 index 70a9518344..0000000000 --- a/worlds/factorio/data/mod/info.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "archipelago-client", - "version": "0.0.1", - "title": "Archipelago", - "author": "Berserker and Dewiniaid", - "homepage": "https://archipelago.gg", - "description": "Integration client for the Archipelago Randomizer", - "factorio_version": "1.1", - "dependencies": [ - "base >= 1.1.0", - "? science-not-invited", - "? factory-levels" - ] -} From 64159a6d0fab4b4c866628f9a0ec9b8a9752179a Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 10 Nov 2023 22:49:55 -0600 Subject: [PATCH 154/327] The Messenger: fix logic rule for spike darts and power seal hunt (#2414) --- worlds/messenger/__init__.py | 7 ++++--- worlds/messenger/regions.py | 6 ------ worlds/messenger/rules.py | 21 ++++++++------------- worlds/messenger/subclasses.py | 6 ++---- worlds/messenger/test/test_shop_chest.py | 10 +++++----- 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 3fe13a3cb4..304b43cf53 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -82,7 +82,10 @@ class MessengerWorld(World): self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) def create_regions(self) -> None: - self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS] + # MessengerRegion adds itself to the multiworld + for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: + if region.name in REGION_CONNECTIONS: + region.add_exits(REGION_CONNECTIONS[region.name]) def create_items(self) -> None: # create items that are always in the item pool @@ -136,8 +139,6 @@ class MessengerWorld(World): self.multiworld.itempool += itempool def set_rules(self) -> None: - for reg_name, connections in REGION_CONNECTIONS.items(): - self.multiworld.get_region(reg_name, self.player).add_exits(connections) logic = self.options.logic_level if logic == Logic.option_normal: MessengerRules(self).set_messenger_rules() diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 28750b949e..3a6c95bff5 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -68,7 +68,6 @@ MEGA_SHARDS: Dict[str, List[str]] = { "Quillshroom Marsh": ["Quillshroom Marsh Mega Shard"], "Searing Crags Upper": ["Searing Crags Mega Shard"], "Glacial Peak": ["Glacial Peak Mega Shard"], - "Tower of Time": [], "Cloud Ruins": ["Cloud Entrance Mega Shard", "Time Warp Mega Shard"], "Cloud Ruins Right": ["Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"], "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"], @@ -84,8 +83,6 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Menu": {"Tower HQ"}, "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, - "Tower of Time": set(), - "Ninja Village": set(), "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, @@ -97,11 +94,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"}, "Cloud Ruins": {"Cloud Ruins Right"}, "Cloud Ruins Right": {"Underworld"}, - "Underworld": set(), "Dark Cave": {"Catacombs", "Riviere Turquoise Entrance"}, "Riviere Turquoise Entrance": {"Riviere Turquoise"}, - "Riviere Turquoise": set(), "Sunken Shrine": {"Howling Grotto"}, - "Elemental Skylands": set(), } """Vanilla layout mapping with all Tower HQ portals open. from -> to""" diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index c9bd9b8625..876acd42c1 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,10 +1,9 @@ from typing import Callable, Dict, TYPE_CHECKING from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, allow_self_locking_items, set_rule +from worlds.generic.Rules import add_rule, allow_self_locking_items from .constants import NOTES, PHOBEKINS -from .options import Goal, MessengerAccessibility -from .subclasses import MessengerShopLocation +from .options import MessengerAccessibility if TYPE_CHECKING: from . import MessengerWorld @@ -37,7 +36,9 @@ class MessengerRules: "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_dart(state), + "Music Box": lambda state: (state.has_all(set(NOTES), self.player) + or state.has("Power Seal", self.player, max(1, self.world.required_seals))) + and self.has_dart(state), } self.location_rules = { @@ -92,8 +93,6 @@ class MessengerRules: # corrupted future "Corrupted Future - Key of Courage": lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player), - # the shop - "Shop Chest": self.has_enough_seals, # tower hq "Money Wrench": self.can_shop, } @@ -143,14 +142,11 @@ class MessengerRules: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] if region.name == "The Shop": - for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]: + for loc in region.locations: loc.access_rule = loc.can_afford - if self.world.options.goal == Goal.option_power_seal_hunt: - set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player), - lambda state: state.has("Shop Chest", self.player)) multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) - if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: + if multiworld.accessibility[self.player]: # not locations accessibility set_self_locking_items(self.world, self.player) @@ -201,8 +197,7 @@ class MessengerHardRules(MessengerRules): self.extra_rules = { "Searing Crags - Key of Strength": lambda state: self.has_dart(state) or self.has_windmill(state), "Elemental Skylands - Key of Symbiosis": lambda state: self.has_windmill(state) or self.can_dboost(state), - "Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state)) - or self.has_wingsuit(state), + "Autumn Hills Seal - Spike Ball Darts": lambda state: self.has_dart(state) or self.has_windmill(state), "Underworld Seal - Fireball Wave": self.has_windmill, } diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index ce31d43d60..0c04bc015c 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -17,8 +17,6 @@ class MessengerRegion(Region): super().__init__(name, world.player, world.multiworld) locations = [loc for loc in REGIONS[self.name]] if self.name == "The Shop": - if world.options.goal > Goal.option_open_music_box: - locations.append("Shop Chest") shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}) @@ -29,9 +27,9 @@ class MessengerRegion(Region): locations += [seal_loc for seal_loc in SEALS[self.name]] if world.options.shuffle_shards and self.name in MEGA_SHARDS: locations += [shard for shard in MEGA_SHARDS[self.name]] - loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None - for loc in locations} + loc_dict = {loc: world.location_name_to_id.get(loc, None) for loc in locations} self.add_locations(loc_dict, MessengerLocation) + world.multiworld.regions.append(self) class MessengerLocation(Location): diff --git a/worlds/messenger/test/test_shop_chest.py b/worlds/messenger/test/test_shop_chest.py index 058a200447..a34fa0fb96 100644 --- a/worlds/messenger/test/test_shop_chest.py +++ b/worlds/messenger/test/test_shop_chest.py @@ -17,18 +17,18 @@ class AllSealsRequired(MessengerTestBase): with self.subTest("Access Dependency"): self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]), self.multiworld.total_seals[self.player]) - locations = ["Shop Chest"] + locations = ["Rescue Phantom"] items = [["Power Seal"]] self.assertAccessDependency(locations, items) self.multiworld.state = CollectionState(self.multiworld) - self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) self.assertBeatable(False) - self.collect_all_but(["Power Seal", "Shop Chest", "Rescue Phantom"]) - self.assertEqual(self.can_reach_location("Shop Chest"), False) + self.collect_all_but(["Power Seal", "Rescue Phantom"]) + self.assertEqual(self.can_reach_location("Rescue Phantom"), False) self.assertBeatable(False) self.collect_by_name("Power Seal") - self.assertEqual(self.can_reach_location("Shop Chest"), True) + self.assertEqual(self.can_reach_location("Rescue Phantom"), True) self.assertBeatable(True) From 2dd904e7586b6ac974c86f1cf778d3b257e9c91a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 10 Nov 2023 22:06:54 -0800 Subject: [PATCH 155/327] Allow worlds to provide item and location descriptions (#2409) These are displayed in the weighted options page as hoverable tooltips. --- WebHostLib/options.py | 24 ++++---- WebHostLib/static/assets/weighted-options.js | 40 +++++++++++-- docs/world api.md | 63 ++++++++++++++++++++ test/bases.py | 21 +++++++ worlds/AutoWorld.py | 35 +++++++++++ worlds/dark_souls_3/Items.py | 8 +++ worlds/dark_souls_3/__init__.py | 3 +- 7 files changed, 177 insertions(+), 17 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 1a2aab6d88..3c0f47f327 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -136,16 +136,20 @@ def create(): option["defaultValue"] = "random" weighted_options["baseOptions"]["game"][game_name] = 0 - weighted_options["games"][game_name] = {} - weighted_options["games"][game_name]["gameSettings"] = game_options - weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names) - weighted_options["games"][game_name]["gameItemGroups"] = [ - group for group in world.item_name_groups.keys() if group != "Everything" - ] - weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names) - weighted_options["games"][game_name]["gameLocationGroups"] = [ - group for group in world.location_name_groups.keys() if group != "Everywhere" - ] + weighted_options["games"][game_name] = { + "gameSettings": game_options, + "gameItems": tuple(world.item_names), + "gameItemGroups": [ + group for group in world.item_name_groups.keys() if group != "Everything" + ], + "gameItemDescriptions": world.item_descriptions, + "gameLocations": tuple(world.location_names), + "gameLocationGroups": [ + group for group in world.location_name_groups.keys() if group != "Everywhere" + ], + "gameLocationDescriptions": world.location_descriptions, + } with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: json.dump(weighted_options, f, indent=2, separators=(',', ': ')) + diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 3811bd42ba..34dfbae4bb 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -1024,12 +1024,18 @@ class GameSettings { // Builds a div for a setting whose value is a list of locations. #buildLocationsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups); + return this.#buildListDiv(setting, this.data.gameLocations, { + groups: this.data.gameLocationGroups, + descriptions: this.data.gameLocationDescriptions, + }); } // Builds a div for a setting whose value is a list of items. #buildItemsDiv(setting) { - return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups); + return this.#buildListDiv(setting, this.data.gameItems, { + groups: this.data.gameItemGroups, + descriptions: this.data.gameItemDescriptions + }); } // Builds a div for a setting named `setting` with a list value that can @@ -1038,12 +1044,15 @@ class GameSettings { // The `groups` option can be a list of additional options for this list // (usually `item_name_groups` or `location_name_groups`) that are displayed // in a special section at the top of the list. - #buildListDiv(setting, items, groups = []) { + // + // The `descriptions` option can be a map from item names or group names to + // descriptions for the user's benefit. + #buildListDiv(setting, items, {groups = [], descriptions = {}} = {}) { const div = document.createElement('div'); div.classList.add('simple-list'); groups.forEach((group) => { - const row = this.#addListRow(setting, group); + const row = this.#addListRow(setting, group, descriptions[group]); div.appendChild(row); }); @@ -1052,7 +1061,7 @@ class GameSettings { } items.forEach((item) => { - const row = this.#addListRow(setting, item); + const row = this.#addListRow(setting, item, descriptions[item]); div.appendChild(row); }); @@ -1060,7 +1069,9 @@ class GameSettings { } // Builds and returns a row for a list of checkboxes. - #addListRow(setting, item) { + // + // If `help` is passed, it's displayed as a help tooltip for this list item. + #addListRow(setting, item, help = undefined) { const row = document.createElement('div'); row.classList.add('list-row'); @@ -1081,6 +1092,23 @@ class GameSettings { const name = document.createElement('span'); name.innerText = item; + + if (help) { + const helpSpan = document.createElement('span'); + helpSpan.classList.add('interactive'); + helpSpan.setAttribute('data-tooltip', help); + helpSpan.innerText = '(?)'; + name.innerText += ' '; + name.appendChild(helpSpan); + + // Put the first 7 tooltips below their rows. CSS tooltips in scrolling + // containers can't be visible outside those containers, so this helps + // ensure they won't be pushed out the top. + if (helpSpan.parentNode.childNodes.length < 7) { + helpSpan.classList.add('tooltip-bottom'); + } + } + label.appendChild(name); row.appendChild(label); diff --git a/docs/world api.md b/docs/world api.md index b128e2b146..9b7573dccd 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -121,6 +121,38 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being required, and will prevent progression and useful items from being placed at excluded locations. +#### Documenting Locations + +Worlds can optionally provide a `location_descriptions` map which contains +human-friendly descriptions of locations or location groups. These descriptions +will show up in location-selection options in the Weighted Options page. Extra +indentation and single newlines will be collapsed into spaces. + +```python +# Locations.py + +location_descriptions = { + "Red Potion #6": "In a secret destructible block under the second stairway", + "L2 Spaceship": """ + The group of all items in the spaceship in Level 2. + + This doesn't include the item on the spaceship door, since it can be + accessed without the Spaeship Key. + """ +} +``` + +```python +# __init__.py + +from worlds.AutoWorld import World +from .Locations import location_descriptions + + +class MyGameWorld(World): + location_descriptions = location_descriptions +``` + ### Items Items are all things that can "drop" for your game. This may be RPG items like @@ -147,6 +179,37 @@ Other classifications include * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that will not be moved around by progression balancing; used, e.g., for currency or tokens +#### Documenting Items + +Worlds can optionally provide an `item_descriptions` map which contains +human-friendly descriptions of items or item groups. These descriptions will +show up in item-selection options in the Weighted Options page. Extra +indentation and single newlines will be collapsed into spaces. + +```python +# Items.py + +item_descriptions = { + "Red Potion": "A standard health potion", + "Spaceship Key": """ + The key to the spaceship in Level 2. + + This is necessary to get to the Star Realm. + """ +} +``` + +```python +# __init__.py + +from worlds.AutoWorld import World +from .Items import item_descriptions + + +class MyGameWorld(World): + item_descriptions = item_descriptions +``` + ### Events Events will mark some progress. You define an event location, an diff --git a/test/bases.py b/test/bases.py index 2054c2d187..3d704579a7 100644 --- a/test/bases.py +++ b/test/bases.py @@ -333,3 +333,24 @@ class WorldTestBase(unittest.TestCase): placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code] self.assertLessEqual(len(self.multiworld.itempool), len(placed_items), "Unplaced Items remaining in itempool") + + def test_descriptions_have_valid_names(self): + """Ensure all item and location descriptions match a name of the corresponding type""" + if not (self.run_default_tests and self.constructed): + return + with self.subTest("Game", game=self.game): + with self.subTest("Items"): + world = self.multiworld.worlds[1] + valid_names = world.item_names.union(world.item_name_groups) + for name in world.item_descriptions.keys(): + with self.subTest("Name should be valid", name=name): + self.assertIn(name, valid_names, + """All item descriptions must match defined item names""") + + with self.subTest("Locations"): + world = self.multiworld.worlds[1] + valid_names = world.location_names.union(world.location_name_groups) + for name in world.location_descriptions.keys(): + with self.subTest("Name should be valid", name=name): + self.assertIn(name, valid_names, + """All item descriptions must match defined item names""") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d05797cf9e..5b4dec8317 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib import logging import pathlib +import re import sys import time from dataclasses import make_dataclass @@ -51,11 +52,17 @@ class AutoWorldRegister(type): dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] + dct["item_descriptions"] = {name: _normalize_description(description) for name, description + in dct.get("item_descriptions", {}).items()} + dct["item_descriptions"]["Everything"] = "All items in the entire game." dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("location_name_groups", {}).items()} dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) + dct["location_descriptions"] = {name: _normalize_description(description) for name, description + in dct.get("location_descriptions", {}).items()} + dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." # move away from get_required_client_version function if "game" in dct: @@ -205,9 +212,23 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" + item_descriptions: ClassVar[Dict[str, str]] = {} + """An optional map from item names (or item group names) to brief descriptions for users. + + Individual newlines and indentation will be collapsed into spaces before these descriptions are + displayed. This may cover only a subset of items. + """ + location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" + location_descriptions: ClassVar[Dict[str, str]] = {} + """An optional map from location names (or location group names) to brief descriptions for users. + + Individual newlines and indentation will be collapsed into spaces before these descriptions are + displayed. This may cover only a subset of locations. + """ + data_version: ClassVar[int] = 0 """ Increment this every time something in your world's names/id mappings changes. @@ -462,3 +483,17 @@ def data_package_checksum(data: "GamesPackage") -> str: assert sorted(data) == list(data), "Data not ordered" from NetUtils import encode return hashlib.sha1(encode(data).encode()).hexdigest() + + +def _normalize_description(description): + """Normalizes a description in item_descriptions or location_descriptions. + + This allows authors to write descritions with nice indentation and line lengths in their world + definitions without having it affect the rendered format. + """ + # First, collapse the whitespace around newlines and the ends of the description. + description = re.sub(r' *\n *', '\n', description.strip()) + # Next, condense individual newlines into spaces. + description = re.sub(r'(? Date: Fri, 10 Nov 2023 22:13:32 -0800 Subject: [PATCH 156/327] WebHost: Sort tracker last activity 'None' as maximum instead of -1 (#2446) When managing an async, it can be useful to sort the tracker by Last Activity to see who has potentially abandoned their slots. Today, if a slot hasn't been started (last activity is None) then it is sorted as if last activity is -1, that it is it has had more recent activity than any other slot. This change makes it so slots that haven't started are treated as if they have last activity MAX_VALUE time ago. This way they get sorted with slots that haven't been touched in a long time which should make intuitive sense as the "last activity" is effectively inf time ago. --- WebHostLib/static/assets/trackerCommon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index 41c4020dac..cb16a4de78 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -55,7 +55,7 @@ window.addEventListener('load', () => { render: function (data, type, row) { if (type === "sort" || type === 'type') { if (data === "None") - return -1; + return Number.MAX_VALUE; return parseInt(data); } From e670ca513bdf808b6d7ba99ceb3c44e6a0a45159 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 11 Nov 2023 10:54:51 +0100 Subject: [PATCH 157/327] Fill: fix swap error found in CI (#2397) * Fill: add test for swap error with item rules https://discord.com/channels/731205301247803413/731214280439103580/1167195750082560121 * Fill: fix swap error found in CI Swap now assumes the unplaced items can be placed before the to-be-swapped item. Unsure if that is safe or unsafe. * Test: clarify docstring and comments in fill swap test * Test: clarify comments in fill swap test more --- Fill.py | 2 +- test/general/test_fill.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index c9660ab708..9fdbcc3843 100644 --- a/Fill.py +++ b/Fill.py @@ -112,7 +112,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: location.item = None placed_item.location = None - swap_state = sweep_from_pool(base_state, [placed_item] if unsafe else []) + swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool) # unsafe means swap_state assumes we can somehow collect placed_item before item_to_place # by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic # to clean that up later, so there is a chance generation fails. diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 1e469ef04d..e454b3e61d 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -442,6 +442,47 @@ class TestFillRestrictive(unittest.TestCase): self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1") self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1") + def test_swap_to_earlier_location_with_item_rule2(self): + """Test that swap works before all items are placed""" + multi_world = generate_multi_world(1) + player1 = generate_player_data(multi_world, 1, 5, 5) + locations = player1.locations[:] # copy required + items = player1.prog_items[:] # copy required + # Two items provide access to sphere 2. + # One of them is forbidden in sphere 1, the other is first placed in sphere 4 because of placement order, + # requiring a swap. + # There are spheres in between, so for the swap to work, it'll have to assume all other items are collected. + one_to_two1 = items[4].name + one_to_two2 = items[3].name + three_to_four = items[2].name + two_to_three1 = items[1].name + two_to_three2 = items[0].name + # Sphere 4 + set_rule(locations[0], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + and state.has(two_to_three1, player1.id) + and state.has(two_to_three2, player1.id) + and state.has(three_to_four, player1.id))) + # Sphere 3 + set_rule(locations[1], lambda state: ((state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + and state.has(two_to_three1, player1.id) + and state.has(two_to_three2, player1.id))) + # Sphere 2 + set_rule(locations[2], lambda state: state.has(one_to_two1, player1.id) or state.has(one_to_two2, player1.id)) + # Sphere 1 + sphere1_loc1 = locations[3] + sphere1_loc2 = locations[4] + # forbid one_to_two2 in sphere 1 to make the swap happen as described above + add_item_rule(sphere1_loc1, lambda item_to_place: item_to_place.name != one_to_two2) + add_item_rule(sphere1_loc2, lambda item_to_place: item_to_place.name != one_to_two2) + + # Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap, + # which it will attempt before two_to_three and three_to_four are placed, testing the behavior. + fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items) + # assert swap happened + self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1") + self.assertTrue(sphere1_loc1.item.name == one_to_two1 or + sphere1_loc2.item.name == one_to_two1, "Wrong item in Sphere 1") + def test_double_sweep(self): """Test that sweep doesn't duplicate Event items when sweeping""" # test for PR1114 From 43041f72920beb20c0f46a67089fe9ab0ebff16f Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 12 Nov 2023 13:39:34 -0800 Subject: [PATCH 158/327] Pokemon Emerald: Implement New Game (#1813) --- .gitignore | 1 + README.md | 1 + docs/CODEOWNERS | 3 + inno_setup.iss | 5 + worlds/pokemon_emerald/LICENSE | 19 + worlds/pokemon_emerald/README.md | 58 + worlds/pokemon_emerald/__init__.py | 882 ++++++ worlds/pokemon_emerald/client.py | 277 ++ worlds/pokemon_emerald/data.py | 995 +++++++ worlds/pokemon_emerald/data/README.md | 99 + .../pokemon_emerald/data/base_patch.bsdiff4 | Bin 0 -> 209743 bytes .../pokemon_emerald/data/extracted_data.json | 1 + worlds/pokemon_emerald/data/items.json | 1481 ++++++++++ worlds/pokemon_emerald/data/locations.json | 1441 +++++++++ .../pokemon_emerald/data/regions/cities.json | 2604 +++++++++++++++++ .../data/regions/dungeons.json | 2231 ++++++++++++++ .../pokemon_emerald/data/regions/routes.json | 1871 ++++++++++++ .../data/regions/unused/battle_frontier.json | 396 +++ .../data/regions/unused/dungeons.json | 52 + .../data/regions/unused/islands.json | 276 ++ .../data/regions/unused/routes.json | 82 + .../docs/en_Pokemon Emerald.md | 78 + worlds/pokemon_emerald/docs/setup_en.md | 72 + worlds/pokemon_emerald/items.py | 77 + worlds/pokemon_emerald/locations.py | 122 + worlds/pokemon_emerald/options.py | 606 ++++ worlds/pokemon_emerald/pokemon.py | 196 ++ worlds/pokemon_emerald/regions.py | 49 + worlds/pokemon_emerald/rom.py | 420 +++ worlds/pokemon_emerald/rules.py | 1368 +++++++++ worlds/pokemon_emerald/sanity_check.py | 352 +++ worlds/pokemon_emerald/test/__init__.py | 5 + .../test/test_accessibility.py | 178 ++ worlds/pokemon_emerald/test/test_warps.py | 21 + worlds/pokemon_emerald/util.py | 19 + 35 files changed, 16338 insertions(+) create mode 100644 worlds/pokemon_emerald/LICENSE create mode 100644 worlds/pokemon_emerald/README.md create mode 100644 worlds/pokemon_emerald/__init__.py create mode 100644 worlds/pokemon_emerald/client.py create mode 100644 worlds/pokemon_emerald/data.py create mode 100644 worlds/pokemon_emerald/data/README.md create mode 100644 worlds/pokemon_emerald/data/base_patch.bsdiff4 create mode 100644 worlds/pokemon_emerald/data/extracted_data.json create mode 100644 worlds/pokemon_emerald/data/items.json create mode 100644 worlds/pokemon_emerald/data/locations.json create mode 100644 worlds/pokemon_emerald/data/regions/cities.json create mode 100644 worlds/pokemon_emerald/data/regions/dungeons.json create mode 100644 worlds/pokemon_emerald/data/regions/routes.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/battle_frontier.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/dungeons.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/islands.json create mode 100644 worlds/pokemon_emerald/data/regions/unused/routes.json create mode 100644 worlds/pokemon_emerald/docs/en_Pokemon Emerald.md create mode 100644 worlds/pokemon_emerald/docs/setup_en.md create mode 100644 worlds/pokemon_emerald/items.py create mode 100644 worlds/pokemon_emerald/locations.py create mode 100644 worlds/pokemon_emerald/options.py create mode 100644 worlds/pokemon_emerald/pokemon.py create mode 100644 worlds/pokemon_emerald/regions.py create mode 100644 worlds/pokemon_emerald/rom.py create mode 100644 worlds/pokemon_emerald/rules.py create mode 100644 worlds/pokemon_emerald/sanity_check.py create mode 100644 worlds/pokemon_emerald/test/__init__.py create mode 100644 worlds/pokemon_emerald/test/test_accessibility.py create mode 100644 worlds/pokemon_emerald/test/test_warps.py create mode 100644 worlds/pokemon_emerald/util.py diff --git a/.gitignore b/.gitignore index f4bcd35c32..aaea45ce98 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.apmc *.apz5 *.aptloz +*.apemerald *.pyc *.pyd *.sfc diff --git a/README.md b/README.md index bcbc885b46..a6a482942e 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Currently, the following games are supported: * DOOM 1993 * Terraria * Lingo +* Pokémon Emerald For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0afc565280..83f4723532 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -95,6 +95,9 @@ # Overcooked! 2 /worlds/overcooked2/ @toasterparty +# Pokemon Emerald +/worlds/pokemon_emerald/ @Zunawe + # Pokemon Red and Blue /worlds/pokemon_rb/ @Alchav diff --git a/inno_setup.iss b/inno_setup.iss index b6f40f7701..d39e2895f4 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -153,6 +153,11 @@ Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Components: client/bizhawk +Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/bizhawk + Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/pokemon_emerald/LICENSE b/worlds/pokemon_emerald/LICENSE new file mode 100644 index 0000000000..30b4f413fe --- /dev/null +++ b/worlds/pokemon_emerald/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Zunawe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/pokemon_emerald/README.md b/worlds/pokemon_emerald/README.md new file mode 100644 index 0000000000..61aee77452 --- /dev/null +++ b/worlds/pokemon_emerald/README.md @@ -0,0 +1,58 @@ +# Pokemon Emerald + +Version 1.2.0 + +This README contains general info useful for understanding the world. Pretty much all the long lists of locations, +regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check +[data/README.md](data/README.md) for more detailed information on the JSON files holding most of the data. + +## Warps + +Quick note to start, you should not be defining or modifying encoded warps from this repository. They're encoded in the +source code repository for the mod, and then assigned to regions in `data/regions/`. All warps in the game already exist +within `extracted_data.json`, and all relevant warps are already placed in `data/regions/` (unless they were deleted +accidentally). + +Many warps are actually two or three events acting as one logical warp. Doorways, for example, are often 2 tiles wide +indoors but only 1 tile wide outdoors. Both indoor warps point to the outdoor warp, and the outdoor warp points to only +one of the indoor warps. We want to describe warps logically in a way that retains information about individual warp +events. That way a 2-tile-wide doorway doesnt look like a one-way warp next to an unrelated two-way warp, but if we want +to randomize the destinations of those warps, we can still get back each individual id of the multi-tile warp. + +This is how warps are encoded: + +`{source_map}:{source_warp_ids}/{dest_map}:{dest_warp_ids}[!]` + +- `source_map`: The map the warp events are located in +- `source_warp_ids`: The ids of all adjacent warp events in source_map which lead to the same destination (these must be +in ascending order) +- `dest_map`: The map of the warp event to which this one is connected +- `dest_warp_ids`: The ids of the warp events in dest_map +- `[!]`: If the warp expects to lead to a destination which doesnot lead back to it, add a ! to the end + +Example: `MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4` + +Example 2: `MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!` + +Note: A warp must have its destination set to another warp event. However, that does not guarantee that the destination +warp event will warp back to the source. + +Note 2: Some warps _only_ act as destinations and cannot actually be interacted with by the player as sources. These are +usually places you fall from a hole above. At the time of writing, these are actually not accounted for, but there are +no instances where it changes logical access. + +Note 3: Some warp destinations go to the map `MAP_DYNAMIC` and have a special warp id. These edge cases are: + +- The Moving Truck +- Terra Cave +- Marine Cave +- The Department Store Elevator +- Secret Bases +- The Trade Center +- The Union Room +- The Record Corner +- 2P/4P Battle Colosseum + +Note 4: The trick house on Route 110 changes the warp destinations of its entrance and ending room as you progress +through the puzzles, but the source code only sets the trick house up for the first puzzle, and I assume the destination +gets overwritten at run time when certain flags are set. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py new file mode 100644 index 0000000000..d3ced5f3ca --- /dev/null +++ b/worlds/pokemon_emerald/__init__.py @@ -0,0 +1,882 @@ +""" +Archipelago World definition for Pokemon Emerald Version +""" +from collections import Counter +import copy +import logging +import os +from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar + +from BaseClasses import ItemClassification, MultiWorld, Tutorial +from Fill import FillError, fill_restrictive +from Options import Toggle +import settings +from worlds.AutoWorld import WebWorld, World + +from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient +from .data import (SpeciesData, MapData, EncounterTableData, LearnsetMove, TrainerPokemonData, StaticEncounterData, + TrainerData, data as emerald_data) +from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, + offset_item_value) +from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, + create_locations_with_tags) +from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms, + RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility, + HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions) +from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type +from .regions import create_regions +from .rom import PokemonEmeraldDeltaPatch, generate_output, location_visited_event_to_id_map +from .rules import set_rules +from .sanity_check import validate_regions +from .util import int_to_bool_array, bool_array_to_int + + +class PokemonEmeraldWebWorld(WebWorld): + """ + Webhost info for Pokemon Emerald + """ + theme = "ocean" + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to playing Pokémon Emerald with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Zunawe"] + ) + + tutorials = [setup_en] + + +class PokemonEmeraldSettings(settings.Group): + class PokemonEmeraldRomFile(settings.UserFilePath): + """File name of your English Pokemon Emerald ROM""" + description = "Pokemon Emerald ROM File" + copy_to = "Pokemon - Emerald Version (USA, Europe).gba" + md5s = [PokemonEmeraldDeltaPatch.hash] + + rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to) + + +class PokemonEmeraldWorld(World): + """ + Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise. + Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots + of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion! + """ + game = "Pokemon Emerald" + web = PokemonEmeraldWebWorld() + topology_present = True + + settings_key = "pokemon_emerald_settings" + settings: ClassVar[PokemonEmeraldSettings] + + options_dataclass = PokemonEmeraldOptions + options: PokemonEmeraldOptions + + item_name_to_id = create_item_label_to_code_map() + location_name_to_id = create_location_label_to_id_map() + item_name_groups = ITEM_GROUPS + location_name_groups = LOCATION_GROUPS + + data_version = 1 + required_client_version = (0, 4, 3) + + badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None + hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None + free_fly_location_id: int = 0 + + modified_species: List[Optional[SpeciesData]] + modified_maps: List[MapData] + modified_tmhm_moves: List[int] + modified_static_encounters: List[int] + modified_starters: Tuple[int, int, int] + modified_trainers: List[TrainerData] + + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld) -> None: + if not os.path.exists(cls.settings.rom_file): + raise FileNotFoundError(cls.settings.rom_file) + + assert validate_regions() + + def get_filler_item_name(self) -> str: + return "Great Ball" + + def generate_early(self) -> None: + # If badges or HMs are vanilla, Norman locks you from using Surf, which means you're not guaranteed to be + # able to reach Fortree Gym, Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those gyms to + # challenge Norman or it creates a circular dependency. + # This is never a problem for completely random badges/hms because the algo will not place Surf/Balance Badge + # on Norman on its own. It's never a problem for shuffled badges/hms because there is no scenario where Cut or + # the Stone Badge can be a lynchpin for access to any gyms, so they can always be put on Norman in a worst case + # scenario. + # This will also be a problem in warp rando if direct access to Norman's room requires Surf or if access + # any gym leader in general requires Surf. We will probably have to force this to 0 in that case. + max_norman_count = 7 + + if self.options.badges == RandomizeBadges.option_vanilla: + max_norman_count = 4 + + if self.options.hms == RandomizeHms.option_vanilla: + if self.options.norman_requirement == NormanRequirement.option_badges: + if self.options.badges != RandomizeBadges.option_completely_random: + max_norman_count = 4 + if self.options.norman_requirement == NormanRequirement.option_gyms: + max_norman_count = 4 + + if self.options.norman_count.value > max_norman_count: + logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with " + "other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player)) + self.options.norman_count.value = max_norman_count + + def create_regions(self) -> None: + regions = create_regions(self) + + tags = {"Badge", "HM", "KeyItem", "Rod", "Bike"} + if self.options.overworld_items: + tags.add("OverworldItem") + if self.options.hidden_items: + tags.add("HiddenItem") + if self.options.npc_gifts: + tags.add("NpcGift") + if self.options.enable_ferry: + tags.add("Ferry") + create_locations_with_tags(self, regions, tags) + + self.multiworld.regions.extend(regions.values()) + + def create_items(self) -> None: + item_locations: List[PokemonEmeraldLocation] = [ + location + for location in self.multiworld.get_locations(self.player) + if location.address is not None + ] + + # Filter progression items which shouldn't be shuffled into the itempool. Their locations + # still exist, but event items will be placed and locked at their vanilla locations instead. + filter_tags = set() + + if not self.options.key_items: + filter_tags.add("KeyItem") + if not self.options.rods: + filter_tags.add("Rod") + if not self.options.bikes: + filter_tags.add("Bike") + + if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}: + filter_tags.add("Badge") + if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}: + filter_tags.add("HM") + + if self.options.badges == RandomizeBadges.option_shuffle: + self.badge_shuffle_info = [ + (location, self.create_item_by_code(location.default_item_code)) + for location in [l for l in item_locations if "Badge" in l.tags] + ] + if self.options.hms == RandomizeHms.option_shuffle: + self.hm_shuffle_info = [ + (location, self.create_item_by_code(location.default_item_code)) + for location in [l for l in item_locations if "HM" in l.tags] + ] + + item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0] + default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations] + + if self.options.item_pool_type == ItemPoolType.option_shuffled: + self.multiworld.itempool += default_itempool + + elif self.options.item_pool_type in {ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced}: + item_categories = ["Ball", "Heal", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc"] + + # Count occurrences of types of vanilla items in pool + item_category_counter = Counter() + for item in default_itempool: + if not item.advancement: + item_category_counter.update([tag for tag in item.tags if tag in item_categories]) + + item_category_weights = [item_category_counter.get(category) for category in item_categories] + item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights] + + # Create lists of item codes that can be used to fill + fill_item_candidates = emerald_data.items.values() + + fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags] + + fill_item_candidates_by_category = {category: [] for category in item_categories} + for item_data in fill_item_candidates: + for category in item_categories: + if category in item_data.tags: + fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id)) + + for category in fill_item_candidates_by_category: + fill_item_candidates_by_category[category].sort() + + # Ignore vanilla occurrences and pick completely randomly + if self.options.item_pool_type == ItemPoolType.option_diverse: + item_category_weights = [ + len(category_list) + for category_list in fill_item_candidates_by_category.values() + ] + + # TMs should not have duplicates until every TM has been used already + all_tm_choices = fill_item_candidates_by_category["TM"].copy() + + def refresh_tm_choices() -> None: + fill_item_candidates_by_category["TM"] = all_tm_choices.copy() + self.random.shuffle(fill_item_candidates_by_category["TM"]) + + # Create items + for item in default_itempool: + if not item.advancement and "Unique" not in item.tags: + category = self.random.choices(item_categories, item_category_weights)[0] + if category == "TM": + if len(fill_item_candidates_by_category["TM"]) == 0: + refresh_tm_choices() + item_code = fill_item_candidates_by_category["TM"].pop() + else: + item_code = self.random.choice(fill_item_candidates_by_category[category]) + item = self.create_item_by_code(item_code) + + self.multiworld.itempool.append(item) + + def set_rules(self) -> None: + set_rules(self) + + def generate_basic(self) -> None: + locations: List[PokemonEmeraldLocation] = self.multiworld.get_locations(self.player) + + # Set our free fly location + # If not enabled, set it to Littleroot Town by default + fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN" + if self.options.free_fly_location: + fly_location_name = self.random.choice([ + "EVENT_VISITED_SLATEPORT_CITY", + "EVENT_VISITED_MAUVILLE_CITY", + "EVENT_VISITED_VERDANTURF_TOWN", + "EVENT_VISITED_FALLARBOR_TOWN", + "EVENT_VISITED_LAVARIDGE_TOWN", + "EVENT_VISITED_FORTREE_CITY", + "EVENT_VISITED_LILYCOVE_CITY", + "EVENT_VISITED_MOSSDEEP_CITY", + "EVENT_VISITED_SOOTOPOLIS_CITY", + "EVENT_VISITED_EVER_GRANDE_CITY" + ]) + + self.free_fly_location_id = location_visited_event_to_id_map[fly_location_name] + + free_fly_location_location = self.multiworld.get_location("FREE_FLY_LOCATION", self.player) + free_fly_location_location.item = None + free_fly_location_location.place_locked_item(self.create_event(fly_location_name)) + + # Key items which are considered in access rules but not randomized are converted to events and placed + # in their vanilla locations so that the player can have them in their inventory for logic. + def convert_unrandomized_items_to_events(tag: str) -> None: + for location in locations: + if location.tags is not None and tag in location.tags: + location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code])) + location.address = None + + if self.options.badges == RandomizeBadges.option_vanilla: + convert_unrandomized_items_to_events("Badge") + if self.options.hms == RandomizeHms.option_vanilla: + convert_unrandomized_items_to_events("HM") + if not self.options.rods: + convert_unrandomized_items_to_events("Rod") + if not self.options.bikes: + convert_unrandomized_items_to_events("Bike") + if not self.options.key_items: + convert_unrandomized_items_to_events("KeyItem") + + def pre_fill(self) -> None: + # Items which are shuffled between their own locations + if self.options.badges == RandomizeBadges.option_shuffle: + badge_locations: List[PokemonEmeraldLocation] + badge_items: List[PokemonEmeraldItem] + + # Sort order makes `fill_restrictive` try to place important badges later, which + # makes it less likely to have to swap at all, and more likely for swaps to work. + # In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms, + # so Knuckle Badge deserves highest priority if Flash is logically required. + badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)] + badge_priority = { + "Knuckle Badge": 0 if (self.options.hms == RandomizeHms.option_vanilla and self.options.require_flash) else 3, + "Balance Badge": 1, + "Dynamo Badge": 1, + "Mind Badge": 2, + "Heat Badge": 2, + "Rain Badge": 3, + "Stone Badge": 4, + "Feather Badge": 5 + } + badge_items.sort(key=lambda item: badge_priority.get(item.name, 0)) + + collection_state = self.multiworld.get_all_state(False) + if self.hm_shuffle_info is not None: + for _, item in self.hm_shuffle_info: + collection_state.collect(item) + + # In specific very constrained conditions, fill_restrictive may run + # out of swaps before it finds a valid solution if it gets unlucky. + # This is a band-aid until fill/swap can reliably find those solutions. + attempts_remaining = 2 + while attempts_remaining > 0: + attempts_remaining -= 1 + self.random.shuffle(badge_locations) + try: + fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items, + single_player_placement=True, lock=True, allow_excluded=True) + break + except FillError as exc: + if attempts_remaining == 0: + raise exc + + logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.") + continue + + if self.options.hms == RandomizeHms.option_shuffle: + hm_locations: List[PokemonEmeraldLocation] + hm_items: List[PokemonEmeraldItem] + + # Sort order makes `fill_restrictive` try to place important HMs later, which + # makes it less likely to have to swap at all, and more likely for swaps to work. + # In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms, + # so Flash deserves highest priority if it's logically required. + hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)] + hm_priority = { + "HM05 Flash": 0 if (self.options.badges == RandomizeBadges.option_vanilla and self.options.require_flash) else 3, + "HM03 Surf": 1, + "HM06 Rock Smash": 1, + "HM08 Dive": 2, + "HM04 Strength": 2, + "HM07 Waterfall": 3, + "HM01 Cut": 4, + "HM02 Fly": 5 + } + hm_items.sort(key=lambda item: hm_priority.get(item.name, 0)) + + collection_state = self.multiworld.get_all_state(False) + + # In specific very constrained conditions, fill_restrictive may run + # out of swaps before it finds a valid solution if it gets unlucky. + # This is a band-aid until fill/swap can reliably find those solutions. + attempts_remaining = 2 + while attempts_remaining > 0: + attempts_remaining -= 1 + self.random.shuffle(hm_locations) + try: + fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items, + single_player_placement=True, lock=True, allow_excluded=True) + break + except FillError as exc: + if attempts_remaining == 0: + raise exc + + logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.") + continue + + def generate_output(self, output_directory: str) -> None: + def randomize_abilities() -> None: + # Creating list of potential abilities + ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in emerald_data.abilities} + + ability_blacklist_labels = {"cacophony"} + option_ability_blacklist = self.options.ability_blacklist.value + if option_ability_blacklist is not None: + ability_blacklist_labels |= {ability_label.lower() for ability_label in option_ability_blacklist} + + ability_blacklist = {ability_label_to_value[label] for label in ability_blacklist_labels} + ability_whitelist = [a.ability_id for a in emerald_data.abilities if a.ability_id not in ability_blacklist] + + if self.options.abilities == RandomizeAbilities.option_follow_evolutions: + already_modified: Set[int] = set() + + # Loops through species and only tries to modify abilities if the pokemon has no pre-evolution + # or if the pre-evolution has already been modified. Then tries to modify all species that evolve + # from this one which have the same abilities. + # The outer while loop only runs three times for vanilla ordering: Once for a first pass, once for + # Hitmonlee/Hitmonchan, and once to verify that there's nothing left to do. + while True: + had_clean_pass = True + for species in self.modified_species: + if species is None: + continue + if species.species_id in already_modified: + continue + if species.pre_evolution is not None and species.pre_evolution not in already_modified: + continue + + had_clean_pass = False + + old_abilities = species.abilities + new_abilities = ( + 0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist), + 0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist) + ) + + evolutions = [species] + while len(evolutions) > 0: + evolution = evolutions.pop() + if evolution.abilities == old_abilities: + evolution.abilities = new_abilities + already_modified.add(evolution.species_id) + evolutions += [ + self.modified_species[evolution.species_id] + for evolution in evolution.evolutions + if evolution.species_id not in already_modified + ] + + if had_clean_pass: + break + else: # Not following evolutions + for species in self.modified_species: + if species is None: + continue + + old_abilities = species.abilities + new_abilities = ( + 0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist), + 0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist) + ) + + species.abilities = new_abilities + + def randomize_types() -> None: + if self.options.types == RandomizeTypes.option_shuffle: + type_map = list(range(18)) + self.random.shuffle(type_map) + + # We never want to map to the ??? type, so swap whatever index maps to ??? with ??? + # So ??? will always map to itself, and there are no pokemon which have the ??? type + mystery_type_index = type_map.index(9) + type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index] + + for species in self.modified_species: + if species is not None: + species.types = (type_map[species.types[0]], type_map[species.types[1]]) + elif self.options.types == RandomizeTypes.option_completely_random: + for species in self.modified_species: + if species is not None: + new_type_1 = get_random_type(self.random) + new_type_2 = new_type_1 + if species.types[0] != species.types[1]: + while new_type_1 == new_type_2: + new_type_2 = get_random_type(self.random) + + species.types = (new_type_1, new_type_2) + elif self.options.types == RandomizeTypes.option_follow_evolutions: + already_modified: Set[int] = set() + + # Similar to follow evolutions for abilities, but only needs to loop through once. + # For every pokemon without a pre-evolution, generates a random mapping from old types to new types + # and then walks through the evolution tree applying that map. This means that evolutions that share + # types will have those types mapped to the same new types, and evolutions with new or diverging types + # will still have new or diverging types. + # Consider: + # - Charmeleon (Fire/Fire) -> Charizard (Fire/Flying) + # - Onyx (Rock/Ground) -> Steelix (Steel/Ground) + # - Nincada (Bug/Ground) -> Ninjask (Bug/Flying) && Shedinja (Bug/Ghost) + # - Azurill (Normal/Normal) -> Marill (Water/Water) + for species in self.modified_species: + if species is None: + continue + if species.species_id in already_modified: + continue + if species.pre_evolution is not None and species.pre_evolution not in already_modified: + continue + + type_map = list(range(18)) + self.random.shuffle(type_map) + + # We never want to map to the ??? type, so swap whatever index maps to ??? with ??? + # So ??? will always map to itself, and there are no pokemon which have the ??? type + mystery_type_index = type_map.index(9) + type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index] + + evolutions = [species] + while len(evolutions) > 0: + evolution = evolutions.pop() + evolution.types = (type_map[evolution.types[0]], type_map[evolution.types[1]]) + already_modified.add(evolution.species_id) + evolutions += [self.modified_species[evo.species_id] for evo in evolution.evolutions] + + def randomize_learnsets() -> None: + type_bias = self.options.move_match_type_bias.value + normal_bias = self.options.move_normal_type_bias.value + + for species in self.modified_species: + if species is None: + continue + + old_learnset = species.learnset + new_learnset: List[LearnsetMove] = [] + + i = 0 + # Replace filler MOVE_NONEs at start of list + while old_learnset[i].move_id == 0: + if self.options.level_up_moves == LevelUpMoves.option_start_with_four_moves: + new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias, + normal_bias, species.types) + else: + new_move = 0 + new_learnset.append(LearnsetMove(old_learnset[i].level, new_move)) + i += 1 + + while i < len(old_learnset): + # Guarantees the starter has a good damaging move + if i == 3: + new_move = get_random_damaging_move(self.random, {move.move_id for move in new_learnset}) + else: + new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias, + normal_bias, species.types) + new_learnset.append(LearnsetMove(old_learnset[i].level, new_move)) + i += 1 + + species.learnset = new_learnset + + def randomize_tm_hm_compatibility() -> None: + for species in self.modified_species: + if species is None: + continue + + combatibility_array = int_to_bool_array(species.tm_hm_compatibility) + + # TMs + for i in range(0, 50): + if self.options.tm_compatibility == TmCompatibility.option_fully_compatible: + combatibility_array[i] = True + elif self.options.tm_compatibility == TmCompatibility.option_completely_random: + combatibility_array[i] = self.random.choice([True, False]) + + # HMs + for i in range(50, 58): + if self.options.hm_compatibility == HmCompatibility.option_fully_compatible: + combatibility_array[i] = True + elif self.options.hm_compatibility == HmCompatibility.option_completely_random: + combatibility_array[i] = self.random.choice([True, False]) + + species.tm_hm_compatibility = bool_array_to_int(combatibility_array) + + def randomize_tm_moves() -> None: + new_moves: Set[int] = set() + + for i in range(50): + new_move = get_random_move(self.random, new_moves) + new_moves.add(new_move) + self.modified_tmhm_moves[i] = new_move + + def randomize_wild_encounters() -> None: + should_match_bst = self.options.wild_pokemon in { + RandomizeWildPokemon.option_match_base_stats, + RandomizeWildPokemon.option_match_base_stats_and_type + } + should_match_type = self.options.wild_pokemon in { + RandomizeWildPokemon.option_match_type, + RandomizeWildPokemon.option_match_base_stats_and_type + } + should_allow_legendaries = self.options.allow_wild_legendaries == Toggle.option_true + + for map_data in self.modified_maps: + new_encounters: List[Optional[EncounterTableData]] = [None, None, None] + old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters] + + for i, table in enumerate(old_encounters): + if table is not None: + species_old_to_new_map: Dict[int, int] = {} + for species_id in table.slots: + if species_id not in species_old_to_new_map: + original_species = emerald_data.species[species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + species_old_to_new_map[species_id] = get_random_species( + self.random, + self.modified_species, + target_bst, + target_type, + should_allow_legendaries + ).species_id + + new_slots: List[int] = [] + for species_id in table.slots: + new_slots.append(species_old_to_new_map[species_id]) + + new_encounters[i] = EncounterTableData(new_slots, table.rom_address) + + map_data.land_encounters = new_encounters[0] + map_data.water_encounters = new_encounters[1] + map_data.fishing_encounters = new_encounters[2] + + def randomize_static_encounters() -> None: + if self.options.static_encounters == RandomizeStaticEncounters.option_shuffle: + shuffled_species = [encounter.species_id for encounter in emerald_data.static_encounters] + self.random.shuffle(shuffled_species) + + self.modified_static_encounters = [] + for i, encounter in enumerate(emerald_data.static_encounters): + self.modified_static_encounters.append(StaticEncounterData( + shuffled_species[i], + encounter.rom_address + )) + else: + should_match_bst = self.options.static_encounters in { + RandomizeStaticEncounters.option_match_base_stats, + RandomizeStaticEncounters.option_match_base_stats_and_type + } + should_match_type = self.options.static_encounters in { + RandomizeStaticEncounters.option_match_type, + RandomizeStaticEncounters.option_match_base_stats_and_type + } + + for encounter in emerald_data.static_encounters: + original_species = self.modified_species[encounter.species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + self.modified_static_encounters.append(StaticEncounterData( + get_random_species(self.random, self.modified_species, target_bst, target_type).species_id, + encounter.rom_address + )) + + def randomize_opponent_parties() -> None: + should_match_bst = self.options.trainer_parties in { + RandomizeTrainerParties.option_match_base_stats, + RandomizeTrainerParties.option_match_base_stats_and_type + } + should_match_type = self.options.trainer_parties in { + RandomizeTrainerParties.option_match_type, + RandomizeTrainerParties.option_match_base_stats_and_type + } + allow_legendaries = self.options.allow_trainer_legendaries == Toggle.option_true + + per_species_tmhm_moves: Dict[int, List[int]] = {} + + for trainer in self.modified_trainers: + new_party = [] + for pokemon in trainer.party.pokemon: + original_species = emerald_data.species[pokemon.species_id] + target_bst = sum(original_species.base_stats) if should_match_bst else None + target_type = self.random.choice(original_species.types) if should_match_type else None + + new_species = get_random_species( + self.random, + self.modified_species, + target_bst, + target_type, + allow_legendaries + ) + + if new_species.species_id not in per_species_tmhm_moves: + per_species_tmhm_moves[new_species.species_id] = list({ + self.modified_tmhm_moves[i] + for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility)) + if is_compatible + }) + + tm_hm_movepool = per_species_tmhm_moves[new_species.species_id] + level_up_movepool = list({ + move.move_id + for move in new_species.learnset + if move.level <= pokemon.level + }) + + new_moves = ( + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool), + self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool) + ) + + new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + + trainer.party.pokemon = new_party + + def randomize_starters() -> None: + match_bst = self.options.starters in { + RandomizeStarters.option_match_base_stats, + RandomizeStarters.option_match_base_stats_and_type + } + match_type = self.options.starters in { + RandomizeStarters.option_match_type, + RandomizeStarters.option_match_base_stats_and_type + } + allow_legendaries = self.options.allow_starter_legendaries == Toggle.option_true + + starter_bsts = ( + sum(emerald_data.species[emerald_data.starters[0]].base_stats) if match_bst else None, + sum(emerald_data.species[emerald_data.starters[1]].base_stats) if match_bst else None, + sum(emerald_data.species[emerald_data.starters[2]].base_stats) if match_bst else None + ) + + starter_types = ( + self.random.choice(emerald_data.species[emerald_data.starters[0]].types) if match_type else None, + self.random.choice(emerald_data.species[emerald_data.starters[1]].types) if match_type else None, + self.random.choice(emerald_data.species[emerald_data.starters[2]].types) if match_type else None + ) + + new_starters = ( + get_random_species(self.random, self.modified_species, + starter_bsts[0], starter_types[0], allow_legendaries), + get_random_species(self.random, self.modified_species, + starter_bsts[1], starter_types[1], allow_legendaries), + get_random_species(self.random, self.modified_species, + starter_bsts[2], starter_types[2], allow_legendaries) + ) + + egg_code = self.options.easter_egg.value + egg_check_1 = 0 + egg_check_2 = 0 + + for i in egg_code: + egg_check_1 += ord(i) + egg_check_2 += egg_check_1 * egg_check_1 + + egg = 96 + egg_check_2 - (egg_check_1 * 0x077C) + if egg_check_2 == 0x14E03A and egg < 411 and egg > 0 and egg not in range(252, 277): + self.modified_starters = (egg, egg, egg) + else: + self.modified_starters = ( + new_starters[0].species_id, + new_starters[1].species_id, + new_starters[2].species_id + ) + + # Putting the unchosen starter onto the rival's team + rival_teams: List[List[Tuple[str, int, bool]]] = [ + [ + ("TRAINER_BRENDAN_ROUTE_103_TREECKO", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_TREECKO", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_TREECKO", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_TREECKO", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_TREECKO", 3, True ), + ("TRAINER_MAY_ROUTE_103_TREECKO", 0, False), + ("TRAINER_MAY_RUSTBORO_TREECKO", 1, False), + ("TRAINER_MAY_ROUTE_110_TREECKO", 2, True ), + ("TRAINER_MAY_ROUTE_119_TREECKO", 2, True ), + ("TRAINER_MAY_LILYCOVE_TREECKO", 3, True ) + ], + [ + ("TRAINER_BRENDAN_ROUTE_103_TORCHIC", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_TORCHIC", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_TORCHIC", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_TORCHIC", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_TORCHIC", 3, True ), + ("TRAINER_MAY_ROUTE_103_TORCHIC", 0, False), + ("TRAINER_MAY_RUSTBORO_TORCHIC", 1, False), + ("TRAINER_MAY_ROUTE_110_TORCHIC", 2, True ), + ("TRAINER_MAY_ROUTE_119_TORCHIC", 2, True ), + ("TRAINER_MAY_LILYCOVE_TORCHIC", 3, True ) + ], + [ + ("TRAINER_BRENDAN_ROUTE_103_MUDKIP", 0, False), + ("TRAINER_BRENDAN_RUSTBORO_MUDKIP", 1, False), + ("TRAINER_BRENDAN_ROUTE_110_MUDKIP", 2, True ), + ("TRAINER_BRENDAN_ROUTE_119_MUDKIP", 2, True ), + ("TRAINER_BRENDAN_LILYCOVE_MUDKIP", 3, True ), + ("TRAINER_MAY_ROUTE_103_MUDKIP", 0, False), + ("TRAINER_MAY_RUSTBORO_MUDKIP", 1, False), + ("TRAINER_MAY_ROUTE_110_MUDKIP", 2, True ), + ("TRAINER_MAY_ROUTE_119_MUDKIP", 2, True ), + ("TRAINER_MAY_LILYCOVE_MUDKIP", 3, True ) + ] + ] + + for i, starter in enumerate([new_starters[1], new_starters[2], new_starters[0]]): + potential_evolutions = [evolution.species_id for evolution in starter.evolutions] + picked_evolution = starter.species_id + if len(potential_evolutions) > 0: + picked_evolution = self.random.choice(potential_evolutions) + + for trainer_name, starter_position, is_evolved in rival_teams[i]: + trainer_data = self.modified_trainers[emerald_data.constants[trainer_name]] + trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + + self.modified_species = copy.deepcopy(emerald_data.species) + self.modified_trainers = copy.deepcopy(emerald_data.trainers) + self.modified_maps = copy.deepcopy(emerald_data.maps) + self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves) + self.modified_static_encounters = copy.deepcopy(emerald_data.static_encounters) + self.modified_starters = copy.deepcopy(emerald_data.starters) + + # Randomize species data + if self.options.abilities != RandomizeAbilities.option_vanilla: + randomize_abilities() + + if self.options.types != RandomizeTypes.option_vanilla: + randomize_types() + + if self.options.level_up_moves != LevelUpMoves.option_vanilla: + randomize_learnsets() + + randomize_tm_hm_compatibility() # Options are checked within this function + + min_catch_rate = min(self.options.min_catch_rate.value, 255) + for species in self.modified_species: + if species is not None: + species.catch_rate = max(species.catch_rate, min_catch_rate) + + if self.options.tm_moves: + randomize_tm_moves() + + # Randomize wild encounters + if self.options.wild_pokemon != RandomizeWildPokemon.option_vanilla: + randomize_wild_encounters() + + # Randomize static encounters + if self.options.static_encounters != RandomizeStaticEncounters.option_vanilla: + randomize_static_encounters() + + # Randomize opponents + if self.options.trainer_parties != RandomizeTrainerParties.option_vanilla: + randomize_opponent_parties() + + # Randomize starters + if self.options.starters != RandomizeStarters.option_vanilla: + randomize_starters() + + generate_output(self, output_directory) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict( + "goal", + "badges", + "hms", + "key_items", + "bikes", + "rods", + "overworld_items", + "hidden_items", + "npc_gifts", + "require_itemfinder", + "require_flash", + "enable_ferry", + "elite_four_requirement", + "elite_four_count", + "norman_requirement", + "norman_count", + "extra_boulders", + "remove_roadblocks", + "free_fly_location", + "fly_without_badge", + ) + slot_data["free_fly_location_id"] = self.free_fly_location_id + return slot_data + + def create_item(self, name: str) -> PokemonEmeraldItem: + return self.create_item_by_code(self.item_name_to_id[name]) + + def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem: + return PokemonEmeraldItem( + self.item_id_to_name[item_code], + get_item_classification(item_code), + item_code, + self.player + ) + + def create_event(self, name: str) -> PokemonEmeraldItem: + return PokemonEmeraldItem( + name, + ItemClassification.progression, + None, + self.player + ) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py new file mode 100644 index 0000000000..5420b15fbe --- /dev/null +++ b/worlds/pokemon_emerald/client.py @@ -0,0 +1,277 @@ +from typing import TYPE_CHECKING, Dict, Set + +from NetUtils import ClientStatus +import worlds._bizhawk as bizhawk +from worlds._bizhawk.client import BizHawkClient + +from .data import BASE_OFFSET, data +from .options import Goal + +if TYPE_CHECKING: + from worlds._bizhawk.context import BizHawkClientContext + + +EXPECTED_ROM_NAME = "pokemon emerald version / AP 2" + +IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"] +DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"] +DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"] + +# These flags are communicated to the tracker as a bitfield using this order. +# Modifying the order will cause undetectable autotracking issues. +TRACKER_EVENT_FLAGS = [ + "FLAG_DEFEATED_RUSTBORO_GYM", + "FLAG_DEFEATED_DEWFORD_GYM", + "FLAG_DEFEATED_MAUVILLE_GYM", + "FLAG_DEFEATED_LAVARIDGE_GYM", + "FLAG_DEFEATED_PETALBURG_GYM", + "FLAG_DEFEATED_FORTREE_GYM", + "FLAG_DEFEATED_MOSSDEEP_GYM", + "FLAG_DEFEATED_SOOTOPOLIS_GYM", + "FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone + "FLAG_DELIVERED_STEVEN_LETTER", + "FLAG_DELIVERED_DEVON_GOODS", + "FLAG_HIDE_ROUTE_119_TEAM_AQUA", # Clear Weather Institute + "FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite + "FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout + "FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine + "FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE", # Clear Aqua Hideout + "FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE", # Clear Space Center + "FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN", + "FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis + "FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt) + "FLAG_IS_CHAMPION", + "FLAG_PURCHASED_HARBOR_MAIL" +] +EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS} + +KEY_LOCATION_FLAGS = [ + "NPC_GIFT_RECEIVED_HM01", + "NPC_GIFT_RECEIVED_HM02", + "NPC_GIFT_RECEIVED_HM03", + "NPC_GIFT_RECEIVED_HM04", + "NPC_GIFT_RECEIVED_HM05", + "NPC_GIFT_RECEIVED_HM06", + "NPC_GIFT_RECEIVED_HM07", + "NPC_GIFT_RECEIVED_HM08", + "NPC_GIFT_RECEIVED_ACRO_BIKE", + "NPC_GIFT_RECEIVED_WAILMER_PAIL", + "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL", + "NPC_GIFT_RECEIVED_LETTER", + "NPC_GIFT_RECEIVED_METEORITE", + "NPC_GIFT_RECEIVED_GO_GOGGLES", + "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON", + "NPC_GIFT_RECEIVED_ITEMFINDER", + "NPC_GIFT_RECEIVED_DEVON_SCOPE", + "NPC_GIFT_RECEIVED_MAGMA_EMBLEM", + "NPC_GIFT_RECEIVED_POKEBLOCK_CASE", + "NPC_GIFT_RECEIVED_SS_TICKET", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY", + "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY", + "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_4_SCANNER", + "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY", + "NPC_GIFT_RECEIVED_OLD_ROD", + "NPC_GIFT_RECEIVED_GOOD_ROD", + "NPC_GIFT_RECEIVED_SUPER_ROD", +] +KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS} + + +class PokemonEmeraldClient(BizHawkClient): + game = "Pokemon Emerald" + system = "GBA" + patch_suffix = ".apemerald" + local_checked_locations: Set[int] + local_set_events: Dict[str, bool] + local_found_key_items: Dict[str, bool] + goal_flag: int + + def __init__(self) -> None: + super().__init__() + self.local_checked_locations = set() + self.local_set_events = {} + self.local_found_key_items = {} + self.goal_flag = IS_CHAMPION_FLAG + + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: + from CommonClient import logger + + try: + # Check ROM name/patch version + rom_name_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x108, 32, "ROM")]))[0]) + rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode("ascii") + if not rom_name.startswith("pokemon emerald version"): + return False + if rom_name == "pokemon emerald version": + logger.info("ERROR: You appear to be running an unpatched version of Pokemon Emerald. " + "You need to generate a patch file and use it to create a patched ROM.") + return False + if rom_name != EXPECTED_ROM_NAME: + logger.info("ERROR: The patch file used to create this ROM is not compatible with " + "this client. Double check your client version against the version being " + "used by the generator.") + return False + except UnicodeDecodeError: + return False + except bizhawk.RequestFailedError: + return False # Should verify on the next pass + + ctx.game = self.game + ctx.items_handling = 0b001 + ctx.want_slot_data = True + ctx.watcher_timeout = 0.125 + + return True + + async def set_auth(self, ctx: "BizHawkClientContext") -> None: + slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 64, "ROM")]))[0] + ctx.auth = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8") + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + if ctx.slot_data is not None: + if ctx.slot_data["goal"] == Goal.option_champion: + self.goal_flag = IS_CHAMPION_FLAG + elif ctx.slot_data["goal"] == Goal.option_steven: + self.goal_flag = DEFEATED_STEVEN_FLAG + elif ctx.slot_data["goal"] == Goal.option_norman: + self.goal_flag = DEFEATED_NORMAN_FLAG + + try: + # Checks that the player is in the overworld + overworld_guard = (data.ram_addresses["gMain"] + 4, (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"), "System Bus") + + # Read save block address + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus")], + [overworld_guard] + ) + if read_result is None: # Not in overworld + return + + # Checks that the save block hasn't moved + save_block_address_guard = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus") + + save_block_address = int.from_bytes(read_result[0], "little") + + # Handle giving the player items + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [ + (save_block_address + 0x3778, 2, "System Bus"), # Number of received items + (data.ram_addresses["gArchipelagoReceivedItem"] + 4, 1, "System Bus") # Received item struct full? + ], + [overworld_guard, save_block_address_guard] + ) + if read_result is None: # Not in overworld, or save block moved + return + + num_received_items = int.from_bytes(read_result[0], "little") + received_item_is_empty = read_result[1][0] == 0 + + # If the game hasn't received all items yet and the received item struct doesn't contain an item, then + # fill it with the next item + if num_received_items < len(ctx.items_received) and received_item_is_empty: + next_item = ctx.items_received[num_received_items] + await bizhawk.write(ctx.bizhawk_ctx, [ + (data.ram_addresses["gArchipelagoReceivedItem"] + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"), + (data.ram_addresses["gArchipelagoReceivedItem"] + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"), + (data.ram_addresses["gArchipelagoReceivedItem"] + 4, [1], "System Bus"), # Mark struct full + (data.ram_addresses["gArchipelagoReceivedItem"] + 5, [next_item.flags & 1], "System Bus"), + ]) + + # Read flags in 2 chunks + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(save_block_address + 0x1450, 0x96, "System Bus")], # Flags + [overworld_guard, save_block_address_guard] + ) + if read_result is None: # Not in overworld, or save block moved + return + + flag_bytes = read_result[0] + + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [(save_block_address + 0x14E6, 0x96, "System Bus")], # Flags + [overworld_guard, save_block_address_guard] + ) + if read_result is not None: + flag_bytes += read_result[0] + + game_clear = False + local_checked_locations = set() + local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS} + local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS} + + # Check set flags + for byte_i, byte in enumerate(flag_bytes): + for i in range(8): + if byte & (1 << i) != 0: + flag_id = byte_i * 8 + i + + location_id = flag_id + BASE_OFFSET + if location_id in ctx.server_locations: + local_checked_locations.add(location_id) + + if flag_id == self.goal_flag: + game_clear = True + + if flag_id in EVENT_FLAG_MAP: + local_set_events[EVENT_FLAG_MAP[flag_id]] = True + + if flag_id in KEY_LOCATION_FLAG_MAP: + local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True + + # Send locations + if local_checked_locations != self.local_checked_locations: + self.local_checked_locations = local_checked_locations + + if local_checked_locations is not None: + await ctx.send_msgs([{ + "cmd": "LocationChecks", + "locations": list(local_checked_locations) + }]) + + # Send game clear + if not ctx.finished_game and game_clear: + await ctx.send_msgs([{ + "cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL + }]) + + # Send tracker event flags + if local_set_events != self.local_set_events and ctx.slot is not None: + event_bitfield = 0 + for i, flag_name in enumerate(TRACKER_EVENT_FLAGS): + if local_set_events[flag_name]: + event_bitfield |= 1 << i + + await ctx.send_msgs([{ + "cmd": "Set", + "key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [{"operation": "replace", "value": event_bitfield}] + }]) + self.local_set_events = local_set_events + + if local_found_key_items != self.local_found_key_items: + key_bitfield = 0 + for i, location_name in enumerate(KEY_LOCATION_FLAGS): + if local_found_key_items[location_name]: + key_bitfield |= 1 << i + + await ctx.send_msgs([{ + "cmd": "Set", + "key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}", + "default": 0, + "want_reply": False, + "operations": [{"operation": "replace", "value": key_bitfield}] + }]) + self.local_found_key_items = local_found_key_items + except bizhawk.RequestFailedError: + # Exit handler and return to main loop to reconnect + pass diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py new file mode 100644 index 0000000000..bc51d84963 --- /dev/null +++ b/worlds/pokemon_emerald/data.py @@ -0,0 +1,995 @@ +""" +Pulls data from JSON files in worlds/pokemon_emerald/data/ into classes. +This also includes marrying automatically extracted data with manually +defined data (like location labels or usable pokemon species), some cleanup +and sorting, and Warp methods. +""" +from dataclasses import dataclass +import copy +from enum import IntEnum +import orjson +from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union +import pkgutil +import pkg_resources + +from BaseClasses import ItemClassification + + +BASE_OFFSET = 3860000 + + +class Warp: + """ + Represents warp events in the game like doorways or warp pads + """ + is_one_way: bool + source_map: str + source_ids: List[int] + dest_map: str + dest_ids: List[int] + parent_region: Optional[str] + + def __init__(self, encoded_string: Optional[str] = None, parent_region: Optional[str] = None) -> None: + if encoded_string is not None: + decoded_warp = Warp.decode(encoded_string) + self.is_one_way = decoded_warp.is_one_way + self.source_map = decoded_warp.source_map + self.source_ids = decoded_warp.source_ids + self.dest_map = decoded_warp.dest_map + self.dest_ids = decoded_warp.dest_ids + self.parent_region = parent_region + + def encode(self) -> str: + """ + Returns a string encoding of this warp + """ + source_ids_string = "" + for source_id in self.source_ids: + source_ids_string += str(source_id) + "," + source_ids_string = source_ids_string[:-1] # Remove last "," + + dest_ids_string = "" + for dest_id in self.dest_ids: + dest_ids_string += str(dest_id) + "," + dest_ids_string = dest_ids_string[:-1] # Remove last "," + + return f"{self.source_map}:{source_ids_string}/{self.dest_map}:{dest_ids_string}{'!' if self.is_one_way else ''}" + + def connects_to(self, other: 'Warp') -> bool: + """ + Returns true if this warp sends the player to `other` + """ + return self.dest_map == other.source_map and set(self.dest_ids) <= set(other.source_ids) + + @staticmethod + def decode(encoded_string: str) -> 'Warp': + """ + Create a Warp object from an encoded string + """ + warp = Warp() + warp.is_one_way = encoded_string.endswith("!") + if warp.is_one_way: + encoded_string = encoded_string[:-1] + + warp_source, warp_dest = encoded_string.split("/") + warp_source_map, warp_source_indices = warp_source.split(":") + warp_dest_map, warp_dest_indices = warp_dest.split(":") + + warp.source_map = warp_source_map + warp.dest_map = warp_dest_map + + warp.source_ids = [int(index) for index in warp_source_indices.split(",")] + warp.dest_ids = [int(index) for index in warp_dest_indices.split(",")] + + return warp + + +class ItemData(NamedTuple): + label: str + item_id: int + classification: ItemClassification + tags: FrozenSet[str] + + +class LocationData(NamedTuple): + name: str + label: str + parent_region: str + default_item: int + rom_address: int + flag: int + tags: FrozenSet[str] + + +class EventData(NamedTuple): + name: str + parent_region: str + + +class RegionData: + name: str + exits: List[str] + warps: List[str] + locations: List[str] + events: List[EventData] + + def __init__(self, name: str): + self.name = name + self.exits = [] + self.warps = [] + self.locations = [] + self.events = [] + + +class BaseStats(NamedTuple): + hp: int + attack: int + defense: int + speed: int + special_attack: int + special_defense: int + + +class LearnsetMove(NamedTuple): + level: int + move_id: int + + +class EvolutionMethodEnum(IntEnum): + LEVEL = 0 + LEVEL_ATK_LT_DEF = 1 + LEVEL_ATK_EQ_DEF = 2 + LEVEL_ATK_GT_DEF = 3 + LEVEL_SILCOON = 4 + LEVEL_CASCOON = 5 + LEVEL_NINJASK = 6 + LEVEL_SHEDINJA = 7 + ITEM = 8 + FRIENDSHIP = 9 + FRIENDSHIP_DAY = 10 + FRIENDSHIP_NIGHT = 11 + + +def _str_to_evolution_method(string: str) -> EvolutionMethodEnum: + if string == "LEVEL": + return EvolutionMethodEnum.LEVEL + if string == "LEVEL_ATK_LT_DEF": + return EvolutionMethodEnum.LEVEL_ATK_LT_DEF + if string == "LEVEL_ATK_EQ_DEF": + return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF + if string == "LEVEL_ATK_GT_DEF": + return EvolutionMethodEnum.LEVEL_ATK_GT_DEF + if string == "LEVEL_SILCOON": + return EvolutionMethodEnum.LEVEL_SILCOON + if string == "LEVEL_CASCOON": + return EvolutionMethodEnum.LEVEL_CASCOON + if string == "LEVEL_NINJASK": + return EvolutionMethodEnum.LEVEL_NINJASK + if string == "LEVEL_SHEDINJA": + return EvolutionMethodEnum.LEVEL_SHEDINJA + if string == "FRIENDSHIP": + return EvolutionMethodEnum.FRIENDSHIP + if string == "FRIENDSHIP_DAY": + return EvolutionMethodEnum.FRIENDSHIP_DAY + if string == "FRIENDSHIP_NIGHT": + return EvolutionMethodEnum.FRIENDSHIP_NIGHT + + +class EvolutionData(NamedTuple): + method: EvolutionMethodEnum + param: int + species_id: int + + +class StaticEncounterData(NamedTuple): + species_id: int + rom_address: int + + +@dataclass +class SpeciesData: + name: str + label: str + species_id: int + base_stats: BaseStats + types: Tuple[int, int] + abilities: Tuple[int, int] + evolutions: List[EvolutionData] + pre_evolution: Optional[int] + catch_rate: int + learnset: List[LearnsetMove] + tm_hm_compatibility: int + learnset_rom_address: int + rom_address: int + + +class AbilityData(NamedTuple): + ability_id: int + label: str + + +class EncounterTableData(NamedTuple): + slots: List[int] + rom_address: int + + +@dataclass +class MapData: + name: str + land_encounters: Optional[EncounterTableData] + water_encounters: Optional[EncounterTableData] + fishing_encounters: Optional[EncounterTableData] + + +class TrainerPokemonDataTypeEnum(IntEnum): + NO_ITEM_DEFAULT_MOVES = 0 + ITEM_DEFAULT_MOVES = 1 + NO_ITEM_CUSTOM_MOVES = 2 + ITEM_CUSTOM_MOVES = 3 + + +def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: + if string == "NO_ITEM_DEFAULT_MOVES": + return TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES + if string == "ITEM_DEFAULT_MOVES": + return TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES + if string == "NO_ITEM_CUSTOM_MOVES": + return TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES + if string == "ITEM_CUSTOM_MOVES": + return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES + + +@dataclass +class TrainerPokemonData: + species_id: int + level: int + moves: Optional[Tuple[int, int, int, int]] + + +@dataclass +class TrainerPartyData: + pokemon: List[TrainerPokemonData] + pokemon_data_type: TrainerPokemonDataTypeEnum + rom_address: int + + +@dataclass +class TrainerData: + trainer_id: int + party: TrainerPartyData + rom_address: int + battle_script_rom_address: int + + +class PokemonEmeraldData: + starters: Tuple[int, int, int] + constants: Dict[str, int] + ram_addresses: Dict[str, int] + rom_addresses: Dict[str, int] + regions: Dict[str, RegionData] + locations: Dict[str, LocationData] + items: Dict[int, ItemData] + species: List[Optional[SpeciesData]] + static_encounters: List[StaticEncounterData] + tmhm_moves: List[int] + abilities: List[AbilityData] + maps: List[MapData] + warps: Dict[str, Warp] + warp_map: Dict[str, Optional[str]] + trainers: List[TrainerData] + + def __init__(self) -> None: + self.starters = (277, 280, 283) + self.constants = {} + self.ram_addresses = {} + self.rom_addresses = {} + self.regions = {} + self.locations = {} + self.items = {} + self.species = [] + self.static_encounters = [] + self.tmhm_moves = [] + self.abilities = [] + self.maps = [] + self.warps = {} + self.warp_map = {} + self.trainers = [] + + +def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]: + return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode('utf-8-sig')) + + +data = PokemonEmeraldData() + +def create_data_copy() -> PokemonEmeraldData: + new_copy = PokemonEmeraldData() + new_copy.species = copy.deepcopy(data.species) + new_copy.tmhm_moves = copy.deepcopy(data.tmhm_moves) + new_copy.maps = copy.deepcopy(data.maps) + new_copy.static_encounters = copy.deepcopy(data.static_encounters) + new_copy.trainers = copy.deepcopy(data.trainers) + + +def _init() -> None: + extracted_data: Dict[str, Any] = load_json_data("extracted_data.json") + data.constants = extracted_data["constants"] + data.ram_addresses = extracted_data["misc_ram_addresses"] + data.rom_addresses = extracted_data["misc_rom_addresses"] + + location_attributes_json = load_json_data("locations.json") + + # Load/merge region json files + region_json_list = [] + for file in pkg_resources.resource_listdir(__name__, "data/regions"): + if not pkg_resources.resource_isdir(__name__, "data/regions/" + file): + region_json_list.append(load_json_data("regions/" + file)) + + regions_json = {} + for region_subset in region_json_list: + for region_name, region_json in region_subset.items(): + if region_name in regions_json: + raise AssertionError("Region [{region_name}] was defined multiple times") + regions_json[region_name] = region_json + + # Create region data + claimed_locations: Set[str] = set() + claimed_warps: Set[str] = set() + + data.regions = {} + for region_name, region_json in regions_json.items(): + new_region = RegionData(region_name) + + # Locations + for location_name in region_json["locations"]: + if location_name in claimed_locations: + raise AssertionError(f"Location [{location_name}] was claimed by multiple regions") + + location_json = extracted_data["locations"][location_name] + new_location = LocationData( + location_name, + location_attributes_json[location_name]["label"], + region_name, + location_json["default_item"], + location_json["rom_address"], + location_json["flag"], + frozenset(location_attributes_json[location_name]["tags"]) + ) + new_region.locations.append(location_name) + data.locations[location_name] = new_location + claimed_locations.add(location_name) + + new_region.locations.sort() + + # Events + for event in region_json["events"]: + new_region.events.append(EventData(event, region_name)) + + # Exits + for region_exit in region_json["exits"]: + new_region.exits.append(region_exit) + + # Warps + for encoded_warp in region_json["warps"]: + if encoded_warp in claimed_warps: + raise AssertionError(f"Warp [{encoded_warp}] was claimed by multiple regions") + new_region.warps.append(encoded_warp) + data.warps[encoded_warp] = Warp(encoded_warp, region_name) + claimed_warps.add(encoded_warp) + + new_region.warps.sort() + + data.regions[region_name] = new_region + + # Create item data + items_json = load_json_data("items.json") + + data.items = {} + for item_constant_name, attributes in items_json.items(): + item_classification = None + if attributes["classification"] == "PROGRESSION": + item_classification = ItemClassification.progression + elif attributes["classification"] == "USEFUL": + item_classification = ItemClassification.useful + elif attributes["classification"] == "FILLER": + item_classification = ItemClassification.filler + elif attributes["classification"] == "TRAP": + item_classification = ItemClassification.trap + else: + raise ValueError(f"Unknown classification {attributes['classification']} for item {item_constant_name}") + + data.items[data.constants[item_constant_name]] = ItemData( + attributes["label"], + data.constants[item_constant_name], + item_classification, + frozenset(attributes["tags"]) + ) + + # Create species data + + # Excludes extras like copies of Unown and special species values like SPECIES_EGG. + all_species: List[Tuple[str, str]] = [ + ("SPECIES_BULBASAUR", "Bulbasaur"), + ("SPECIES_IVYSAUR", "Ivysaur"), + ("SPECIES_VENUSAUR", "Venusaur"), + ("SPECIES_CHARMANDER", "Charmander"), + ("SPECIES_CHARMELEON", "Charmeleon"), + ("SPECIES_CHARIZARD", "Charizard"), + ("SPECIES_SQUIRTLE", "Squirtle"), + ("SPECIES_WARTORTLE", "Wartortle"), + ("SPECIES_BLASTOISE", "Blastoise"), + ("SPECIES_CATERPIE", "Caterpie"), + ("SPECIES_METAPOD", "Metapod"), + ("SPECIES_BUTTERFREE", "Butterfree"), + ("SPECIES_WEEDLE", "Weedle"), + ("SPECIES_KAKUNA", "Kakuna"), + ("SPECIES_BEEDRILL", "Beedrill"), + ("SPECIES_PIDGEY", "Pidgey"), + ("SPECIES_PIDGEOTTO", "Pidgeotto"), + ("SPECIES_PIDGEOT", "Pidgeot"), + ("SPECIES_RATTATA", "Rattata"), + ("SPECIES_RATICATE", "Raticate"), + ("SPECIES_SPEAROW", "Spearow"), + ("SPECIES_FEAROW", "Fearow"), + ("SPECIES_EKANS", "Ekans"), + ("SPECIES_ARBOK", "Arbok"), + ("SPECIES_PIKACHU", "Pikachu"), + ("SPECIES_RAICHU", "Raichu"), + ("SPECIES_SANDSHREW", "Sandshrew"), + ("SPECIES_SANDSLASH", "Sandslash"), + ("SPECIES_NIDORAN_F", "Nidoran Female"), + ("SPECIES_NIDORINA", "Nidorina"), + ("SPECIES_NIDOQUEEN", "Nidoqueen"), + ("SPECIES_NIDORAN_M", "Nidoran Male"), + ("SPECIES_NIDORINO", "Nidorino"), + ("SPECIES_NIDOKING", "Nidoking"), + ("SPECIES_CLEFAIRY", "Clefairy"), + ("SPECIES_CLEFABLE", "Clefable"), + ("SPECIES_VULPIX", "Vulpix"), + ("SPECIES_NINETALES", "Ninetales"), + ("SPECIES_JIGGLYPUFF", "Jigglypuff"), + ("SPECIES_WIGGLYTUFF", "Wigglytuff"), + ("SPECIES_ZUBAT", "Zubat"), + ("SPECIES_GOLBAT", "Golbat"), + ("SPECIES_ODDISH", "Oddish"), + ("SPECIES_GLOOM", "Gloom"), + ("SPECIES_VILEPLUME", "Vileplume"), + ("SPECIES_PARAS", "Paras"), + ("SPECIES_PARASECT", "Parasect"), + ("SPECIES_VENONAT", "Venonat"), + ("SPECIES_VENOMOTH", "Venomoth"), + ("SPECIES_DIGLETT", "Diglett"), + ("SPECIES_DUGTRIO", "Dugtrio"), + ("SPECIES_MEOWTH", "Meowth"), + ("SPECIES_PERSIAN", "Persian"), + ("SPECIES_PSYDUCK", "Psyduck"), + ("SPECIES_GOLDUCK", "Golduck"), + ("SPECIES_MANKEY", "Mankey"), + ("SPECIES_PRIMEAPE", "Primeape"), + ("SPECIES_GROWLITHE", "Growlithe"), + ("SPECIES_ARCANINE", "Arcanine"), + ("SPECIES_POLIWAG", "Poliwag"), + ("SPECIES_POLIWHIRL", "Poliwhirl"), + ("SPECIES_POLIWRATH", "Poliwrath"), + ("SPECIES_ABRA", "Abra"), + ("SPECIES_KADABRA", "Kadabra"), + ("SPECIES_ALAKAZAM", "Alakazam"), + ("SPECIES_MACHOP", "Machop"), + ("SPECIES_MACHOKE", "Machoke"), + ("SPECIES_MACHAMP", "Machamp"), + ("SPECIES_BELLSPROUT", "Bellsprout"), + ("SPECIES_WEEPINBELL", "Weepinbell"), + ("SPECIES_VICTREEBEL", "Victreebel"), + ("SPECIES_TENTACOOL", "Tentacool"), + ("SPECIES_TENTACRUEL", "Tentacruel"), + ("SPECIES_GEODUDE", "Geodude"), + ("SPECIES_GRAVELER", "Graveler"), + ("SPECIES_GOLEM", "Golem"), + ("SPECIES_PONYTA", "Ponyta"), + ("SPECIES_RAPIDASH", "Rapidash"), + ("SPECIES_SLOWPOKE", "Slowpoke"), + ("SPECIES_SLOWBRO", "Slowbro"), + ("SPECIES_MAGNEMITE", "Magnemite"), + ("SPECIES_MAGNETON", "Magneton"), + ("SPECIES_FARFETCHD", "Farfetch'd"), + ("SPECIES_DODUO", "Doduo"), + ("SPECIES_DODRIO", "Dodrio"), + ("SPECIES_SEEL", "Seel"), + ("SPECIES_DEWGONG", "Dewgong"), + ("SPECIES_GRIMER", "Grimer"), + ("SPECIES_MUK", "Muk"), + ("SPECIES_SHELLDER", "Shellder"), + ("SPECIES_CLOYSTER", "Cloyster"), + ("SPECIES_GASTLY", "Gastly"), + ("SPECIES_HAUNTER", "Haunter"), + ("SPECIES_GENGAR", "Gengar"), + ("SPECIES_ONIX", "Onix"), + ("SPECIES_DROWZEE", "Drowzee"), + ("SPECIES_HYPNO", "Hypno"), + ("SPECIES_KRABBY", "Krabby"), + ("SPECIES_KINGLER", "Kingler"), + ("SPECIES_VOLTORB", "Voltorb"), + ("SPECIES_ELECTRODE", "Electrode"), + ("SPECIES_EXEGGCUTE", "Exeggcute"), + ("SPECIES_EXEGGUTOR", "Exeggutor"), + ("SPECIES_CUBONE", "Cubone"), + ("SPECIES_MAROWAK", "Marowak"), + ("SPECIES_HITMONLEE", "Hitmonlee"), + ("SPECIES_HITMONCHAN", "Hitmonchan"), + ("SPECIES_LICKITUNG", "Lickitung"), + ("SPECIES_KOFFING", "Koffing"), + ("SPECIES_WEEZING", "Weezing"), + ("SPECIES_RHYHORN", "Rhyhorn"), + ("SPECIES_RHYDON", "Rhydon"), + ("SPECIES_CHANSEY", "Chansey"), + ("SPECIES_TANGELA", "Tangela"), + ("SPECIES_KANGASKHAN", "Kangaskhan"), + ("SPECIES_HORSEA", "Horsea"), + ("SPECIES_SEADRA", "Seadra"), + ("SPECIES_GOLDEEN", "Goldeen"), + ("SPECIES_SEAKING", "Seaking"), + ("SPECIES_STARYU", "Staryu"), + ("SPECIES_STARMIE", "Starmie"), + ("SPECIES_MR_MIME", "Mr. Mime"), + ("SPECIES_SCYTHER", "Scyther"), + ("SPECIES_JYNX", "Jynx"), + ("SPECIES_ELECTABUZZ", "Electabuzz"), + ("SPECIES_MAGMAR", "Magmar"), + ("SPECIES_PINSIR", "Pinsir"), + ("SPECIES_TAUROS", "Tauros"), + ("SPECIES_MAGIKARP", "Magikarp"), + ("SPECIES_GYARADOS", "Gyarados"), + ("SPECIES_LAPRAS", "Lapras"), + ("SPECIES_DITTO", "Ditto"), + ("SPECIES_EEVEE", "Eevee"), + ("SPECIES_VAPOREON", "Vaporeon"), + ("SPECIES_JOLTEON", "Jolteon"), + ("SPECIES_FLAREON", "Flareon"), + ("SPECIES_PORYGON", "Porygon"), + ("SPECIES_OMANYTE", "Omanyte"), + ("SPECIES_OMASTAR", "Omastar"), + ("SPECIES_KABUTO", "Kabuto"), + ("SPECIES_KABUTOPS", "Kabutops"), + ("SPECIES_AERODACTYL", "Aerodactyl"), + ("SPECIES_SNORLAX", "Snorlax"), + ("SPECIES_ARTICUNO", "Articuno"), + ("SPECIES_ZAPDOS", "Zapdos"), + ("SPECIES_MOLTRES", "Moltres"), + ("SPECIES_DRATINI", "Dratini"), + ("SPECIES_DRAGONAIR", "Dragonair"), + ("SPECIES_DRAGONITE", "Dragonite"), + ("SPECIES_MEWTWO", "Mewtwo"), + ("SPECIES_MEW", "Mew"), + ("SPECIES_CHIKORITA", "Chikorita"), + ("SPECIES_BAYLEEF", "Bayleaf"), + ("SPECIES_MEGANIUM", "Meganium"), + ("SPECIES_CYNDAQUIL", "Cindaquil"), + ("SPECIES_QUILAVA", "Quilava"), + ("SPECIES_TYPHLOSION", "Typhlosion"), + ("SPECIES_TOTODILE", "Totodile"), + ("SPECIES_CROCONAW", "Croconaw"), + ("SPECIES_FERALIGATR", "Feraligatr"), + ("SPECIES_SENTRET", "Sentret"), + ("SPECIES_FURRET", "Furret"), + ("SPECIES_HOOTHOOT", "Hoothoot"), + ("SPECIES_NOCTOWL", "Noctowl"), + ("SPECIES_LEDYBA", "Ledyba"), + ("SPECIES_LEDIAN", "Ledian"), + ("SPECIES_SPINARAK", "Spinarak"), + ("SPECIES_ARIADOS", "Ariados"), + ("SPECIES_CROBAT", "Crobat"), + ("SPECIES_CHINCHOU", "Chinchou"), + ("SPECIES_LANTURN", "Lanturn"), + ("SPECIES_PICHU", "Pichu"), + ("SPECIES_CLEFFA", "Cleffa"), + ("SPECIES_IGGLYBUFF", "Igglybuff"), + ("SPECIES_TOGEPI", "Togepi"), + ("SPECIES_TOGETIC", "Togetic"), + ("SPECIES_NATU", "Natu"), + ("SPECIES_XATU", "Xatu"), + ("SPECIES_MAREEP", "Mareep"), + ("SPECIES_FLAAFFY", "Flaafy"), + ("SPECIES_AMPHAROS", "Ampharos"), + ("SPECIES_BELLOSSOM", "Bellossom"), + ("SPECIES_MARILL", "Marill"), + ("SPECIES_AZUMARILL", "Azumarill"), + ("SPECIES_SUDOWOODO", "Sudowoodo"), + ("SPECIES_POLITOED", "Politoed"), + ("SPECIES_HOPPIP", "Hoppip"), + ("SPECIES_SKIPLOOM", "Skiploom"), + ("SPECIES_JUMPLUFF", "Jumpluff"), + ("SPECIES_AIPOM", "Aipom"), + ("SPECIES_SUNKERN", "Sunkern"), + ("SPECIES_SUNFLORA", "Sunflora"), + ("SPECIES_YANMA", "Yanma"), + ("SPECIES_WOOPER", "Wooper"), + ("SPECIES_QUAGSIRE", "Quagsire"), + ("SPECIES_ESPEON", "Espeon"), + ("SPECIES_UMBREON", "Umbreon"), + ("SPECIES_MURKROW", "Murkrow"), + ("SPECIES_SLOWKING", "Slowking"), + ("SPECIES_MISDREAVUS", "Misdreavus"), + ("SPECIES_UNOWN", "Unown"), + ("SPECIES_WOBBUFFET", "Wobbuffet"), + ("SPECIES_GIRAFARIG", "Girafarig"), + ("SPECIES_PINECO", "Pineco"), + ("SPECIES_FORRETRESS", "Forretress"), + ("SPECIES_DUNSPARCE", "Dunsparce"), + ("SPECIES_GLIGAR", "Gligar"), + ("SPECIES_STEELIX", "Steelix"), + ("SPECIES_SNUBBULL", "Snubbull"), + ("SPECIES_GRANBULL", "Granbull"), + ("SPECIES_QWILFISH", "Qwilfish"), + ("SPECIES_SCIZOR", "Scizor"), + ("SPECIES_SHUCKLE", "Shuckle"), + ("SPECIES_HERACROSS", "Heracross"), + ("SPECIES_SNEASEL", "Sneasel"), + ("SPECIES_TEDDIURSA", "Teddiursa"), + ("SPECIES_URSARING", "Ursaring"), + ("SPECIES_SLUGMA", "Slugma"), + ("SPECIES_MAGCARGO", "Magcargo"), + ("SPECIES_SWINUB", "Swinub"), + ("SPECIES_PILOSWINE", "Piloswine"), + ("SPECIES_CORSOLA", "Corsola"), + ("SPECIES_REMORAID", "Remoraid"), + ("SPECIES_OCTILLERY", "Octillery"), + ("SPECIES_DELIBIRD", "Delibird"), + ("SPECIES_MANTINE", "Mantine"), + ("SPECIES_SKARMORY", "Skarmory"), + ("SPECIES_HOUNDOUR", "Houndour"), + ("SPECIES_HOUNDOOM", "Houndoom"), + ("SPECIES_KINGDRA", "Kingdra"), + ("SPECIES_PHANPY", "Phanpy"), + ("SPECIES_DONPHAN", "Donphan"), + ("SPECIES_PORYGON2", "Porygon2"), + ("SPECIES_STANTLER", "Stantler"), + ("SPECIES_SMEARGLE", "Smeargle"), + ("SPECIES_TYROGUE", "Tyrogue"), + ("SPECIES_HITMONTOP", "Hitmontop"), + ("SPECIES_SMOOCHUM", "Smoochum"), + ("SPECIES_ELEKID", "Elekid"), + ("SPECIES_MAGBY", "Magby"), + ("SPECIES_MILTANK", "Miltank"), + ("SPECIES_BLISSEY", "Blissey"), + ("SPECIES_RAIKOU", "Raikou"), + ("SPECIES_ENTEI", "Entei"), + ("SPECIES_SUICUNE", "Suicune"), + ("SPECIES_LARVITAR", "Larvitar"), + ("SPECIES_PUPITAR", "Pupitar"), + ("SPECIES_TYRANITAR", "Tyranitar"), + ("SPECIES_LUGIA", "Lugia"), + ("SPECIES_HO_OH", "Ho-oh"), + ("SPECIES_CELEBI", "Celebi"), + ("SPECIES_TREECKO", "Treecko"), + ("SPECIES_GROVYLE", "Grovyle"), + ("SPECIES_SCEPTILE", "Sceptile"), + ("SPECIES_TORCHIC", "Torchic"), + ("SPECIES_COMBUSKEN", "Combusken"), + ("SPECIES_BLAZIKEN", "Blaziken"), + ("SPECIES_MUDKIP", "Mudkip"), + ("SPECIES_MARSHTOMP", "Marshtomp"), + ("SPECIES_SWAMPERT", "Swampert"), + ("SPECIES_POOCHYENA", "Poochyena"), + ("SPECIES_MIGHTYENA", "Mightyena"), + ("SPECIES_ZIGZAGOON", "Zigzagoon"), + ("SPECIES_LINOONE", "Linoon"), + ("SPECIES_WURMPLE", "Wurmple"), + ("SPECIES_SILCOON", "Silcoon"), + ("SPECIES_BEAUTIFLY", "Beautifly"), + ("SPECIES_CASCOON", "Cascoon"), + ("SPECIES_DUSTOX", "Dustox"), + ("SPECIES_LOTAD", "Lotad"), + ("SPECIES_LOMBRE", "Lombre"), + ("SPECIES_LUDICOLO", "Ludicolo"), + ("SPECIES_SEEDOT", "Seedot"), + ("SPECIES_NUZLEAF", "Nuzleaf"), + ("SPECIES_SHIFTRY", "Shiftry"), + ("SPECIES_NINCADA", "Nincada"), + ("SPECIES_NINJASK", "Ninjask"), + ("SPECIES_SHEDINJA", "Shedinja"), + ("SPECIES_TAILLOW", "Taillow"), + ("SPECIES_SWELLOW", "Swellow"), + ("SPECIES_SHROOMISH", "Shroomish"), + ("SPECIES_BRELOOM", "Breloom"), + ("SPECIES_SPINDA", "Spinda"), + ("SPECIES_WINGULL", "Wingull"), + ("SPECIES_PELIPPER", "Pelipper"), + ("SPECIES_SURSKIT", "Surskit"), + ("SPECIES_MASQUERAIN", "Masquerain"), + ("SPECIES_WAILMER", "Wailmer"), + ("SPECIES_WAILORD", "Wailord"), + ("SPECIES_SKITTY", "Skitty"), + ("SPECIES_DELCATTY", "Delcatty"), + ("SPECIES_KECLEON", "Kecleon"), + ("SPECIES_BALTOY", "Baltoy"), + ("SPECIES_CLAYDOL", "Claydol"), + ("SPECIES_NOSEPASS", "Nosepass"), + ("SPECIES_TORKOAL", "Torkoal"), + ("SPECIES_SABLEYE", "Sableye"), + ("SPECIES_BARBOACH", "Barboach"), + ("SPECIES_WHISCASH", "Whiscash"), + ("SPECIES_LUVDISC", "Luvdisc"), + ("SPECIES_CORPHISH", "Corphish"), + ("SPECIES_CRAWDAUNT", "Crawdaunt"), + ("SPECIES_FEEBAS", "Feebas"), + ("SPECIES_MILOTIC", "Milotic"), + ("SPECIES_CARVANHA", "Carvanha"), + ("SPECIES_SHARPEDO", "Sharpedo"), + ("SPECIES_TRAPINCH", "Trapinch"), + ("SPECIES_VIBRAVA", "Vibrava"), + ("SPECIES_FLYGON", "Flygon"), + ("SPECIES_MAKUHITA", "Makuhita"), + ("SPECIES_HARIYAMA", "Hariyama"), + ("SPECIES_ELECTRIKE", "Electrike"), + ("SPECIES_MANECTRIC", "Manectric"), + ("SPECIES_NUMEL", "Numel"), + ("SPECIES_CAMERUPT", "Camerupt"), + ("SPECIES_SPHEAL", "Spheal"), + ("SPECIES_SEALEO", "Sealeo"), + ("SPECIES_WALREIN", "Walrein"), + ("SPECIES_CACNEA", "Cacnea"), + ("SPECIES_CACTURNE", "Cacturne"), + ("SPECIES_SNORUNT", "Snorunt"), + ("SPECIES_GLALIE", "Glalie"), + ("SPECIES_LUNATONE", "Lunatone"), + ("SPECIES_SOLROCK", "Solrock"), + ("SPECIES_AZURILL", "Azurill"), + ("SPECIES_SPOINK", "Spoink"), + ("SPECIES_GRUMPIG", "Grumpig"), + ("SPECIES_PLUSLE", "Plusle"), + ("SPECIES_MINUN", "Minun"), + ("SPECIES_MAWILE", "Mawile"), + ("SPECIES_MEDITITE", "Meditite"), + ("SPECIES_MEDICHAM", "Medicham"), + ("SPECIES_SWABLU", "Swablu"), + ("SPECIES_ALTARIA", "Altaria"), + ("SPECIES_WYNAUT", "Wynaut"), + ("SPECIES_DUSKULL", "Duskull"), + ("SPECIES_DUSCLOPS", "Dusclops"), + ("SPECIES_ROSELIA", "Roselia"), + ("SPECIES_SLAKOTH", "Slakoth"), + ("SPECIES_VIGOROTH", "Vigoroth"), + ("SPECIES_SLAKING", "Slaking"), + ("SPECIES_GULPIN", "Gulpin"), + ("SPECIES_SWALOT", "Swalot"), + ("SPECIES_TROPIUS", "Tropius"), + ("SPECIES_WHISMUR", "Whismur"), + ("SPECIES_LOUDRED", "Loudred"), + ("SPECIES_EXPLOUD", "Exploud"), + ("SPECIES_CLAMPERL", "Clamperl"), + ("SPECIES_HUNTAIL", "Huntail"), + ("SPECIES_GOREBYSS", "Gorebyss"), + ("SPECIES_ABSOL", "Absol"), + ("SPECIES_SHUPPET", "Shuppet"), + ("SPECIES_BANETTE", "Banette"), + ("SPECIES_SEVIPER", "Seviper"), + ("SPECIES_ZANGOOSE", "Zangoose"), + ("SPECIES_RELICANTH", "Relicanth"), + ("SPECIES_ARON", "Aron"), + ("SPECIES_LAIRON", "Lairon"), + ("SPECIES_AGGRON", "Aggron"), + ("SPECIES_CASTFORM", "Castform"), + ("SPECIES_VOLBEAT", "Volbeat"), + ("SPECIES_ILLUMISE", "Illumise"), + ("SPECIES_LILEEP", "Lileep"), + ("SPECIES_CRADILY", "Cradily"), + ("SPECIES_ANORITH", "Anorith"), + ("SPECIES_ARMALDO", "Armaldo"), + ("SPECIES_RALTS", "Ralts"), + ("SPECIES_KIRLIA", "Kirlia"), + ("SPECIES_GARDEVOIR", "Gardevoir"), + ("SPECIES_BAGON", "Bagon"), + ("SPECIES_SHELGON", "Shelgon"), + ("SPECIES_SALAMENCE", "Salamence"), + ("SPECIES_BELDUM", "Beldum"), + ("SPECIES_METANG", "Metang"), + ("SPECIES_METAGROSS", "Metagross"), + ("SPECIES_REGIROCK", "Regirock"), + ("SPECIES_REGICE", "Regice"), + ("SPECIES_REGISTEEL", "Registeel"), + ("SPECIES_KYOGRE", "Kyogre"), + ("SPECIES_GROUDON", "Groudon"), + ("SPECIES_RAYQUAZA", "Rayquaza"), + ("SPECIES_LATIAS", "Latias"), + ("SPECIES_LATIOS", "Latios"), + ("SPECIES_JIRACHI", "Jirachi"), + ("SPECIES_DEOXYS", "Deoxys"), + ("SPECIES_CHIMECHO", "Chimecho") + ] + + species_list: List[SpeciesData] = [] + max_species_id = 0 + for species_name, species_label in all_species: + species_id = data.constants[species_name] + max_species_id = max(species_id, max_species_id) + species_data = extracted_data["species"][species_id] + + learnset = [LearnsetMove(item["level"], item["move_id"]) for item in species_data["learnset"]["moves"]] + + species_list.append(SpeciesData( + species_name, + species_label, + species_id, + BaseStats( + species_data["base_stats"][0], + species_data["base_stats"][1], + species_data["base_stats"][2], + species_data["base_stats"][3], + species_data["base_stats"][4], + species_data["base_stats"][5] + ), + (species_data["types"][0], species_data["types"][1]), + (species_data["abilities"][0], species_data["abilities"][1]), + [EvolutionData( + _str_to_evolution_method(evolution_json["method"]), + evolution_json["param"], + evolution_json["species"], + ) for evolution_json in species_data["evolutions"]], + None, + species_data["catch_rate"], + learnset, + int(species_data["tmhm_learnset"], 16), + species_data["learnset"]["rom_address"], + species_data["rom_address"] + )) + + data.species = [None for i in range(max_species_id + 1)] + + for species_data in species_list: + data.species[species_data.species_id] = species_data + + for species in data.species: + if species is not None: + for evolution in species.evolutions: + data.species[evolution.species_id].pre_evolution = species.species_id + + # Create static encounter data + for static_encounter_json in extracted_data["static_encounters"]: + data.static_encounters.append(StaticEncounterData( + static_encounter_json["species"], + static_encounter_json["rom_address"] + )) + + # TM moves + data.tmhm_moves = extracted_data["tmhm_moves"] + + # Create ability data + data.abilities = [AbilityData(data.constants[ability_data[0]], ability_data[1]) for ability_data in [ + ("ABILITY_STENCH", "Stench"), + ("ABILITY_DRIZZLE", "Drizzle"), + ("ABILITY_SPEED_BOOST", "Speed Boost"), + ("ABILITY_BATTLE_ARMOR", "Battle Armor"), + ("ABILITY_STURDY", "Sturdy"), + ("ABILITY_DAMP", "Damp"), + ("ABILITY_LIMBER", "Limber"), + ("ABILITY_SAND_VEIL", "Sand Veil"), + ("ABILITY_STATIC", "Static"), + ("ABILITY_VOLT_ABSORB", "Volt Absorb"), + ("ABILITY_WATER_ABSORB", "Water Absorb"), + ("ABILITY_OBLIVIOUS", "Oblivious"), + ("ABILITY_CLOUD_NINE", "Cloud Nine"), + ("ABILITY_COMPOUND_EYES", "Compound Eyes"), + ("ABILITY_INSOMNIA", "Insomnia"), + ("ABILITY_COLOR_CHANGE", "Color Change"), + ("ABILITY_IMMUNITY", "Immunity"), + ("ABILITY_FLASH_FIRE", "Flash Fire"), + ("ABILITY_SHIELD_DUST", "Shield Dust"), + ("ABILITY_OWN_TEMPO", "Own Tempo"), + ("ABILITY_SUCTION_CUPS", "Suction Cups"), + ("ABILITY_INTIMIDATE", "Intimidate"), + ("ABILITY_SHADOW_TAG", "Shadow Tag"), + ("ABILITY_ROUGH_SKIN", "Rough Skin"), + ("ABILITY_WONDER_GUARD", "Wonder Guard"), + ("ABILITY_LEVITATE", "Levitate"), + ("ABILITY_EFFECT_SPORE", "Effect Spore"), + ("ABILITY_SYNCHRONIZE", "Synchronize"), + ("ABILITY_CLEAR_BODY", "Clear Body"), + ("ABILITY_NATURAL_CURE", "Natural Cure"), + ("ABILITY_LIGHTNING_ROD", "Lightning Rod"), + ("ABILITY_SERENE_GRACE", "Serene Grace"), + ("ABILITY_SWIFT_SWIM", "Swift Swim"), + ("ABILITY_CHLOROPHYLL", "Chlorophyll"), + ("ABILITY_ILLUMINATE", "Illuminate"), + ("ABILITY_TRACE", "Trace"), + ("ABILITY_HUGE_POWER", "Huge Power"), + ("ABILITY_POISON_POINT", "Poison Point"), + ("ABILITY_INNER_FOCUS", "Inner Focus"), + ("ABILITY_MAGMA_ARMOR", "Magma Armor"), + ("ABILITY_WATER_VEIL", "Water Veil"), + ("ABILITY_MAGNET_PULL", "Magnet Pull"), + ("ABILITY_SOUNDPROOF", "Soundproof"), + ("ABILITY_RAIN_DISH", "Rain Dish"), + ("ABILITY_SAND_STREAM", "Sand Stream"), + ("ABILITY_PRESSURE", "Pressure"), + ("ABILITY_THICK_FAT", "Thick Fat"), + ("ABILITY_EARLY_BIRD", "Early Bird"), + ("ABILITY_FLAME_BODY", "Flame Body"), + ("ABILITY_RUN_AWAY", "Run Away"), + ("ABILITY_KEEN_EYE", "Keen Eye"), + ("ABILITY_HYPER_CUTTER", "Hyper Cutter"), + ("ABILITY_PICKUP", "Pickup"), + ("ABILITY_TRUANT", "Truant"), + ("ABILITY_HUSTLE", "Hustle"), + ("ABILITY_CUTE_CHARM", "Cute Charm"), + ("ABILITY_PLUS", "Plus"), + ("ABILITY_MINUS", "Minus"), + ("ABILITY_FORECAST", "Forecast"), + ("ABILITY_STICKY_HOLD", "Sticky Hold"), + ("ABILITY_SHED_SKIN", "Shed Skin"), + ("ABILITY_GUTS", "Guts"), + ("ABILITY_MARVEL_SCALE", "Marvel Scale"), + ("ABILITY_LIQUID_OOZE", "Liquid Ooze"), + ("ABILITY_OVERGROW", "Overgrow"), + ("ABILITY_BLAZE", "Blaze"), + ("ABILITY_TORRENT", "Torrent"), + ("ABILITY_SWARM", "Swarm"), + ("ABILITY_ROCK_HEAD", "Rock Head"), + ("ABILITY_DROUGHT", "Drought"), + ("ABILITY_ARENA_TRAP", "Arena Trap"), + ("ABILITY_VITAL_SPIRIT", "Vital Spirit"), + ("ABILITY_WHITE_SMOKE", "White Smoke"), + ("ABILITY_PURE_POWER", "Pure Power"), + ("ABILITY_SHELL_ARMOR", "Shell Armor"), + ("ABILITY_CACOPHONY", "Cacophony"), + ("ABILITY_AIR_LOCK", "Air Lock") + ]] + + # Create map data + for map_name, map_json in extracted_data["maps"].items(): + land_encounters = None + water_encounters = None + fishing_encounters = None + + if map_json["land_encounters"] is not None: + land_encounters = EncounterTableData( + map_json["land_encounters"]["encounter_slots"], + map_json["land_encounters"]["rom_address"] + ) + if map_json["water_encounters"] is not None: + water_encounters = EncounterTableData( + map_json["water_encounters"]["encounter_slots"], + map_json["water_encounters"]["rom_address"] + ) + if map_json["fishing_encounters"] is not None: + fishing_encounters = EncounterTableData( + map_json["fishing_encounters"]["encounter_slots"], + map_json["fishing_encounters"]["rom_address"] + ) + + data.maps.append(MapData( + map_name, + land_encounters, + water_encounters, + fishing_encounters + )) + + data.maps.sort(key=lambda map: map.name) + + # Create warp map + for warp, destination in extracted_data["warps"].items(): + data.warp_map[warp] = None if destination == "" else destination + + if encoded_warp not in data.warp_map: + data.warp_map[encoded_warp] = None + + # Create trainer data + for i, trainer_json in enumerate(extracted_data["trainers"]): + party_json = trainer_json["party"] + pokemon_data_type = _str_to_pokemon_data_type(trainer_json["pokemon_data_type"]) + data.trainers.append(TrainerData( + i, + TrainerPartyData( + [TrainerPokemonData( + p["species"], + p["level"], + (p["moves"][0], p["moves"][1], p["moves"][2], p["moves"][3]) + ) for p in party_json], + pokemon_data_type, + trainer_json["party_rom_address"] + ), + trainer_json["rom_address"], + trainer_json["battle_script_rom_address"] + )) + + +_init() diff --git a/worlds/pokemon_emerald/data/README.md b/worlds/pokemon_emerald/data/README.md new file mode 100644 index 0000000000..a7c5d3f293 --- /dev/null +++ b/worlds/pokemon_emerald/data/README.md @@ -0,0 +1,99 @@ +## `regions/` + +These define regions, connections, and where locations are. If you know what you're doing, it should be pretty clear how +this works by taking a quick look through the files. The rest of this section is pretty verbose to cover everything. Not +to say you shouldn't read it, but the tl;dr is: + +- Every map, even trivial ones, gets a region definition, and they cannot be coalesced (because of warp rando) +- Stick to the naming convention for regions and events (look at Route 103 and Petalburg City for guidance) +- Locations and warps can only be claimed by one region +- Events are declared here + +A `Map`, which you will see referenced in `parent_map` attribute in the region JSON, is an id from the source code. +`Map`s are sets of tiles, encounters, warps, events, and so on. Route 103, Littleroot Town, the Oldale Town Mart, the +second floor of Devon Corp, and each level of Victory Road are all examples of `Map`s. You transition between `Map`s by +stepping on a warp (warp pads, doorways, etc...) or walking over a border between `Map`s in the overworld. Some warps +don't go to a different `Map`. + +Regions usually describe physical areas which are subsets of a `Map`. Every `Map` must have one or more defined regions. +A region should not contain area from more than one `Map`. We'll need to draw those lines now even when there is no +logical boundary (like between two the first and second floors of your rival's house), for warp rando. + +Most `Map`s have been split into multiple regions. In the example below, `MAP_ROUTE103` was split into +`REGION_ROUTE_103/WEST`, `REGION_ROUTE_103/WATER`, and `REGION_ROUTE_103/EAST` (this document may be out of date; the +example is demonstrative). Keeping the name consistent with the `Map` name and adding a label suffix for the subarea +makes it clearer where we are in the world and where within a `Map` we're describing. + +Every region (except `Menu`) is configured here. All files in this directory are combined with each other at runtime, +and are only split and ordered for organization. Regions defined in `data/regions/unused` are entirely unused because +they're not yet reachable in the randomizer. They're there for future reference in case we want to pull those maps in +later. Any locations or warps in here should be ignored. Data for a single region looks like this: + +```json +"REGION_ROUTE103/EAST": { + "parent_map": "MAP_ROUTE103", + "locations": [ + "ITEM_ROUTE_103_GUARD_SPEC", + "ITEM_ROUTE_103_PP_UP" + ], + "events": [], + "exits": [ + "REGION_ROUTE103/WATER", + "REGION_ROUTE110/MAIN" + ], + "warps": [ + "MAP_ROUTE103:0/MAP_ALTERING_CAVE:0" + ] +} +``` + +- `[key]`: The name of the object, in this case `REGION_ROUTE103/EAST`, should be the value of `parent_map` where the +`MAP` prefix is replaced with `REGION`. Then there should be a following `/` and a label describing this specific region +within the `Map`. This is not enforced or required by the code, but it makes things much more clear. +- `parent_map`: The name of the `Map` this region exists under. It can relate this region to information like encounter +tables. +- `locations`: Locations contained within this region. This can be anything from an item on the ground to a badge to a +gift from an NPC. Locations themselves are defined in `data/extracted_data.json`, and the names used here should come +directly from it. +- `events`: Events that can be completed in this region. Defeating a gym leader or Aqua/Magma team leader, for example, +can trigger story progression and unblock roads and buildings. Events are defined here and nowhere else, and access +rules are set in `rules.py`. +- `exits`: Names of regions that can be directly accessed from this one. Most often regions within the same `Map`, +neighboring maps in the overworld, or transitions from using HM08 Dive. Most connections between maps/regions come from +warps. Any region in this list should be defined somewhere in `data/regions`. +- `warps`: Warp events contained within this region. Warps are defined in `data/extracted_data.json`, and must exist +there to be referenced here. More on warps in [../README.md](../README.md). + +Think of this data as defining which regions are "claiming" a given location, event, or warp. No more than one region +may claim ownership of a location. Even if some "thing" may happen in two different regions and set the same flag, they +should be defined as two different events and anything conditional on said "thing" happening can check whether either of +the two events is accessible. (e.g. Interacting with the Poke Ball in your rival's room and going back downstairs will +both trigger a conversation with them which enables you to rescue Professor Birch. It's the same "thing" on two +different `Map`s.) + +Conceptually, you shouldn't have to "add" any new regions. You should only have to "split" existing regions. When you +split a region, make sure to correctly reassign `locations`, `events`, `exits`, and `warps` according to which new +region they now exist in. Make sure to define new `exits` to link the new regions to each other if applicable. And +especially remember to rename incoming `exits` defined in other regions which are still pointing to the pre-split +region. `sanity_check.py` should catch you if there are other regions that point to a region that no longer exists, but +if one of your newly-split regions still has the same name as the original, it won't be detected and you may find that +things aren't connected correctly. + +## `extracted_data.json` + +DO NOT TOUCH + +Contains data automatically pulled from the base rom and its source code when it is built. There should be no reason to +manually modify it. Data from this file is piped through `data.py` to create a data object that's more useful and +complete. + +## `items.json` + +A map from items as defined in the `constants` in `extracted_data.json` to useful info like a human-friendly label, the +type of progression it enables, and tags to associate. There are many unused items and extra helper constants in +`extracted_data.json`, so this file contains an exhaustive list of items which can actually be found in the modded game. + +## `locations.json` + +Similar to `items.json`, this associates locations with human-friendly labels and tags that are used for filtering. Any +locations claimed by any region need an entry here. diff --git a/worlds/pokemon_emerald/data/base_patch.bsdiff4 b/worlds/pokemon_emerald/data/base_patch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..c1843904a9caa81d52b766eb7056bf6e1bbc6546 GIT binary patch literal 209743 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%0DUCS6yUV%C+ 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

    9ejY2o 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 file for this game.

    -


    - -

    +
    +
    + + +
    +
    + + +
    + +

    Game Options

    diff --git a/docs/world api.md b/docs/world api.md index 67a44c0625..4008c9c4dd 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -73,6 +73,53 @@ for your world specifically on the webhost: `game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be prefixed with the same string as defined here. Default already has 'en'. +`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values +are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of +the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. + +Note: The values must be a non-aliased value for the option type and can only include the following option types: + + - If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + - If you have a `SpecialRange` option, the value can alternatively be a `str` that is one of the + `special_range_names` keys. + - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. + - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. + - `random` is also a valid value for any of these option types. + +`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time. + +Here is an example of a defined preset: +```python +# presets.py +options_presets = { + "Limited Potential": { + "progression_balancing": 0, + "fairy_chests_per_zone": 2, + "starting_class": "random", + "chests_per_zone": 30, + "vendors": "normal", + "architect": "disabled", + "gold_gain_multiplier": "half", + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 5, + "crit_damage_pool": 5, + } +} + +# __init__.py +class RLWeb(WebWorld): + options_presets = options_presets + # ... +``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py new file mode 100644 index 0000000000..8c6ebea208 --- /dev/null +++ b/test/webhost/test_option_presets.py @@ -0,0 +1,63 @@ +import unittest + +from worlds import AutoWorldRegister +from Options import Choice, SpecialRange, Toggle, Range + + +class TestOptionPresets(unittest.TestCase): + def test_option_presets_have_valid_options(self): + """Test that all predefined option presets are valid options.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + presets = world_type.web.options_presets + for preset_name, preset in presets.items(): + for option_name, option_value in preset.items(): + with self.subTest(game=game_name, preset=preset_name, option=option_name): + try: + option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + supported_types = [Choice, Toggle, Range, SpecialRange] + if not any([issubclass(option.__class__, t) for t in supported_types]): + self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " + f"is not a supported type for webhost. " + f"Supported types: {', '.join([t.__name__ for t in supported_types])}") + except AssertionError as ex: + self.fail(f"Option '{option_name}': '{option_value}' in preset '{preset_name}' for game " + f"'{game_name}' is not valid. Error: {ex}") + except KeyError as ex: + self.fail(f"Option '{option_name}' in preset '{preset_name}' for game '{game_name}' is " + f"not a defined option. Error: {ex}") + + def test_option_preset_values_are_explicitly_defined(self): + """Test that option preset values are not a special flavor of 'random' or use from_text to resolve another + value. + """ + for game_name, world_type in AutoWorldRegister.world_types.items(): + presets = world_type.web.options_presets + for preset_name, preset in presets.items(): + for option_name, option_value in preset.items(): + with self.subTest(game=game_name, preset=preset_name, option=option_name): + # Check for non-standard random values. + self.assertFalse( + str(option_value).startswith("random-"), + f"'{option_name}': '{option_value}' in preset '{preset_name}' for game '{game_name}' " + f"is not supported for webhost. Special random values are not supported for presets." + ) + + option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + + # Check for from_text resolving to a different value. ("random" is allowed though.) + if option_value != "random" and isinstance(option_value, str): + # Allow special named values for SpecialRange option presets. + if isinstance(option, SpecialRange): + self.assertTrue( + option_value in option.special_range_names, + f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' " + f"for game '{game_name}'. Expected {option.special_range_names.keys()} or " + f"{option.range_start}-{option.range_end}." + ) + else: + self.assertTrue( + option.name_lookup.get(option.value, None) == option_value, + f"'{option_name}': '{option_value}' in preset '{preset_name}' for game " + f"'{game_name}' is not supported for webhost. Values must not be resolved to a " + f"different option via option.from_text (or an alias)." + ) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 67403472fc..5d0533e068 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -186,6 +186,9 @@ class WebWorld: bug_report_page: Optional[str] """display a link to a bug report page, most likely a link to a GitHub issue page.""" + options_presets: Dict[str, Dict[str, Any]] = {} + """A dictionary containing a collection of developer-defined game option presets.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py new file mode 100644 index 0000000000..a4284e9f7d --- /dev/null +++ b/worlds/rogue_legacy/Presets.py @@ -0,0 +1,61 @@ +from typing import Any, Dict + +from .Options import Architect, GoldGainMultiplier, Vendors + +rl_options_presets: Dict[str, Dict[str, Any]] = { + # Example preset using only literal values. + "Unknown Fate": { + "progression_balancing": "random", + "accessibility": "random", + "starting_gender": "random", + "starting_class": "random", + "new_game_plus": "random", + "fairy_chests_per_zone": "random", + "chests_per_zone": "random", + "universal_fairy_chests": "random", + "universal_chests": "random", + "vendors": "random", + "architect": "random", + "architect_fee": "random", + "disable_charon": "random", + "require_purchasing": "random", + "progressive_blueprints": "random", + "gold_gain_multiplier": "random", + "number_of_children": "random", + "free_diary_on_generation": "random", + "khidr": "random", + "alexander": "random", + "leon": "random", + "herodotus": "random", + "health_pool": "random", + "mana_pool": "random", + "attack_pool": "random", + "magic_damage_pool": "random", + "armor_pool": "random", + "equip_pool": "random", + "crit_chance_pool": "random", + "crit_damage_pool": "random", + "allow_default_names": False, + "death_link": "random", + }, + # A preset I actually use, using some literal values and some from the option itself. + "Limited Potential": { + "progression_balancing": "disabled", + "fairy_chests_per_zone": 2, + "starting_class": "random", + "chests_per_zone": 30, + "vendors": Vendors.option_normal, + "architect": Architect.option_disabled, + "gold_gain_multiplier": GoldGainMultiplier.option_half, + "number_of_children": 2, + "free_diary_on_generation": False, + "health_pool": 10, + "mana_pool": 10, + "attack_pool": 10, + "magic_damage_pool": 10, + "armor_pool": 5, + "equip_pool": 10, + "crit_chance_pool": 5, + "crit_damage_pool": 5, + } +} diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 68a0c856c8..c5a8d71b5d 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -5,6 +5,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table from .Options import rl_options +from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -22,6 +23,7 @@ class RLWeb(WebWorld): )] bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \ "report-an-issue---.md&title=%5BIssue%5D" + options_presets = rl_options_presets class RLWorld(World): From 185a5192481a20ddd8877f58398f13becf38bbf8 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 16 Nov 2023 04:55:18 -0600 Subject: [PATCH 181/327] Core: fix item links around core changes (#2452) --- BaseClasses.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 4ce99b6980..b25d998311 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,6 +113,11 @@ class MultiWorld(): for region in regions: self.region_cache[region.player][region.name] = region + def add_group(self, new_id: int): + self.region_cache[new_id] = {} + self.entrance_cache[new_id] = {} + self.location_cache[new_id] = {} + def __iter__(self) -> Iterator[Region]: for regions in self.region_cache.values(): yield from regions.values() @@ -220,6 +225,7 @@ class MultiWorld(): return group_id, group new_id: int = self.players + len(self.groups) + 1 + self.regions.add_group(new_id) self.game[new_id] = game self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] @@ -617,7 +623,7 @@ class CollectionState(): additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] def __init__(self, parent: MultiWorld): - self.prog_items = {player: Counter() for player in parent.player_ids} + 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()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} From 790f192dedd454b2aac027ea6d392c55af1461ae Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 18 Nov 2023 12:29:35 -0600 Subject: [PATCH 182/327] WebHost: Refactor `tracker.py`, removal of dead code, and tweaks to layouts of some tracker pages. (#2438) --- WebHostLib/customserver.py | 6 +- WebHostLib/static/assets/trackerCommon.js | 19 +- WebHostLib/static/styles/tracker.css | 235 +- WebHostLib/templates/genericTracker.html | 115 +- WebHostLib/templates/hintTable.html | 28 - WebHostLib/templates/lttpMultiTracker.html | 171 - WebHostLib/templates/multiTracker.html | 92 - .../templates/multiTrackerNavigation.html | 9 - WebHostLib/templates/multitracker.html | 144 + .../templates/multitrackerHintTable.html | 37 + .../templates/multitrackerNavigation.html | 16 + .../multitracker__ALinkToThePast.html | 205 + ...acker.html => multitracker__Factorio.html} | 21 +- .../templates/tracker__ALinkToThePast.html | 154 + ...racker.html => tracker__ChecksFinder.html} | 5 + ...ftTracker.html => tracker__Minecraft.html} | 7 +- .../templates/tracker__OcarinaOfTime.html | 185 + ...=> tracker__Starcraft2WingsOfLiberty.html} | 5 + ...racker.html => tracker__SuperMetroid.html} | 5 + ...Tracker.html => tracker__Timespinner.html} | 11 +- WebHostLib/tracker.py | 3593 +++++++++-------- worlds/__init__.py | 55 +- 22 files changed, 2915 insertions(+), 2203 deletions(-) delete mode 100644 WebHostLib/templates/hintTable.html delete mode 100644 WebHostLib/templates/lttpMultiTracker.html delete mode 100644 WebHostLib/templates/multiTracker.html delete mode 100644 WebHostLib/templates/multiTrackerNavigation.html create mode 100644 WebHostLib/templates/multitracker.html create mode 100644 WebHostLib/templates/multitrackerHintTable.html create mode 100644 WebHostLib/templates/multitrackerNavigation.html create mode 100644 WebHostLib/templates/multitracker__ALinkToThePast.html rename WebHostLib/templates/{multiFactorioTracker.html => multitracker__Factorio.html} (79%) create mode 100644 WebHostLib/templates/tracker__ALinkToThePast.html rename WebHostLib/templates/{checksfinderTracker.html => tracker__ChecksFinder.html} (82%) rename WebHostLib/templates/{minecraftTracker.html => tracker__Minecraft.html} (94%) create mode 100644 WebHostLib/templates/tracker__OcarinaOfTime.html rename WebHostLib/templates/{sc2wolTracker.html => tracker__Starcraft2WingsOfLiberty.html} (99%) rename WebHostLib/templates/{supermetroidTracker.html => tracker__SuperMetroid.html} (94%) rename WebHostLib/templates/{timespinnerTracker.html => tracker__Timespinner.html} (95%) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 6d633314b2..998fec5e73 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -27,8 +27,10 @@ from .models import Command, GameDataPackage, Room, db class CustomClientMessageProcessor(ClientMessageProcessor): ctx: WebHostContext - def _cmd_video(self, platform, user): - """Set a link for your name in the WebHostLib tracker pointing to a video stream""" + def _cmd_video(self, platform: str, user: str): + """Set a link for your name in the WebHostLib tracker pointing to a video stream. + Currently, only YouTube and Twitch platforms are supported. + """ if platform.lower().startswith("t"): # twitch self.ctx.video[self.client.team, self.client.slot] = "Twitch", user self.ctx.save() diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index cb16a4de78..b8e089ece5 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -4,13 +4,20 @@ const adjustTableHeight = () => { return; const upperDistance = tablesContainer.getBoundingClientRect().top; - const containerHeight = window.innerHeight - upperDistance; - tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`; - const tableWrappers = document.getElementsByClassName('table-wrapper'); - for(let i=0; i < tableWrappers.length; i++){ - const maxHeight = (window.innerHeight - upperDistance) / 2; - tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`; + for (let i = 0; i < tableWrappers.length; i++) { + // Ensure we are starting from maximum size prior to calculation. + tableWrappers[i].style.height = null; + tableWrappers[i].style.maxHeight = null; + + // Set as a reasonable height, but still allows the user to resize element if they desire. + const currentHeight = tableWrappers[i].offsetHeight; + const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4); + if (currentHeight > maxHeight) { + tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`; + } + + tableWrappers[i].style.maxHeight = `${currentHeight}px`; } }; diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index 0cc2ede59f..8fcb0c9230 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -7,138 +7,55 @@ width: calc(100% - 1rem); } -#tracker-wrapper a{ +#tracker-wrapper a { color: #234ae4; text-decoration: none; cursor: pointer; } -.table-wrapper{ - overflow-y: auto; - overflow-x: auto; - margin-bottom: 1rem; -} - -#tracker-header-bar{ +#tracker-header-bar { display: flex; flex-direction: row; justify-content: flex-start; + align-content: center; line-height: 20px; + gap: 0.5rem; + margin-bottom: 1rem; } -#tracker-header-bar .info{ +#tracker-header-bar .info { color: #ffffff; -} - -#search{ - border: 1px solid #000000; - border-radius: 3px; - padding: 3px; - width: 200px; - margin-bottom: 0.5rem; - margin-right: 1rem; -} - -#multi-stream-link{ - margin-right: 1rem; -} - -div.dataTables_wrapper.no-footer .dataTables_scrollBody{ - border: none; -} - -table.dataTable{ - color: #000000; -} - -table.dataTable thead{ - font-family: LexendDeca-Regular, sans-serif; -} - -table.dataTable tbody, table.dataTable tfoot{ - background-color: #dce2bd; - font-family: LexendDeca-Light, sans-serif; -} - -table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ - background-color: #e2eabb; -} - -table.dataTable tbody td, table.dataTable tfoot td{ - padding: 4px 6px; -} - -table.dataTable, table.dataTable.no-footer{ - border-left: 1px solid #bba967; - width: calc(100% - 2px) !important; - font-size: 1rem; -} - -table.dataTable thead th{ - position: -webkit-sticky; - position: sticky; - background-color: #b0a77d; - top: 0; -} - -table.dataTable thead th.upper-row{ - position: -webkit-sticky; - position: sticky; - background-color: #b0a77d; - height: 36px; - top: 0; -} - -table.dataTable thead th.lower-row{ - position: -webkit-sticky; - position: sticky; - background-color: #b0a77d; - height: 22px; - top: 46px; -} - -table.dataTable tbody td, table.dataTable tfoot td{ - border: 1px solid #bba967; -} - -table.dataTable tfoot td{ - font-weight: bold; -} - -div.dataTables_scrollBody{ - background-color: inherit !important; -} - -table.dataTable .center-column{ - text-align: center; -} - -img.alttp-sprite { - height: auto; - max-height: 32px; - min-height: 14px; -} - -.item-acquired{ - background-color: #d3c97d; + padding: 2px; + flex-grow: 1; + align-self: center; + text-align: justify; } #tracker-navigation { - display: inline-flex; + display: flex; + flex-wrap: wrap; + margin: 0 0.5rem 0.5rem 0.5rem; + user-select: none; + height: 2rem; +} + +.tracker-navigation-bar { + display: flex; background-color: #b0a77d; - margin: 0.5rem; border-radius: 4px; } .tracker-navigation-button { - display: block; + display: flex; + justify-content: center; + align-items: center; margin: 4px; padding-left: 12px; padding-right: 12px; border-radius: 4px; text-align: center; font-size: 14px; - color: #000; + color: black !important; font-weight: lighter; } @@ -150,6 +67,100 @@ img.alttp-sprite { background-color: rgb(220, 226, 189); } +.table-wrapper { + overflow-y: auto; + overflow-x: auto; + margin-bottom: 1rem; + resize: vertical; +} + +#search { + border: 1px solid #000000; + border-radius: 3px; + padding: 3px; + width: 200px; +} + +div.dataTables_wrapper.no-footer .dataTables_scrollBody { + border: none; +} + +table.dataTable { + color: #000000; +} + +table.dataTable thead { + font-family: LexendDeca-Regular, sans-serif; +} + +table.dataTable tbody, table.dataTable tfoot { + background-color: #dce2bd; + font-family: LexendDeca-Light, sans-serif; +} + +table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover { + background-color: #e2eabb; +} + +table.dataTable tbody td, table.dataTable tfoot td { + padding: 4px 6px; +} + +table.dataTable, table.dataTable.no-footer { + border-left: 1px solid #bba967; + width: calc(100% - 2px) !important; + font-size: 1rem; +} + +table.dataTable thead th { + position: -webkit-sticky; + position: sticky; + background-color: #b0a77d; + top: 0; +} + +table.dataTable thead th.upper-row { + position: -webkit-sticky; + position: sticky; + background-color: #b0a77d; + height: 36px; + top: 0; +} + +table.dataTable thead th.lower-row { + position: -webkit-sticky; + position: sticky; + background-color: #b0a77d; + height: 22px; + top: 46px; +} + +table.dataTable tbody td, table.dataTable tfoot td { + border: 1px solid #bba967; +} + +table.dataTable tfoot td { + font-weight: bold; +} + +div.dataTables_scrollBody { + background-color: inherit !important; +} + +table.dataTable .center-column { + text-align: center; +} + +img.icon-sprite { + height: auto; + max-height: 32px; + min-height: 14px; +} + +.item-acquired { + background-color: #d3c97d; +} + @media all and (max-width: 1700px) { table.dataTable thead th.upper-row{ position: -webkit-sticky; @@ -159,7 +170,7 @@ img.alttp-sprite { top: 0; } - table.dataTable thead th.lower-row{ + table.dataTable thead th.lower-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -167,11 +178,11 @@ img.alttp-sprite { top: 37px; } - table.dataTable, table.dataTable.no-footer{ + table.dataTable, table.dataTable.no-footer { font-size: 0.8rem; } - img.alttp-sprite { + img.icon-sprite { height: auto; max-height: 24px; min-height: 10px; @@ -187,7 +198,7 @@ img.alttp-sprite { top: 0; } - table.dataTable thead th.lower-row{ + table.dataTable thead th.lower-row { position: -webkit-sticky; position: sticky; background-color: #b0a77d; @@ -195,11 +206,11 @@ img.alttp-sprite { top: 32px; } - table.dataTable, table.dataTable.no-footer{ + table.dataTable, table.dataTable.no-footer { font-size: 0.6rem; } - img.alttp-sprite { + img.icon-sprite { height: auto; max-height: 20px; min-height: 10px; diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 1c2fcd44c0..5a53320408 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -1,36 +1,57 @@ -{% extends 'tablepage.html' %} +{% extends "tablepage.html" %} {% block head %} {{ super() }} {{ player_name }}'s Tracker - - - + + + {% endblock %} {% block body %} - {% include 'header/dirtHeader.html' %} -
    -
    - - This tracker will automatically update itself periodically. + {% include "header/dirtHeader.html" %} + +
    +
    + + 🡸 Return to Multiworld Tracker + + {% if game_specific_tracker %} + + Game-Specific Tracker + + {% endif %}
    +
    + +
    +
    + +
    This tracker will automatically update itself periodically.
    +
    +
    - + - {% for id, count in inventory.items() %} - - - - - + {% for id, count in inventory.items() if count > 0 %} + + + + + {%- endfor -%} @@ -39,24 +60,62 @@
    Item AmountOrder ReceivedLast Order Received
    {{ id | item_name }}{{ count }}{{received_items[id]}}
    {{ item_id_to_name[game][id] }}{{ count }}{{ received_items[id] }}
    - - - - + + + + - {% for name in checked_locations %} + + {%- for location in locations -%} - - + + - {%- endfor -%} - {% for name in not_checked_locations %} + {%- endfor -%} + + +
    LocationChecked
    LocationChecked
    {{ name | location_name}}{{ location_id_to_name[game][location] }} + {% if location in checked_locations %}✔{% endif %} +
    +
    +
    + + - - + + + + + + + - {%- endfor -%} + + + {%- for hint in hints -%} + + + + + + + + + + {%- endfor -%}
    {{ name | location_name}}FinderReceiverItemLocationGameEntranceFound
    + {% if hint.finding_player == player %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + {% if hint.receiving_player == player %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}{{ games[(team, hint.finding_player)] }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
    diff --git a/WebHostLib/templates/hintTable.html b/WebHostLib/templates/hintTable.html deleted file mode 100644 index 00b74111ea..0000000000 --- a/WebHostLib/templates/hintTable.html +++ /dev/null @@ -1,28 +0,0 @@ -{% for team, hints in hints.items() %} -
    - - - - - - - - - - - - - {%- for hint in hints -%} - - - - - - - - - {%- endfor -%} - -
    FinderReceiverItemLocationEntranceFound
    {{ long_player_names[team, hint.finding_player] }}{{ long_player_names[team, hint.receiving_player] }}{{ hint.item|item_name }}{{ hint.location|location_name }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
    -
    -{% endfor %} \ No newline at end of file diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html deleted file mode 100644 index 8eb471be39..0000000000 --- a/WebHostLib/templates/lttpMultiTracker.html +++ /dev/null @@ -1,171 +0,0 @@ -{% extends 'tablepage.html' %} -{% block head %} - {{ super() }} - ALttP Multiworld Tracker - - - - -{% endblock %} - -{% block body %} - {% include 'header/dirtHeader.html' %} - {% include 'multiTrackerNavigation.html' %} -
    -
    - - - - Multistream - - - Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. -
    -
    - {% for team, players in inventory.items() %} -
    - - - - - - {%- for name in tracking_names -%} - {%- if name in icons -%} - - {%- else -%} - - {%- endif -%} - {%- endfor -%} - - - - {%- for player, items in players.items() -%} - - - {%- if (team, loop.index) in video -%} - {%- if video[(team, loop.index)][0] == "Twitch" -%} - - {%- elif video[(team, loop.index)][0] == "Youtube" -%} - - {%- endif -%} - {%- else -%} - - {%- endif -%} - {%- for id in tracking_ids -%} - {%- if items[id] -%} - - {%- else -%} - - {%- endif -%} - {% endfor %} - - {%- endfor -%} - -
    #Name - {{ name|e }} - {{ name|e }}
    {{ loop.index }} - - {{ player_names[(team, loop.index)] }} - ▶️ - - {{ player_names[(team, loop.index)] }} - ▶️{{ player_names[(team, loop.index)] }} - {% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}
    -
    - {% endfor %} - - {% for team, players in checks_done.items() %} -
    - - - - - - {% for area in ordered_areas %} - {% set colspan = 1 %} - {% if area in key_locations %} - {% set colspan = colspan + 1 %} - {% endif %} - {% if area in big_key_locations %} - {% set colspan = colspan + 1 %} - {% endif %} - {% if area in icons %} - - {%- else -%} - - {%- endif -%} - {%- endfor -%} - - - - - {% for area in ordered_areas %} - - {% if area in key_locations %} - - {% endif %} - {% if area in big_key_locations %} - - {%- endif -%} - {%- endfor -%} - - - - {%- for player, checks in players.items() -%} - - - - {%- for area in ordered_areas -%} - {% if player in checks_in_area and area in checks_in_area[player] %} - {%- set checks_done = checks[area] -%} - {%- set checks_total = checks_in_area[player][area] -%} - {%- if checks_done == checks_total -%} - - {%- else -%} - - {%- endif -%} - {%- if area in key_locations -%} - - {%- endif -%} - {%- if area in big_key_locations -%} - - {%- endif -%} - {% else %} - - {%- if area in key_locations -%} - - {%- endif -%} - {%- if area in big_key_locations -%} - - {%- endif -%} - {% endif %} - {%- endfor -%} - - {%- if activity_timers[(team, player)] -%} - - {%- else -%} - - {%- endif -%} - - {%- endfor -%} - -
    #Name - {{ area }}{{ area }}%Last
    Activity
    - Checks - - Small Key - - Big Key -
    {{ loop.index }}{{ player_names[(team, loop.index)]|e }} - {{ checks_done }}/{{ checks_total }}{{ checks_done }}/{{ checks_total }}{{ inventory[team][player][small_key_ids[area]] }}{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}{{ activity_timers[(team, player)].total_seconds() }}None
    -
    - {% endfor %} - {% include "hintTable.html" with context %} -
    -
    -{% endblock %} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html deleted file mode 100644 index 1a3d353de1..0000000000 --- a/WebHostLib/templates/multiTracker.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends 'tablepage.html' %} -{% block head %} - {{ super() }} - Multiworld Tracker - - -{% endblock %} - -{% block body %} - {% include 'header/dirtHeader.html' %} - {% include 'multiTrackerNavigation.html' %} -
    -
    - - - - Multistream - - - Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. -
    -
    - {% for team, players in checks_done.items() %} -
    - - - - - - - - {% block custom_table_headers %} - {# implement this block in game-specific multi trackers #} - {% endblock %} - - - - - - - {%- for player, checks in players.items() -%} - - - - - - {% block custom_table_row scoped %} - {# implement this block in game-specific multi trackers #} - {% endblock %} - - - {%- if activity_timers[team, player] -%} - - {%- else -%} - - {%- endif -%} - - {%- endfor -%} - - {% if not self.custom_table_headers() | trim %} - - - - - - - - - - - - {% endif %} -
    #NameGameStatusChecks%Last
    Activity
    {{ loop.index }}{{ player_names[(team, loop.index)]|e }}{{ games[player] }}{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing", - 30: "Goal Completed"}.get(states[team, player], "Unknown State") }} - {{ checks["Total"] }}/{{ locations[player] | length }} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}{{ activity_timers[team, player].total_seconds() }}None
    TotalAll Games{{ completed_worlds }}/{{ players|length }} Complete{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} - {% if total_locations[team] == 0 %} - 100 - {% else %} - {{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }} - {% endif %} -
    -
    - {% endfor %} - {% include "hintTable.html" with context %} -
    -
    -{% endblock %} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html deleted file mode 100644 index 7fc405b6fb..0000000000 --- a/WebHostLib/templates/multiTrackerNavigation.html +++ /dev/null @@ -1,9 +0,0 @@ -{%- if enabled_multiworld_trackers|length > 1 -%} -
    - {% for enabled_tracker in enabled_multiworld_trackers %} - {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} - {{ enabled_tracker.name }} - {% endfor %} -
    -{%- endif -%} diff --git a/WebHostLib/templates/multitracker.html b/WebHostLib/templates/multitracker.html new file mode 100644 index 0000000000..b16d4714ec --- /dev/null +++ b/WebHostLib/templates/multitracker.html @@ -0,0 +1,144 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + {% include "multitrackerNavigation.html" %} + +
    +
    + + + + +
    + Clicking on a slot's number will bring up the slot-specific tracker. + This tracker will automatically update itself periodically. +
    +
    + +
    + {%- for team, players in room_players.items() -%} +
    + + + + + + {% if current_tracker == "Generic" %}{% endif %} + + {% block custom_table_headers %} + {# Implement this block in game-specific multi-trackers. #} + {% endblock %} + + + + + + + {%- for player in players -%} + {%- if current_tracker == "Generic" or games[(team, player)] == current_tracker -%} + + + + {%- if current_tracker == "Generic" -%} + + {%- endif -%} + + + {% block custom_table_row scoped %} + {# Implement this block in game-specific multi-trackers. #} + {% endblock %} + + {% set location_count = locations[(team, player)] | length %} + + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endif -%} + {%- endfor -%} + + + {%- if not self.custom_table_headers() | trim -%} + + + + + + + + + + + {%- endif -%} +
    #NameGameStatusChecks%Last
    Activity
    + + {{ player }} + + {{ player_names_with_alias[(team, player)] | e }}{{ games[(team, player)] }} + {{ + { + 0: "Disconnected", + 5: "Connected", + 10: "Ready", + 20: "Playing", + 30: "Goal Completed" + }.get(states[(team, player)], "Unknown State") + }} + + {{ locations_complete[(team, player)] }}/{{ location_count }} + + {%- if locations[(team, player)] | length > 0 -%} + {% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %} + {{ "{0:.2f}".format(percentage_of_completion) }} + {%- else -%} + 100.00 + {%- endif -%} + {{ activity_timers[(team, player)].total_seconds() }}None
    TotalAll Games{{ completed_worlds[team] }}/{{ players | length }} Complete + {{ total_team_locations_complete[team] }}/{{ total_team_locations[team] }} + + {%- if total_team_locations[team] == 0 -%} + 100 + {%- else -%} + {{ "{0:.2f}".format(total_team_locations_complete[team] / total_team_locations[team] * 100) }} + {%- endif -%} +
    +
    + + {%- endfor -%} + + {% block custom_tables %} + {# Implement this block to create custom tables in game-specific multi-trackers. #} + {% endblock %} + + {% include "multitrackerHintTable.html" with context %} +
    +
    +{% endblock %} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html new file mode 100644 index 0000000000..a931e9b048 --- /dev/null +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -0,0 +1,37 @@ +{% for team, hints in hints.items() %} +
    + + + + + + + + + + + + + + {%- for hint in hints -%} + {%- + if current_tracker == "Generic" or ( + games[(team, hint.finding_player)] == current_tracker or + games[(team, hint.receiving_player)] == current_tracker + ) + -%} + + + + + + + + + + {% endif %} + {%- endfor -%} + +
    FinderReceiverItemLocationGameEntranceFound
    {{ player_names_with_alias[(team, hint.finding_player)] }}{{ player_names_with_alias[(team, hint.receiving_player)] }}{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}{{ games[(team, hint.finding_player)] }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
    +
    +{% endfor %} diff --git a/WebHostLib/templates/multitrackerNavigation.html b/WebHostLib/templates/multitrackerNavigation.html new file mode 100644 index 0000000000..1256181b27 --- /dev/null +++ b/WebHostLib/templates/multitrackerNavigation.html @@ -0,0 +1,16 @@ +{% if enabled_trackers | length > 1 %} +
    + {# Multitracker game navigation. #} +
    + {%- for game_tracker in enabled_trackers -%} + {%- set tracker_url = url_for("get_multiworld_tracker", tracker=room.tracker, game=game_tracker) -%} + + {{ game_tracker }} + + {%- endfor -%} +
    +
    +{% endif %} diff --git a/WebHostLib/templates/multitracker__ALinkToThePast.html b/WebHostLib/templates/multitracker__ALinkToThePast.html new file mode 100644 index 0000000000..8cea5ba057 --- /dev/null +++ b/WebHostLib/templates/multitracker__ALinkToThePast.html @@ -0,0 +1,205 @@ +{% extends "multitracker.html" %} +{% block head %} + {{ super() }} + + +{% endblock %} + +{# List all tracker-relevant icons. Format: (Name, Image URL) #} +{%- set icons = { + "Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", + "Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", + "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", + "Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", + "Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", + "Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", + "Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", + "Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", + "Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", + "Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", + "Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", + "Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", + "Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", + "Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", + "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", + "Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", + "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", + "Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", + "Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", + "Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", + "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", + "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", + "Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png", + "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", + "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", + "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", + "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", + "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", + "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", + "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", + "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", + "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", + "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", + "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", + "Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", + "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", + "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", + "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", + "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", + "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", + "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png", + "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", + "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", + "Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", + "Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", + "Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", + "Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", + "Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", + "Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", + "Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", + "Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", + "Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", + "Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", + "Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png", + "Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", + "Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", + "Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", + "Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", + "Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74", +} -%} + +{%- block custom_table_headers %} +{#- macro that creates a table header with display name and image -#} +{%- macro make_header(name, img_src) %} + + {{ name }} + +{% endmacro -%} + +{#- call the macro to build the table header -#} +{%- for name in tracking_names %} + {%- if name in icons -%} + + {{ name | e }} + + {%- endif %} +{% endfor -%} +{% endblock %} + +{# build each row of custom entries #} +{% block custom_table_row scoped %} + {%- for id in tracking_ids -%} +{# {{ checks }}#} + {%- if inventories[(team, player)][id] -%} + + {% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %} + + {%- else -%} + + {%- endif -%} + {% endfor %} +{% endblock %} + +{% block custom_tables %} + +{% for team, _ in total_team_locations.items() %} +
    + + + + + + {% for area in ordered_areas %} + {% set colspan = 1 %} + {% if area in key_locations %} + {% set colspan = colspan + 1 %} + {% endif %} + {% if area in big_key_locations %} + {% set colspan = colspan + 1 %} + {% endif %} + {% if area in icons %} + + {%- else -%} + + {%- endif -%} + {%- endfor -%} + + + + + {% for area in ordered_areas %} + + {% if area in key_locations %} + + {% endif %} + {% if area in big_key_locations %} + + {%- endif -%} + {%- endfor -%} + + + + {%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%} + + + + {%- for area in ordered_areas -%} + {% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %} + {%- set checks_done = area_checks[area] -%} + {%- set checks_total = checks_in_area[(team, player)][area] -%} + {%- if checks_done == checks_total -%} + + {%- else -%} + + {%- endif -%} + {%- if area in key_locations -%} + + {%- endif -%} + {%- if area in big_key_locations -%} + + {%- endif -%} + {% else %} + + {%- if area in key_locations -%} + + {%- endif -%} + {%- if area in big_key_locations -%} + + {%- endif -%} + {% endif %} + {%- endfor -%} + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endfor -%} + +
    #Name + {{ area }}{{ area }}%Last
    Activity
    + Checks + + Small Key + + Big Key +
    {{ player }}{{ player_names_with_alias[(team, player)] | e }} + {{ checks_done }}/{{ checks_total }}{{ checks_done }}/{{ checks_total }}{{ inventories[(team, player)][small_key_ids[area]] }}{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %} + {% set location_count = locations[(team, player)] | length %} + {%- if locations[(team, player)] | length > 0 -%} + {% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %} + {{ "{0:.2f}".format(percentage_of_completion) }} + {%- else -%} + 100.00 + {%- endif -%} + {{ activity_timers[(team, player)].total_seconds() }}None
    +
    +{% endfor %} + +{% endblock %} diff --git a/WebHostLib/templates/multiFactorioTracker.html b/WebHostLib/templates/multitracker__Factorio.html similarity index 79% rename from WebHostLib/templates/multiFactorioTracker.html rename to WebHostLib/templates/multitracker__Factorio.html index 389a79d411..a7ad824db4 100644 --- a/WebHostLib/templates/multiFactorioTracker.html +++ b/WebHostLib/templates/multitracker__Factorio.html @@ -1,4 +1,4 @@ -{% extends "multiTracker.html" %} +{% extends "multitracker.html" %} {# establish the to be tracked data. Display Name, factorio/AP internal name, display image #} {%- set science_packs = [ ("Logistic Science Pack", "logistic-science-pack", @@ -14,12 +14,12 @@ ("Space Science Pack", "space-science-pack", "https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"), ] -%} + {%- block custom_table_headers %} {#- macro that creates a table header with display name and image -#} {%- macro make_header(name, img_src) %} - {{ name }} + {{ name }} {% endmacro -%} {#- call the macro to build the table header -#} @@ -27,16 +27,15 @@ {{ make_header(name, img_src) }} {% endfor -%} {% endblock %} + {% block custom_table_row scoped %} -{% if games[player] == "Factorio" %} - {%- set player_inventory = named_inventory[team][player] -%} + {%- set player_inventory = inventories[(team, player)] -%} {%- set prog_science = player_inventory["progressive-science-pack"] -%} {%- for name, internal_name, img_src in science_packs %} - {% if player_inventory[internal_name] or prog_science > loop.index0 %}✔{% endif %} + {% if player_inventory[internal_name] or prog_science > loop.index0 %} + ✔️ + {% else %} + + {% endif %} {% endfor -%} -{% else %} - {%- for _ in science_packs %} - ❌ - {% endfor -%} -{% endif %} {% endblock%} diff --git a/WebHostLib/templates/tracker__ALinkToThePast.html b/WebHostLib/templates/tracker__ALinkToThePast.html new file mode 100644 index 0000000000..b7bae26fd3 --- /dev/null +++ b/WebHostLib/templates/tracker__ALinkToThePast.html @@ -0,0 +1,154 @@ +{%- set icons = { + "Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", + "Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", + "Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", + "Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", + "Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", + "Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", + "Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", + "Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", + "Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", + "Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", + "Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", + "Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", + "Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", + "Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", + "Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", + "Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", + "Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", + "Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", + "Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", + "Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", + "Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", + "Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", + "Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", + "Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec", + "Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", + "Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", + "Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", + "Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", + "Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", + "Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", + "Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", + "Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", + "Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", + "Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", + "Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", + "Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", + "Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", + "Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", + "Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", + "Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", + "Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", + "Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png", + "Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", + "Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", + "Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", + "Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", + "Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", + "Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", + "Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", + "Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", + "Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", + "Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", + "Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", + "Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", + "Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png", + "Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", + "Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", + "Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", + "Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", + "Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74", +} -%} + + + + + {{ player_name }}'s Tracker + + + + + + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + {% if key_locations and "Universal" not in key_locations %} + + {% endif %} + {% if big_key_locations %} + + {% endif %} + + {% for area in sp_areas %} + + + + {% if key_locations and "Universal" not in key_locations %} + + {% endif %} + {% if big_key_locations %} + + {% endif %} + + {% endfor %} +
    {{ area }}{{ checks_done[area] }} / {{ checks_in_area[area] }} + {{ inventory[small_key_ids[area]] if area in key_locations else '—' }} + + {{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }} +
    +
    + + diff --git a/WebHostLib/templates/checksfinderTracker.html b/WebHostLib/templates/tracker__ChecksFinder.html similarity index 82% rename from WebHostLib/templates/checksfinderTracker.html rename to WebHostLib/templates/tracker__ChecksFinder.html index 5df77f5e74..f0995c8548 100644 --- a/WebHostLib/templates/checksfinderTracker.html +++ b/WebHostLib/templates/tracker__ChecksFinder.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
    diff --git a/WebHostLib/templates/minecraftTracker.html b/WebHostLib/templates/tracker__Minecraft.html similarity index 94% rename from WebHostLib/templates/minecraftTracker.html rename to WebHostLib/templates/tracker__Minecraft.html index 9f5022b4cc..248f2778bd 100644 --- a/WebHostLib/templates/minecraftTracker.html +++ b/WebHostLib/templates/tracker__Minecraft.html @@ -8,13 +8,18 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
    -
    diff --git a/WebHostLib/templates/tracker__OcarinaOfTime.html b/WebHostLib/templates/tracker__OcarinaOfTime.html new file mode 100644 index 0000000000..41b76816cf --- /dev/null +++ b/WebHostLib/templates/tracker__OcarinaOfTime.html @@ -0,0 +1,185 @@ + + + + {{ player_name }}'s Tracker + + + + + + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    {{ hookshot_length }}
    +
    +
    +
    + +
    {{ bottle_count if bottle_count > 0 else '' }}
    +
    +
    +
    + +
    {{ wallet_size }}
    +
    +
    +
    + +
    Zelda
    +
    +
    +
    + +
    Epona
    +
    +
    +
    + +
    Saria
    +
    +
    +
    + +
    Sun
    +
    +
    +
    + +
    Time
    +
    +
    +
    + +
    Storms
    +
    +
    +
    + +
    {{ token_count }}
    +
    +
    +
    + +
    Min
    +
    +
    +
    + +
    Bol
    +
    +
    +
    + +
    Ser
    +
    +
    +
    + +
    Req
    +
    +
    +
    + +
    Noc
    +
    +
    +
    + +
    Pre
    +
    +
    +
    + +
    {{ piece_count if piece_count > 0 else '' }}
    +
    +
    + + + + + + + + {% for area in checks_done %} + + + + + + + + {% for location in location_info[area] %} + + + + + + + {% endfor %} + + {% endfor %} +
    Items
    {{ area }} {{'▼' if area != 'Total'}}{{ small_key_counts.get(area, '-') }}{{ boss_key_counts.get(area, '-') }}{{ checks_done[area] }} / {{ checks_in_area[area] }}
    {{ location }}{{ '✔' if location_info[area][location] else '' }}
    +
    + + diff --git a/WebHostLib/templates/sc2wolTracker.html b/WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html similarity index 99% rename from WebHostLib/templates/sc2wolTracker.html rename to WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html index 49c31a5795..c27f690dfd 100644 --- a/WebHostLib/templates/sc2wolTracker.html +++ b/WebHostLib/templates/tracker__Starcraft2WingsOfLiberty.html @@ -8,6 +8,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
    diff --git a/WebHostLib/templates/supermetroidTracker.html b/WebHostLib/templates/tracker__SuperMetroid.html similarity index 94% rename from WebHostLib/templates/supermetroidTracker.html rename to WebHostLib/templates/tracker__SuperMetroid.html index 342f75642f..0c64817651 100644 --- a/WebHostLib/templates/supermetroidTracker.html +++ b/WebHostLib/templates/tracker__SuperMetroid.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
    diff --git a/WebHostLib/templates/timespinnerTracker.html b/WebHostLib/templates/tracker__Timespinner.html similarity index 95% rename from WebHostLib/templates/timespinnerTracker.html rename to WebHostLib/templates/tracker__Timespinner.html index f02ec6daab..b118c33833 100644 --- a/WebHostLib/templates/timespinnerTracker.html +++ b/WebHostLib/templates/tracker__Timespinner.html @@ -7,6 +7,11 @@ + {# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #} + +
    @@ -51,16 +56,16 @@
    {% if 'DownloadableItems' in options %}
    - {% endif %} + {% endif %}
    {% if 'DownloadableItems' in options %}
    - {% endif %} + {% endif %}
    {% if 'EyeSpy' in options %}
    - {% endif %} + {% endif %}
    diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 55b98df59e..8a7155afec 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1,1773 +1,1960 @@ -import collections import datetime -import typing -from typing import Counter, Optional, Dict, Any, Tuple, List +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from uuid import UUID from flask import render_template -from jinja2 import pass_context, runtime from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second -from NetUtils import ClientStatus, SlotType, NetworkSlot +from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType from Utils import restricted_loads -from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games -from worlds.alttp import Items from . import app, cache from .models import GameDataPackage, Room -alttp_icons = { - "Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png", - "Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png", - "Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png", - "Fighter Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920", - "Master Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920", - "Tempered Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920", - "Golden Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920", - "Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c", - "Silver Bow": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920", - "Green Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920", - "Blue Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920", - "Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920", - "Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920", - "Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", - "Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725", - "Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9", - "Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920", - "Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920", - "Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e", - "Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed", - "Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e", - "Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400", - "Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b", - "Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59", - "Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec", - "Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0", - "Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc", - "Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26", - "Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5", - "Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879", - "Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce", - "Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500", - "Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05", - "Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390", - "Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6", - "Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744", - "Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b", - "Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943", - "Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54", - "Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832", - "Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc", - "Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48", - "Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e", - "Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d", - "Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda", - "Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6", - "Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc", - "Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be", - "Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5", - "Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png", - "Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png", - "Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7", - "Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022", - "Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5", - "Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png", - "Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222", - "Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0", - "Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8", - "Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be", - "Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74" -} - - -def get_alttp_id(item_name): - return Items.item_table[item_name][2] - - -links = {"Bow": "Progressive Bow", - "Silver Arrows": "Progressive Bow", - "Silver Bow": "Progressive Bow", - "Progressive Bow (Alt)": "Progressive Bow", - "Bottle (Red Potion)": "Bottle", - "Bottle (Green Potion)": "Bottle", - "Bottle (Blue Potion)": "Bottle", - "Bottle (Fairy)": "Bottle", - "Bottle (Bee)": "Bottle", - "Bottle (Good Bee)": "Bottle", - "Fighter Sword": "Progressive Sword", - "Master Sword": "Progressive Sword", - "Tempered Sword": "Progressive Sword", - "Golden Sword": "Progressive Sword", - "Power Glove": "Progressive Glove", - "Titans Mitts": "Progressive Glove" - } - -levels = {"Fighter Sword": 1, - "Master Sword": 2, - "Tempered Sword": 3, - "Golden Sword": 4, - "Power Glove": 1, - "Titans Mitts": 2, - "Bow": 1, - "Silver Bow": 2} - -multi_items = {get_alttp_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")} -links = {get_alttp_id(key): get_alttp_id(value) for key, value in links.items()} -levels = {get_alttp_id(key): value for key, value in levels.items()} - -tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", - "Hookshot", "Magic Mirror", "Flute", - "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", - "Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp", - "Mushroom", "Magic Powder", - "Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", - "Bottle", "Triforce"] - -default_locations = { - 'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, - 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, - 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, - 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, - 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, - 59881, 59761, 59890, 59770, 193020, 212605}, - 'Dark World': {59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, - 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031}, - 'Desert Palace': {1573216, 59842, 59851, 59791, 1573201, 59830}, - 'Eastern Palace': {1573200, 59827, 59893, 59767, 59833, 59773}, - 'Hyrule Castle': {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, - 'Agahnims Tower': {60082, 60085}, - 'Tower of Hera': {1573218, 59878, 59821, 1573202, 59896, 59899}, - 'Swamp Palace': {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, - 'Thieves Town': {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, - 'Skull Woods': {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, - 'Ice Palace': {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, - 'Misery Mire': {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, - 'Turtle Rock': {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, - 'Palace of Darkness': {59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, - 59965}, - 'Ganons Tower': {60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, - 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157}, - 'Total': set()} - -key_only_locations = { - 'Light World': set(), - 'Dark World': set(), - 'Desert Palace': {0x140031, 0x14002b, 0x140061, 0x140028}, - 'Eastern Palace': {0x14005b, 0x140049}, - 'Hyrule Castle': {0x140037, 0x140034, 0x14000d, 0x14003d}, - 'Agahnims Tower': {0x140061, 0x140052}, - 'Tower of Hera': set(), - 'Swamp Palace': {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, - 'Thieves Town': {0x14005e, 0x14004f}, - 'Skull Woods': {0x14002e, 0x14001c}, - 'Ice Palace': {0x140004, 0x140022, 0x140025, 0x140046}, - 'Misery Mire': {0x140055, 0x14004c, 0x140064}, - 'Turtle Rock': {0x140058, 0x140007}, - 'Palace of Darkness': set(), - 'Ganons Tower': {0x140040, 0x140043, 0x14003a, 0x14001f}, - 'Total': set() -} - -location_to_area = {} -for area, locations in default_locations.items(): - for location in locations: - location_to_area[location] = area - -for area, locations in key_only_locations.items(): - for location in locations: - location_to_area[location] = area - -checks_in_area = {area: len(checks) for area, checks in default_locations.items()} -checks_in_area["Total"] = 216 - -ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', - 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', - 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") - -tracking_ids = [] - -for item in tracking_names: - tracking_ids.append(get_alttp_id(item)) - -small_key_ids = {} -big_key_ids = {} -ids_small_key = {} -ids_big_key = {} - -for item_name, data in Items.item_table.items(): - if "Key" in item_name: - area = item_name.split("(")[1][:-1] - if "Small" in item_name: - small_key_ids[area] = data[2] - ids_small_key[data[2]] = area - else: - big_key_ids[area] = data[2] - ids_big_key[data[2]] = area - -# cleanup global namespace -del item_name -del data -del item - - -def attribute_item_solo(inventory, item): - """Adds item to inventory counter, converts everything to progressive.""" - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[target_item] = max(inventory[target_item], levels[item]) - else: - inventory[target_item] += 1 - - -@app.template_filter() -def render_timedelta(delta: datetime.timedelta): - hours, minutes = divmod(delta.total_seconds() / 60, 60) - hours = str(int(hours)) - minutes = str(int(minutes)).zfill(2) - return f"{hours}:{minutes}" - - -@pass_context -def get_location_name(context: runtime.Context, loc: int) -> str: - # once all rooms embed data package, the chain lookup can be dropped - context_locations = context.get("custom_locations", {}) - return collections.ChainMap(context_locations, lookup_any_location_id_to_name).get(loc, loc) - - -@pass_context -def get_item_name(context: runtime.Context, item: int) -> str: - context_items = context.get("custom_items", {}) - return collections.ChainMap(context_items, lookup_any_item_id_to_name).get(item, item) - - -app.jinja_env.filters["location_name"] = get_location_name -app.jinja_env.filters["item_name"] = get_item_name - +# Multisave is currently updated, at most, every minute. +TRACKER_CACHE_TIMEOUT_IN_SECONDS = 60 _multidata_cache = {} +_multiworld_trackers: Dict[str, Callable] = {} +_player_trackers: Dict[str, Callable] = {} + +TeamPlayer = Tuple[int, int] +ItemMetadata = Tuple[int, int, int] -def get_location_table(checks_table: dict) -> dict: - loc_to_area = {} - for area, locations in checks_table.items(): - if area == "Total": - continue - for location in locations: - loc_to_area[location] = area - return loc_to_area +def _cache_results(func: Callable) -> Callable: + """Stores the results of any computationally expensive methods after the initial call in TrackerData. + If called again, returns the cached result instead, as results will not change for the lifetime of TrackerData. + """ + def method_wrapper(self: "TrackerData", *args): + cache_key = f"{func.__name__}{''.join(f'_[{arg.__repr__()}]' for arg in args)}" + if cache_key in self._tracker_cache: + return self._tracker_cache[cache_key] - -def get_static_room_data(room: Room): - result = _multidata_cache.get(room.seed.id, None) - if result: + result = func(self, *args) + self._tracker_cache[cache_key] = result return result - multidata = Context.decompress(room.seed.multidata) - # in > 100 players this can take a bit of time and is the main reason for the cache - locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations'] - names: List[List[str]] = multidata.get("names", []) - games = multidata.get("games", {}) - groups = {} - custom_locations = {} - custom_items = {} - if "slot_info" in multidata: - slot_info_dict: Dict[int, NetworkSlot] = multidata["slot_info"] - games = {slot: slot_info.game for slot, slot_info in slot_info_dict.items()} - groups = {slot: slot_info.group_members for slot, slot_info in slot_info_dict.items() - if slot_info.type == SlotType.group} - names = [[slot_info.name for slot, slot_info in sorted(slot_info_dict.items())]] - for game in games.values(): - if game not in multidata["datapackage"]: - continue - game_data = multidata["datapackage"][game] - if "checksum" in game_data: - if network_data_package["games"].get(game, {}).get("checksum") == game_data["checksum"]: - # non-custom. remove from multidata - # network_data_package import could be skipped once all rooms embed data package - del multidata["datapackage"][game] - continue - else: - game_data = restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data) - custom_locations.update( - {id_: name for name, id_ in game_data["location_name_to_id"].items()}) - custom_items.update( - {id_: name for name, id_ in game_data["item_name_to_id"].items()}) - seed_checks_in_area = checks_in_area.copy() + return method_wrapper - use_door_tracker = False - if "tags" in multidata: - use_door_tracker = "DR" in multidata["tags"] - if use_door_tracker: - for area, checks in key_only_locations.items(): - seed_checks_in_area[area] += len(checks) - seed_checks_in_area["Total"] = 249 - player_checks_in_area = { - playernumber: { - areaname: len(multidata["checks_in_area"][playernumber][areaname]) if areaname != "Total" else - multidata["checks_in_area"][playernumber]["Total"] - for areaname in ordered_areas +@dataclass +class TrackerData: + """A helper dataclass that is instantiated each time an HTTP request comes in for tracker data. + + Provides helper methods to lazily load necessary data that each tracker require and caches any results so any + subsequent helper method calls do not need to recompute results during the lifetime of this instance. + """ + room: Room + _multidata: Dict[str, Any] + _multisave: Dict[str, Any] + _tracker_cache: Dict[str, Any] + + def __init__(self, room: Room): + """Initialize a new RoomMultidata object for the current room.""" + self.room = room + self._multidata = Context.decompress(room.seed.multidata) + self._multisave = restricted_loads(room.multisave) if room.multisave else {} + self._tracker_cache = {} + + self.item_name_to_id: Dict[str, Dict[str, int]] = {} + self.location_name_to_id: Dict[str, Dict[str, int]] = {} + + # Generate inverse lookup tables from data package, useful for trackers. + self.item_id_to_name: Dict[str, Dict[int, str]] = {} + self.location_id_to_name: Dict[str, Dict[int, str]] = {} + for game, game_package in self._multidata["datapackage"].items(): + game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data) + self.item_id_to_name[game] = {id: name for name, id in game_package["item_name_to_id"].items()} + self.location_id_to_name[game] = {id: name for name, id in game_package["location_name_to_id"].items()} + + # Normal lookup tables as well. + self.item_name_to_id[game] = game_package["item_name_to_id"] + self.location_name_to_id[game] = game_package["item_name_to_id"] + + def get_seed_name(self) -> str: + """Retrieves the seed name.""" + return self._multidata["seed_name"] + + def get_slot_data(self, team: int, player: int) -> Dict[str, Any]: + """Retrieves the slot data for a given player.""" + return self._multidata["slot_data"][player] + + def get_slot_info(self, team: int, player: int) -> NetworkSlot: + """Retrieves the NetworkSlot data for a given player.""" + return self._multidata["slot_info"][player] + + def get_player_name(self, team: int, player: int) -> str: + """Retrieves the slot name for a given player.""" + return self.get_slot_info(team, player).name + + def get_player_game(self, team: int, player: int) -> str: + """Retrieves the game for a given player.""" + return self.get_slot_info(team, player).game + + def get_player_locations(self, team: int, player: int) -> Dict[int, ItemMetadata]: + """Retrieves all locations with their containing item's metadata for a given player.""" + return self._multidata["locations"][player] + + def get_player_starting_inventory(self, team: int, player: int) -> List[int]: + """Retrieves a list of all item codes a given slot starts with.""" + return self._multidata["precollected_items"][player] + + def get_player_checked_locations(self, team: int, player: int) -> Set[int]: + """Retrieves the set of all locations marked complete by this player.""" + return self._multisave.get("location_checks", {}).get((team, player), set()) + + @_cache_results + def get_player_missing_locations(self, team: int, player: int) -> Set[int]: + """Retrieves the set of all locations not marked complete by this player.""" + return set(self.get_player_locations(team, player)) - self.get_player_checked_locations(team, player) + + def get_player_received_items(self, team: int, player: int) -> List[NetworkItem]: + """Returns all items received to this player in order of received.""" + return self._multisave.get("received_items", {}).get((team, player, True), []) + + @_cache_results + def get_player_inventory_counts(self, team: int, player: int) -> Dict[int, int]: + """Retrieves a dictionary of all items received by their id and their received count.""" + items = self.get_player_received_items(team, player) + inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]} + for item in items: + inventory[item.item] += 1 + + return inventory + + @_cache_results + def get_player_hints(self, team: int, player: int) -> Set[Hint]: + """Retrieves a set of all hints relevant for a particular player.""" + return self._multisave.get("hints", {}).get((team, player), set()) + + @_cache_results + def get_player_last_activity(self, team: int, player: int) -> Optional[datetime.timedelta]: + """Retrieves the relative timedelta for when a particular player was last active. + Returns None if no activity was ever recorded. + """ + return self.get_room_last_activity().get((team, player), None) + + def get_player_client_status(self, team: int, player: int) -> ClientStatus: + """Retrieves the ClientStatus of a particular player.""" + return self._multisave.get("client_game_state", {}).get((team, player), ClientStatus.CLIENT_UNKNOWN) + + def get_player_alias(self, team: int, player: int) -> Optional[str]: + """Returns the alias of a particular player, if any.""" + return self._multisave.get("name_aliases", {}).get((team, player), None) + + @_cache_results + def get_team_completed_worlds_count(self) -> Dict[int, int]: + """Retrieves a dictionary of number of completed worlds per team.""" + return { + team: sum( + self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL + for player in players if self.get_slot_info(team, player).type == SlotType.player + ) for team, players in self.get_team_players().items() } - for playernumber in multidata["checks_in_area"] - } - player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber]) - for playernumber in multidata["checks_in_area"]} - saving_second = get_saving_second(multidata["seed_name"]) - result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ - multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \ - custom_locations, custom_items - _multidata_cache[room.seed.id] = result - return result + @_cache_results + def get_team_hints(self) -> Dict[int, Set[Hint]]: + """Retrieves a dictionary of all hints per team.""" + hints = {} + for team, players in self.get_team_players().items(): + hints[team] = set() + for player in players: + hints[team] |= self.get_player_hints(team, player) + + return hints + + @_cache_results + def get_team_locations_total_count(self) -> Dict[int, int]: + """Retrieves a dictionary of total player locations each team has.""" + return { + team: sum(len(self.get_player_locations(team, player)) for player in players) + for team, players in self.get_team_players().items() + } + + @_cache_results + def get_team_locations_checked_count(self) -> Dict[int, int]: + """Retrieves a dictionary of checked player locations each team has.""" + return { + team: sum(len(self.get_player_checked_locations(team, player)) for player in players) + for team, players in self.get_team_players().items() + } + + # TODO: Change this method to properly build for each team once teams are properly implemented, as they don't + # currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0 + @_cache_results + def get_team_players(self) -> Dict[int, List[int]]: + """Retrieves a dictionary of all players ids on each team.""" + return { + 0: [player for player, slot_info in self._multidata["slot_info"].items()] + } + + @_cache_results + def get_room_saving_second(self) -> int: + """Retrieves the saving second value for this seed. + + Useful for knowing when the multisave gets updated so trackers can attempt to update. + """ + return get_saving_second(self.get_seed_name()) + + @_cache_results + def get_room_locations(self) -> Dict[TeamPlayer, Dict[int, ItemMetadata]]: + """Retrieves a dictionary of all locations and their associated item metadata per player.""" + return { + (team, player): self.get_player_locations(team, player) + for team, players in self.get_team_players().items() for player in players + } + + @_cache_results + def get_room_games(self) -> Dict[TeamPlayer, str]: + """Retrieves a dictionary of games for each player.""" + return { + (team, player): self.get_player_game(team, player) + for team, players in self.get_team_players().items() for player in players + } + + @_cache_results + def get_room_locations_complete(self) -> Dict[TeamPlayer, int]: + """Retrieves a dictionary of all locations complete per player.""" + return { + (team, player): len(self.get_player_checked_locations(team, player)) + for team, players in self.get_team_players().items() for player in players + } + + @_cache_results + def get_room_client_statuses(self) -> Dict[TeamPlayer, ClientStatus]: + """Retrieves a dictionary of all ClientStatus values per player.""" + return { + (team, player): self.get_player_client_status(team, player) + for team, players in self.get_team_players().items() for player in players + } + + @_cache_results + def get_room_long_player_names(self) -> Dict[TeamPlayer, str]: + """Retrieves a dictionary of names with aliases for each player.""" + long_player_names = {} + for team, players in self.get_team_players().items(): + for player in players: + alias = self.get_player_alias(team, player) + if alias: + long_player_names[team, player] = f"{alias} ({self.get_player_name(team, player)})" + else: + long_player_names[team, player] = self.get_player_name(team, player) + + return long_player_names + + @_cache_results + def get_room_last_activity(self) -> Dict[TeamPlayer, datetime.timedelta]: + """Retrieves a dictionary of all players and the timedelta from now to their last activity. + Does not include players who have no activity recorded. + """ + last_activity: Dict[TeamPlayer, datetime.timedelta] = {} + now = datetime.datetime.utcnow() + for (team, player), timestamp in self._multisave.get("client_activity_timers", []): + last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + + return last_activity + + @_cache_results + def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]: + """Retrieves a dictionary of any players who have video streaming enabled and their feeds. + + Only supported platforms are Twitch and YouTube. + """ + video_feeds = {} + for (team, player), video_data in self._multisave.get("video", []): + video_feeds[team, player] = video_data + + return video_feeds -@app.route('/tracker///') -def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): - key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}" +@app.route("/tracker///") +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: + key = f"{tracker}_{tracked_team}_{tracked_player}_{generic}" tracker_page = cache.get(key) if tracker_page: return tracker_page - timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic) + + timeout, tracker_page = get_timeout_and_tracker(tracker, tracked_team, tracked_player, generic) cache.set(key, tracker_page, timeout) return tracker_page -def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool): - # Team and player must be positive and greater than zero - if tracked_team < 0 or tracked_player < 1: - abort(404) - - room: Optional[Room] = Room.get(tracker=tracker) - if not room: - abort(404) - - # Collect seed information and pare it down to a single player - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) - player_name = names[tracked_team][tracked_player - 1] - location_to_area = player_location_to_area.get(tracked_player, {}) - inventory = collections.Counter() - checks_done = {loc_name: 0 for loc_name in default_locations} - - # Add starting items to inventory - starting_items = precollected_items[tracked_player] - if starting_items: - for item_id in starting_items: - attribute_item_solo(inventory, item_id) - - if room.multisave: - multisave: Dict[str, Any] = restricted_loads(room.multisave) - else: - multisave: Dict[str, Any] = {} - - slots_aimed_at_player = {tracked_player} - for group_id, group_members in groups.items(): - if tracked_player in group_members: - slots_aimed_at_player.add(group_id) - - # Add items to player inventory - for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items(): - # Skip teams and players not matching the request - player_locations = locations[ms_player] - if ms_team == tracked_team: - # If the player does not have the item, do nothing - for location in locations_checked: - if location in player_locations: - item, recipient, flags = player_locations[location] - if recipient in slots_aimed_at_player: # a check done for the tracked player - attribute_item_solo(inventory, item) - if ms_player == tracked_player: # a check done by the tracked player - area_name = location_to_area.get(location, None) - if area_name: - checks_done[area_name] += 1 - checks_done["Total"] += 1 - specific_tracker = game_specific_trackers.get(games[tracked_player], None) - if specific_tracker and not want_generic: - tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second) - else: - tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, - player_name, seed_checks_in_area, checks_done, saving_second, - custom_locations, custom_items) - - return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker - - -@app.route('/generic_tracker///') -def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): +@app.route("/generic_tracker///") +def get_generic_game_tracker(tracker: UUID, tracked_team: int, tracked_player: int) -> str: return get_player_tracker(tracker, tracked_team, tracked_player, True) -def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, player_name: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - # Note the presence of the triforce item - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - if game_state == 30: - inventory[106] = 1 # Triforce - - # Progressive items need special handling for icons and class - progressive_items = { - "Progressive Sword": 94, - "Progressive Glove": 97, - "Progressive Bow": 100, - "Progressive Mail": 96, - "Progressive Shield": 95, - } - progressive_names = { - "Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], - "Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], - "Progressive Bow": [None, "Bow", "Silver Bow"], - "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], - "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] - } - - # Determine which icon to use - display_data = {} - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - acquired = True - if not display_name: - acquired = False - display_name = progressive_names[item_name][level + 1] - base_name = item_name.split(maxsplit=1)[1].lower() - display_data[base_name + "_acquired"] = acquired - display_data[base_name + "_url"] = alttp_icons[display_name] - - # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? - sp_areas = ordered_areas[2:15] - - player_big_key_locations = set() - player_small_key_locations = set() - for loc_data in locations.values(): - for values in loc_data.values(): - item_id, item_player, flags = values - if item_player == player: - if item_id in ids_big_key: - player_big_key_locations.add(ids_big_key[item_id]) - elif item_id in ids_small_key: - player_small_key_locations.add(ids_small_key[item_id]) - - return render_template("lttpTracker.html", inventory=inventory, - player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done, - checks_in_area=seed_checks_in_area[player], - acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, - small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, - key_locations=player_small_key_locations, - big_key_locations=player_big_key_locations, - **display_data) - - -def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", - "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", - "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", - "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", - "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", - "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", - "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", - "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", - "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", - "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", - "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", - "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", - "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", - "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", - "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", - "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", - "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", - "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", - "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", - "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", - "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", - "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", - "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", - "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", - "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", - "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", - "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", - "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", - "Saddle": "https://i.imgur.com/2QtDyR0.png", - "Channeling Book": "https://i.imgur.com/J3WsYZw.png", - "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", - "Piercing IV Book": "https://i.imgur.com/OzJptGz.png", - } - - minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, - 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], - "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, - 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], - "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], - "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020, - 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105, 42099, 42103, 42110, 42100], - "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111, 42112, - 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], - "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], - } - - display_data = {} - - # Determine display for progressive items - progressive_items = { - "Progressive Tools": 45013, - "Progressive Weapons": 45012, - "Progressive Armor": 45014, - "Progressive Resource Crafting": 45001 - } - progressive_names = { - "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], - "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], - "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], - "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] - } - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') - display_data[base_name + "_url"] = icons[display_name] - - # Multi-items - multi_items = { - "3 Ender Pearls": 45029, - "8 Netherite Scrap": 45015, - "Dragon Egg Shard": 45043 - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if count >= 0: - display_data[base_name + "_count"] = count - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in minecraft_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("minecraftTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, saving_second = saving_second, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - - -def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", - "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png", - "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png", - "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png", - "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png", - "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png", - "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png", - "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png", - "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png", - "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png", - "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png", - "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png", - "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png", - "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png", - "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png", - "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png", - "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", - "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", - "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png", - "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png", - "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png", - "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png", - "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png", - "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png", - "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png", - "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png", - "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png", - "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png", - "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png", - "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png", - "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png", - "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png", - "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png", - "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png", - "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png", - "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png", - "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", - "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png", - "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png", - "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", - "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png", - "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png", - "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png", - "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png", - "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png", - "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png", - "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png", - "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", - } - - display_data = {} - - # Determine display for progressive items - progressive_items = { - "Progressive Hookshot": 66128, - "Progressive Strength Upgrade": 66129, - "Progressive Wallet": 66133, - "Progressive Scale": 66134, - "Magic Meter": 66138, - "Ocarina": 66139, - } - - progressive_names = { - "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"], - "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"], - "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"], - "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"], - "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"], - "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"] - } - - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name])-1) - display_name = progressive_names[item_name][level] - if item_name.startswith("Progressive"): - base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_') - else: - base_name = item_name.lower().replace(' ', '_') - display_data[base_name+"_url"] = icons[display_name] - - if base_name == "hookshot": - display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) - if base_name == "wallet": - display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) - - # Determine display for bottles. Show letter if it's obtained, determine bottle count - bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148] - display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4) - display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle'] - - # Determine bombchu display - display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137])) - - # Multi-items - multi_items = { - "Gold Skulltula Token": 66091, - "Triforce Piece": 66202, - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - display_data[base_name+"_count"] = inventory[item_id] - - # Gather dungeon locations - area_id_ranges = { - "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)), - "Deku Tree": ((67281, 67303), (68063, 68077)), - "Dodongo's Cavern": ((67304, 67334), (68078, 68160)), - "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)), - "Bottom of the Well": ((67360, 67384), (68189, 68230)), - "Forest Temple": ((67385, 67420), (68231, 68281)), - "Fire Temple": ((67421, 67457), (68282, 68350)), - "Water Temple": ((67458, 67484), (68351, 68483)), - "Shadow Temple": ((67485, 67532), (68484, 68565)), - "Spirit Temple": ((67533, 67582), (68566, 68625)), - "Ice Cavern": ((67583, 67596), (68626, 68649)), - "Gerudo Training Ground": ((67597, 67635), (68650, 68656)), - "Thieves' Hideout": ((67264, 67268), (68025, 68053)), - "Ganon's Castle": ((67636, 67673), (68657, 68705)), - } - - def lookup_and_trim(id, area): - full_name = lookup_any_location_id_to_name[id] - if 'Ganons Tower' in full_name: - return full_name - if area not in ["Overworld", "Thieves' Hideout"]: - # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC - return full_name[len(area):] - return full_name - - checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player])) - location_info = {} - checks_done = {} - checks_in_area = {} - for area, ranges in area_id_ranges.items(): - location_info[area] = {} - checks_done[area] = 0 - checks_in_area[area] = 0 - for r in ranges: - min_id, max_id = r - for id in range(min_id, max_id+1): - if id in locations[player]: - checked = id in checked_locations - location_info[area][lookup_and_trim(id, area)] = checked - checks_in_area[area] += 1 - checks_done[area] += checked - - checks_done['Total'] = sum(checks_done.values()) - checks_in_area['Total'] = sum(checks_in_area.values()) - - # Give skulltulas on non-tracked locations - non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player])) - for id in non_tracked_locations: - if "GS" in lookup_and_trim(id, ''): - display_data["token_count"] += 1 - - oot_y = '✔' - oot_x = '✕' - - # Gather small and boss key info - small_key_counts = { - "Forest Temple": oot_y if inventory[66203] else inventory[66175], - "Fire Temple": oot_y if inventory[66204] else inventory[66176], - "Water Temple": oot_y if inventory[66205] else inventory[66177], - "Spirit Temple": oot_y if inventory[66206] else inventory[66178], - "Shadow Temple": oot_y if inventory[66207] else inventory[66179], - "Bottom of the Well": oot_y if inventory[66208] else inventory[66180], - "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181], - "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182], - "Ganon's Castle": oot_y if inventory[66211] else inventory[66183], - } - boss_key_counts = { - "Forest Temple": oot_y if inventory[66149] else oot_x, - "Fire Temple": oot_y if inventory[66150] else oot_x, - "Water Temple": oot_y if inventory[66151] else oot_x, - "Spirit Temple": oot_y if inventory[66152] else oot_x, - "Shadow Temple": oot_y if inventory[66153] else oot_x, - "Ganon's Castle": oot_y if inventory[66154] else oot_x, - } - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - return render_template("ootTracker.html", - inventory=inventory, player=player, team=team, room=room, player_name=playerName, - icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - small_key_counts=small_key_counts, boss_key_counts=boss_key_counts, - **display_data) - - -def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - slot_data: Dict[str, Any], saving_second: int) -> str: - - icons = { - "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png", - "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png", - "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png", - "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png", - "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png", - "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png", - "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png", - "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png", - "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png", - "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png", - "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png", - "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png", - "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png", - "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png", - "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png", - "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png", - "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png", - "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png", - "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png", - "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png", - "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png", - "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png", - "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png", - "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png", - "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png", - "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png", - "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png", - "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png", - "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png", - } - - timespinner_location_ids = { - "Present": [ - 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, - 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, - 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, - 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039, - 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049, - 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059, - 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069, - 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079, - 1337080, 1337081, 1337082, 1337083, 1337084, 1337085], - "Past": [ - 1337086, 1337087, 1337088, 1337089, - 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099, - 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109, - 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119, - 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129, - 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139, - 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149, - 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, - 1337171, 1337172, 1337173, 1337174, 1337175], - "Ancient Pyramid": [ - 1337236, - 1337246, 1337247, 1337248, 1337249] - } - - if(slot_data["DownloadableItems"]): - timespinner_location_ids["Present"] += [ - 1337156, 1337157, 1337159, - 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, - 1337170] - if(slot_data["Cantoran"]): - timespinner_location_ids["Past"].append(1337176) - if(slot_data["LoreChecks"]): - timespinner_location_ids["Present"] += [ - 1337177, 1337178, 1337179, - 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] - timespinner_location_ids["Past"] += [ - 1337188, 1337189, - 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198] - if(slot_data["GyreArchives"]): - timespinner_location_ids["Ancient Pyramid"] += [ - 1337237, 1337238, 1337239, - 1337240, 1337241, 1337242, 1337243, 1337244, 1337245] - - display_data = {} - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in timespinner_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in timespinner_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name} - options = {k for k, v in slot_data.items() if v} - - return render_template("timespinnerTracker.html", - inventory=inventory, icons=icons, acquired_items=acquired_items, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - options=options, **display_data) - -def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, - saving_second: int) -> str: - - icons = { - "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png", - "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png", - "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png", - "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png", - "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png", - "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png", - "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png", - "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png", - "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png", - "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png", - "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png", - "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png", - "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png", - "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png", - "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png", - "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png", - "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png", - "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png", - "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png", - "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png", - "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png", - "Nothing": "", - "No Energy": "", - "Kraid": "", - "Phantoon": "", - "Draygon": "", - "Ridley": "", - "Mother Brain": "", - } - - multi_items = { - "Energy Tank": 83000, - "Missile": 83001, - "Super Missile": 83002, - "Power Bomb": 83003, - "Reserve Tank": 83020, - } - - supermetroid_location_ids = { - 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029, - 82000, 82004, 82006, 82009, 82010, - 82011, 82012, 82027, 82028, 82034, - 82036, 82037], - 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035, - 82013, 82014, 82015, 82016, 82018, - 82019, 82021, 82022, 82024, 82025, - 82031], - 'Red Brinstar': [82038, 82042, 82039, 82040, 82041], - 'Kraid': [82043, 82048, 82044], - 'Norfair': [82050, 82053, 82061, 82066, 82068, - 82049, 82051, 82054, 82055, 82056, - 82062, 82063, 82064, 82065, 82067], - 'Lower Norfair': [82078, 82079, 82080, 82070, 82071, - 82073, 82074, 82075, 82076, 82077], - 'Crocomire': [82052, 82060, 82057, 82058, 82059], - 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001, - 82002, 82003, 82128, 82130, 82131, - 82133], - 'West Maridia': [82138, 82136, 82137, 82139, 82140, - 82141, 82142], - 'East Maridia': [82143, 82145, 82150, 82152, 82154, - 82144, 82146, 82147, 82148, 82149, - 82151], - } - - display_data = {} - - - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[0].lower() - display_data[base_name+"_count"] = inventory[item_id] - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into advancement tab counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} - for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) - for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("supermetroidTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - slot_data: Dict, saving_second: int) -> str: - - SC2WOL_LOC_ID_OFFSET = 1000 - SC2WOL_ITEM_ID_OFFSET = 1000 - - - icons = { - "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", - "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", - "Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif", - - "Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png", - "Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png", - "Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png", - "Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png", - "Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png", - "Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png", - "Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png", - "Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png", - "Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png", - "Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png", - "Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png", - "Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png", - "Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png", - "Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png", - "Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png", - "Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png", - "Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png", - "Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png", - - "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", - "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", - "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg", - - "Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png", - "Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png", - "Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png", - "Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", - "Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png", - "Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png", - "Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png", - "Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png", - - "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg", - "Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg", - "Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg", - "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg", - "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", - - "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png", - "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png", - "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png", - "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png", - "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png", - "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png", - "Restoration (Medic)": "/static/static/icons/sc2/restoration.png", - "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png", - "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png", - "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png", - "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png", - "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png", - "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png", - "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png", - "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png", - "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png", - "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png", - "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png", - "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png", - "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png", - "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png", - "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png", - "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png", - "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png", - - "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", - "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg", - "Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg", - "Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg", - "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg", - - "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", - "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", - "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png", - "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png", - "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png", - "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png", - "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", - "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png", - "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png", - "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png", - "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png", - "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png", - "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png", - "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png", - "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png", - "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png", - "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png", - "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png", - "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", - "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png", - "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png", - "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png", - "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png", - "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png", - "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png", - "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png", - "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png", - "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png", - "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png", - "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png", - - "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", - "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg", - "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg", - "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg", - "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg", - - "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png", - "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png", - "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png", - "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png", - "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png", - "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png", - "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png", - "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png", - "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png", - "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png", - "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png", - "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png", - "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png", - "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png", - "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png", - "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png", - "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png", - "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", - "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png", - "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png", - "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png", - "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png", - "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png", - - "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", - "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg", - "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", - - "Widow Mine": "/static/static/icons/sc2/widowmine.png", - "Cyclone": "/static/static/icons/sc2/cyclone.png", - "Liberator": "/static/static/icons/sc2/liberator.png", - "Valkyrie": "/static/static/icons/sc2/valkyrie.png", - - "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png", - "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png", - "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png", - "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png", - "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png", - "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png", - "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png", - "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png", - "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png", - "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png", - "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png", - - "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png", - "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png", - "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png", - "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png", - "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png", - "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png", - "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png", - "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png", - "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png", - "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png", - "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png", - "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png", - "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png", - "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png", - "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png", - "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png", - "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png", - "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", - "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png", - "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png", - "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png", - "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png", - "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png", - "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", - "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", - "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png", - "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png", - - "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", - "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", - "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg", - "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg", - "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg", - "Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg", - "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg", - "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg", - - "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png", - "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png", - "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png", - "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png", - "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png", - "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png", - "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png", - "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png", - "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", - "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", - - "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", - "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", - "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", - "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", - "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png", - "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", - "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", - "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png", - "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png", - "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", - "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", - - "Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg", - "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg", - "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg", - "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg", - "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg", - "Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg", - "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg", - "Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg", - "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg", - - "Nothing": "", - } - sc2wol_location_ids = { - "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), - "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), - "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), - "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), - "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), - "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), - "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), - "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), - "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), - "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), - "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), - "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), - "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), - "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), - "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), - "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), - "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), - "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), - "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), - "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), - "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), - "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), - "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), - "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), - "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), - "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), - "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), - "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), - } - - display_data = {} - - # Grouped Items - grouped_item_ids = { - "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET - } - grouped_item_replacements = { - "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"], - "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"], - "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"], - "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"], - "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"] - } - grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"] - replacement_item_ids = { - "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - } - for grouped_item_name, grouped_item_id in grouped_item_ids.items(): - count: int = inventory[grouped_item_id] - if count > 0: - for replacement_item in grouped_item_replacements[grouped_item_name]: - replacement_id: int = replacement_item_ids[replacement_item] - inventory[replacement_id] = count - - # Determine display for progressive items - progressive_items = { - "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET, - "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET - } - progressive_names = { - "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"], - "Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", "Infantry Armor Level 2", "Infantry Armor Level 3"], - "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"], - "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"], - "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"], - "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"], - "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"], - "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"], - "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"], - "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"], - "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"], - "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"], - "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"], - "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"] - } - for item_name, item_id in progressive_items.items(): - level = min(inventory[item_id], len(progressive_names[item_name]) - 1) - display_name = progressive_names[item_name][level] - base_name = (item_name.split(maxsplit=1)[1].lower() - .replace(' ', '_') - .replace("-", "") - .replace("(", "") - .replace(")", "")) - display_data[base_name + "_level"] = level - display_data[base_name + "_url"] = icons[display_name] - display_data[base_name + "_name"] = display_name - - # Multi-items - multi_items = { - "+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET, - "+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET, - "+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - if base_name == "supply": - count = count * 2 - display_data[base_name + "_count"] = count - else: - count = count * 15 - display_data[base_name + "_count"] = count - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - # Turn location IDs into mission objective counts - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if id in set(locations[player])} for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_done = {mission_name: len([id for id in mission_locations if id in checked_locations and id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_done['Total'] = len(checked_locations) - checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()} - checks_in_area['Total'] = sum(checks_in_area.values()) - - return render_template("sc2wolTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str: - - icons = { - "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", - "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", - "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", - "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", - - "Nothing": "", - } - - checksfinder_location_ids = { - "Tile 1": 81000, - "Tile 2": 81001, - "Tile 3": 81002, - "Tile 4": 81003, - "Tile 5": 81004, - "Tile 6": 81005, - "Tile 7": 81006, - "Tile 8": 81007, - "Tile 9": 81008, - "Tile 10": 81009, - "Tile 11": 81010, - "Tile 12": 81011, - "Tile 13": 81012, - "Tile 14": 81013, - "Tile 15": 81014, - "Tile 16": 81015, - "Tile 17": 81016, - "Tile 18": 81017, - "Tile 19": 81018, - "Tile 20": 81019, - "Tile 21": 81020, - "Tile 22": 81021, - "Tile 23": 81022, - "Tile 24": 81023, - "Tile 25": 81024, - } - - display_data = {} - - # Multi-items - multi_items = { - "Map Width": 80000, - "Map Height": 80001, - "Map Bombs": 80002 - } - for item_name, item_id in multi_items.items(): - base_name = item_name.split()[-1].lower() - count = inventory[item_id] - display_data[base_name + "_count"] = count - display_data[base_name + "_display"] = count + 5 - - # Get location info - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - lookup_name = lambda id: lookup_any_location_id_to_name[id] - location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])} - checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])} - checks_done['Total'] = len(checked_locations) - checks_in_area = checks_done - - # Calculate checks available - display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) - display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) - - # Victory condition - game_state = multisave.get("client_game_state", {}).get((team, player), 0) - display_data['game_finished'] = game_state == 30 - - return render_template("checksfinderTracker.html", - inventory=inventory, icons=icons, - acquired_items={lookup_any_item_id_to_name[id] for id in inventory if - id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, - checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, - **display_data) - -def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], - saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str: - - checked_locations = multisave.get("location_checks", {}).get((team, player), set()) - player_received_items = {} - if multisave.get('version', 0) > 0: - ordered_items = multisave.get('received_items', {}).get((team, player, True), []) - else: - ordered_items = multisave.get('received_items', {}).get((team, player), []) - - # add numbering to all items but starter_inventory - for order_index, networkItem in enumerate(ordered_items, start=1): - player_received_items[networkItem.item] = order_index - - return render_template("genericTracker.html", - inventory=inventory, - player=player, team=team, room=room, player_name=playerName, - checked_locations=checked_locations, - not_checked_locations=set(locations[player]) - checked_locations, - received_items=player_received_items, saving_second=saving_second, - custom_items=custom_items, custom_locations=custom_locations) - - -def get_enabled_multiworld_trackers(room: Room, current: str): - enabled = [ - { - "name": "Generic", - "endpoint": "get_multiworld_tracker", - "current": current == "Generic" - } - ] - for game_name, endpoint in multi_trackers.items(): - if any(slot.game == game_name for slot in room.seed.slots) or current == game_name: - enabled.append({ - "name": game_name, - "endpoint": endpoint.__name__, - "current": current == game_name} - ) - return enabled - - -def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]: - room: Room = Room.get(tracker=tracker) +@app.route("/tracker/", defaults={"game": "Generic"}) +@app.route("/tracker//") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_tracker(tracker: UUID, game: str): + # Room must exist. + room = Room.get(tracker=tracker) if not room: - return None + abort(404) - locations, names, use_door_tracker, checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) + tracker_data = TrackerData(room) + enabled_trackers = list(get_enabled_multiworld_trackers(room).keys()) + if game not in _multiworld_trackers: + return render_generic_multiworld_tracker(tracker_data, enabled_trackers) - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} + return _multiworld_trackers[game](tracker_data, enabled_trackers) - percent_total_checks_done = {teamnumber: {playernumber: 0 - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - total_locations = {teamnumber: sum(len(locations[playernumber]) - for playernumber in range(1, len(team) + 1) if playernumber not in groups) - for teamnumber, team in enumerate(names)} +def get_timeout_and_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool) -> Tuple[int, str]: + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) - hints = {team: set() for team in range(len(names))} - if room.multisave: - multisave = restricted_loads(room.multisave) + tracker_data = TrackerData(room) + + # Load and render the game-specific player tracker, or fallback to generic tracker if none exists. + game_specific_tracker = _player_trackers.get(tracker_data.get_player_game(tracked_team, tracked_player), None) + if game_specific_tracker and not generic: + tracker = game_specific_tracker(tracker_data, tracked_team, tracked_player) else: - multisave = {} - if "hints" in multisave: - for (team, slot), slot_hints in multisave["hints"].items(): - hints[team] |= set(slot_hints) + tracker = render_generic_tracker(tracker_data, tracked_team, tracked_player) - for (team, player), locations_checked in multisave.get("location_checks", {}).items(): - if player in groups: - continue - player_locations = locations[player] - checks_done[team][player]["Total"] = len(locations_checked) - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 - ) + return (tracker_data.get_room_saving_second() - datetime.datetime.now().second) % 60 or 60, tracker - activity_timers = {} - now = datetime.datetime.utcnow() - for (team, player), timestamp in multisave.get("client_activity_timers", []): - activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) - player_names = {} - completed_worlds = 0 - states: typing.Dict[typing.Tuple[int, int], int] = {} - for team, names in enumerate(names): - for player, name in enumerate(names, 1): - player_names[team, player] = name - states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) - if states[team, player] == ClientStatus.CLIENT_GOAL and player not in groups: - completed_worlds += 1 - long_player_names = player_names.copy() - for (team, player), alias in multisave.get("name_aliases", {}).items(): - player_names[team, player] = alias - long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})" +def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: + # Render the multitracker for any games that exist in the current room if they are defined. + enabled_trackers = {} + for game_name, endpoint in _multiworld_trackers.items(): + if any(slot.game == game_name for slot in room.seed.slots): + enabled_trackers[game_name] = endpoint - video = {} - for (team, player), data in multisave.get("video", []): - video[team, player] = data + # We resort the tracker to have Generic first, then lexicographically each enabled game. + return { + "Generic": render_generic_multiworld_tracker, + **{key: enabled_trackers[key] for key in sorted(enabled_trackers.keys())}, + } - return dict( - player_names=player_names, room=room, checks_done=checks_done, - percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area, - activity_timers=activity_timers, video=video, hints=hints, - long_player_names=long_player_names, - multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, total_locations=total_locations, games=games, states=states, - completed_worlds=completed_worlds, - custom_locations=custom_locations, custom_items=custom_items, + +def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + game = tracker_data.get_player_game(team, player) + + # Add received index to all received items, excluding starting inventory. + received_items_in_order = {} + for received_index, network_item in enumerate(tracker_data.get_player_received_items(team, player), start=1): + received_items_in_order[network_item.item] = received_index + + return render_template( + template_name_or_list="genericTracker.html", + game_specific_tracker=game in _player_trackers, + room=tracker_data.room, + team=team, + player=player, + player_name=tracker_data.get_room_long_player_names()[team, player], + inventory=tracker_data.get_player_inventory_counts(team, player), + locations=tracker_data.get_player_locations(team, player), + checked_locations=tracker_data.get_player_checked_locations(team, player), + received_items=received_items_in_order, + saving_second=tracker_data.get_room_saving_second(), + game=game, + games=tracker_data.get_room_games(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + location_id_to_name=tracker_data.location_id_to_name, + item_id_to_name=tracker_data.item_id_to_name, + hints=tracker_data.get_player_hints(team, player), ) -def _get_inventory_data(data: typing.Dict[str, typing.Any]) \ - -> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]: - inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = { - teamnumber: {playernumber: collections.Counter() for playernumber in team_data} - for teamnumber, team_data in data["checks_done"].items() - } - - groups = data["groups"] - - for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items(): - if player in data["groups"]: - continue - player_locations = data["locations"][player] - precollected = data["precollected_items"][player] - for item_id in precollected: - inventory[team][player][item_id] += 1 - for location in locations_checked: - item_id, recipient, flags = player_locations[location] - recipients = groups.get(recipient, [recipient]) - for recipient in recipients: - inventory[team][recipient][item_id] += 1 - return inventory +def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]) -> str: + return render_template( + "multitracker.html", + enabled_trackers=enabled_trackers, + current_tracker="Generic", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + ) -def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \ - -> typing.Dict[str, int]: - """slow""" - if custom_items: - mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name) - else: - mapping = lookup_any_item_id_to_name +# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to +# live in their respective world folders. +import collections - return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()}) +from worlds import network_data_package -@app.route('/tracker/') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def get_multiworld_tracker(tracker: UUID): - data = _get_multiworld_tracker_data(tracker) - if not data: - abort(404) +if "Factorio" in network_data_package["games"]: + def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]): + inventories: Dict[TeamPlayer, Dict[int, int]] = { + (team, player): { + tracker_data.item_id_to_name["Factorio"][item_id]: count + for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() + } for team, players in tracker_data.get_team_players().items() for player in players + if tracker_data.get_player_game(team, player) == "Factorio" + } - data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic") - - return render_template("multiTracker.html", **data) - -if "Factorio" in games: - @app.route('/tracker//Factorio') - @cache.memoize(timeout=60) # multisave is currently created at most every minute - def get_Factorio_multiworld_tracker(tracker: UUID): - data = _get_multiworld_tracker_data(tracker) - if not data: - abort(404) - - data["inventory"] = _get_inventory_data(data) - data["named_inventory"] = {team_id : { - player_id: _get_named_inventory(inventory, data["custom_items"]) - for player_id, inventory in team_inventory.items() - } for team_id, team_inventory in data["inventory"].items()} - data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio") - - return render_template("multiFactorioTracker.html", **data) - - -@app.route('/tracker//A Link to the Past') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def get_LttP_multiworld_tracker(tracker: UUID): - room: Room = Room.get(tracker=tracker) - if not room: - abort(404) - locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ - get_static_room_data(room) - - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if - playernumber not in groups} - for teamnumber, team in enumerate(names)} - - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - - percent_total_checks_done = {teamnumber: {playernumber: 0 - for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - - hints = {team: set() for team in range(len(names))} - if room.multisave: - multisave = restricted_loads(room.multisave) - else: - multisave = {} - if "hints" in multisave: - for (team, slot), slot_hints in multisave["hints"].items(): - hints[team] |= set(slot_hints) - - def attribute_item(team: int, recipient: int, item: int): - nonlocal inventory - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) - else: - inventory[team][recipient][target_item] += 1 - - for (team, player), locations_checked in multisave.get("location_checks", {}).items(): - if player in groups: - continue - player_locations = locations[player] - if precollected_items: - precollected = precollected_items[player] - for item_id in precollected: - attribute_item(team, player, item_id) - for location in locations_checked: - if location not in player_locations or location not in player_location_to_area.get(player, {}): - continue - item, recipient, flags = player_locations[location] - recipients = groups.get(recipient, [recipient]) - for recipient in recipients: - attribute_item(team, recipient, item) - checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] = len(locations_checked) - - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 + return render_template( + "multitracker__Factorio.html", + enabled_trackers=enabled_trackers, + current_tracker="Factorio", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + inventories=inventories, ) - for (team, player), game_state in multisave.get("client_game_state", {}).items(): - if player in groups: - continue - if game_state == 30: - inventory[team][player][106] = 1 # Triforce + _multiworld_trackers["Factorio"] = render_Factorio_multiworld_tracker - player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} - player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} - for loc_data in locations.values(): - for values in loc_data.values(): - item_id, item_player, flags = values +if "A Link to the Past" in network_data_package["games"]: + def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_trackers: List[str]): + # Helper objects. + alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"] + multi_items = { + alttp_id_lookup[name] + for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove", "Triforce Piece") + } + links = { + "Bow": "Progressive Bow", + "Silver Arrows": "Progressive Bow", + "Silver Bow": "Progressive Bow", + "Progressive Bow (Alt)": "Progressive Bow", + "Bottle (Red Potion)": "Bottle", + "Bottle (Green Potion)": "Bottle", + "Bottle (Blue Potion)": "Bottle", + "Bottle (Fairy)": "Bottle", + "Bottle (Bee)": "Bottle", + "Bottle (Good Bee)": "Bottle", + "Fighter Sword": "Progressive Sword", + "Master Sword": "Progressive Sword", + "Tempered Sword": "Progressive Sword", + "Golden Sword": "Progressive Sword", + "Power Glove": "Progressive Glove", + "Titans Mitts": "Progressive Glove", + } + links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()} + levels = { + "Fighter Sword": 1, + "Master Sword": 2, + "Tempered Sword": 3, + "Golden Sword": 4, + "Power Glove": 1, + "Titans Mitts": 2, + "Bow": 1, + "Silver Bow": 2, + "Triforce Piece": 90, + } + tracking_names = [ + "Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute", + "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang", + "Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria", + "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce", + ] + default_locations = { + "Light World": { + 1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, + 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, + 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, + 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, + 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, + 59881, 59761, 59890, 59770, 193020, 212605 + }, + "Dark World": { + 59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, + 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031 + }, + "Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830}, + "Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773}, + "Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, + "Agahnims Tower": {60082, 60085}, + "Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899}, + "Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, + "Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, + "Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, + "Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, + "Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, + "Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, + "Palace of Darkness": { + 59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, + 59965 + }, + "Ganons Tower": { + 60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, + 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157 + }, + "Total": set() + } + key_only_locations = { + "Light World": set(), + "Dark World": set(), + "Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028}, + "Eastern Palace": {0x14005b, 0x140049}, + "Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d}, + "Agahnims Tower": {0x140061, 0x140052}, + "Tower of Hera": set(), + "Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, + "Thieves Town": {0x14005e, 0x14004f}, + "Skull Woods": {0x14002e, 0x14001c}, + "Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046}, + "Misery Mire": {0x140055, 0x14004c, 0x140064}, + "Turtle Rock": {0x140058, 0x140007}, + "Palace of Darkness": set(), + "Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f}, + "Total": set() + } + location_to_area = {} + for area, locations in default_locations.items(): + for location in locations: + location_to_area[location] = area + for area, locations in key_only_locations.items(): + for location in locations: + location_to_area[location] = area + + checks_in_area = {area: len(checks) for area, checks in default_locations.items()} + checks_in_area["Total"] = 216 + ordered_areas = ( + "Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace", + "Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace", + "Misery Mire", "Turtle Rock", "Ganons Tower", "Total" + ) + + player_checks_in_area = { + (team, player): { + area_name: len(tracker_data._multidata["checks_in_area"][player][area_name]) + if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"] + for area_name in ordered_areas + } + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } + + tracking_ids = [] + for item in tracking_names: + tracking_ids.append(alttp_id_lookup[item]) + + # Can't wait to get this into the apworld. Oof. + from worlds.alttp import Items + + small_key_ids = {} + big_key_ids = {} + ids_small_key = {} + ids_big_key = {} + for item_name, data in Items.item_table.items(): + if "Key" in item_name: + area = item_name.split("(")[1][:-1] + if "Small" in item_name: + small_key_ids[area] = data[2] + ids_small_key[data[2]] = area + else: + big_key_ids[area] = data[2] + ids_big_key[data[2]] = area + + def _get_location_table(checks_table: dict) -> dict: + loc_to_area = {} + for area, locations in checks_table.items(): + if area == "Total": + continue + for location in locations: + loc_to_area[location] = area + return loc_to_area + + player_location_to_area = { + (team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player]) + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } + + checks_done: Dict[TeamPlayer, Dict[str: int]] = { + (team, player): {location_name: 0 for location_name in default_locations} + for team, players in tracker_data.get_team_players().items() + for player in players + if tracker_data.get_slot_info(team, player).type != SlotType.group and + tracker_data.get_slot_info(team, player).game == "A Link to the Past" + } + + inventories: Dict[TeamPlayer, Dict[int, int]] = {} + player_big_key_locations = {(player): set() for player in tracker_data.get_team_players()[0]} + player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]} + group_big_key_locations = set() + group_key_locations = set() + + for (team, player), locations in checks_done.items(): + # Check if game complete. + if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL: + inventories[team, player][106] = 1 # Triforce + + # Count number of locations checked. + for location in tracker_data.get_player_checked_locations(team, player): + checks_done[team, player][player_location_to_area[team, player][location]] += 1 + checks_done[team, player]["Total"] += 1 + + # Count keys. + for location, (item, receiving, _) in tracker_data.get_player_locations(team, player).items(): + if item in ids_big_key: + player_big_key_locations[receiving].add(ids_big_key[item]) + elif item in ids_small_key: + player_small_key_locations[receiving].add(ids_small_key[item]) + + # Iterate over received items and build inventory/key counts. + inventories[team, player] = collections.Counter() + for network_item in tracker_data.get_player_received_items(team, player): + target_item = links.get(network_item.item, network_item.item) + if network_item.item in levels: # non-progressive + inventories[team, player][target_item] = (max(inventories[team, player][target_item], levels[network_item.item])) + else: + inventories[team, player][target_item] += 1 + + group_key_locations |= player_small_key_locations[player] + group_big_key_locations |= player_big_key_locations[player] + + return render_template( + "multitracker__ALinkToThePast.html", + enabled_trackers=enabled_trackers, + current_tracker="A Link to the Past", + room=tracker_data.room, + room_players=tracker_data.get_team_players(), + locations=tracker_data.get_room_locations(), + locations_complete=tracker_data.get_room_locations_complete(), + total_team_locations=tracker_data.get_team_locations_total_count(), + total_team_locations_complete=tracker_data.get_team_locations_checked_count(), + player_names_with_alias=tracker_data.get_room_long_player_names(), + completed_worlds=tracker_data.get_team_completed_worlds_count(), + games=tracker_data.get_room_games(), + states=tracker_data.get_room_client_statuses(), + hints=tracker_data.get_team_hints(), + activity_timers=tracker_data.get_room_last_activity(), + videos=tracker_data.get_room_videos(), + item_id_to_name=tracker_data.item_id_to_name, + location_id_to_name=tracker_data.location_id_to_name, + inventories=inventories, + tracking_names=tracking_names, + tracking_ids=tracking_ids, + multi_items=multi_items, + checks_done=checks_done, + ordered_areas=ordered_areas, + checks_in_area=player_checks_in_area, + key_locations=group_key_locations, + big_key_locations=group_big_key_locations, + small_key_ids=small_key_ids, + big_key_ids=big_key_ids, + ) + + def render_ALinkToThePast_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + # Helper objects. + alttp_id_lookup = tracker_data.item_name_to_id["A Link to the Past"] + + links = { + "Bow": "Progressive Bow", + "Silver Arrows": "Progressive Bow", + "Silver Bow": "Progressive Bow", + "Progressive Bow (Alt)": "Progressive Bow", + "Bottle (Red Potion)": "Bottle", + "Bottle (Green Potion)": "Bottle", + "Bottle (Blue Potion)": "Bottle", + "Bottle (Fairy)": "Bottle", + "Bottle (Bee)": "Bottle", + "Bottle (Good Bee)": "Bottle", + "Fighter Sword": "Progressive Sword", + "Master Sword": "Progressive Sword", + "Tempered Sword": "Progressive Sword", + "Golden Sword": "Progressive Sword", + "Power Glove": "Progressive Glove", + "Titans Mitts": "Progressive Glove", + } + links = {alttp_id_lookup[key]: alttp_id_lookup[value] for key, value in links.items()} + levels = { + "Fighter Sword": 1, + "Master Sword": 2, + "Tempered Sword": 3, + "Golden Sword": 4, + "Power Glove": 1, + "Titans Mitts": 2, + "Bow": 1, + "Silver Bow": 2, + "Triforce Piece": 90, + } + tracking_names = [ + "Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer", "Hookshot", "Magic Mirror", "Flute", + "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", "Red Boomerang", + "Bug Catching Net", "Cape", "Shovel", "Lamp", "Mushroom", "Magic Powder", "Cane of Somaria", + "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Bottle", "Triforce Piece", "Triforce", + ] + default_locations = { + "Light World": { + 1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175, + 1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884, + 1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836, + 60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193, + 1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328, + 59881, 59761, 59890, 59770, 193020, 212605 + }, + "Dark World": { + 59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095, + 1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031 + }, + "Desert Palace": {1573216, 59842, 59851, 59791, 1573201, 59830}, + "Eastern Palace": {1573200, 59827, 59893, 59767, 59833, 59773}, + "Hyrule Castle": {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253}, + "Agahnims Tower": {60082, 60085}, + "Tower of Hera": {1573218, 59878, 59821, 1573202, 59896, 59899}, + "Swamp Palace": {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061}, + "Thieves Town": {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206}, + "Skull Woods": {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806}, + "Ice Palace": {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869}, + "Misery Mire": {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998}, + "Turtle Rock": {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935}, + "Palace of Darkness": { + 59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995, + 59965 + }, + "Ganons Tower": { + 60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118, + 60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157 + }, + "Total": set() + } + key_only_locations = { + "Light World": set(), + "Dark World": set(), + "Desert Palace": {0x140031, 0x14002b, 0x140061, 0x140028}, + "Eastern Palace": {0x14005b, 0x140049}, + "Hyrule Castle": {0x140037, 0x140034, 0x14000d, 0x14003d}, + "Agahnims Tower": {0x140061, 0x140052}, + "Tower of Hera": set(), + "Swamp Palace": {0x140019, 0x140016, 0x140013, 0x140010, 0x14000a}, + "Thieves Town": {0x14005e, 0x14004f}, + "Skull Woods": {0x14002e, 0x14001c}, + "Ice Palace": {0x140004, 0x140022, 0x140025, 0x140046}, + "Misery Mire": {0x140055, 0x14004c, 0x140064}, + "Turtle Rock": {0x140058, 0x140007}, + "Palace of Darkness": set(), + "Ganons Tower": {0x140040, 0x140043, 0x14003a, 0x14001f}, + "Total": set() + } + location_to_area = {} + for area, locations in default_locations.items(): + for checked_location in locations: + location_to_area[checked_location] = area + for area, locations in key_only_locations.items(): + for checked_location in locations: + location_to_area[checked_location] = area + + checks_in_area = {area: len(checks) for area, checks in default_locations.items()} + checks_in_area["Total"] = 216 + ordered_areas = ( + "Light World", "Dark World", "Hyrule Castle", "Agahnims Tower", "Eastern Palace", "Desert Palace", + "Tower of Hera", "Palace of Darkness", "Swamp Palace", "Skull Woods", "Thieves Town", "Ice Palace", + "Misery Mire", "Turtle Rock", "Ganons Tower", "Total" + ) + + tracking_ids = [] + for item in tracking_names: + tracking_ids.append(alttp_id_lookup[item]) + + # Can't wait to get this into the apworld. Oof. + from worlds.alttp import Items + + small_key_ids = {} + big_key_ids = {} + ids_small_key = {} + ids_big_key = {} + for item_name, data in Items.item_table.items(): + if "Key" in item_name: + area = item_name.split("(")[1][:-1] + if "Small" in item_name: + small_key_ids[area] = data[2] + ids_small_key[data[2]] = area + else: + big_key_ids[area] = data[2] + ids_big_key[data[2]] = area + + inventory = collections.Counter() + checks_done = {loc_name: 0 for loc_name in default_locations} + player_big_key_locations = set() + player_small_key_locations = set() + + player_locations = tracker_data.get_player_locations(team, player) + for checked_location in tracker_data.get_player_checked_locations(team, player): + if checked_location in player_locations: + area_name = location_to_area.get(checked_location, None) + if area_name: + checks_done[area_name] += 1 + + checks_done["Total"] += 1 + + for received_item in tracker_data.get_player_received_items(team, player): + target_item = links.get(received_item.item, received_item.item) + if received_item.item in levels: # non-progressive + inventory[target_item] = max(inventory[target_item], levels[received_item.item]) + else: + inventory[target_item] += 1 + + for location, (item_id, _, _) in player_locations.items(): if item_id in ids_big_key: - player_big_key_locations[item_player].add(ids_big_key[item_id]) + player_big_key_locations.add(ids_big_key[item_id]) elif item_id in ids_small_key: - player_small_key_locations[item_player].add(ids_small_key[item_id]) - group_big_key_locations = set() - group_key_locations = set() - for player in [player for player in range(1, len(names[0]) + 1) if player not in groups]: - group_key_locations |= player_small_key_locations[player] - group_big_key_locations |= player_big_key_locations[player] + player_small_key_locations.add(ids_small_key[item_id]) - activity_timers = {} - now = datetime.datetime.utcnow() - for (team, player), timestamp in multisave.get("client_activity_timers", []): - activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + # Note the presence of the triforce item + if tracker_data.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL: + inventory[106] = 1 # Triforce - player_names = {} - for team, names in enumerate(names): - for player, name in enumerate(names, 1): - player_names[(team, player)] = name - long_player_names = player_names.copy() - for (team, player), alias in multisave.get("name_aliases", {}).items(): - player_names[(team, player)] = alias - long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" + # Progressive items need special handling for icons and class + progressive_items = { + "Progressive Sword": 94, + "Progressive Glove": 97, + "Progressive Bow": 100, + "Progressive Mail": 96, + "Progressive Shield": 95, + } + progressive_names = { + "Progressive Sword": [None, "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword"], + "Progressive Glove": [None, "Power Glove", "Titan Mitts"], + "Progressive Bow": [None, "Bow", "Silver Bow"], + "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], + "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] + } - video = {} - for (team, player), data in multisave.get("video", []): - video[(team, player)] = data + # Determine which icon to use + display_data = {} + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + acquired = True + if not display_name: + acquired = False + display_name = progressive_names[item_name][level + 1] + base_name = item_name.split(maxsplit=1)[1].lower() + display_data[base_name + "_acquired"] = acquired + display_data[base_name + "_icon"] = display_name - enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past") + # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? + sp_areas = ordered_areas[2:15] - return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, - lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, - tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, - multi_items=multi_items, checks_done=checks_done, - percent_total_checks_done=percent_total_checks_done, - ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, - activity_timers=activity_timers, - key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, - video=video, big_key_locations=group_big_key_locations, - hints=hints, long_player_names=long_player_names, - enabled_multiworld_trackers=enabled_multiworld_trackers) + return render_template( + template_name_or_list="tracker__ALinkToThePast.html", + room=tracker_data.room, + team=team, + player=player, + inventory=inventory, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + acquired_items={tracker_data.item_id_to_name["A Link to the Past"][id] for id in inventory}, + sp_areas=sp_areas, + small_key_ids=small_key_ids, + key_locations=player_small_key_locations, + big_key_ids=big_key_ids, + big_key_locations=player_big_key_locations, + **display_data, + ) + _multiworld_trackers["A Link to the Past"] = render_ALinkToThePast_multiworld_tracker + _player_trackers["A Link to the Past"] = render_ALinkToThePast_tracker -game_specific_trackers: typing.Dict[str, typing.Callable] = { - "Minecraft": __renderMinecraftTracker, - "Ocarina of Time": __renderOoTTracker, - "Timespinner": __renderTimespinnerTracker, - "A Link to the Past": __renderAlttpTracker, - "ChecksFinder": __renderChecksfinder, - "Super Metroid": __renderSuperMetroidTracker, - "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker -} +if "Minecraft" in network_data_package["games"]: + def render_Minecraft_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", + "Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png", + "Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png", + "Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png", + "Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png", + "Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png", + "Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png", + "Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png", + "Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png", + "Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png", + "Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png", + "Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png", + "Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png", + "Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png", + "Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png", + "Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png", + "Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png", + "Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png", + "Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png", + "Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png", + "Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png", + "Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif", + "Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png", + "Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif", + "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", + "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", + "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", + "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", + "Saddle": "https://i.imgur.com/2QtDyR0.png", + "Channeling Book": "https://i.imgur.com/J3WsYZw.png", + "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", + "Piercing IV Book": "https://i.imgur.com/OzJptGz.png", + } -multi_trackers: typing.Dict[str, typing.Callable] = { - "A Link to the Past": get_LttP_multiworld_tracker, -} + minecraft_location_ids = { + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], + "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, + 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], + "The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046], + "Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42109, 42035, 42016, 42020, + 42048, 42054, 42068, 42043, 42106, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42105, + 42099, 42103, 42110, 42100], + "Husbandry": [42065, 42067, 42078, 42022, 42113, 42107, 42007, 42079, 42013, 42028, 42036, 42108, 42111, + 42112, + 42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095], + "Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091], + } -if "Factorio" in games: - multi_trackers["Factorio"] = get_Factorio_multiworld_tracker + display_data = {} + + # Determine display for progressive items + progressive_items = { + "Progressive Tools": 45013, + "Progressive Weapons": 45012, + "Progressive Armor": 45014, + "Progressive Resource Crafting": 45001 + } + progressive_names = { + "Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"], + "Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"], + "Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"], + "Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"] + } + + inventory = tracker_data.get_player_inventory_counts(team, player) + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_") + display_data[base_name + "_url"] = icons[display_name] + + # Multi-items + multi_items = { + "3 Ender Pearls": 45029, + "8 Netherite Scrap": 45015, + "Dragon Egg Shard": 45043 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + if count >= 0: + display_data[base_name + "_count"] = count + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Minecraft"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in minecraft_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in minecraft_location_ids.items()} + checks_done["Total"] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()} + checks_in_area["Total"] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Minecraft"] + return render_template( + "tracker__Minecraft.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + saving_second=tracker_data.get_room_saving_second(), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) + + _player_trackers["Minecraft"] = render_Minecraft_tracker + +if "Ocarina of Time" in network_data_package["games"]: + def render_OcarinaOfTime_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", + "Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png", + "Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png", + "Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png", + "Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png", + "Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png", + "Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png", + "Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png", + "Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png", + "Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png", + "Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png", + "Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png", + "Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png", + "Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png", + "Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png", + "Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png", + "Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png", + "Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png", + "Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png", + "Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png", + "Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png", + "Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png", + "Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png", + "Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png", + "Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png", + "Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png", + "Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png", + "Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png", + "Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png", + "Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png", + "Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png", + "Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png", + "Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png", + "Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png", + "Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png", + "Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png", + "Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png", + "Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png", + "Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png", + "Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png", + "Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png", + "Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png", + "Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png", + "Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png", + "Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png", + "Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png", + "Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png", + "Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png", + } + + display_data = {} + + # Determine display for progressive items + progressive_items = { + "Progressive Hookshot": 66128, + "Progressive Strength Upgrade": 66129, + "Progressive Wallet": 66133, + "Progressive Scale": 66134, + "Magic Meter": 66138, + "Ocarina": 66139, + } + + progressive_names = { + "Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"], + "Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", + "Golden Gauntlets"], + "Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"], + "Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"], + "Magic Meter": ["Small Magic", "Small Magic", "Large Magic"], + "Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"] + } + + inventory = tracker_data.get_player_inventory_counts(team, player) + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + if item_name.startswith("Progressive"): + base_name = item_name.split(maxsplit=1)[1].lower().replace(" ", "_") + else: + base_name = item_name.lower().replace(" ", "_") + display_data[base_name + "_url"] = icons[display_name] + + if base_name == "hookshot": + display_data["hookshot_length"] = {0: "", 1: "H", 2: "L"}.get(level) + if base_name == "wallet": + display_data["wallet_size"] = {0: "99", 1: "200", 2: "500", 3: "999"}.get(level) + + # Determine display for bottles. Show letter if it's obtained, determine bottle count + bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148] + display_data["bottle_count"] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4) + display_data["bottle_url"] = icons["Rutos Letter"] if inventory[66021] > 0 else icons["Bottle"] + + # Determine bombchu display + display_data["has_bombchus"] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137])) + + # Multi-items + multi_items = { + "Gold Skulltula Token": 66091, + "Triforce Piece": 66202, + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + display_data[base_name + "_count"] = inventory[item_id] + + # Gather dungeon locations + area_id_ranges = { + "Overworld": ((67000, 67263), (67269, 67280), (67747, 68024), (68054, 68062)), + "Deku Tree": ((67281, 67303), (68063, 68077)), + "Dodongo's Cavern": ((67304, 67334), (68078, 68160)), + "Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)), + "Bottom of the Well": ((67360, 67384), (68189, 68230)), + "Forest Temple": ((67385, 67420), (68231, 68281)), + "Fire Temple": ((67421, 67457), (68282, 68350)), + "Water Temple": ((67458, 67484), (68351, 68483)), + "Shadow Temple": ((67485, 67532), (68484, 68565)), + "Spirit Temple": ((67533, 67582), (68566, 68625)), + "Ice Cavern": ((67583, 67596), (68626, 68649)), + "Gerudo Training Ground": ((67597, 67635), (68650, 68656)), + "Thieves' Hideout": ((67264, 67268), (68025, 68053)), + "Ganon's Castle": ((67636, 67673), (68657, 68705)), + } + + def lookup_and_trim(id, area): + full_name = tracker_data.location_id_to_name["Ocarina of Time"][id] + if "Ganons Tower" in full_name: + return full_name + if area not in ["Overworld", "Thieves' Hideout"]: + # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC + return full_name[len(area):] + return full_name + + locations = tracker_data.get_player_locations(team, player) + checked_locations = tracker_data.get_player_checked_locations(team, player).intersection(set(locations)) + location_info = {} + checks_done = {} + checks_in_area = {} + for area, ranges in area_id_ranges.items(): + location_info[area] = {} + checks_done[area] = 0 + checks_in_area[area] = 0 + for r in ranges: + min_id, max_id = r + for id in range(min_id, max_id + 1): + if id in locations: + checked = id in checked_locations + location_info[area][lookup_and_trim(id, area)] = checked + checks_in_area[area] += 1 + checks_done[area] += checked + + checks_done["Total"] = sum(checks_done.values()) + checks_in_area["Total"] = sum(checks_in_area.values()) + + # Give skulltulas on non-tracked locations + non_tracked_locations = tracker_data.get_player_checked_locations(team, player).difference(set(locations)) + for id in non_tracked_locations: + if "GS" in lookup_and_trim(id, ""): + display_data["token_count"] += 1 + + oot_y = "✔" + oot_x = "✕" + + # Gather small and boss key info + small_key_counts = { + "Forest Temple": oot_y if inventory[66203] else inventory[66175], + "Fire Temple": oot_y if inventory[66204] else inventory[66176], + "Water Temple": oot_y if inventory[66205] else inventory[66177], + "Spirit Temple": oot_y if inventory[66206] else inventory[66178], + "Shadow Temple": oot_y if inventory[66207] else inventory[66179], + "Bottom of the Well": oot_y if inventory[66208] else inventory[66180], + "Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181], + "Thieves' Hideout": oot_y if inventory[66210] else inventory[66182], + "Ganon's Castle": oot_y if inventory[66211] else inventory[66183], + } + boss_key_counts = { + "Forest Temple": oot_y if inventory[66149] else oot_x, + "Fire Temple": oot_y if inventory[66150] else oot_x, + "Water Temple": oot_y if inventory[66151] else oot_x, + "Spirit Temple": oot_y if inventory[66152] else oot_x, + "Shadow Temple": oot_y if inventory[66153] else oot_x, + "Ganon's Castle": oot_y if inventory[66154] else oot_x, + } + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Ocarina of Time"] + return render_template( + "tracker__OcarinaOfTime.html", + inventory=inventory, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, + small_key_counts=small_key_counts, + boss_key_counts=boss_key_counts, + **display_data, + ) + + _player_trackers["Ocarina of Time"] = render_OcarinaOfTime_tracker + +if "Timespinner" in network_data_package["games"]: + def render_Timespinner_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png", + "Timespinner Spindle": "https://timespinnerwiki.com/mediawiki/images/1/1a/Timespinner_Spindle.png", + "Timespinner Gear 1": "https://timespinnerwiki.com/mediawiki/images/3/3c/Timespinner_Gear_1.png", + "Timespinner Gear 2": "https://timespinnerwiki.com/mediawiki/images/e/e9/Timespinner_Gear_2.png", + "Timespinner Gear 3": "https://timespinnerwiki.com/mediawiki/images/2/22/Timespinner_Gear_3.png", + "Talaria Attachment": "https://timespinnerwiki.com/mediawiki/images/6/61/Talaria_Attachment.png", + "Succubus Hairpin": "https://timespinnerwiki.com/mediawiki/images/4/49/Succubus_Hairpin.png", + "Lightwall": "https://timespinnerwiki.com/mediawiki/images/0/03/Lightwall.png", + "Celestial Sash": "https://timespinnerwiki.com/mediawiki/images/f/f1/Celestial_Sash.png", + "Twin Pyramid Key": "https://timespinnerwiki.com/mediawiki/images/4/49/Twin_Pyramid_Key.png", + "Security Keycard D": "https://timespinnerwiki.com/mediawiki/images/1/1b/Security_Keycard_D.png", + "Security Keycard C": "https://timespinnerwiki.com/mediawiki/images/e/e5/Security_Keycard_C.png", + "Security Keycard B": "https://timespinnerwiki.com/mediawiki/images/f/f6/Security_Keycard_B.png", + "Security Keycard A": "https://timespinnerwiki.com/mediawiki/images/b/b9/Security_Keycard_A.png", + "Library Keycard V": "https://timespinnerwiki.com/mediawiki/images/5/50/Library_Keycard_V.png", + "Tablet": "https://timespinnerwiki.com/mediawiki/images/a/a0/Tablet.png", + "Elevator Keycard": "https://timespinnerwiki.com/mediawiki/images/5/55/Elevator_Keycard.png", + "Oculus Ring": "https://timespinnerwiki.com/mediawiki/images/8/8d/Oculus_Ring.png", + "Water Mask": "https://timespinnerwiki.com/mediawiki/images/0/04/Water_Mask.png", + "Gas Mask": "https://timespinnerwiki.com/mediawiki/images/2/2e/Gas_Mask.png", + "Djinn Inferno": "https://timespinnerwiki.com/mediawiki/images/f/f6/Djinn_Inferno.png", + "Pyro Ring": "https://timespinnerwiki.com/mediawiki/images/2/2c/Pyro_Ring.png", + "Infernal Flames": "https://timespinnerwiki.com/mediawiki/images/1/1f/Infernal_Flames.png", + "Fire Orb": "https://timespinnerwiki.com/mediawiki/images/3/3e/Fire_Orb.png", + "Royal Ring": "https://timespinnerwiki.com/mediawiki/images/f/f3/Royal_Ring.png", + "Plasma Geyser": "https://timespinnerwiki.com/mediawiki/images/1/12/Plasma_Geyser.png", + "Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png", + "Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png", + "Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png", + } + + timespinner_location_ids = { + "Present": [ + 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, + 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, + 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, + 1337030, 1337031, 1337032, 1337033, 1337034, 1337035, 1337036, 1337037, 1337038, 1337039, + 1337040, 1337041, 1337042, 1337043, 1337044, 1337045, 1337046, 1337047, 1337048, 1337049, + 1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059, + 1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069, + 1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079, + 1337080, 1337081, 1337082, 1337083, 1337084, 1337085], + "Past": [ + 1337086, 1337087, 1337088, 1337089, + 1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099, + 1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109, + 1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119, + 1337120, 1337121, 1337122, 1337123, 1337124, 1337125, 1337126, 1337127, 1337128, 1337129, + 1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139, + 1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149, + 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, + 1337171, 1337172, 1337173, 1337174, 1337175], + "Ancient Pyramid": [ + 1337236, + 1337246, 1337247, 1337248, 1337249] + } + + slot_data = tracker_data.get_slot_data(team, player) + if (slot_data["DownloadableItems"]): + timespinner_location_ids["Present"] += [ + 1337156, 1337157, 1337159, + 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, + 1337170] + if (slot_data["Cantoran"]): + timespinner_location_ids["Past"].append(1337176) + if (slot_data["LoreChecks"]): + timespinner_location_ids["Present"] += [ + 1337177, 1337178, 1337179, + 1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187] + timespinner_location_ids["Past"] += [ + 1337188, 1337189, + 1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198] + if (slot_data["GyreArchives"]): + timespinner_location_ids["Ancient Pyramid"] += [ + 1337237, 1337238, 1337239, + 1337240, 1337241, 1337242, 1337243, 1337244, 1337245] + + display_data = {} + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + inventory = tracker_data.get_player_inventory_counts(team, player) + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Timespinner"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in timespinner_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in timespinner_location_ids.items()} + checks_done["Total"] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()} + checks_in_area["Total"] = sum(checks_in_area.values()) + options = {k for k, v in slot_data.items() if v} + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Timespinner"] + return render_template( + "tracker__Timespinner.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + options=options, + **display_data, + ) + + _player_trackers["Timespinner"] = render_Timespinner_tracker + +if "Super Metroid" in network_data_package["games"]: + def render_SuperMetroid_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png", + "Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Missile.png", + "Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Super.png", + "Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/PowerBomb.png", + "Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Bomb.png", + "Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Charge.png", + "Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Ice.png", + "Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/HiJump.png", + "Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpeedBooster.png", + "Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Wave.png", + "Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Spazer.png", + "Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpringBall.png", + "Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Varia.png", + "Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Plasma.png", + "Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Grapple.png", + "Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Morph.png", + "Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Reserve.png", + "Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/Gravity.png", + "X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/XRayScope.png", + "Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/SpaceJump.png", + "Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ScrewAttack.png", + "Nothing": "", + "No Energy": "", + "Kraid": "", + "Phantoon": "", + "Draygon": "", + "Ridley": "", + "Mother Brain": "", + } + + multi_items = { + "Energy Tank": 83000, + "Missile": 83001, + "Super Missile": 83002, + "Power Bomb": 83003, + "Reserve Tank": 83020, + } + + supermetroid_location_ids = { + 'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029, + 82000, 82004, 82006, 82009, 82010, + 82011, 82012, 82027, 82028, 82034, + 82036, 82037], + 'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035, + 82013, 82014, 82015, 82016, 82018, + 82019, 82021, 82022, 82024, 82025, + 82031], + 'Red Brinstar': [82038, 82042, 82039, 82040, 82041], + 'Kraid': [82043, 82048, 82044], + 'Norfair': [82050, 82053, 82061, 82066, 82068, + 82049, 82051, 82054, 82055, 82056, + 82062, 82063, 82064, 82065, 82067], + 'Lower Norfair': [82078, 82079, 82080, 82070, 82071, + 82073, 82074, 82075, 82076, 82077], + 'Crocomire': [82052, 82060, 82057, 82058, 82059], + 'Wrecked Ship': [82129, 82132, 82134, 82135, 82001, + 82002, 82003, 82128, 82130, 82131, + 82133], + 'West Maridia': [82138, 82136, 82137, 82139, 82140, + 82141, 82142], + 'East Maridia': [82143, 82145, 82150, 82152, 82154, + 82144, 82146, 82147, 82148, 82149, + 82151], + } + + display_data = {} + inventory = tracker_data.get_player_inventory_counts(team, player) + + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[0].lower() + display_data[base_name + "_count"] = inventory[item_id] + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into advancement tab counts + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Super Metroid"][id] + location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations} + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations]) + for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_done['Total'] = len(checked_locations) + checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()} + checks_in_area['Total'] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Super Metroid"] + return render_template( + "tracker__SuperMetroid.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) + + _player_trackers["Super Metroid"] = render_SuperMetroid_tracker + +if "ChecksFinder" in network_data_package["games"]: + def render_ChecksFinder_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + icons = { + "Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png", + "Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png", + "Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png", + "Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png", + + "Nothing": "", + } + + checksfinder_location_ids = { + "Tile 1": 81000, + "Tile 2": 81001, + "Tile 3": 81002, + "Tile 4": 81003, + "Tile 5": 81004, + "Tile 6": 81005, + "Tile 7": 81006, + "Tile 8": 81007, + "Tile 9": 81008, + "Tile 10": 81009, + "Tile 11": 81010, + "Tile 12": 81011, + "Tile 13": 81012, + "Tile 14": 81013, + "Tile 15": 81014, + "Tile 16": 81015, + "Tile 17": 81016, + "Tile 18": 81017, + "Tile 19": 81018, + "Tile 20": 81019, + "Tile 21": 81020, + "Tile 22": 81021, + "Tile 23": 81022, + "Tile 24": 81023, + "Tile 25": 81024, + } + + display_data = {} + inventory = tracker_data.get_player_inventory_counts(team, player) + locations = tracker_data.get_player_locations(team, player) + + # Multi-items + multi_items = { + "Map Width": 80000, + "Map Height": 80001, + "Map Bombs": 80002 + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + display_data[base_name + "_count"] = count + display_data[base_name + "_display"] = count + 5 + + # Get location info + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["ChecksFinder"][id] + location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for + tile_name, tile_location in checksfinder_location_ids.items() if + tile_location in set(locations)} + checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() + if tile_location in checked_locations and tile_location in set(locations)} + checks_done['Total'] = len(checked_locations) + checks_in_area = checks_done + + # Calculate checks available + display_data["checks_unlocked"] = min( + display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25) + display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0) + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["ChecksFinder"] + return render_template( + "tracker__ChecksFinder.html", + inventory=inventory, icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) + + _player_trackers["ChecksFinder"] = render_ChecksFinder_tracker + +if "Starcraft 2 Wings of Liberty" in network_data_package["games"]: + def render_Starcraft2WingsOfLiberty_tracker(tracker_data: TrackerData, team: int, player: int) -> str: + SC2WOL_LOC_ID_OFFSET = 1000 + SC2WOL_ITEM_ID_OFFSET = 1000 + + icons = { + "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", + "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", + "Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif", + + "Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png", + "Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png", + "Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png", + "Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png", + "Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png", + "Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png", + "Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png", + "Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png", + "Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png", + "Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png", + "Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png", + "Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png", + "Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png", + "Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png", + "Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png", + "Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png", + "Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png", + "Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png", + + "Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg", + "Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg", + "Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg", + + "Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png", + "Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png", + "Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png", + "Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", + "Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png", + "Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png", + "Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png", + "Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png", + + "Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg", + "Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg", + "Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg", + "Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_SC2_Icon1.jpg", + "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", + + "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png", + "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png", + "Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png", + "Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png", + "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png", + "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png", + "Restoration (Medic)": "/static/static/icons/sc2/restoration.png", + "Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png", + "Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png", + "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png", + "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png", + "Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png", + "Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png", + "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png", + "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png", + "Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png", + "Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png", + "Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png", + "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png", + "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png", + "Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png", + "Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png", + "Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png", + "Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png", + + "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", + "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg", + "Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg", + "Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg", + "Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg", + + "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", + "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", + "Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png", + "Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png", + "Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png", + "Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png", + "Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", + "Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png", + "Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png", + "High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png", + "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png", + "Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png", + "Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png", + "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png", + "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png", + "Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png", + "Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png", + "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png", + "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", + "Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png", + "Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png", + "Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png", + "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png", + "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png", + "Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png", + "Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png", + "Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png", + "Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png", + "Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png", + "Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png", + + "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", + "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg", + "Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg", + "Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg", + "Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg", + + "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png", + "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png", + "Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png", + "Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png", + "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png", + "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png", + "Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png", + "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png", + "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png", + "Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png", + "Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png", + "Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png", + "Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png", + "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png", + "Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png", + "Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png", + "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png", + "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", + "Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png", + "Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png", + "ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png", + "Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png", + "Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png", + + "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", + "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg", + "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", + + "Widow Mine": "/static/static/icons/sc2/widowmine.png", + "Cyclone": "/static/static/icons/sc2/cyclone.png", + "Liberator": "/static/static/icons/sc2/liberator.png", + "Valkyrie": "/static/static/icons/sc2/valkyrie.png", + + "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png", + "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png", + "EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png", + "Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png", + "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png", + "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png", + "Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png", + "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png", + "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png", + "High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png", + "Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png", + + "Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png", + "Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png", + "Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png", + "Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png", + "Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png", + "Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png", + "Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png", + "Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png", + "Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png", + "Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png", + "Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png", + "Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png", + "Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png", + "Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png", + "Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png", + "Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png", + "EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png", + "Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", + "Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png", + "Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png", + "Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png", + "Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png", + "Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png", + "Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png", + "Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", + "Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png", + "Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png", + + "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", + "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", + "Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg", + "Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg", + "Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg", + "Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg", + "Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg", + "Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg", + + "Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png", + "Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png", + "Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png", + "Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png", + "Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png", + "Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png", + "Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png", + "Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_Icon.png", + "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", + "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", + + "Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", + "Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", + "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", + "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", + "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png", + "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", + "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", + "Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png", + "Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png", + "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", + "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", + + "Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg", + "Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg", + "High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg", + "Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg", + "Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg", + "Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg", + "Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg", + "Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg", + "Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg", + + "Nothing": "", + } + sc2wol_location_ids = { + "Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), + "The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), + "Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), + "Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), + "Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), + "Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), + "Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), + "Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), + "The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), + "The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), + "Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), + "Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), + "Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), + "Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), + "Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), + "Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), + "The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), + "Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), + "Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), + "Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), + "Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), + "Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), + "A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), + "Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), + "In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), + "Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), + "Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), + "Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), + } + + display_data = {} + + # Grouped Items + grouped_item_ids = { + "Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET + } + grouped_item_replacements = { + "Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", + "Progressive Ship Weapon"], + "Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", + "Progressive Ship Armor"], + "Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"], + "Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"], + "Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"] + } + grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements[ + "Progressive Weapon Upgrade"] + \ + grouped_item_replacements[ + "Progressive Armor Upgrade"] + replacement_item_ids = { + "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, + } + + inventory = tracker_data.get_player_inventory_counts(team, player) + for grouped_item_name, grouped_item_id in grouped_item_ids.items(): + count: int = inventory[grouped_item_id] + if count > 0: + for replacement_item in grouped_item_replacements[grouped_item_name]: + replacement_id: int = replacement_item_ids[replacement_item] + inventory[replacement_id] = count + + # Determine display for progressive items + progressive_items = { + "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET, + "Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET + } + progressive_names = { + "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", + "Infantry Weapons Level 2", "Infantry Weapons Level 3"], + "Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", + "Infantry Armor Level 2", "Infantry Armor Level 3"], + "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", + "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"], + "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", + "Vehicle Armor Level 2", "Vehicle Armor Level 3"], + "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", + "Ship Weapons Level 2", "Ship Weapons Level 3"], + "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", + "Ship Armor Level 2", "Ship Armor Level 3"], + "Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", + "Super Stimpack (Marine)"], + "Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", + "Super Stimpack (Firebat)"], + "Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", + "Super Stimpack (Marauder)"], + "Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", + "Super Stimpack (Reaper)"], + "Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", + "Super Stimpack (Hellion)"], + "Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", + "High Impact Payload (Thor)", "Smart Servos (Thor)"], + "Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", + "Cross-Spectrum Dampeners (Banshee)", + "Advanced Cross-Spectrum Dampeners (Banshee)"], + "Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", + "Regenerative Bio-Steel Level 1", + "Regenerative Bio-Steel Level 2"] + } + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name]) - 1) + display_name = progressive_names[item_name][level] + base_name = (item_name.split(maxsplit=1)[1].lower() + .replace(' ', '_') + .replace("-", "") + .replace("(", "") + .replace(")", "")) + display_data[base_name + "_level"] = level + display_data[base_name + "_url"] = icons[display_name] + display_data[base_name + "_name"] = display_name + + # Multi-items + multi_items = { + "+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET, + "+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET, + "+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET + } + for item_name, item_id in multi_items.items(): + base_name = item_name.split()[-1].lower() + count = inventory[item_id] + if base_name == "supply": + count = count * 2 + display_data[base_name + "_count"] = count + else: + count = count * 15 + display_data[base_name + "_count"] = count + + # Victory condition + game_state = tracker_data.get_player_client_status(team, player) + display_data["game_finished"] = game_state == 30 + + # Turn location IDs into mission objective counts + locations = tracker_data.get_player_locations(team, player) + checked_locations = tracker_data.get_player_checked_locations(team, player) + lookup_name = lambda id: tracker_data.location_id_to_name["Starcraft 2 Wings of Liberty"][id] + location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if + id in set(locations)} for mission_name, mission_locations in + sc2wol_location_ids.items()} + checks_done = {mission_name: len( + [id for id in mission_locations if id in checked_locations and id in set(locations)]) for + mission_name, mission_locations in sc2wol_location_ids.items()} + checks_done['Total'] = len(checked_locations) + checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations)]) for + mission_name, mission_locations in sc2wol_location_ids.items()} + checks_in_area['Total'] = sum(checks_in_area.values()) + + lookup_any_item_id_to_name = tracker_data.item_id_to_name["Starcraft 2 Wings of Liberty"] + return render_template( + "tracker__Starcraft2WingsOfLiberty.html", + inventory=inventory, + icons=icons, + acquired_items={lookup_any_item_id_to_name[id] for id, count in inventory.items() if count > 0}, + player=player, + team=team, + room=tracker_data.room, + player_name=tracker_data.get_player_name(team, player), + checks_done=checks_done, + checks_in_area=checks_in_area, + location_info=location_info, + **display_data, + ) + + _player_trackers["Starcraft 2 Wings of Liberty"] = render_Starcraft2WingsOfLiberty_tracker diff --git a/worlds/__init__.py b/worlds/__init__.py index 40e0b20f19..66c91639b9 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -1,43 +1,40 @@ import importlib import os import sys -import typing import warnings import zipimport +from typing import Dict, List, NamedTuple, TypedDict -from Utils import user_path, local_path +from Utils import local_path, user_path local_folder = os.path.dirname(__file__) user_folder = user_path("worlds") if user_path() != local_path() else None -__all__ = ( - "lookup_any_item_id_to_name", - "lookup_any_location_id_to_name", +__all__ = { "network_data_package", "AutoWorldRegister", "world_sources", "local_folder", "user_folder", -) + "GamesPackage", + "DataPackage", +} -class GamesData(typing.TypedDict): - item_name_groups: typing.Dict[str, typing.List[str]] - item_name_to_id: typing.Dict[str, int] - location_name_groups: typing.Dict[str, typing.List[str]] - location_name_to_id: typing.Dict[str, int] - version: int - - -class GamesPackage(GamesData, total=False): +class GamesPackage(TypedDict, total=False): + item_name_groups: Dict[str, List[str]] + item_name_to_id: Dict[str, int] + location_name_groups: Dict[str, List[str]] + location_name_to_id: Dict[str, int] checksum: str + version: int # TODO: Remove support after per game data packages API change. -class DataPackage(typing.TypedDict): - games: typing.Dict[str, GamesPackage] +class DataPackage(TypedDict): + games: Dict[str, GamesPackage] -class WorldSource(typing.NamedTuple): +class WorldSource(NamedTuple): path: str # typically relative path from this module is_zip: bool = False relative: bool = True # relative to regular world import folder @@ -88,7 +85,7 @@ class WorldSource(typing.NamedTuple): # find potential world containers, currently folders and zip-importable .apworld's -world_sources: typing.List[WorldSource] = [] +world_sources: List[WorldSource] = [] for folder in (folder for folder in (user_folder, local_folder) if folder): relative = folder == local_folder for entry in os.scandir(folder): @@ -105,25 +102,9 @@ world_sources.sort() for world_source in world_sources: world_source.load() -lookup_any_item_id_to_name = {} -lookup_any_location_id_to_name = {} -games: typing.Dict[str, GamesPackage] = {} - -from .AutoWorld import AutoWorldRegister # noqa: E402 - # Build the data package for each game. -for world_name, world in AutoWorldRegister.world_types.items(): - games[world_name] = world.get_data_package_data() - lookup_any_item_id_to_name.update(world.item_id_to_name) - lookup_any_location_id_to_name.update(world.location_id_to_name) +from .AutoWorld import AutoWorldRegister network_data_package: DataPackage = { - "games": games, + "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()}, } - -# Set entire datapackage to version 0 if any of them are set to 0 -if any(not world.data_version for world in AutoWorldRegister.world_types.values()): - import logging - - logging.warning(f"Datapackage is in custom mode. Custom Worlds: " - f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}") From e916b0d6b0447f6637d7b143ccf20766f9d6e6db Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sat, 18 Nov 2023 13:35:57 -0500 Subject: [PATCH 183/327] Stardew Valley: Add Options presets (#2470) --- worlds/stardew_valley/__init__.py | 2 + worlds/stardew_valley/data/items.csv | 4 +- worlds/stardew_valley/items.py | 14 +- worlds/stardew_valley/logic.py | 7 +- worlds/stardew_valley/options.py | 131 +++---- worlds/stardew_valley/presets.py | 323 ++++++++++++++++++ worlds/stardew_valley/test/TestRules.py | 2 +- worlds/stardew_valley/test/__init__.py | 4 +- .../test/checks/option_checks.py | 2 +- 9 files changed, 405 insertions(+), 84 deletions(-) create mode 100644 worlds/stardew_valley/presets.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 177b6436ae..24ffa8c1ad 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -11,6 +11,7 @@ from .locations import location_table, create_locations, LocationData from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ BackpackProgression, BuildingProgression, ExcludeGingerIsland +from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules from worlds.generic.Rules import set_rule @@ -34,6 +35,7 @@ class StardewItem(Item): class StardewWebWorld(WebWorld): theme = "dirt" bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" + options_presets = sv_options_presets tutorials = [ Tutorial( diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index a3d61e8b58..3c4ddb8415 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -1,7 +1,7 @@ id,name,classification,groups,mod_name 0,Joja Cola,filler,TRASH, -15,Rusty Key,progression,MUSEUM, -16,Dwarvish Translation Guide,progression,MUSEUM, +15,Rusty Key,progression,, +16,Dwarvish Translation Guide,progression,, 17,Bridge Repair,progression,COMMUNITY_REWARD, 18,Greenhouse,progression,COMMUNITY_REWARD, 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD, diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 2d28b4de43..a5a370aa08 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -300,15 +300,15 @@ def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOpt def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.museumsanity == Museumsanity.option_none: - return - items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) - items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) - items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4) - items.append(item_factory("Ancient Seeds Recipe")) - items.append(item_factory("Stardrop")) items.append(item_factory("Rusty Key")) items.append(item_factory("Dwarvish Translation Guide")) + items.append(item_factory("Ancient Seeds Recipe")) + if options.museumsanity == Museumsanity.option_none: + return + items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 10) + items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) + items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4) + items.append(item_factory("Stardrop")) def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 0746bd7752..5a6244cf37 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -8,7 +8,7 @@ from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops from .data.bundle_data import BundleItem from .data.crops_data import crops_by_name from .data.fish_data import island_fish -from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, dwarf_scrolls, all_museum_minerals +from .data.museum_data import all_museum_items, MuseumItem, all_museum_artifacts, all_museum_minerals from .data.recipe_data import all_cooking_recipes, CookingRecipe, RecipeSource, FriendshipSource, QueenOfSauceSource, \ StarterSource, ShopSource, SkillSource from .data.villagers_data import all_villagers_by_name, Villager @@ -1283,8 +1283,6 @@ class StardewLogic: return self.has_lived_months(8) def can_speak_dwarf(self) -> StardewRule: - if self.options.museumsanity == Museumsanity.option_none: - return And([self.can_donate_museum_item(item) for item in dwarf_scrolls]) return self.received("Dwarvish Translation Guide") def can_donate_museum_item(self, item: MuseumItem) -> StardewRule: @@ -1370,9 +1368,6 @@ class StardewLogic: return self.received("Month End", number) def has_rusty_key(self) -> StardewRule: - if self.options.museumsanity == Museumsanity.option_none: - required_donations = 80 # It's 60, but without a metal detector I'd rather overshoot so players don't get screwed by RNG - return self.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum) return self.received(Wallet.rusty_key) def can_win_egg_hunt(self) -> StardewRule: diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index f462f507d4..d85bbf06f6 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -7,15 +7,15 @@ from .mods.mod_data import ModNames class Goal(Choice): """What's your goal with this play-through? - Community Center: The world will be completed once you complete the Community Center. - Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine. - Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft. - Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern. - Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with Fishsanity. - Complete Collection: The world will be completed once you have completed the museum by donating every possible item. Pairs well with Museumsanity. - Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity. - Greatest Walnut Hunter: The world will be completed once you find all 130 Golden Walnuts - Perfection: The world will be completed once you attain Perfection, based on the vanilla definition. + Community Center: Complete the Community Center. + Grandpa's Evaluation: Succeed grandpa's evaluation with 4 lit candles. + Bottom of the Mines: Reach level 120 in the mineshaft. + Cryptic Note: Complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern. + Master Angler: Catch every fish in the game. Pairs well with Fishsanity. + Complete Collection: Complete the museum by donating every possible item. Pairs well with Museumsanity. + Full House: Get married and have two children. Pairs well with Friendsanity. + Greatest Walnut Hunter: Find all 130 Golden Walnuts + Perfection: Attain Perfection, based on the vanilla definition. """ internal_name = "goal" display_name = "Goal" @@ -50,7 +50,7 @@ class Goal(Choice): class StartingMoney(SpecialRange): """Amount of gold when arriving at the farm. - Set to -1 or unlimited for infinite money in this playthrough""" + Set to -1 or unlimited for infinite money""" internal_name = "starting_money" display_name = "Starting Gold" range_start = -1 @@ -117,10 +117,10 @@ class BundlePrice(Choice): class EntranceRandomization(Choice): """Should area entrances be randomized? Disabled: No entrance randomization is done - Pelican Town: Only buildings in the main town area are randomized among each other - Non Progression: Only buildings that are always available are randomized with each other - Buildings: All Entrances that Allow you to enter a building using a door are randomized with each other - Chaos: Same as above, but the entrances get reshuffled every single day! + Pelican Town: Only doors in the main town area are randomized with each other + Non Progression: Only entrances that are always available are randomized with each other + Buildings: All Entrances that Allow you to enter a building are randomized with each other + Chaos: Same as "Buildings", but the entrances get reshuffled every single day! """ # Everything: All buildings and areas are randomized with each other # Chaos, same as everything: but the buildings are shuffled again every in-game day. You can't learn it! @@ -144,11 +144,10 @@ class EntranceRandomization(Choice): class SeasonRandomization(Choice): """Should seasons be randomized? - All settings allow you to choose which season you want to play next (from those unlocked) at the end of a season. - Disabled: You will start in Spring with all seasons unlocked. - Randomized: The seasons will be unlocked randomly as Archipelago items. - Randomized Not Winter: The seasons are randomized, but you're guaranteed not to start with winter. - Progressive: You will start in Spring and unlock the seasons in their original order. + Disabled: Start in Spring with all seasons unlocked. + Randomized: Start in a random season and the other 3 must be unlocked randomly. + Randomized Not Winter: Same as randomized, but the start season is guaranteed not to be winter. + Progressive: Start in Spring and unlock the seasons in their original order. """ internal_name = "season_randomization" display_name = "Season Randomization" @@ -163,20 +162,21 @@ class Cropsanity(Choice): """Formerly named "Seed Shuffle" Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs. Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops - Shuffled: Seeds are unlocked as archipelago item, for each seed there is a location check for growing and harvesting that crop + Shuffled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop """ internal_name = "cropsanity" display_name = "Cropsanity" default = 1 option_disabled = 0 - option_shuffled = 1 + option_enabled = 1 + alias_shuffled = option_enabled class BackpackProgression(Choice): - """How is the backpack progression handled? - Vanilla: You can buy them at Pierre's General Store. + """Shuffle the backpack? + Vanilla: You can buy backpacks at Pierre's General Store. Progressive: You will randomly find Progressive Backpack upgrades. - Early Progressive: You can expect your first Backpack in sphere 1. + Early Progressive: Same as progressive, but one backpack will be placed early in the multiworld. """ internal_name = "backpack_progression" display_name = "Backpack Progression" @@ -187,8 +187,8 @@ class BackpackProgression(Choice): class ToolProgression(Choice): - """How is the tool progression handled? - Vanilla: Clint will upgrade your tools with ore. + """Shuffle the tool upgrades? + Vanilla: Clint will upgrade your tools with metal bars. Progressive: You will randomly find Progressive Tool upgrades.""" internal_name = "tool_progression" display_name = "Tool Progression" @@ -198,12 +198,11 @@ class ToolProgression(Choice): class ElevatorProgression(Choice): - """How is Elevator progression handled? - Vanilla: You will unlock new elevator floors for yourself. - Progressive: You will randomly find Progressive Mine Elevators to go deeper. Locations are sent for reaching - every elevator level. - Progressive from previous floor: Same as progressive, but you must reach elevator floors on your own, - you cannot use the elevator to check elevator locations""" + """Shuffle the elevator? + Vanilla: Reaching a mineshaft floor unlocks the elevator for it + Progressive: You will randomly find Progressive Mine Elevators to go deeper. + Progressive from previous floor: Same as progressive, but you cannot use the elevator to check elevator locations. + You must reach elevator floors on your own.""" internal_name = "elevator_progression" display_name = "Elevator Progression" default = 2 @@ -213,10 +212,9 @@ class ElevatorProgression(Choice): class SkillProgression(Choice): - """How is the skill progression handled? - Vanilla: You will level up and get the normal reward at each level. - Progressive: The xp will be earned internally, locations will be sent when you earn a level. Your real - levels will be scattered around the multiworld.""" + """Shuffle skill levels? + Vanilla: Leveling up skills is normal + Progressive: Skill levels are unlocked randomly, and earning xp sends checks""" internal_name = "skill_progression" display_name = "Skill Progression" default = 1 @@ -225,11 +223,11 @@ class SkillProgression(Choice): class BuildingProgression(Choice): - """How is the building progression handled? - Vanilla: You will buy each building normally. + """Shuffle Carpenter Buildings? + Vanilla: You can buy each building normally. Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - Progressive early shipping bin: You can expect your shipping bin in sphere 1. + Progressive early shipping bin: Same as Progressive, but the shipping bin will be placed early in the multiworld. """ internal_name = "building_progression" display_name = "Building Progression" @@ -240,10 +238,10 @@ class BuildingProgression(Choice): class FestivalLocations(Choice): - """Locations for attending and participating in festivals - With Disabled, you do not need to attend festivals - With Easy, there are checks for participating in festivals - With Hard, the festival checks are only granted when the player performs well in the festival + """Shuffle Festival Activities? + Disabled: You do not need to attend festivals + Easy: Every festival has checks, but they are easy and usually only require attendance + Hard: Festivals have more checks, and many require performing well, not just attending """ internal_name = "festival_locations" display_name = "Festival Locations" @@ -254,11 +252,10 @@ class FestivalLocations(Choice): class ArcadeMachineLocations(Choice): - """How are the Arcade Machines handled? - Disabled: The arcade machines are not included in the Archipelago shuffling. + """Shuffle the arcade machines? + Disabled: The arcade machines are not included. Victories: Each Arcade Machine will contain one check on victory - Victories Easy: The arcade machines are both made considerably easier to be more accessible for the average - player. + Victories Easy: Same as Victories, but both games are made considerably easier. Full Shuffling: The arcade machines will contain multiple checks each, and different buffs that make the game easier are in the item pool. Junimo Kart has one check at the end of each level. Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment. @@ -273,10 +270,10 @@ class ArcadeMachineLocations(Choice): class SpecialOrderLocations(Choice): - """How are the Special Orders handled? + """Shuffle Special Orders? Disabled: The special orders are not included in the Archipelago shuffling. Board Only: The Special Orders on the board in town are location checks - Board and Qi: The Special Orders from Qi's walnut room are checks, as well as the board in town + Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town """ internal_name = "special_order_locations" display_name = "Special Order Locations" @@ -287,7 +284,7 @@ class SpecialOrderLocations(Choice): class HelpWantedLocations(SpecialRange): - """How many "Help Wanted" quests need to be completed as Archipelago Locations + """Include location checks for Help Wanted quests Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" internal_name = "help_wanted_locations" @@ -307,7 +304,7 @@ class HelpWantedLocations(SpecialRange): class Fishsanity(Choice): - """Locations for catching fish? + """Locations for catching a fish the first time? None: There are no locations for catching fish Legendaries: Each of the 5 legendary fish are checks Special: A curated selection of strong fish are checks @@ -336,7 +333,7 @@ class Museumsanity(Choice): None: There are no locations for donating artifacts and minerals to the museum Milestones: The donation milestones from the vanilla game are checks Randomized: A random selection of minerals and artifacts are checks - All: Every single donation will be a check + All: Every single donation is a check """ internal_name = "museumsanity" display_name = "Museumsanity" @@ -348,12 +345,12 @@ class Museumsanity(Choice): class Friendsanity(Choice): - """Locations for friendships? - None: There are no checks for befriending villagers - Bachelors: Each heart of a bachelor is a check - Starting NPCs: Each heart for npcs that are immediately available is a check - All: Every heart with every NPC is a check, including Leo, Kent, Sandy, etc - All With Marriage: Marriage candidates must also be dated, married, and befriended up to 14 hearts. + """Shuffle Friendships? + None: Friendship hearts are earned normally + Bachelors: Hearts with bachelors are shuffled + Starting NPCs: Hearts for NPCs available immediately are checks + All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc + All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable """ internal_name = "friendsanity" display_name = "Friendsanity" @@ -368,7 +365,7 @@ class Friendsanity(Choice): # Conditional Setting - Friendsanity not None class FriendsanityHeartSize(Range): - """If using friendsanity, how many hearts are received per item, and how many hearts must be earned to send a check + """If using friendsanity, how many hearts are received per heart item, and how many hearts must be earned to send a check A higher value will lead to fewer heart items in the item pool, reducing bloat""" internal_name = "friendsanity_heart_size" display_name = "Friendsanity Heart Size" @@ -411,6 +408,7 @@ class ExcludeGingerIsland(Toggle): class TrapItems(Choice): """When rolling filler items, including resource packs, the game can also roll trap items. + Trap items are negative items that cause problems or annoyances for the player This setting is for choosing if traps will be in the item pool, and if so, how punishing they will be. """ internal_name = "trap_items" @@ -441,14 +439,16 @@ class MultipleDaySleepCost(SpecialRange): special_range_names = { "free": 0, - "cheap": 25, - "medium": 50, - "expensive": 100, + "cheap": 10, + "medium": 25, + "expensive": 50, + "very expensive": 100, } class ExperienceMultiplier(SpecialRange): - """How fast you want to earn skill experience. A lower setting mean less experience. + """How fast you want to earn skill experience. + A lower setting mean less experience. A higher setting means more experience.""" internal_name = "experience_multiplier" display_name = "Experience Multiplier" @@ -513,14 +513,15 @@ class QuickStart(Toggle): class Gifting(Toggle): - """Do you want to enable gifting items to and from other Stardew Valley worlds?""" + """Do you want to enable gifting items to and from other Archipelago slots? + Items can only be sent to games that also support gifting""" internal_name = "gifting" display_name = "Gifting" default = 1 class Mods(OptionSet): - """List of mods that will be considered for shuffling.""" + """List of mods that will be included in the shuffling.""" internal_name = "mods" display_name = "Mods" valid_keys = { diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py new file mode 100644 index 0000000000..8823c52e5b --- /dev/null +++ b/worlds/stardew_valley/presets.py @@ -0,0 +1,323 @@ +from typing import Any, Dict + +from Options import Accessibility, ProgressionBalancing, DeathLink +from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ + BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ + SpecialOrderLocations, HelpWantedLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, NumberOfLuckBuffs, \ + ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ + Gifting + +all_random_settings = { + "progression_balancing": "random", + "accessibility": "random", + Goal.internal_name: "random", + StartingMoney.internal_name: "random", + ProfitMargin.internal_name: "random", + BundleRandomization.internal_name: "random", + BundlePrice.internal_name: "random", + EntranceRandomization.internal_name: "random", + SeasonRandomization.internal_name: "random", + Cropsanity.internal_name: "random", + BackpackProgression.internal_name: "random", + ToolProgression.internal_name: "random", + ElevatorProgression.internal_name: "random", + SkillProgression.internal_name: "random", + BuildingProgression.internal_name: "random", + FestivalLocations.internal_name: "random", + ArcadeMachineLocations.internal_name: "random", + SpecialOrderLocations.internal_name: "random", + HelpWantedLocations.internal_name: "random", + Fishsanity.internal_name: "random", + Museumsanity.internal_name: "random", + Friendsanity.internal_name: "random", + FriendsanityHeartSize.internal_name: "random", + NumberOfMovementBuffs.internal_name: "random", + NumberOfLuckBuffs.internal_name: "random", + ExcludeGingerIsland.internal_name: "random", + TrapItems.internal_name: "random", + MultipleDaySleepEnabled.internal_name: "random", + MultipleDaySleepCost.internal_name: "random", + ExperienceMultiplier.internal_name: "random", + FriendshipMultiplier.internal_name: "random", + DebrisMultiplier.internal_name: "random", + QuickStart.internal_name: "random", + Gifting.internal_name: "random", + "death_link": "random", +} + +easy_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_items, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "very rich", + ProfitMargin.internal_name: "double", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_cheap, + EntranceRandomization.internal_name: EntranceRandomization.option_disabled, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_easy, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "minimum", + Fishsanity.internal_name: Fishsanity.option_only_easy_fish, + Museumsanity.internal_name: Museumsanity.option_milestones, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 8, + NumberOfLuckBuffs.internal_name: 8, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_easy, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "triple", + FriendshipMultiplier.internal_name: "quadruple", + DebrisMultiplier.internal_name: DebrisMultiplier.option_quarter, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +medium_settings = { + "progression_balancing": 25, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "rich", + ProfitMargin.internal_name: 150, + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_normal, + EntranceRandomization.internal_name: EntranceRandomization.option_non_progression, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only, + HelpWantedLocations.internal_name: "normal", + Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, + Museumsanity.internal_name: Museumsanity.option_milestones, + Friendsanity.internal_name: Friendsanity.option_starting_npcs, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 6, + NumberOfLuckBuffs.internal_name: 6, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_medium, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "double", + FriendshipMultiplier.internal_name: "triple", + DebrisMultiplier.internal_name: DebrisMultiplier.option_half, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +hard_settings = { + "progression_balancing": 0, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_grandpa_evaluation, + StartingMoney.internal_name: "extra", + ProfitMargin.internal_name: "normal", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_expensive, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "lots", + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 4, + NumberOfLuckBuffs.internal_name: 4, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_hard, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "cheap", + ExperienceMultiplier.internal_name: "vanilla", + FriendshipMultiplier.internal_name: "double", + DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "true", +} + +nightmare_settings = { + "progression_balancing": 0, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.option_community_center, + StartingMoney.internal_name: "vanilla", + ProfitMargin.internal_name: "half", + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + BundlePrice.internal_name: BundlePrice.option_expensive, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "maximum", + Fishsanity.internal_name: Fishsanity.option_special, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 2, + NumberOfLuckBuffs.internal_name: 2, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_hell, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "expensive", + ExperienceMultiplier.internal_name: "half", + FriendshipMultiplier.internal_name: "vanilla", + DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, + QuickStart.internal_name: QuickStart.option_false, + Gifting.internal_name: Gifting.option_true, + "death_link": "true", +} + +short_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_items, + Goal.internal_name: Goal.option_bottom_of_the_mines, + StartingMoney.internal_name: "filthy rich", + ProfitMargin.internal_name: "quadruple", + BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundlePrice.internal_name: BundlePrice.option_very_cheap, + EntranceRandomization.internal_name: EntranceRandomization.option_disabled, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Cropsanity.internal_name: Cropsanity.option_disabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_disabled, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "none", + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: 4, + NumberOfMovementBuffs.internal_name: 10, + NumberOfLuckBuffs.internal_name: 10, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.option_easy, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, + MultipleDaySleepCost.internal_name: "free", + ExperienceMultiplier.internal_name: "quadruple", + FriendshipMultiplier.internal_name: 800, + DebrisMultiplier.internal_name: DebrisMultiplier.option_none, + QuickStart.internal_name: QuickStart.option_true, + Gifting.internal_name: Gifting.option_true, + "death_link": "false", +} + +lowsanity_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_minimal, + Goal.internal_name: Goal.default, + StartingMoney.internal_name: StartingMoney.default, + ProfitMargin.internal_name: ProfitMargin.default, + BundleRandomization.internal_name: BundleRandomization.default, + BundlePrice.internal_name: BundlePrice.default, + EntranceRandomization.internal_name: EntranceRandomization.default, + SeasonRandomization.internal_name: SeasonRandomization.option_disabled, + Cropsanity.internal_name: Cropsanity.option_disabled, + BackpackProgression.internal_name: BackpackProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + BuildingProgression.internal_name: BuildingProgression.option_vanilla, + FestivalLocations.internal_name: FestivalLocations.option_disabled, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: "none", + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, + NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, + NumberOfLuckBuffs.internal_name: NumberOfLuckBuffs.default, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + TrapItems.internal_name: TrapItems.default, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, + MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, + ExperienceMultiplier.internal_name: ExperienceMultiplier.default, + FriendshipMultiplier.internal_name: FriendshipMultiplier.default, + DebrisMultiplier.internal_name: DebrisMultiplier.default, + QuickStart.internal_name: QuickStart.default, + Gifting.internal_name: Gifting.default, + "death_link": DeathLink.default, +} + +allsanity_settings = { + "progression_balancing": ProgressionBalancing.default, + "accessibility": Accessibility.option_locations, + Goal.internal_name: Goal.default, + StartingMoney.internal_name: StartingMoney.default, + ProfitMargin.internal_name: ProfitMargin.default, + BundleRandomization.internal_name: BundleRandomization.default, + BundlePrice.internal_name: BundlePrice.default, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_enabled, + BackpackProgression.internal_name: BackpackProgression.option_early_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive_early_shipping_bin, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: "maximum", + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all, + FriendsanityHeartSize.internal_name: 1, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.default, + MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, + MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, + ExperienceMultiplier.internal_name: ExperienceMultiplier.default, + FriendshipMultiplier.internal_name: FriendshipMultiplier.default, + DebrisMultiplier.internal_name: DebrisMultiplier.default, + QuickStart.internal_name: QuickStart.default, + Gifting.internal_name: Gifting.default, + "death_link": DeathLink.default, +} + +sv_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Easy": easy_settings, + "Medium": medium_settings, + "Hard": hard_settings, + "Nightmare": nightmare_settings, + "Short": short_settings, + "Lowsanity": lowsanity_settings, + "Allsanity": allsanity_settings, +} diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 72337812cd..0749b1a8f1 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -329,7 +329,7 @@ class TestRecipeLogic(SVTestBase): options = { options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, } # I wanted to make a test for different ways to obtain a pizza, but I'm stuck not knowing how to block the immediate purchase from Gus diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index b0c4ba2c7b..ba037f7a65 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -47,7 +47,7 @@ class SVTestBase(WorldTestBase, SVTestCase): def minimal_locations_maximal_items(): min_max_options = { SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, + Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_vanilla, ToolProgression.internal_name: ToolProgression.option_vanilla, SkillProgression.internal_name: SkillProgression.option_vanilla, @@ -72,7 +72,7 @@ def allsanity_options_without_mods(): BundleRandomization.internal_name: BundleRandomization.option_shuffled, BundlePrice.internal_name: BundlePrice.option_expensive, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, + Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, diff --git a/worlds/stardew_valley/test/checks/option_checks.py b/worlds/stardew_valley/test/checks/option_checks.py index ce8e552461..c9d9860cf5 100644 --- a/worlds/stardew_valley/test/checks/option_checks.py +++ b/worlds/stardew_valley/test/checks/option_checks.py @@ -40,7 +40,7 @@ def assert_can_reach_island_if_should(tester: SVTestBase, multiworld: MultiWorld def assert_cropsanity_same_number_items_and_locations(tester: SVTestBase, multiworld: MultiWorld): - is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_shuffled + is_cropsanity = get_stardew_options(multiworld).cropsanity.value == options.Cropsanity.option_enabled if not is_cropsanity: return From f959819801fa153dc2461ed22881fe2f80f206bf Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Wed, 22 Nov 2023 06:15:09 -0800 Subject: [PATCH 184/327] Hollow Knight: Don't force mimics local (#2482) --- worlds/hk/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c16a108cd1..f7e7e22e69 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -170,7 +170,6 @@ class HKWorld(World): charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) # world.exclude_locations[self.player].value.update(white_palace_locations) - world.local_items[self.player].value.add("Mimic_Grub") for term, data in cost_terms.items(): mini = getattr(world, f"Minimum{data.option}Price")[self.player] maxi = getattr(world, f"Maximum{data.option}Price")[self.player] From 3b357315ee8704f3727e50c28b67c712468531fb Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 22 Nov 2023 09:15:35 -0500 Subject: [PATCH 185/327] Git: Added file type .smc to gitignore (#2476) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aaea45ce98..022abe38fe 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.z64 *.n64 *.nes +*.smc *.sms *.gb *.gbc From d1b22935b44f0de1f8eddb982e22d5522ef99394 Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 22 Nov 2023 15:17:33 +0100 Subject: [PATCH 186/327] Timespinner: New options from TS Rando v1.25 + Logic fix (#2090) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/timespinner/Locations.py | 30 +++++------ worlds/timespinner/Options.py | 35 ++++++++++--- worlds/timespinner/PreCalculatedWeights.py | 32 +++++++----- worlds/timespinner/Regions.py | 59 ++++++++++------------ worlds/timespinner/__init__.py | 21 +++++--- 5 files changed, 103 insertions(+), 74 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 70c76b8638..7b378b4637 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Optional, Callable, NamedTuple +from typing import List, Optional, Callable, NamedTuple from BaseClasses import MultiWorld, CollectionState from .Options import is_option_enabled from .PreCalculatedWeights import PreCalculatedWeights @@ -11,11 +11,11 @@ class LocationData(NamedTuple): region: str name: str code: Optional[int] - rule: Callable[[CollectionState], bool] = lambda state: True + rule: Optional[Callable[[CollectionState], bool]] = None def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> Tuple[LocationData, ...]: + precalculated_weights: PreCalculatedWeights) -> List[LocationData]: flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) @@ -88,9 +88,9 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Military Fortress (hangar)', 'Military Fortress: Soldiers bridge', 1337060), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess room', 1337061), LocationData('Military Fortress (hangar)', 'Military Fortress: Giantess bridge', 1337062), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), - LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_doublejump(state) and logic.has_keycard_B(state)), - LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state)), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 2', 1337063, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))), + LocationData('Military Fortress (hangar)', 'Military Fortress: B door chest 1', 1337064, lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))), + LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))), LocationData('The lab', 'Lab: Coffee break', 1337066), LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump), LocationData('The lab', 'Lab: Lower trash left', 1337068, logic.has_upwarddash), @@ -139,17 +139,17 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: not flooded.dry_lake_serene or logic.has_doublejump_of_npc(state)), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: not flooded.flood_maw or logic.has_doublejump(state)), + LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Bottom left room', 1337114, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Single shroom room', 1337115), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), - LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: logic.has_forwarddash_doublejump(state) or flooded.flood_maw), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 1', 1337116, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 2', 1337117, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), + LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), @@ -197,7 +197,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Ancient Pyramid (entrance)', 'Ancient Pyramid: Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Conviction guarded room', 1337247), LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Pit secret room', 1337248, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), - LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (not flooded.flood_pyramid_shaft or state.has('Water Mask', player))), + LocationData('Ancient Pyramid (left)', 'Ancient Pyramid: Regret chest', 1337249, lambda state: logic.can_break_walls(state) and (state.has('Water Mask', player) if flooded.flood_pyramid_shaft else logic.has_doublejump(state))), LocationData('Ancient Pyramid (right)', 'Ancient Pyramid: Nightmare Door chest', 1337236, lambda state: not flooded.flood_pyramid_back or state.has('Water Mask', player)), LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId, lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player) and (not flooded.flood_pyramid_back or state.has('Water Mask', player))) ] @@ -271,4 +271,4 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (chest)', 1337245), ) - return tuple(location_table) + return location_table diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 8b11184944..f7921fcb81 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -54,14 +54,23 @@ class LoreChecks(Toggle): display_name = "Lore Checks" -class BossRando(Toggle): - "Shuffles the positions of all bosses." +class BossRando(Choice): + "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" + option_off = 0 + option_scaled = 1 + option_unscaled = 2 + alias_true = 1 -class BossScaling(DefaultOnToggle): - "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Recommended)" - display_name = "Scale Random Boss Stats" +class EnemyRando(Choice): + "Wheter enemies will be randomized, and if their damage/hp should be scaled." + display_name = "Enemy Randomization" + option_off = 0 + option_scaled = 1 + option_unscaled = 2 + option_ryshia = 3 + alias_true = 1 class DamageRando(Choice): @@ -336,6 +345,7 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + display_name = "Rising Tides Overrides" schema = Schema({ **rising_tide_option("Xarion"), **rising_tide_option("Maw"), @@ -345,9 +355,10 @@ class RisingTidesOverrides(OptionDict): **rising_tide_option("CastleBasement", with_save_point_option=True), **rising_tide_option("CastleCourtyard"), **rising_tide_option("LakeDesolation"), - **rising_tide_option("LakeSerene") + **rising_tide_option("LakeSerene"), + **rising_tide_option("LakeSereneBridge"), + **rising_tide_option("Lab"), }) - display_name = "Rising Tides Overrides" default = { "Xarion": { "Dry": 67, "Flooded": 33 }, "Maw": { "Dry": 67, "Flooded": 33 }, @@ -358,6 +369,8 @@ class RisingTidesOverrides(OptionDict): "CastleCourtyard": { "Dry": 67, "Flooded": 33 }, "LakeDesolation": { "Dry": 67, "Flooded": 33 }, "LakeSerene": { "Dry": 33, "Flooded": 67 }, + "LakeSereneBridge": { "Dry": 67, "Flooded": 33 }, + "Lab": { "Dry": 67, "Flooded": 33 }, } @@ -383,6 +396,11 @@ class Traps(OptionList): default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] +class PresentAccessWithWheelAndSpindle(Toggle): + """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" + display_name = "Past Wheel & Spindle Warp" + + # Some options that are available in the timespinner randomizer arent currently implemented timespinner_options: Dict[str, Option] = { "StartWithJewelryBox": StartWithJewelryBox, @@ -396,7 +414,7 @@ timespinner_options: Dict[str, Option] = { "Cantoran": Cantoran, "LoreChecks": LoreChecks, "BossRando": BossRando, - "BossScaling": BossScaling, + "EnemyRando": EnemyRando, "DamageRando": DamageRando, "DamageRandoOverrides": DamageRandoOverrides, "HpCap": HpCap, @@ -419,6 +437,7 @@ timespinner_options: Dict[str, Option] = { "UnchainedKeys": UnchainedKeys, "TrapChance": TrapChance, "Traps": Traps, + "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, "DeathLink": DeathLink, } diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index 64243e25ed..ff7f031d3b 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,4 +1,4 @@ -from typing import Tuple, Dict, Union +from typing import Tuple, Dict, Union, List from BaseClasses import MultiWorld from .Options import timespinner_options, is_option_enabled, get_option_value @@ -17,7 +17,9 @@ class PreCalculatedWeights: flood_moat: bool flood_courtyard: bool flood_lake_desolation: bool - dry_lake_serene: bool + flood_lake_serene: bool + flood_lake_serene_bridge: bool + flood_lab: bool def __init__(self, world: MultiWorld, player: int): if world and is_option_enabled(world, player, "RisingTides"): @@ -32,8 +34,9 @@ class PreCalculatedWeights: self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.dry_lake_serene = not flood_lake_serene + self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") + self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") + self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") else: self.flood_basement = False self.flood_basement_high = False @@ -44,30 +47,32 @@ class PreCalculatedWeights: self.flood_moat = False self.flood_courtyard = False self.flood_lake_desolation = False - self.dry_lake_serene = False + self.flood_lake_serene = True + self.flood_lake_serene_bridge = False + self.flood_lab = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw) + self.get_pyramid_keys_unlocks(world, player, self.flood_maw, self.flood_xarion) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool) -> Tuple[str, str, str, str]: - present_teleportation_gates: Tuple[str, ...] = ( + def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: List[str] = [ "GateKittyBoss", "GateLeftLibrary", "GateMilitaryGate", "GateSealedCaves", "GateSealedSirensCave", "GateLakeDesolation" - ) + ] - past_teleportation_gates: Tuple[str, ...] = ( + past_teleportation_gates: List[str] = [ "GateLakeSereneRight", "GateAccessToPast", "GateCastleRamparts", "GateCastleKeep", "GateRoyalTowers", "GateCavesOfBanishment" - ) + ] ancient_pyramid_teleportation_gates: Tuple[str, ...] = ( "GateGyre", @@ -84,7 +89,10 @@ class PreCalculatedWeights: ) if not is_maw_flooded: - past_teleportation_gates += ("GateMaw", ) + past_teleportation_gates.append("GateMaw") + + if not is_xarion_flooded: + present_teleportation_gates.append("GateXarion") if is_option_enabled(world, player, "Inverted"): all_gates: Tuple[str, ...] = present_teleportation_gates diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 905cae867e..fc75356429 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,4 +1,4 @@ -from typing import List, Set, Dict, Tuple, Optional, Callable +from typing import List, Set, Dict, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location from .Options import is_option_enabled from .Locations import LocationData, get_location_datas @@ -7,9 +7,8 @@ from .LogicExtensions import TimespinnerLogic def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): - locationn_datas: Tuple[LocationData] = get_location_datas(world, player, precalculated_weights) - - locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region(locationn_datas) + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( + get_location_datas(world, player, precalculated_weights)) regions = [ create_region(world, player, locations_per_region, 'Menu'), @@ -32,7 +31,6 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'The lab (upper)'), create_region(world, player, locations_per_region, 'Emperors tower'), create_region(world, player, locations_per_region, 'Skeleton Shaft'), - create_region(world, player, locations_per_region, 'Sealed Caves (upper)'), create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'), create_region(world, player, locations_per_region, 'Refugee Camp'), create_region(world, player, locations_per_region, 'Forest'), @@ -63,7 +61,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w if __debug__: throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys()) - + world.regions += regions connectStartingRegion(world, player) @@ -71,9 +69,9 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w flooded: PreCalculatedWeights = precalculated_weights logic = TimespinnerLogic(world, player, precalculated_weights) - connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: logic.has_timestop(state) or state.has('Talaria Attachment', player) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) - connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: logic.has_doublejump(state) or flooded.flood_lake_desolation) + connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state)) connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper lake desolation', 'Lake desolation') connect(world, player, 'Upper lake desolation', 'Eastern lake desolation') @@ -109,40 +107,38 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) connect(world, player, 'Military Fortress', 'Military Fortress (hangar)', logic.has_doublejump) connect(world, player, 'Military Fortress (hangar)', 'Military Fortress') - connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and logic.has_doublejump(state)) + connect(world, player, 'Military Fortress (hangar)', 'The lab', lambda state: logic.has_keycard_B(state) and (state.has('Water Mask', player) if flooded.flood_lab else logic.has_doublejump(state))) connect(world, player, 'Temporal Gyre', 'Military Fortress') connect(world, player, 'The lab', 'Military Fortress') connect(world, player, 'The lab', 'The lab (power off)', logic.has_doublejump_of_npc) - connect(world, player, 'The lab (power off)', 'The lab') + connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player)) connect(world, player, 'The lab (power off)', 'The lab (upper)', logic.has_forwarddash_doublejump) connect(world, player, 'The lab (upper)', 'The lab (power off)') connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) connect(world, player, 'Emperors tower', 'The lab (upper)') connect(world, player, 'Skeleton Shaft', 'Lake desolation') - connect(world, player, 'Skeleton Shaft', 'Sealed Caves (upper)', logic.has_keycard_A) + connect(world, player, 'Skeleton Shaft', 'Sealed Caves (Xarion)', logic.has_keycard_A) connect(world, player, 'Skeleton Shaft', 'Space time continuum', logic.has_teleport) - connect(world, player, 'Sealed Caves (upper)', 'Skeleton Shaft') - connect(world, player, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: logic.has_teleport(state) or logic.has_doublejump(state)) - connect(world, player, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', logic.has_doublejump) + connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - #connect(world, player, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted")) + connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') - connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or logic.has_timestop(state)) + connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) connect(world, player, 'Forest', 'Caves of Banishment (Sirens)') connect(world, player, 'Forest', 'Castle Ramparts') connect(world, player, 'Left Side forest Caves', 'Forest') connect(world, player, 'Left Side forest Caves', 'Upper Lake Serene', logic.has_timestop) - connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Left Side forest Caves', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Left Side forest Caves', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Upper Lake Serene', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Lower Lake Serene', 'Upper Lake Serene') connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') - connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: not flooded.dry_lake_serene or logic.has_doublejump(state)) - connect(world, player, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player) or flooded.dry_lake_serene) + connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)) + connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player)) connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) @@ -153,7 +149,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Castle Ramparts', 'Castle Keep') connect(world, player, 'Castle Ramparts', 'Space time continuum', logic.has_teleport) connect(world, player, 'Castle Keep', 'Castle Ramparts') - connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: state.has('Water Mask', player) or not flooded.flood_basement) + connect(world, player, 'Castle Keep', 'Castle Basement', lambda state: not flooded.flood_basement or state.has('Water Mask', player)) connect(world, player, 'Castle Keep', 'Royal towers (lower)', logic.has_doublejump) connect(world, player, 'Castle Keep', 'Space time continuum', logic.has_teleport) connect(world, player, 'Royal towers (lower)', 'Castle Keep') @@ -165,14 +161,15 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w #connect(world, player, 'Ancient Pyramid (entrance)', 'The lab (upper)', lambda state: not is_option_enabled(world, player, "EnterSandman")) connect(world, player, 'Ancient Pyramid (entrance)', 'Ancient Pyramid (left)', logic.has_doublejump) connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (entrance)') - connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) - connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: logic.has_upwarddash(state) or flooded.flood_pyramid_shaft) + connect(world, player, 'Ancient Pyramid (left)', 'Ancient Pyramid (right)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state)) + connect(world, player, 'Ancient Pyramid (right)', 'Ancient Pyramid (left)', lambda state: flooded.flood_pyramid_shaft or logic.has_upwarddash(state)) connect(world, player, 'Space time continuum', 'Lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateLakeDesolation")) connect(world, player, 'Space time continuum', 'Lower lake desolation', lambda state: logic.can_teleport_to(state, "Present", "GateKittyBoss")) connect(world, player, 'Space time continuum', 'Library', lambda state: logic.can_teleport_to(state, "Present", "GateLeftLibrary")) connect(world, player, 'Space time continuum', 'Varndagroth tower right (lower)', lambda state: logic.can_teleport_to(state, "Present", "GateMilitaryGate")) connect(world, player, 'Space time continuum', 'Skeleton Shaft', lambda state: logic.can_teleport_to(state, "Present", "GateSealedCaves")) connect(world, player, 'Space time continuum', 'Sealed Caves (Sirens)', lambda state: logic.can_teleport_to(state, "Present", "GateSealedSirensCave")) + connect(world, player, 'Space time continuum', 'Sealed Caves (Xarion)', lambda state: logic.can_teleport_to(state, "Present", "GateXarion")) connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) @@ -204,12 +201,13 @@ def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: def create_location(player: int, location_data: LocationData, region: Region) -> Location: location = Location(player, location_data.name, location_data.code, region) - location.access_rule = location_data.rule + + if location_data.rule: + location.access_rule = location_data.rule if id is None: location.event = True location.locked = True - return location @@ -220,7 +218,6 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str for location_data in locations_per_region[name]: location = create_location(player, location_data, region) region.locations.append(location) - return region @@ -237,11 +234,9 @@ def connectStartingRegion(world: MultiWorld, player: int): menu_to_tutorial = Entrance(player, 'Tutorial', menu) menu_to_tutorial.connect(tutorial) menu.exits.append(menu_to_tutorial) - tutorial_to_start = Entrance(player, 'Start Game', tutorial) tutorial_to_start.connect(starting_region) tutorial.exits.append(tutorial_to_start) - teleport_back_to_start = Entrance(player, 'Teleport back to start', space_time_continuum) teleport_back_to_start.connect(starting_region) space_time_continuum.exits.append(teleport_back_to_start) @@ -249,7 +244,7 @@ def connectStartingRegion(world: MultiWorld, player: int): def connect(world: MultiWorld, player: int, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None): - + sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) @@ -257,15 +252,13 @@ def connect(world: MultiWorld, player: int, source: str, target: str, if rule: connection.access_rule = rule - sourceRegion.exits.append(connection) connection.connect(targetRegion) -def split_location_datas_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]: +def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]: per_region: Dict[str, List[LocationData]] = {} for location in locations: per_region.setdefault(location.region, []).append(location) - - return per_region + return per_region \ No newline at end of file diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 24230862bd..ff7b3515e6 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -39,9 +39,9 @@ class TimespinnerWorld(World): option_definitions = timespinner_options game = "Timespinner" topology_present = True - data_version = 11 + data_version = 12 web = TimespinnerWebWorld() - required_client_version = (0, 3, 7) + required_client_version = (0, 4, 2) item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} @@ -108,7 +108,9 @@ class TimespinnerWorld(World): slot_data["CastleMoat"] = self.precalculated_weights.flood_moat slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation - slot_data["DryLakeSerene"] = self.precalculated_weights.dry_lake_serene + slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene + slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge + slot_data["Lab"] = self.precalculated_weights.flood_lab return slot_data @@ -144,8 +146,12 @@ class TimespinnerWorld(World): flooded_areas.append("Castle Courtyard") if self.precalculated_weights.flood_lake_desolation: flooded_areas.append("Lake Desolation") - if not self.precalculated_weights.dry_lake_serene: + if self.precalculated_weights.flood_lake_serene: flooded_areas.append("Lake Serene") + if self.precalculated_weights.flood_lake_serene_bridge: + flooded_areas.append("Lake Serene Bridge") + if self.precalculated_weights.flood_lab: + flooded_areas.append("Lab") if len(flooded_areas) == 0: flooded_areas_string: str = "None" @@ -220,15 +226,18 @@ class TimespinnerWorld(World): def assign_starter_items(self, excluded_items: Set[str]) -> None: non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value + local_items: Set[str] = self.multiworld.local_items[self.player].value - local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items) + local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if + item in local_items or not item in non_local_items) if not local_starter_melee_weapons: if 'Plasma Orb' in non_local_items: raise Exception("Atleast one melee orb must be local") else: local_starter_melee_weapons = ('Plasma Orb',) - local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items) + local_starter_spells = tuple(item for item in starter_spells if + item in local_items or not item in non_local_items) if not local_starter_spells: if 'Lightwall' in non_local_items: raise Exception("Atleast one spell must be local") From 01b566b798ac568c35a35c78576a5cd0c771477d Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 22 Nov 2023 06:29:33 -0800 Subject: [PATCH 187/327] LADX: Text shuffle (#2051) --- worlds/ladx/LADXR/generator.py | 17 +++++++++ worlds/ladx/LADXR/patches/owl.py | 8 +++-- worlds/ladx/LADXR/patches/phone.py | 57 +++++++++++++++--------------- worlds/ladx/LADXR/pointerTable.py | 5 ++- worlds/ladx/Options.py | 8 +++++ 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 72d631da86..0406ad51f8 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -3,6 +3,7 @@ import importlib.util import importlib.machinery import os import pkgutil +from collections import defaultdict from .romTables import ROMWithTables from . import assembler @@ -322,6 +323,22 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m if args.doubletrouble: patches.enemies.doubleTrouble(rom) + if ap_settings["text_shuffle"]: + buckets = defaultdict(list) + # For each ROM bank, shuffle text within the bank + for n, data in enumerate(rom.texts._PointerTable__data): + # Don't muck up which text boxes are questions and which are statements + if type(data) != int and data and data != b'\xFF': + buckets[(rom.texts._PointerTable__banks[n], data[len(data) - 1] == 0xfe)].append((n, data)) + for bucket in buckets.values(): + # For each bucket, make a copy and shuffle + shuffled = bucket.copy() + rnd.shuffle(shuffled) + # Then put new text in + for bucket_idx, (orig_idx, data) in enumerate(bucket): + rom.texts[shuffled[bucket_idx][0]] = data + + if ap_settings["trendy_game"] != TrendyGame.option_normal: # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py index b22386a6cb..47e575191a 100644 --- a/worlds/ladx/LADXR/patches/owl.py +++ b/worlds/ladx/LADXR/patches/owl.py @@ -11,15 +11,17 @@ def removeOwlEvents(rom): re.removeEntities(0x41) re.store(rom) # Clear texts used by the owl. Potentially reused somewhere o else. - rom.texts[0x0D9] = b'\xff' # used by boomerang # 1 Used by empty chest (master stalfos message) # 8 unused (0x0C0-0x0C7) # 1 used by bowwow in chest # 1 used by item for other player message # 2 used by arrow chest messages # 2 used by tunics - for idx in range(0x0BE, 0x0CE): - rom.texts[idx] = b'\xff' + + # Undoing this, we use it for text shuffle now + #rom.texts[0x0D9] = b'\xff' # used by boomerang + # for idx in range(0x0BE, 0x0CE): + # rom.texts[idx] = b'\xff' # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows diff --git a/worlds/ladx/LADXR/patches/phone.py b/worlds/ladx/LADXR/patches/phone.py index f38745606c..a2f3939a08 100644 --- a/worlds/ladx/LADXR/patches/phone.py +++ b/worlds/ladx/LADXR/patches/phone.py @@ -2,34 +2,35 @@ from ..assembler import ASM def patchPhone(rom): - rom.texts[0x141] = b"" - rom.texts[0x142] = b"" - rom.texts[0x143] = b"" - rom.texts[0x144] = b"" - rom.texts[0x145] = b"" - rom.texts[0x146] = b"" - rom.texts[0x147] = b"" - rom.texts[0x148] = b"" - rom.texts[0x149] = b"" - rom.texts[0x14A] = b"" - rom.texts[0x14B] = b"" - rom.texts[0x14C] = b"" - rom.texts[0x14D] = b"" - rom.texts[0x14E] = b"" - rom.texts[0x14F] = b"" - rom.texts[0x16E] = b"" - rom.texts[0x1FD] = b"" - rom.texts[0x228] = b"" - rom.texts[0x229] = b"" - rom.texts[0x22A] = b"" - rom.texts[0x240] = b"" - rom.texts[0x241] = b"" - rom.texts[0x242] = b"" - rom.texts[0x243] = b"" - rom.texts[0x244] = b"" - rom.texts[0x245] = b"" - rom.texts[0x247] = b"" - rom.texts[0x248] = b"" + # reenabled for text shuffle +# rom.texts[0x141] = b"" +# rom.texts[0x142] = b"" +# rom.texts[0x143] = b"" +# rom.texts[0x144] = b"" +# rom.texts[0x145] = b"" +# rom.texts[0x146] = b"" +# rom.texts[0x147] = b"" +# rom.texts[0x148] = b"" +# rom.texts[0x149] = b"" +# rom.texts[0x14A] = b"" +# rom.texts[0x14B] = b"" +# rom.texts[0x14C] = b"" +# rom.texts[0x14D] = b"" +# rom.texts[0x14E] = b"" +# rom.texts[0x14F] = b"" +# rom.texts[0x16E] = b"" +# rom.texts[0x1FD] = b"" +# rom.texts[0x228] = b"" +# rom.texts[0x229] = b"" +# rom.texts[0x22A] = b"" +# rom.texts[0x240] = b"" +# rom.texts[0x241] = b"" +# rom.texts[0x242] = b"" +# rom.texts[0x243] = b"" +# rom.texts[0x244] = b"" +# rom.texts[0x245] = b"" +# rom.texts[0x247] = b"" +# rom.texts[0x248] = b"" rom.patch(0x06, 0x2A8F, 0x2BBC, ASM(""" ; We use $DB6D to store which tunics we have. This is normally the Dungeon9 instrument, which does not exist. ld a, [$DC0F] diff --git a/worlds/ladx/LADXR/pointerTable.py b/worlds/ladx/LADXR/pointerTable.py index 9b8d49466c..a1a92ba178 100644 --- a/worlds/ladx/LADXR/pointerTable.py +++ b/worlds/ladx/LADXR/pointerTable.py @@ -116,7 +116,10 @@ class PointerTable: rom.banks[ptr_bank][ptr_addr] = pointer & 0xFF rom.banks[ptr_bank][ptr_addr + 1] = (pointer >> 8) | 0x40 - for n, s in enumerate(self.__data): + data = list(enumerate(self.__data)) + data.sort(key=lambda t: type(t[1]) == int or -len(t[1])) + + for n, s in data: if isinstance(s, int): pointer = s else: diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index f80ad15520..f1d5c51301 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -43,6 +43,12 @@ class TradeQuest(DefaultOffToggle, LADXROption): display_name = "Trade Quest" ladxr_name = "tradequest" +class TextShuffle(DefaultOffToggle): + """ + [On] Shuffles all the text in the game + [Off] (default) doesn't shuffle them. + """ + class Rooster(DefaultOnToggle, LADXROption): """ [On] Adds the rooster to the item pool. @@ -431,6 +437,7 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'trendy_game': TrendyGame, 'gfxmod': GfxMod, 'palette': Palette, + 'text_shuffle': TextShuffle, 'shuffle_nightmare_keys': ShuffleNightmareKeys, 'shuffle_small_keys': ShuffleSmallKeys, 'shuffle_maps': ShuffleMaps, @@ -439,4 +446,5 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = { 'music_change_condition': MusicChangeCondition, 'nag_messages': NagMessages, 'ap_title_screen': APTitleScreen, + } From 79406faf27400d03d48259899aac2ab42014c688 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Wed, 22 Nov 2023 08:20:32 -0700 Subject: [PATCH 188/327] RoR2: 1.3.0 content update (#2425) --- worlds/ror2/Items.py | 194 ---------------- worlds/ror2/Locations.py | 119 ---------- worlds/ror2/RoR2Environments.py | 118 ---------- worlds/ror2/__init__.py | 244 +++++++++---------- worlds/ror2/docs/setup_en.md | 13 +- worlds/ror2/items.py | 309 +++++++++++++++++++++++++ worlds/ror2/locations.py | 89 +++++++ worlds/ror2/{Options.py => options.py} | 124 +++++++++- worlds/ror2/{Regions.py => regions.py} | 95 ++++++-- worlds/ror2/ror2environments.py | 118 ++++++++++ worlds/ror2/{Rules.py => rules.py} | 126 +++++----- worlds/ror2/test/__init__.py | 5 + worlds/ror2/test/test_any_goal.py | 26 +++ worlds/ror2/test/test_classic.py | 7 + worlds/ror2/test/test_limbo_goal.py | 15 ++ worlds/ror2/test/test_mithrix_goal.py | 25 ++ worlds/ror2/test/test_voidling_goal.py | 28 +++ 17 files changed, 999 insertions(+), 656 deletions(-) delete mode 100644 worlds/ror2/Items.py delete mode 100644 worlds/ror2/Locations.py delete mode 100644 worlds/ror2/RoR2Environments.py create mode 100644 worlds/ror2/items.py create mode 100644 worlds/ror2/locations.py rename worlds/ror2/{Options.py => options.py} (73%) rename worlds/ror2/{Regions.py => regions.py} (59%) create mode 100644 worlds/ror2/ror2environments.py rename worlds/ror2/{Rules.py => rules.py} (60%) create mode 100644 worlds/ror2/test/__init__.py create mode 100644 worlds/ror2/test/test_any_goal.py create mode 100644 worlds/ror2/test/test_classic.py create mode 100644 worlds/ror2/test/test_limbo_goal.py create mode 100644 worlds/ror2/test/test_mithrix_goal.py create mode 100644 worlds/ror2/test/test_voidling_goal.py diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py deleted file mode 100644 index 448e3272ae..0000000000 --- a/worlds/ror2/Items.py +++ /dev/null @@ -1,194 +0,0 @@ -from BaseClasses import Item -from .Options import ItemWeights -from .RoR2Environments import * - - -class RiskOfRainItem(Item): - game: str = "Risk of Rain 2" - - -# 37000 - 37699, 38000 -item_table: Dict[str, int] = { - "Dio's Best Friend": 37001, - "Common Item": 37002, - "Uncommon Item": 37003, - "Legendary Item": 37004, - "Boss Item": 37005, - "Lunar Item": 37006, - "Equipment": 37007, - "Item Scrap, White": 37008, - "Item Scrap, Green": 37009, - "Item Scrap, Red": 37010, - "Item Scrap, Yellow": 37011, - "Void Item": 37012, - "Beads of Fealty": 37013 -} - -# 37700 - 37699 -################################################## -# environments - -environment_offest = 37700 - -# add ALL environments into the item table -environment_offset_table = shift_by_offset(environment_ALL_table, environment_offest) -item_table.update(shift_by_offset(environment_ALL_table, environment_offest)) -# use the sotv dlc in the item table so that all names can be looked up regardless of use - -# end of environments -################################################## - -default_weights: Dict[str, int] = { - "Item Scrap, Green": 16, - "Item Scrap, Red": 4, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 32, - "Common Item": 64, - "Uncommon Item": 32, - "Legendary Item": 8, - "Boss Item": 4, - "Lunar Item": 16, - "Void Item": 16, - "Equipment": 32 -} - -new_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 75, - "Uncommon Item": 40, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 20 -} - -uncommon_weights: Dict[str, int] = { - "Item Scrap, Green": 45, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 45, - "Uncommon Item": 100, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -legendary_weights: Dict[str, int] = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 50, - "Uncommon Item": 25, - "Legendary Item": 100, - "Boss Item": 5, - "Lunar Item": 15, - "Void Item": 16, - "Equipment": 20 -} - -lunartic_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 100, - "Void Item": 0, - "Equipment": 0 -} - -chaos_weights: Dict[str, int] = { - "Item Scrap, Green": 80, - "Item Scrap, Red": 45, - "Item Scrap, Yellow": 30, - "Item Scrap, White": 100, - "Common Item": 100, - "Uncommon Item": 70, - "Legendary Item": 30, - "Boss Item": 20, - "Lunar Item": 60, - "Void Item": 60, - "Equipment": 40 -} - -no_scraps_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 100, - "Uncommon Item": 40, - "Legendary Item": 15, - "Boss Item": 5, - "Lunar Item": 10, - "Void Item": 16, - "Equipment": 25 -} - -even_weights: Dict[str, int] = { - "Item Scrap, Green": 1, - "Item Scrap, Red": 1, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 1, - "Common Item": 1, - "Uncommon Item": 1, - "Legendary Item": 1, - "Boss Item": 1, - "Lunar Item": 1, - "Void Item": 1, - "Equipment": 1 -} - -scraps_only: Dict[str, int] = { - "Item Scrap, Green": 70, - "Item Scrap, White": 100, - "Item Scrap, Red": 30, - "Item Scrap, Yellow": 5, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 0, - "Equipment": 0 -} - -void_weights: Dict[str, int] = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Void Item": 100, - "Equipment": 0 -} - -item_pool_weights: Dict[int, Dict[str, int]] = { - ItemWeights.option_default: default_weights, - ItemWeights.option_new: new_weights, - ItemWeights.option_uncommon: uncommon_weights, - ItemWeights.option_legendary: legendary_weights, - ItemWeights.option_lunartic: lunartic_weights, - ItemWeights.option_chaos: chaos_weights, - ItemWeights.option_no_scraps: no_scraps_weights, - ItemWeights.option_even: even_weights, - ItemWeights.option_scraps_only: scraps_only, - ItemWeights.option_void: void_weights, -} - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py deleted file mode 100644 index 7db3ceca73..0000000000 --- a/worlds/ror2/Locations.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Tuple -from BaseClasses import Location -from .Options import TotalLocations -from .Options import ChestsPerEnvironment -from .Options import ShrinesPerEnvironment -from .Options import ScavengersPerEnvironment -from .Options import ScannersPerEnvironment -from .Options import AltarsPerEnvironment -from .RoR2Environments import * - - -class RiskOfRainLocation(Location): - game: str = "Risk of Rain 2" - - -ror2_locations_start_id = 38000 - - -def get_classic_item_pickups(n: int) -> Dict[str, int]: - """Get n ItemPickups, capped at the max value for TotalLocations""" - n = max(n, 0) - n = min(n, TotalLocations.range_end) - return { f"ItemPickup{i+1}": ror2_locations_start_id+i for i in range(n) } - - -item_pickups = get_classic_item_pickups(TotalLocations.range_end) -location_table = item_pickups - - -def environment_abreviation(long_name:str) -> str: - """convert long environment names to initials""" - abrev = "" - # go through every word finding a letter (or number) for an initial - for word in long_name.split(): - initial = word[0] - for letter in word: - if letter.isalnum(): - initial = letter - break - abrev+= initial - return abrev - -# highest numbered orderedstages (this is so we can treat the easily caculate the check ids based on the environment and location "offset") -highest_orderedstage: int= max(compress_dict_list_horizontal(environment_orderedstages_table).values()) - -ror2_locations_start_orderedstage = ror2_locations_start_id + TotalLocations.range_end - -class orderedstage_location: - """A class to behave like a struct for storing the offsets of location types in the allocated space per orderedstage environments.""" - # TODO is there a better, more generic way to do this? - offset_ChestsPerEnvironment = 0 - offset_ShrinesPerEnvironment = offset_ChestsPerEnvironment + ChestsPerEnvironment.range_end - offset_ScavengersPerEnvironment = offset_ShrinesPerEnvironment + ShrinesPerEnvironment.range_end - offset_ScannersPerEnvironment = offset_ScavengersPerEnvironment + ScavengersPerEnvironment.range_end - offset_AltarsPerEnvironment = offset_ScannersPerEnvironment + ScannersPerEnvironment.range_end - - # total space allocated to the locations in a single orderedstage environment - allocation = offset_AltarsPerEnvironment + AltarsPerEnvironment.range_end - - def get_environment_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, environment: Tuple[str, int]) -> Dict[str, int]: - """Get the locations within a specific environment""" - environment_name = environment[0] - environment_index = environment[1] - locations = {} - - # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers - # TODO perhaps a hashing algorithm could be used to compress this range and save "wasted" ids - environment_start_id = environment_index * orderedstage_location.allocation + ror2_locations_start_orderedstage - for n in range(chests): - locations.update({f"{environment_name}: Chest {n+1}": n + orderedstage_location.offset_ChestsPerEnvironment + environment_start_id}) - for n in range(shrines): - locations.update({f"{environment_name}: Shrine {n+1}": n + orderedstage_location.offset_ShrinesPerEnvironment + environment_start_id}) - for n in range(scavengers): - locations.update({f"{environment_name}: Scavenger {n+1}": n + orderedstage_location.offset_ScavengersPerEnvironment + environment_start_id}) - for n in range(scanners): - locations.update({f"{environment_name}: Radio Scanner {n+1}": n + orderedstage_location.offset_ScannersPerEnvironment + environment_start_id}) - for n in range(altars): - locations.update({f"{environment_name}: Newt Altar {n+1}": n + orderedstage_location.offset_AltarsPerEnvironment + environment_start_id}) - return locations - - def get_locations(chests:int, shrines:int, scavengers:int, scanners:int, altars:int, dlc_sotv:bool) -> Dict[str, int]: - """Get a dictionary of locations for the ordedstage environments with the locations from the parameters.""" - locations = {} - orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) - if(dlc_sotv): orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) - # for every environment, generate the respective locations - for environment_name, environment_index in orderedstages.items(): - # locations = locations | orderedstage_location.get_environment_locations( - locations.update(orderedstage_location.get_environment_locations( - chests=chests, - shrines=shrines, - scavengers=scavengers, - scanners=scanners, - altars=altars, - environment=(environment_name, environment_index) - )) - return locations - - def getall_locations(dlc_sotv:bool=True) -> Dict[str, int]: - """ - Get all locations in ordered stages. - Set dlc_sotv to true for the SOTV DLC to be included. - """ - # to get all locations, attempt using as many locations as possible - return orderedstage_location.get_locations( - chests=ChestsPerEnvironment.range_end, - shrines=ShrinesPerEnvironment.range_end, - scavengers=ScavengersPerEnvironment.range_end, - scanners=ScannersPerEnvironment.range_end, - altars=AltarsPerEnvironment.range_end, - dlc_sotv=dlc_sotv - ) - - -ror2_location_post_orderedstage = ror2_locations_start_orderedstage + highest_orderedstage*orderedstage_location.allocation -location_table.update(orderedstage_location.getall_locations()) -# use the sotv dlc in the lookup table so that all ids can be looked up regardless of use - -lookup_id_to_name: Dict[int, str] = {id: name for name, id in location_table.items()} diff --git a/worlds/ror2/RoR2Environments.py b/worlds/ror2/RoR2Environments.py deleted file mode 100644 index 2a9bf73e98..0000000000 --- a/worlds/ror2/RoR2Environments.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Dict, List, TypeVar - -# TODO probably move to Locations - -environment_vanilla_orderedstage_1_table: Dict[str, int] = { - "Distant Roost": 7, # blackbeach - "Distant Roost (2)": 8, # blackbeach2 - "Titanic Plains": 15, # golemplains - "Titanic Plains (2)": 16, # golemplains2 -} -environment_vanilla_orderedstage_2_table: Dict[str, int] = { - "Abandoned Aqueduct": 17, # goolake - "Wetland Aspect": 12, # foggyswamp -} -environment_vanilla_orderedstage_3_table: Dict[str, int] = { - "Rallypoint Delta": 13, # frozenwall - "Scorched Acres": 47, # wispgraveyard -} -environment_vanilla_orderedstage_4_table: Dict[str, int] = { - "Abyssal Depths": 10, # dampcavesimple - "Siren's Call": 37, # shipgraveyard - "Sundered Grove": 35, # rootjungle -} -environment_vanilla_orderedstage_5_table: Dict[str, int] = { - "Sky Meadow": 38, # skymeadow -} - -environment_vanilla_hidden_realm_table: Dict[str, int] = { - "Hidden Realm: Bulwark's Ambry": 5, # artifactworld - "Hidden Realm: Bazaar Between Time": 6, # bazaar - "Hidden Realm: Gilded Coast": 14, # goldshores - "Hidden Realm: A Moment, Whole": 27, # limbo - "Hidden Realm: A Moment, Fractured": 33, # mysteryspace -} - -environment_vanilla_special_table: Dict[str, int] = { - "Void Fields": 4, # arena - "Commencement": 32, # moon2 -} - -environment_sotv_orderedstage_1_table: Dict[str, int] = { - "Siphoned Forest": 39, # snowyforest -} -environment_sotv_orderedstage_2_table: Dict[str, int] = { - "Aphelian Sanctuary": 3, # ancientloft -} -environment_sotv_orderedstage_3_table: Dict[str, int] = { - "Sulfur Pools": 41, # sulfurpools -} -environment_sotv_orderedstage_4_table: Dict[str, int] = { } -environment_sotv_orderedstage_5_table: Dict[str, int] = { } - -# TODO idk much and idc much about simulacrum, is there a forced order or something? -environment_sotv_simulacrum_table: Dict[str, int] = { - "The Simulacrum (Aphelian Sanctuary)": 20, # itancientloft - "The Simulacrum (Abyssal Depths)": 21, # itdampcave - "The Simulacrum (Rallypoint Delta)": 22, # itfrozenwall - "The Simulacrum (Titanic Plains)": 23, # itgolemplains - "The Simulacrum (Abandoned Aqueduct)": 24, # itgoolake - "The Simulacrum (Commencement)": 25, # itmoon - "The Simulacrum (Sky Meadow)": 26, # itskymeadow -} - -environment_sotv_special_table: Dict[str, int] = { - "Void Locus": 46, # voidstage - "The Planetarium": 45, # voidraid -} - -X = TypeVar("X") -Y = TypeVar("Y") - - -def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: - """Combine all dictionaries in a list together into one dictionary.""" - compressed: Dict[X,Y] = {} - for individual in list_of_dict: compressed.update(individual) - return compressed - -def collapse_dict_list_vertical(list_of_dict1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: - """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" - # find the length of the longest list - length = len(list_of_dict1) - for list_of_dictN in args: - length = max(length, len(list_of_dictN)) - - # create a combined list with a length the same as the longest list - collapsed = [{}] * (length) - # The reason the list_of_dict1 is not directly used to make collapsed is - # side effects can occur if all the dictionaries are not manually unioned. - - # merge contents from list_of_dict1 - for i in range(len(list_of_dict1)): - collapsed[i] = {**collapsed[i], **list_of_dict1[i]} - - # merge contents of remaining lists_of_dicts - for list_of_dictN in args: - for i in range(len(list_of_dictN)): - collapsed[i] = {**collapsed[i], **list_of_dictN[i]} - - return collapsed - -# TODO potentially these should only be created when they are directly referenced (unsure of the space/time cost of creating these initially) - -environment_vanilla_orderedstages_table = [ environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, environment_vanilla_orderedstage_5_table ] -environment_vanilla_table = {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} - -environment_sotv_orderedstages_table = [ environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table, environment_sotv_orderedstage_4_table, environment_sotv_orderedstage_5_table ] -environment_sotv_non_simulacrum_table = {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} -environment_sotv_table = {**environment_sotv_non_simulacrum_table} - -environment_non_orderedstages_table = {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_simulacrum_table, **environment_sotv_special_table} -environment_orderedstages_table = collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) -environment_ALL_table = {**environment_vanilla_table, **environment_sotv_table} - - -def shift_by_offset(dictionary: Dict[str, int], offset:int) -> Dict[str, int]: - """Shift all indexes in a dictionary by an offset""" - return {name:index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 22c65dd9de..8735ce81fd 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,14 +1,16 @@ import string -from .Items import RiskOfRainItem, item_table, item_pool_weights, environment_offest -from .Locations import RiskOfRainLocation, get_classic_item_pickups, item_pickups, orderedstage_location -from .Rules import set_rules -from .RoR2Environments import * +from .items import RiskOfRainItem, item_table, item_pool_weights, offset, filler_table, environment_offset +from .locations import RiskOfRainLocation, item_pickups, get_locations +from .rules import set_rules +from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset -from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ItemWeights, ROR2Options +from BaseClasses import Item, ItemClassification, Tutorial +from .options import ItemWeights, ROR2Options from worlds.AutoWorld import World, WebWorld -from .Regions import create_regions +from .regions import create_explore_regions, create_classic_regions +from typing import List, Dict, Any class RiskOfWeb(WebWorld): @@ -18,7 +20,7 @@ class RiskOfWeb(WebWorld): "English", "setup_en.md", "setup/en", - ["Ijwu"] + ["Ijwu", "Kindasneaki"] )] @@ -32,38 +34,53 @@ class RiskOfRainWorld(World): options_dataclass = ROR2Options options: ROR2Options topology_present = False - - item_name_to_id = item_table + item_name_to_id = {name: data.code for name, data in item_table.items()} + item_name_groups = { + "Stages": {name for name, data in item_table.items() if data.category == "Stage"}, + "Environments": {name for name, data in item_table.items() if data.category == "Environment"}, + "Upgrades": {name for name, data in item_table.items() if data.category == "Upgrade"}, + "Fillers": {name for name, data in item_table.items() if data.category == "Filler"}, + "Traps": {name for name, data in item_table.items() if data.category == "Trap"}, + } location_name_to_id = item_pickups - data_version = 7 - required_client_version = (0, 4, 2) + data_version = 8 + required_client_version = (0, 4, 4) web = RiskOfWeb() total_revivals: int - def __init__(self, multiworld: "MultiWorld", player: int): - super().__init__(multiworld, player) - self.junk_pool: Dict[str, int] = {} - def generate_early(self) -> None: # figure out how many revivals should exist in the pool if self.options.goal == "classic": total_locations = self.options.total_locations.value else: total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * total_locations) if self.options.start_with_revive: self.total_revivals -= 1 + if self.options.victory == "voidling" and not self.options.dlc_sotv: + self.options.victory.value = self.options.victory.option_any + + def create_regions(self) -> None: + + if self.options.goal == "classic": + # classic mode + create_classic_regions(self) + else: + # explore mode + create_explore_regions(self) + + self.create_events() def create_items(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend @@ -77,25 +94,26 @@ class RiskOfRainWorld(World): # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table if self.options.dlc_sotv: - environment_available_orderedstages_table = collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sotv_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offest) + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) if self.options.dlc_sotv: - environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest) + environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} environments_to_precollect = 5 if self.options.begin_with_loop else 1 # percollect environments for each stage (or just stage 1) for i in range(environments_to_precollect): - unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) + unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) environments_pool.pop(unlock[0]) # Generate item pool - itempool: List = [] + itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] # Add revive items for the player itempool += ["Dio's Best Friend"] * self.total_revivals - itempool += ["Beads of Fealty"] for env_name, _ in environments_pool.items(): itempool += [env_name] @@ -105,38 +123,28 @@ class RiskOfRainWorld(World): total_locations = self.options.total_locations.value else: # explore mode + # Add Stage items for logic gates + itempool += ["Stage 1", "Stage 2", "Stage 3", "Stage 4"] total_locations = len( - orderedstage_location.get_locations( + get_locations( chests=self.options.chests_per_stage.value, shrines=self.options.shrines_per_stage.value, scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=self.options.dlc_sotv.value + dlc_sotv=bool(self.options.dlc_sotv.value) ) ) # Create junk items - self.junk_pool = self.create_junk_pool() + junk_pool = self.create_junk_pool() # Fill remaining items with randomly generated junk - while len(itempool) < total_locations: - itempool.append(self.get_filler_item_name()) + filler = self.random.choices(*zip(*junk_pool.items()), k=total_locations - len(itempool)) + itempool.extend(filler) # Convert itempool into real items - itempool = list(map(lambda name: self.create_item(name), itempool)) - self.multiworld.itempool += itempool + self.multiworld.itempool += map(self.create_item, itempool) - def set_rules(self) -> None: - set_rules(self.multiworld, self.player) - - def get_filler_item_name(self) -> str: - if not self.junk_pool: - self.junk_pool = self.create_junk_pool() - weights = [data for data in self.junk_pool.values()] - filler = self.multiworld.random.choices([filler for filler in self.junk_pool.keys()], weights, - k=1)[0] - return filler - - def create_junk_pool(self) -> Dict: + def create_junk_pool(self) -> Dict[str, int]: # if presets are enabled generate junk_pool from the selected preset pool_option = self.options.item_weights.value junk_pool: Dict[str, int] = {} @@ -144,7 +152,7 @@ class RiskOfRainWorld(World): # generate chaos weights if the preset is chosen if pool_option == ItemWeights.option_chaos: for name, max_value in item_pool_weights[pool_option].items(): - junk_pool[name] = self.multiworld.random.randint(0, max_value) + junk_pool[name] = self.random.randint(0, max_value) else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets @@ -159,10 +167,22 @@ class RiskOfRainWorld(World): "Boss Item": self.options.boss_item.value, "Lunar Item": self.options.lunar_item.value, "Void Item": self.options.void_item.value, - "Equipment": self.options.equipment.value + "Equipment": self.options.equipment.value, + "Money": self.options.money.value, + "Lunar Coin": self.options.lunar_coin.value, + "1000 Exp": self.options.experience.value, + "Mountain Trap": self.options.mountain_trap.value, + "Time Warp Trap": self.options.time_warp_trap.value, + "Combat Trap": self.options.combat_trap.value, + "Teleport Trap": self.options.teleport_trap.value, } - - # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled + # remove trap items from the pool (excluding lunar items) + if not self.options.enable_trap: + junk_pool.pop("Mountain Trap") + junk_pool.pop("Time Warp Trap") + junk_pool.pop("Combat Trap") + junk_pool.pop("Teleport Trap") + # remove lunar items from the pool if not (self.options.enable_lunar or pool_option == ItemWeights.option_lunartic): junk_pool.pop("Lunar Item") # remove void items from the pool @@ -171,98 +191,58 @@ class RiskOfRainWorld(World): return junk_pool - def create_regions(self) -> None: + def create_item(self, name: str) -> Item: + data = item_table[name] + return RiskOfRainItem(name, data.item_type, data.code, self.player) - if self.options.goal == "classic": - # classic mode - menu = create_region(self.multiworld, self.player, "Menu") - self.multiworld.regions.append(menu) - # By using a victory region, we can define it as being connected to by several regions - # which can then determine the availability of the victory. - victory_region = create_region(self.multiworld, self.player, "Victory") - self.multiworld.regions.append(victory_region) - petrichor = create_region(self.multiworld, self.player, "Petrichor V", - get_classic_item_pickups(self.options.total_locations.value)) - self.multiworld.regions.append(petrichor) + def set_rules(self) -> None: + set_rules(self) - # classic mode can get to victory from the beginning of the game - to_victory = Entrance(self.player, "beating game", petrichor) - petrichor.exits.append(to_victory) - to_victory.connect(victory_region) + def get_filler_item_name(self) -> str: + weights = [data.weight for data in filler_table.values()] + filler = self.multiworld.random.choices([filler for filler in filler_table.keys()], weights, + k=1)[0] + return filler - connection = Entrance(self.player, "Lobby", menu) - menu.exits.append(connection) - connection.connect(petrichor) - else: - # explore mode - create_regions(self.multiworld, self.player) - - create_events(self.multiworld, self.player) - - def fill_slot_data(self): - options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "total_locations", - "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", - "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", - "final_stage_death", "death_link", casing="camel") + def fill_slot_data(self) -> Dict[str, Any]: + options_dict = self.options.as_dict("item_pickup_step", "shrine_use_step", "goal", "victory", "total_locations", + "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", + "scanner_per_stage", "altars_per_stage", "total_revivals", + "start_with_revive", "final_stage_death", "death_link", + casing="camel") return { **options_dict, - "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), + "seed": "".join(self.random.choice(string.digits) for _ in range(16)), + "offset": offset } - def create_item(self, name: str) -> Item: - item_id = item_table[name] - classification = ItemClassification.filler - if name in {"Dio's Best Friend", "Beads of Fealty"}: - classification = ItemClassification.progression - elif name in {"Legendary Item", "Boss Item"}: - classification = ItemClassification.useful - elif name == "Lunar Item": - classification = ItemClassification.trap - - # Only check for an item to be a environment unlock if those are known to be in the pool. - # This should shave down comparisons. - - elif name in environment_ALL_table.keys(): - if name in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast,"}: - classification = ItemClassification.useful - else: - classification = ItemClassification.progression - - item = RiskOfRainItem(name, classification, item_id, self.player) - return item - - -def create_events(world: MultiWorld, player: int) -> None: - total_locations = world.worlds[player].options.total_locations.value - num_of_events = total_locations // 25 - if total_locations / 25 == num_of_events: - num_of_events -= 1 - world_region = world.get_region("Petrichor V", player) - if world.worlds[player].options.goal == "classic": - # only setup Pickups when using classic_mode - for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) - event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) - event_loc.access_rule = \ - lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player) - world_region.locations.append(event_loc) - elif world.worlds[player].options.goal == "explore": - for n in range(1, 6): - - event_region = world.get_region(f"OrderedStage_{n}", player) - event_loc = RiskOfRainLocation(player, f"Stage_{n}", None, event_region) - event_loc.place_locked_item(RiskOfRainItem(f"Stage_{n}", ItemClassification.progression, None, player)) + def create_events(self) -> None: + total_locations = self.options.total_locations.value + num_of_events = total_locations // 25 + if total_locations / 25 == num_of_events: + num_of_events -= 1 + world_region = self.multiworld.get_region("Petrichor V", self.player) + if self.options.goal == "classic": + # classic mode + # only setup Pickups when using classic_mode + for i in range(num_of_events): + event_loc = RiskOfRainLocation(self.player, f"Pickup{(i + 1) * 25}", None, world_region) + event_loc.place_locked_item( + RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, + self.player)) + event_loc.access_rule = \ + lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", self.player) + world_region.locations.append(event_loc) + else: + # explore mode + event_region = self.multiworld.get_region("OrderedStage_5", self.player) + event_loc = RiskOfRainLocation(self.player, "Stage 5", None, event_region) + event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player)) event_loc.show_in_spoiler = False event_region.locations.append(event_loc) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) - victory_region = world.get_region("Victory", player) - victory_event = RiskOfRainLocation(player, "Victory", None, victory_region) - victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) - world_region.locations.append(victory_event) - - -def create_region(world: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: - ret = Region(name, player, world) - for location_name, location_id in locations.items(): - ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) - return ret + victory_region = self.multiworld.get_region("Victory", self.player) + victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, self.player)) + victory_region.locations.append(victory_event) diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 4e59d2bf41..0fa99c071b 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -55,4 +55,15 @@ the player's YAML. You can talk to other in the multiworld chat using the RoR2 chat. All other multiworld remote commands list in the [commands guide](/tutorial/Archipelago/commands/en) work as well in the RoR2 chat. You can also optionally connect to the multiworld using the text client, which can be found in the -[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). \ No newline at end of file +[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases). + +### In-Game Commands +These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". + - `archipelago_deathlink true/false` Toggle deathlink. + - `archipelago_disconnect` Disconnect from AP. + - `archipelago_final_stage_death true/false` Toggle final stage death. + +Explore Mode only + - `archipelago_show_unlocked_stages` Show which stages have been received. + - `archipelago_highlight_satellite true/false` This will highlight the satellite to make it easier to see (Default false). \ No newline at end of file diff --git a/worlds/ror2/items.py b/worlds/ror2/items.py new file mode 100644 index 0000000000..449686d04b --- /dev/null +++ b/worlds/ror2/items.py @@ -0,0 +1,309 @@ +from BaseClasses import Item, ItemClassification +from .options import ItemWeights +from .ror2environments import environment_all_table +from typing import NamedTuple, Optional, Dict + + +class RiskOfRainItem(Item): + game: str = "Risk of Rain 2" + + +class RiskOfRainItemData(NamedTuple): + category: str + code: int + item_type: ItemClassification = ItemClassification.filler + weight: Optional[int] = None + + +offset: int = 37000 +filler_offset: int = offset + 300 +trap_offset: int = offset + 400 +stage_offset: int = offset + 500 +environment_offset: int = offset + 700 +# Upgrade item ids 37002 - 37012 +upgrade_table: Dict[str, RiskOfRainItemData] = { + "Common Item": RiskOfRainItemData("Upgrade", 2 + offset, ItemClassification.filler, 64), + "Uncommon Item": RiskOfRainItemData("Upgrade", 3 + offset, ItemClassification.filler, 32), + "Legendary Item": RiskOfRainItemData("Upgrade", 4 + offset, ItemClassification.useful, 8), + "Boss Item": RiskOfRainItemData("Upgrade", 5 + offset, ItemClassification.useful, 4), + "Equipment": RiskOfRainItemData("Upgrade", 7 + offset, ItemClassification.filler, 32), + "Item Scrap, White": RiskOfRainItemData("Upgrade", 8 + offset, ItemClassification.filler, 32), + "Item Scrap, Green": RiskOfRainItemData("Upgrade", 9 + offset, ItemClassification.filler, 16), + "Item Scrap, Red": RiskOfRainItemData("Upgrade", 10 + offset, ItemClassification.filler, 4), + "Item Scrap, Yellow": RiskOfRainItemData("Upgrade", 11 + offset, ItemClassification.filler, 1), + "Void Item": RiskOfRainItemData("Upgrade", 12 + offset, ItemClassification.filler, 16), +} +# Other item ids 37001, 37013-37014 +other_table: Dict[str, RiskOfRainItemData] = { + "Dio's Best Friend": RiskOfRainItemData("ExtraLife", 1 + offset, ItemClassification.progression_skip_balancing), + "Beads of Fealty": RiskOfRainItemData("Beads", 13 + offset, ItemClassification.progression), + "Radar Scanner": RiskOfRainItemData("Radar", 14 + offset, ItemClassification.useful), +} +# Filler item ids 37301 - 37303 +filler_table: Dict[str, RiskOfRainItemData] = { + "Money": RiskOfRainItemData("Filler", 1 + filler_offset, ItemClassification.filler, 64), + "Lunar Coin": RiskOfRainItemData("Filler", 2 + filler_offset, ItemClassification.filler, 20), + "1000 Exp": RiskOfRainItemData("Filler", 3 + filler_offset, ItemClassification.filler, 40), +} +# Trap item ids 37401 - 37404 (Lunar items used to be part of the upgrade item list, so keeping the id the same) +trap_table: Dict[str, RiskOfRainItemData] = { + "Lunar Item": RiskOfRainItemData("Trap", 6 + offset, ItemClassification.trap, 16), + "Mountain Trap": RiskOfRainItemData("Trap", 1 + trap_offset, ItemClassification.trap, 5), + "Time Warp Trap": RiskOfRainItemData("Trap", 2 + trap_offset, ItemClassification.trap, 20), + "Combat Trap": RiskOfRainItemData("Trap", 3 + trap_offset, ItemClassification.trap, 20), + "Teleport Trap": RiskOfRainItemData("Trap", 4 + trap_offset, ItemClassification.trap, 10), +} +# Stage item ids 37501 - 37504 +stage_table: Dict[str, RiskOfRainItemData] = { + "Stage 1": RiskOfRainItemData("Stage", 1 + stage_offset, ItemClassification.progression), + "Stage 2": RiskOfRainItemData("Stage", 2 + stage_offset, ItemClassification.progression), + "Stage 3": RiskOfRainItemData("Stage", 3 + stage_offset, ItemClassification.progression), + "Stage 4": RiskOfRainItemData("Stage", 4 + stage_offset, ItemClassification.progression), + +} + +item_table = {**upgrade_table, **other_table, **filler_table, **trap_table, **stage_table} +# Environment item ids 37700 - 37746 +################################################## +# environments + + +# add ALL environments into the item table +def create_environment_table(name: str, environment_id: int, environment_classification: ItemClassification) \ + -> Dict[str, RiskOfRainItemData]: + return {name: RiskOfRainItemData("Environment", environment_offset + environment_id, environment_classification)} + + +environment_table: Dict[str, RiskOfRainItemData] = {} +# use the sotv dlc in the item table so that all names can be looked up regardless of use +for data, key in environment_all_table.items(): + classification = ItemClassification.progression + if data in {"Hidden Realm: Bulwark's Ambry", "Hidden Realm: Gilded Coast"}: + classification = ItemClassification.useful + environment_table.update(create_environment_table(data, key, classification)) + +item_table.update(environment_table) + +# end of environments +################################################## + +default_weights: Dict[str, int] = { + "Item Scrap, Green": 16, + "Item Scrap, Red": 4, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 32, + "Common Item": 64, + "Uncommon Item": 32, + "Legendary Item": 8, + "Boss Item": 4, + "Void Item": 16, + "Equipment": 32, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +new_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 75, + "Uncommon Item": 40, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +uncommon_weights: Dict[str, int] = { + "Item Scrap, Green": 45, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 45, + "Uncommon Item": 100, + "Legendary Item": 10, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +legendary_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 50, + "Uncommon Item": 25, + "Legendary Item": 100, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 20, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +chaos_weights: Dict[str, int] = { + "Item Scrap, Green": 80, + "Item Scrap, Red": 45, + "Item Scrap, Yellow": 30, + "Item Scrap, White": 100, + "Common Item": 100, + "Uncommon Item": 70, + "Legendary Item": 30, + "Boss Item": 20, + "Void Item": 60, + "Equipment": 40, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +no_scraps_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 100, + "Uncommon Item": 40, + "Legendary Item": 15, + "Boss Item": 5, + "Void Item": 16, + "Equipment": 25, + "Money": 64, + "Lunar Coin": 20, + "1000 Exp": 40, + "Lunar Item": 10, + "Mountain Trap": 4, + "Time Warp Trap": 20, + "Combat Trap": 20, + "Teleport Trap": 20 +} + +even_weights: Dict[str, int] = { + "Item Scrap, Green": 1, + "Item Scrap, Red": 1, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 1, + "Common Item": 1, + "Uncommon Item": 1, + "Legendary Item": 1, + "Boss Item": 1, + "Void Item": 1, + "Equipment": 1, + "Money": 1, + "Lunar Coin": 1, + "1000 Exp": 1, + "Lunar Item": 1, + "Mountain Trap": 1, + "Time Warp Trap": 1, + "Combat Trap": 1, + "Teleport Trap": 1 +} + +scraps_only: Dict[str, int] = { + "Item Scrap, Green": 70, + "Item Scrap, White": 100, + "Item Scrap, Red": 30, + "Item Scrap, Yellow": 5, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +lunartic_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 0, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 100, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} +void_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Void Item": 100, + "Equipment": 0, + "Money": 20, + "Lunar Coin": 10, + "1000 Exp": 10, + "Lunar Item": 0, + "Mountain Trap": 5, + "Time Warp Trap": 10, + "Combat Trap": 10, + "Teleport Trap": 10 +} + +item_pool_weights: Dict[int, Dict[str, int]] = { + ItemWeights.option_default: default_weights, + ItemWeights.option_new: new_weights, + ItemWeights.option_uncommon: uncommon_weights, + ItemWeights.option_legendary: legendary_weights, + ItemWeights.option_chaos: chaos_weights, + ItemWeights.option_no_scraps: no_scraps_weights, + ItemWeights.option_even: even_weights, + ItemWeights.option_scraps_only: scraps_only, + ItemWeights.option_lunartic: lunartic_weights, + ItemWeights.option_void: void_weights, +} diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py new file mode 100644 index 0000000000..13077b3e14 --- /dev/null +++ b/worlds/ror2/locations.py @@ -0,0 +1,89 @@ +from typing import Dict +from BaseClasses import Location +from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \ + ScannersPerEnvironment, AltarsPerEnvironment +from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \ + environment_sotv_orderedstages_table + + +class RiskOfRainLocation(Location): + game: str = "Risk of Rain 2" + + +ror2_locations_start_id = 38000 + + +def get_classic_item_pickups(n: int) -> Dict[str, int]: + """Get n ItemPickups, capped at the max value for TotalLocations""" + n = max(n, 0) + n = min(n, TotalLocations.range_end) + return {f"ItemPickup{i + 1}": ror2_locations_start_id + i for i in range(n)} + + +item_pickups = get_classic_item_pickups(TotalLocations.range_end) +location_table = item_pickups + +# this is so we can easily calculate the environment and location "offset" ids +ror2_locations_start_ordered_stage = ror2_locations_start_id + TotalLocations.range_end + +# TODO is there a better, more generic way to do this? +offset_chests = 0 +offset_shrines = offset_chests + ChestsPerEnvironment.range_end +offset_scavengers = offset_shrines + ShrinesPerEnvironment.range_end +offset_scanners = offset_scavengers + ScavengersPerEnvironment.range_end +offset_altars = offset_scanners + ScannersPerEnvironment.range_end + +# total space allocated to the locations in a single orderedstage environment +allocation = offset_altars + AltarsPerEnvironment.range_end + + +def get_environment_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, + environment_name: str, environment_index: int) -> Dict[str, int]: + """Get the locations within a specific environment""" + locations = {} + + # due to this mapping, since environment ids are not consecutive, there are lots of "wasted" id numbers + environment_start_id = environment_index * allocation + ror2_locations_start_ordered_stage + for n in range(chests): + locations.update({f"{environment_name}: Chest {n + 1}": n + offset_chests + environment_start_id}) + for n in range(shrines): + locations.update({f"{environment_name}: Shrine {n + 1}": n + offset_shrines + environment_start_id}) + for n in range(scavengers): + locations.update({f"{environment_name}: Scavenger {n + 1}": n + offset_scavengers + environment_start_id}) + for n in range(scanners): + locations.update({f"{environment_name}: Radio Scanner {n + 1}": n + offset_scanners + environment_start_id}) + for n in range(altars): + locations.update({f"{environment_name}: Newt Altar {n + 1}": n + offset_altars + environment_start_id}) + return locations + + +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ + -> Dict[str, int]: + """Get a dictionary of locations for the orderedstage environments with the locations from the parameters.""" + locations = {} + orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) + if dlc_sotv: + orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + # for every environment, generate the respective locations + for environment_name, environment_index in orderedstages.items(): + locations.update(get_environment_locations( + chests=chests, + shrines=shrines, + scavengers=scavengers, + scanners=scanners, + altars=altars, + environment_name=environment_name, + environment_index=environment_index), + ) + return locations + + +# Get all locations in ordered stages. +location_table.update(get_locations( + chests=ChestsPerEnvironment.range_end, + shrines=ShrinesPerEnvironment.range_end, + scavengers=ScavengersPerEnvironment.range_end, + scanners=ScannersPerEnvironment.range_end, + altars=AltarsPerEnvironment.range_end, + dlc_sotv=True, +)) diff --git a/worlds/ror2/Options.py b/worlds/ror2/options.py similarity index 73% rename from worlds/ror2/Options.py rename to worlds/ror2/options.py index 0ed0a87b17..7daf8a8446 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/options.py @@ -4,7 +4,7 @@ from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCo # NOTE be aware that since the range of item ids that RoR2 uses is based off of the maximums of checks # Be careful when changing the range_end values not to go into another game's IDs -# NOTE that these changes to range_end must also be reflected in the RoR2 client so it understands the same ids. +# NOTE that these changes to range_end must also be reflected in the RoR2 client, so it understands the same ids. class Goal(Choice): """ @@ -19,6 +19,21 @@ class Goal(Choice): default = 1 +class Victory(Choice): + """ + Mithrix: Defeat Mithrix in Commencement + Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Any: Any victory in the game will count. See Final Stage Death for additional ways. + """ + display_name = "Victory Condition" + option_any = 0 + option_mithrix = 1 + option_voidling = 2 + option_limbo = 3 + default = 0 + + class TotalLocations(Range): """Classic Mode: Number of location checks which are added to the Risk of Rain playthrough.""" display_name = "Total Locations" @@ -100,6 +115,11 @@ class ShrineUseStep(Range): default = 0 +class AllowTrapItems(Toggle): + """Allows Trap items in the item pool.""" + display_name = "Enable Trap Items" + + class AllowLunarItems(DefaultOnToggle): """Allows Lunar items in the item pool.""" display_name = "Enable Lunar Item Shuffling" @@ -111,10 +131,14 @@ class StartWithRevive(DefaultOnToggle): class FinalStageDeath(Toggle): - """The following will count as a win if set to true: + """The following will count as a win if set to "true", and victory is set to "any": Dying in Commencement. Dying in The Planetarium. - Obliterating yourself""" + Obliterating yourself + If not use the following to tell if final stage death will count: + Victory: mithrix - only dying in Commencement will count. + Victory: voidling - only dying in The Planetarium will count. + Victory: limbo - Obliterating yourself will count.""" display_name = "Final Stage Death is Win" @@ -247,6 +271,76 @@ class Equipment(Range): default = 32 +class Money(Range): + """Weight of money items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Money" + range_start = 0 + range_end = 100 + default = 64 + + +class LunarCoin(Range): + """Weight of lunar coin items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Lunar Coins" + range_start = 0 + range_end = 100 + default = 20 + + +class Experience(Range): + """Weight of 1000 exp items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "1000 Exp" + range_start = 0 + range_end = 100 + default = 40 + + +class MountainTrap(Range): + """Weight of mountain trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Mountain Trap" + range_start = 0 + range_end = 100 + default = 5 + + +class TimeWarpTrap(Range): + """Weight of time warp trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Time Warp Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class CombatTrap(Range): + """Weight of combat trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Combat Trap" + range_start = 0 + range_end = 100 + default = 20 + + +class TeleportTrap(Range): + """Weight of teleport trap items in the item pool. + + (Ignored unless Item Weight Presets is 'No')""" + display_name = "Teleport Trap" + range_start = 0 + range_end = 100 + default = 20 + + class ItemPoolPresetToggle(Toggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" display_name = "Use Item Weight Presets" @@ -258,28 +352,30 @@ class ItemWeights(Choice): - New is a test for a potential adjustment to the default weights. - Uncommon puts a large number of uncommon items in the pool. - Legendary puts a large number of legendary items in the pool. - - Lunartic makes everything a lunar item. - - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy. + - Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being + too easy. - No Scraps removes all scrap items from the item pool. - Even generates the item pool with every item having an even weight. - Scraps Only will be only scrap items in the item pool. + - Lunartic makes everything a lunar item. - Void makes everything a void item.""" display_name = "Item Weights" option_default = 0 option_new = 1 option_uncommon = 2 option_legendary = 3 - option_lunartic = 4 - option_chaos = 5 - option_no_scraps = 6 - option_even = 7 - option_scraps_only = 8 + option_chaos = 4 + option_no_scraps = 5 + option_even = 6 + option_scraps_only = 7 + option_lunartic = 8 option_void = 9 @dataclass class ROR2Options(PerGameCommonOptions): goal: Goal + victory: Victory total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment shrines_per_stage: ShrinesPerEnvironment @@ -294,6 +390,7 @@ class ROR2Options(PerGameCommonOptions): death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep + enable_trap: AllowTrapItems enable_lunar: AllowLunarItems item_weights: ItemWeights item_pool_presets: ItemPoolPresetToggle @@ -309,3 +406,10 @@ class ROR2Options(PerGameCommonOptions): lunar_item: LunarItem void_item: VoidItem equipment: Equipment + money: Money + lunar_coin: LunarCoin + experience: Experience + mountain_trap: MountainTrap + time_warp_trap: TimeWarpTrap + combat_trap: CombatTrap + teleport_trap: TeleportTrap diff --git a/worlds/ror2/Regions.py b/worlds/ror2/regions.py similarity index 59% rename from worlds/ror2/Regions.py rename to worlds/ror2/regions.py index 94f5aaf71e..13b229da92 100644 --- a/worlds/ror2/Regions.py +++ b/worlds/ror2/regions.py @@ -1,7 +1,10 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import MultiWorld, Region, Entrance -from .Locations import location_table, RiskOfRainLocation +from BaseClasses import Region, Entrance, MultiWorld +from .locations import location_table, RiskOfRainLocation, get_classic_item_pickups + +if TYPE_CHECKING: + from . import RiskOfRainWorld class RoRRegionData(NamedTuple): @@ -9,10 +12,14 @@ class RoRRegionData(NamedTuple): region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld # Default Locations non_dlc_regions: Dict[str, RoRRegionData] = { - "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", "Titanic Plains", "Titanic Plains (2)"]), + "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", + "Titanic Plains", "Titanic Plains (2)"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), @@ -34,33 +41,36 @@ def create_regions(multiworld: MultiWorld, player: int): } other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), - "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", "Commencement"]), + "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", + "Commencement"]), "OrderedStage_1": RoRRegionData(None, ["Hidden Realm: Bazaar Between Time", - "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", "Wetland Aspect"]), + "Hidden Realm: Gilded Coast", "Abandoned Aqueduct", + "Wetland Aspect"]), "OrderedStage_2": RoRRegionData(None, ["Rallypoint Delta", "Scorched Acres"]), - "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", "Sundered Grove"]), + "OrderedStage_3": RoRRegionData(None, ["Abyssal Depths", "Siren's Call", + "Sundered Grove"]), "OrderedStage_4": RoRRegionData(None, ["Sky Meadow"]), "Hidden Realm: A Moment, Fractured": RoRRegionData(None, ["Hidden Realm: A Moment, Whole"]), - "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory"]), + "Hidden Realm: A Moment, Whole": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Fields": RoRRegionData(None, []), "Victory": RoRRegionData(None, None), - "Petrichor V": RoRRegionData(None, ["Victory"]), + "Petrichor V": RoRRegionData(None, []), "Hidden Realm: Bulwark's Ambry": RoRRegionData(None, None), "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } dlc_other_regions: Dict[str, RoRRegionData] = { - "The Planetarium": RoRRegionData(None, ["Victory"]), + "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } # Totals of each item - chests = int(multiworld.chests_per_stage[player]) - shrines = int(multiworld.shrines_per_stage[player]) - scavengers = int(multiworld.scavengers_per_stage[player]) - scanners = int(multiworld.scanner_per_stage[player]) - newt = int(multiworld.altars_per_stage[player]) + chests = int(ror2_options.chests_per_stage) + shrines = int(ror2_options.shrines_per_stage) + scavengers = int(ror2_options.scavengers_per_stage) + scanners = int(ror2_options.scanner_per_stage) + newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: all_location_regions = {**non_dlc_regions, **dlc_regions} # Locations @@ -88,23 +98,35 @@ def create_regions(multiworld: MultiWorld, player: int): regions_pool: Dict = {**all_location_regions, **other_regions} # DLC Locations - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: non_dlc_regions["Menu"].region_exits.append("Siphoned Forest") other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary") other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools") other_regions["Void Fields"].region_exits.append("Void Locus") + other_regions["Commencement"].region_exits.append("The Planetarium") regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions} + # Check to see if Victory needs to be removed from regions + if ror2_options.victory == "mithrix": + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + elif ror2_options.victory == "voidling": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + elif ror2_options.victory == "limbo": + other_regions["Commencement"].region_exits.pop(0) + dlc_other_regions["The Planetarium"].region_exits.pop(0) + # Create all the regions for name, data in regions_pool.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) + multiworld.regions.append(create_explore_region(multiworld, player, name, data)) # Connect all the regions to their exits for name, data in regions_pool.items(): create_connections_in_regions(multiworld, player, name, data) -def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_explore_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> Region: region = Region(name, player, multiworld) if data.locations: for location_name in data.locations: @@ -115,7 +137,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, data: RoRRegio return region -def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData): +def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str, data: RoRRegionData) -> None: region = multiworld.get_region(name, player) if data.region_exits: for region_exit in data.region_exits: @@ -123,3 +145,34 @@ def create_connections_in_regions(multiworld: MultiWorld, player: int, name: str exit_region = multiworld.get_region(region_exit, player) r_exit_stage.connect(exit_region) region.exits.append(r_exit_stage) + + +def create_classic_regions(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + ror2_options = ror2_world.options + multiworld = ror2_world.multiworld + menu = create_classic_region(multiworld, player, "Menu") + multiworld.regions.append(menu) + # By using a victory region, we can define it as being connected to by several regions + # which can then determine the availability of the victory. + victory_region = create_classic_region(multiworld, player, "Victory") + multiworld.regions.append(victory_region) + petrichor = create_classic_region(multiworld, player, "Petrichor V", + get_classic_item_pickups(ror2_options.total_locations.value)) + multiworld.regions.append(petrichor) + + # classic mode can get to victory from the beginning of the game + to_victory = Entrance(player, "beating game", petrichor) + petrichor.exits.append(to_victory) + to_victory.connect(victory_region) + + connection = Entrance(player, "Lobby", menu) + menu.exits.append(connection) + connection.connect(petrichor) + + +def create_classic_region(multiworld: MultiWorld, player: int, name: str, locations: Dict[str, int] = {}) -> Region: + ret = Region(name, player, multiworld) + for location_name, location_id in locations.items(): + ret.locations.append(RiskOfRainLocation(player, location_name, location_id, ret)) + return ret diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py new file mode 100644 index 0000000000..d821763ef4 --- /dev/null +++ b/worlds/ror2/ror2environments.py @@ -0,0 +1,118 @@ +from typing import Dict, List, TypeVar + +# TODO probably move to Locations + +environment_vanilla_orderedstage_1_table: Dict[str, int] = { + "Distant Roost": 7, # blackbeach + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains": 15, # golemplains + "Titanic Plains (2)": 16, # golemplains2 +} +environment_vanilla_orderedstage_2_table: Dict[str, int] = { + "Abandoned Aqueduct": 17, # goolake + "Wetland Aspect": 12, # foggyswamp +} +environment_vanilla_orderedstage_3_table: Dict[str, int] = { + "Rallypoint Delta": 13, # frozenwall + "Scorched Acres": 47, # wispgraveyard +} +environment_vanilla_orderedstage_4_table: Dict[str, int] = { + "Abyssal Depths": 10, # dampcavesimple + "Siren's Call": 37, # shipgraveyard + "Sundered Grove": 35, # rootjungle +} +environment_vanilla_orderedstage_5_table: Dict[str, int] = { + "Sky Meadow": 38, # skymeadow +} + +environment_vanilla_hidden_realm_table: Dict[str, int] = { + "Hidden Realm: Bulwark's Ambry": 5, # artifactworld + "Hidden Realm: Bazaar Between Time": 6, # bazaar + "Hidden Realm: Gilded Coast": 14, # goldshores + "Hidden Realm: A Moment, Whole": 27, # limbo + "Hidden Realm: A Moment, Fractured": 33, # mysteryspace +} + +environment_vanilla_special_table: Dict[str, int] = { + "Void Fields": 4, # arena + "Commencement": 32, # moon2 +} + +environment_sotv_orderedstage_1_table: Dict[str, int] = { + "Siphoned Forest": 39, # snowyforest +} +environment_sotv_orderedstage_2_table: Dict[str, int] = { + "Aphelian Sanctuary": 3, # ancientloft +} +environment_sotv_orderedstage_3_table: Dict[str, int] = { + "Sulfur Pools": 41, # sulfurpools +} + +environment_sotv_special_table: Dict[str, int] = { + "Void Locus": 46, # voidstage + "The Planetarium": 45, # voidraid +} + +X = TypeVar("X") +Y = TypeVar("Y") + + +def compress_dict_list_horizontal(list_of_dict: List[Dict[X, Y]]) -> Dict[X, Y]: + """Combine all dictionaries in a list together into one dictionary.""" + compressed: Dict[X, Y] = {} + for individual in list_of_dict: + compressed.update(individual) + return compressed + + +def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Dict[X, Y]]) -> List[Dict[X, Y]]: + """Combine all parallel dictionaries in lists together to make a new list of dictionaries of the same length.""" + # find the length of the longest list + length = len(list_of_dict_1) + for list_of_dict_n in args: + length = max(length, len(list_of_dict_n)) + + # create a combined list with a length the same as the longest list + collapsed: List[Dict[X, Y]] = [{}] * length + # The reason the list_of_dict_1 is not directly used to make collapsed is + # side effects can occur if all the dictionaries are not manually unioned. + + # merge contents from list_of_dict_1 + for i in range(len(list_of_dict_1)): + collapsed[i] = {**collapsed[i], **list_of_dict_1[i]} + + # merge contents of remaining lists_of_dicts + for list_of_dict_n in args: + for i in range(len(list_of_dict_n)): + collapsed[i] = {**collapsed[i], **list_of_dict_n[i]} + + return collapsed + + +# TODO potentially these should only be created when they are directly referenced +# (unsure of the space/time cost of creating these initially) + +environment_vanilla_orderedstages_table = \ + [environment_vanilla_orderedstage_1_table, environment_vanilla_orderedstage_2_table, + environment_vanilla_orderedstage_3_table, environment_vanilla_orderedstage_4_table, + environment_vanilla_orderedstage_5_table] +environment_vanilla_table = \ + {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), + **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} + +environment_sotv_orderedstages_table = \ + [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, + environment_sotv_orderedstage_3_table] +environment_sotv_table = \ + {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} + +environment_non_orderedstages_table = \ + {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table} +environment_orderedstages_table = \ + collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) +environment_all_table = {**environment_vanilla_table, **environment_sotv_table} + + +def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: + """Shift all indexes in a dictionary by an offset""" + return {name: index+offset for name, index in dictionary.items()} diff --git a/worlds/ror2/Rules.py b/worlds/ror2/rules.py similarity index 60% rename from worlds/ror2/Rules.py rename to worlds/ror2/rules.py index 65c04d06cb..442e6c0002 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/rules.py @@ -1,62 +1,71 @@ -from BaseClasses import MultiWorld, CollectionState from worlds.generic.Rules import set_rule, add_rule -from .Locations import orderedstage_location -from .RoR2Environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ - environment_orderedstages_table +from BaseClasses import MultiWorld +from .locations import get_locations +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from typing import Set, TYPE_CHECKING + +if TYPE_CHECKING: + from . import RiskOfRainWorld # Rule to see if it has access to the previous stage -def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int): +def has_entrance_access_rule(multiworld: MultiWorld, stage: str, entrance: str, player: int) -> None: multiworld.get_entrance(entrance, player).access_rule = \ lambda state: state.has(entrance, player) and state.has(stage, player) +def has_all_items(multiworld: MultiWorld, items: Set[str], entrance: str, player: int) -> None: + multiworld.get_entrance(entrance, player).access_rule = \ + lambda state: state.has_all(items, player) and state.has(entrance, player) + + # Checks to see if chest/shrine are accessible -def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str): +def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ + -> None: if item_number == 1: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: state.has(environment, player) + # scavengers need to be locked till after a full loop since that is when they are capable of spawning. + # (While technically the requirement is just beating 5 stages, this will ensure that the player will have + # a long enough run to have enough director credits for scavengers and + # help prevent being stuck in the same stages until that point). if item_type == "Scavenger": multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage_4", player) + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ lambda state: check_location(state, environment, player, item_number, item_type) -def check_location(state, environment: str, player: int, item_number: int, item_name: str): +def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) # unlock event to next set of stages -def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int): - if not multiworld.dlc_sotv[player]: - environment_name = multiworld.random.choices(list(environment_vanilla_orderedstages_table[stage_number].keys()), - k=1) - else: - environment_name = multiworld.random.choices(list(environment_orderedstages_table[stage_number].keys()), k=1) - multiworld.get_location(f"Stage_{stage_number + 1}", player).access_rule = \ - lambda state: get_one_of_the_stages(state, environment_name[0], player) +def get_stage_event(multiworld: MultiWorld, player: int, stage_number: int) -> None: + if stage_number == 4: + return + multiworld.get_entrance(f"OrderedStage_{stage_number + 1}", player).access_rule = \ + lambda state: state.has(f"Stage {stage_number + 1}", player) -def get_one_of_the_stages(state: CollectionState, stage: str, player: int): - return state.has(stage, player) - - -def set_rules(multiworld: MultiWorld, player: int) -> None: - if multiworld.goal[player] == "classic": +def set_rules(ror2_world: "RiskOfRainWorld") -> None: + player = ror2_world.player + multiworld = ror2_world.multiworld + ror2_options = ror2_world.options + if ror2_options.goal == "classic": # classic mode - total_locations = multiworld.total_locations[player].value # total locations for current player + total_locations = ror2_options.total_locations.value # total locations for current player else: # explore mode total_locations = len( - orderedstage_location.get_locations( - chests=multiworld.chests_per_stage[player].value, - shrines=multiworld.shrines_per_stage[player].value, - scavengers=multiworld.scavengers_per_stage[player].value, - scanners=multiworld.scanner_per_stage[player].value, - altars=multiworld.altars_per_stage[player].value, - dlc_sotv=multiworld.dlc_sotv[player].value + get_locations( + chests=ror2_options.chests_per_stage.value, + shrines=ror2_options.shrines_per_stage.value, + scavengers=ror2_options.scavengers_per_stage.value, + scanners=ror2_options.scanner_per_stage.value, + altars=ror2_options.altars_per_stage.value, + dlc_sotv=bool(ror2_options.dlc_sotv.value) ) ) @@ -64,14 +73,15 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: divisions = total_locations // event_location_step total_revivals = multiworld.worlds[player].total_revivals # pulling this info we calculated in generate_basic - if multiworld.goal[player] == "classic": + if ror2_options.goal == "classic": # classic mode if divisions: for i in range(1, divisions + 1): # since divisions is the floor of total_locations / 25 if i * event_location_step != total_locations: event_loc = multiworld.get_location(f"Pickup{i * event_location_step}", player) set_rule(event_loc, - lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) + lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", + "Location", player)) # we want to create a rule for each of the 25 locations per division for n in range(i * event_location_step, (i + 1) * event_location_step + 1): if n > total_locations: @@ -84,27 +94,18 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player)) set_rule(multiworld.get_location("Victory", player), lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player)) - if total_revivals or multiworld.start_with_revive[player].value: + if total_revivals or ror2_options.start_with_revive.value: add_rule(multiworld.get_location("Victory", player), lambda state: state.has("Dio's Best Friend", player, - total_revivals + multiworld.start_with_revive[player])) + total_revivals + ror2_options.start_with_revive)) - elif multiworld.goal[player] == "explore": - # When explore_mode is used, - # scavengers need to be locked till after a full loop since that is when they are capable of spawning. - # (While technically the requirement is just beating 5 stages, this will ensure that the player will have - # a long enough run to have enough director credits for scavengers and - # help prevent being stuck in the same stages until that point.) - - for location in multiworld.get_locations(player): - if "Scavenger" in location.name: - add_rule(location, lambda state: state.has("Stage_5", player)) - # Regions - chests = multiworld.chests_per_stage[player] - shrines = multiworld.shrines_per_stage[player] - newts = multiworld.altars_per_stage[player] - scavengers = multiworld.scavengers_per_stage[player] - scanners = multiworld.scanner_per_stage[player] + else: + # explore mode + chests = ror2_options.chests_per_stage.value + shrines = ror2_options.shrines_per_stage.value + newts = ror2_options.altars_per_stage.value + scavengers = ror2_options.scavengers_per_stage.value + scanners = ror2_options.scanner_per_stage.value for i in range(len(environment_vanilla_orderedstages_table)): for environment_name, _ in environment_vanilla_orderedstages_table[i].items(): # Make sure to go through each location @@ -120,10 +121,10 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) get_stage_event(multiworld, player, i) - if multiworld.dlc_sotv[player]: + if ror2_options.dlc_sotv: for i in range(len(environment_sotv_orderedstages_table)): for environment_name, _ in environment_sotv_orderedstages_table[i].items(): # Make sure to go through each location @@ -139,16 +140,19 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: for newt in range(1, newts + 1): has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") if i > 0: - has_entrance_access_rule(multiworld, f"Stage_{i}", environment_name, player) - has_entrance_access_rule(multiworld, f"Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", + has_entrance_access_rule(multiworld, f"Stage {i}", environment_name, player) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) - has_entrance_access_rule(multiworld, f"Stage_1", "Hidden Realm: Bazaar Between Time", player) - has_entrance_access_rule(multiworld, f"Hidden Realm: Bazaar Between Time", "Void Fields", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Commencement", player) - has_entrance_access_rule(multiworld, f"Stage_5", "Hidden Realm: A Moment, Fractured", player) + has_entrance_access_rule(multiworld, "Stage 1", "Hidden Realm: Bazaar Between Time", player) + has_entrance_access_rule(multiworld, "Hidden Realm: Bazaar Between Time", "Void Fields", player) + has_entrance_access_rule(multiworld, "Stage 5", "Commencement", player) + has_entrance_access_rule(multiworld, "Stage 5", "Hidden Realm: A Moment, Fractured", player) has_entrance_access_rule(multiworld, "Beads of Fealty", "Hidden Realm: A Moment, Whole", player) - if multiworld.dlc_sotv[player]: - has_entrance_access_rule(multiworld, f"Stage_5", "Void Locus", player) - has_entrance_access_rule(multiworld, f"Void Locus", "The Planetarium", player) + if ror2_options.dlc_sotv: + has_entrance_access_rule(multiworld, "Stage 5", "The Planetarium", player) + has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player) + if ror2_options.victory == "voidling": + has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player) + # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/__init__.py b/worlds/ror2/test/__init__.py new file mode 100644 index 0000000000..87d8183ab8 --- /dev/null +++ b/worlds/ror2/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class RoR2TestBase(WorldTestBase): + game = "Risk of Rain 2" diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py new file mode 100644 index 0000000000..18d4994419 --- /dev/null +++ b/worlds/ror2/test/test_any_goal.py @@ -0,0 +1,26 @@ +from . import RoR2TestBase + + +class DLCTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "any" + } + + def test_commencement_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertBeatable(True) + + def test_planetarium_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertBeatable(True) + + def test_moment_whole_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_classic.py b/worlds/ror2/test/test_classic.py new file mode 100644 index 0000000000..90ed2302b2 --- /dev/null +++ b/worlds/ror2/test/test_classic.py @@ -0,0 +1,7 @@ +from . import RoR2TestBase + + +class ClassicTest(RoR2TestBase): + options = { + "goal": "classic", + } diff --git a/worlds/ror2/test/test_limbo_goal.py b/worlds/ror2/test/test_limbo_goal.py new file mode 100644 index 0000000000..f8757a9176 --- /dev/null +++ b/worlds/ror2/test/test_limbo_goal.py @@ -0,0 +1,15 @@ +from . import RoR2TestBase + + +class LimboGoalTest(RoR2TestBase): + options = { + "victory": "limbo" + } + + def test_limbo(self) -> None: + self.collect_all_but(["Hidden Realm: A Moment, Whole", "Victory"]) + self.assertFalse(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(False) + self.collect_by_name("Hidden Realm: A Moment, Whole") + self.assertTrue(self.can_reach_entrance("Hidden Realm: A Moment, Whole")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_mithrix_goal.py b/worlds/ror2/test/test_mithrix_goal.py new file mode 100644 index 0000000000..7ed9a2cd73 --- /dev/null +++ b/worlds/ror2/test/test_mithrix_goal.py @@ -0,0 +1,25 @@ +from . import RoR2TestBase + + +class MithrixGoalTest(RoR2TestBase): + options = { + "victory": "mithrix" + } + + def test_mithrix(self) -> None: + self.collect_all_but(["Commencement", "Victory"]) + self.assertFalse(self.can_reach_entrance("Commencement")) + self.assertBeatable(False) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_entrance("Commencement")) + self.assertBeatable(True) + + def test_stage5(self) -> None: + self.collect_all_but(["Stage 4", "Sky Meadow", "Victory"]) + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(False) + self.collect_by_name("Sky Meadow") + self.assertFalse(self.can_reach_entrance("Sky Meadow")) + self.collect_by_name("Stage 4") + self.assertTrue(self.can_reach_entrance("Sky Meadow")) + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_voidling_goal.py b/worlds/ror2/test/test_voidling_goal.py new file mode 100644 index 0000000000..a7520a5c5f --- /dev/null +++ b/worlds/ror2/test/test_voidling_goal.py @@ -0,0 +1,28 @@ +from . import RoR2TestBase + + +class VoidlingGoalTest(RoR2TestBase): + options = { + "dlc_sotv": "true", + "victory": "voidling" + } + + def test_planetarium(self) -> None: + self.collect_all_but(["The Planetarium", "Victory"]) + self.assertFalse(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(False) + self.collect_by_name("The Planetarium") + self.assertTrue(self.can_reach_entrance("The Planetarium")) + self.assertBeatable(True) + + def test_void_locus_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Void Locus") + self.assertTrue(self.can_reach_entrance("Victory")) + + def test_commencement_to_victory(self) -> None: + self.collect_all_but(["Void Locus", "Commencement"]) + self.assertFalse(self.can_reach_location("Victory")) + self.collect_by_name("Commencement") + self.assertTrue(self.can_reach_location("Victory")) From 4a9d075b776501ea94b6805f04a822eac70e4d3f Mon Sep 17 00:00:00 2001 From: digiholic Date: Wed, 22 Nov 2023 08:45:32 -0700 Subject: [PATCH 189/327] MMBN3: Adds instructions for using the Legacy Collection ROM for setup (#2120) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> --- worlds/mmbn3/docs/setup_en.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/mmbn3/docs/setup_en.md b/worlds/mmbn3/docs/setup_en.md index 309c07f5cf..b5ff1625c8 100644 --- a/worlds/mmbn3/docs/setup_en.md +++ b/worlds/mmbn3/docs/setup_en.md @@ -12,7 +12,8 @@ As we are using Bizhawk, this guide is only applicable to Windows and Linux syst - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) (select `MegaMan Battle Network 3 Client` during installation). -- A US MegaMan Battle Network 3 Blue Rom +- A US MegaMan Battle Network 3 Blue Rom. If you have the [MegaMan Battle Network Legacy Collection Vol. 1](https://store.steampowered.com/app/1798010/Mega_Man_Battle_Network_Legacy_Collection_Vol_1/) +on Steam, you can obtain a copy of this ROM from the game's files, see instructions below. ## Configuring Bizhawk @@ -35,6 +36,14 @@ To do so, we simply have to search any GBA rom we happened to own, right click a the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder and select EmuHawk.exe. +## Extracting a ROM from the Legacy Collection + +The Steam version of the Legacy Collection contains unmodified GBA ROMs in its files. You can extract these for use with Archipelago. + +1. Open the Legacy Collection Vol. 1's Game Files (Right click on the game in your Library, then open Properties -> Installed Files -> Browse) +2. Open the file `exe/data/exe3b.dat` in a zip-extracting program such as 7-Zip or WinRAR. +3. Extract the file `rom_b_e.srl` somewhere and rename it to `Mega Man Battle Network 3 - Blue Version (USA).gba` + ## Configuring your YAML file ### What is a YAML file and why do I need one? @@ -76,4 +85,4 @@ Don't forget to start manipulating RNG early by shouting during generation: JACK IN! [Your name]! EXECUTE! -``` \ No newline at end of file +``` From cfd2e9c47f3f3bd06defbf05c6fd2d42d9e697dd Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 22 Nov 2023 10:04:10 -0600 Subject: [PATCH 190/327] Core: Increment Archipelago Version (#2483) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index bb68602cce..5955e92432 100644 --- a/Utils.py +++ b/Utils.py @@ -47,7 +47,7 @@ class Version(typing.NamedTuple): return ".".join(str(item) for item in self) -__version__ = "0.4.3" +__version__ = "0.4.4" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From 0f98cf525f89e1c8d608a431923fb9c42df8c99c Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 22 Nov 2023 11:04:33 -0500 Subject: [PATCH 191/327] Stardew Valley: Generate proper filler for item links (#2069) Co-authored-by: Zach Parks --- worlds/stardew_valley/__init__.py | 35 +++++++- worlds/stardew_valley/data/bundle_data.py | 3 +- worlds/stardew_valley/items.py | 26 ++++-- worlds/stardew_valley/test/TestItemLink.py | 100 +++++++++++++++++++++ 4 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 worlds/stardew_valley/test/TestItemLink.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 24ffa8c1ad..aa825af302 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,16 +1,16 @@ import logging from typing import Dict, Any, Iterable, Optional, Union, Set, List -from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld +from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld, Group as ItemLinkGroup from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles import get_all_bundles, Bundle -from .items import item_table, create_items, ItemData, Group, items_by_group +from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs from .locations import location_table, create_locations, LocationData from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland + BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules @@ -74,6 +74,7 @@ class StardewValleyWorld(World): def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) self.all_progression_items = set() + self.filler_item_pool_names = [] def generate_early(self): self.force_change_options_if_incompatible() @@ -270,7 +271,33 @@ class StardewValleyWorld(World): pass def get_filler_item_name(self) -> str: - return "Joja Cola" + if not self.filler_item_pool_names: + self.generate_filler_item_pool_names() + return self.random.choice(self.filler_item_pool_names) + + def generate_filler_item_pool_names(self): + include_traps, exclude_island = self.get_filler_item_rules() + available_filler = get_all_filler_items(include_traps, exclude_island) + available_filler = remove_limited_amount_packs(available_filler) + self.filler_item_pool_names = [item.name for item in available_filler] + + def get_filler_item_rules(self): + if self.player in self.multiworld.groups: + link_group: ItemLinkGroup = self.multiworld.groups[self.player] + include_traps = True + exclude_island = False + for player in link_group["players"]: + player_options = self.multiworld.worlds[player].options + if self.multiworld.game[player] != self.game: + + continue + if player_options.trap_items == TrapItems.option_no_traps: + include_traps = False + if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true: + exclude_island = True + return include_traps, exclude_island + else: + return self.options.trap_items != TrapItems.option_no_traps, self.options.exclude_ginger_island == ExcludeGingerIsland.option_true def fill_slot_data(self) -> Dict[str, Any]: diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 8a1a6a5bcf..183383ccbf 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -303,8 +303,7 @@ artisan_goods_items = [truffle_oil, cloth, goat_cheese, cheese, honey, beer, jui river_fish_items = [chub, catfish, rainbow_trout, lingcod, walleye, perch, pike, bream, salmon, sunfish, tiger_trout, shad, smallmouth_bass, dorado] -lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, - largemouth_bass, sturgeon, bullhead, midnight_carp] +lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, largemouth_bass, sturgeon, bullhead] ocean_fish_items = [tilapia, pufferfish, tuna, super_cucumber, flounder, anchovy, sardine, red_mullet, herring, eel, octopus, red_snapper, squid, sea_cucumber, albacore, halibut] night_fish_items = [walleye, bream, super_cucumber, eel, squid, midnight_carp] diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index a5a370aa08..1f0735f4ae 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -468,10 +468,6 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options items_already_added: List[Item], number_locations: int) -> List[Item]: include_traps = options.trap_items != TrapItems.option_no_traps - all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]] - all_filler_packs.extend(items_by_group[Group.TRASH]) - if include_traps: - all_filler_packs.extend(items_by_group[Group.TRAP]) items_already_added_names = [item.name for item in items_already_added] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL] if pack.name not in items_already_added_names] @@ -484,8 +480,9 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options if include_traps: priority_filler_items.extend(trap_items) - all_filler_packs = remove_excluded_packs(all_filler_packs, options) - priority_filler_items = remove_excluded_packs(priority_filler_items, options) + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + all_filler_packs = get_all_filler_items(include_traps, exclude_ginger_island) + priority_filler_items = remove_excluded_packs(priority_filler_items, exclude_ginger_island) number_priority_items = len(priority_filler_items) required_resource_pack = number_locations - len(items_already_added) @@ -519,8 +516,21 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options return items -def remove_excluded_packs(packs, options: StardewValleyOptions): +def remove_excluded_packs(packs, exclude_ginger_island: bool): included_packs = [pack for pack in packs if Group.DEPRECATED not in pack.groups] - if options.exclude_ginger_island == ExcludeGingerIsland.option_true: + if exclude_ginger_island: included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups] return included_packs + + +def remove_limited_amount_packs(packs): + return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] + + +def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): + all_filler_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK]] + all_filler_packs.extend(items_by_group[Group.TRASH]) + if include_traps: + all_filler_packs.extend(items_by_group[Group.TRAP]) + all_filler_packs = remove_excluded_packs(all_filler_packs, exclude_ginger_island) + return all_filler_packs diff --git a/worlds/stardew_valley/test/TestItemLink.py b/worlds/stardew_valley/test/TestItemLink.py new file mode 100644 index 0000000000..f55ab8ca34 --- /dev/null +++ b/worlds/stardew_valley/test/TestItemLink.py @@ -0,0 +1,100 @@ +from . import SVTestBase +from .. import options, item_table, Group + +max_iterations = 2000 + + +class TestItemLinksEverythingIncluded(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_medium} + + def test_filler_of_all_types_generated(self): + max_number_filler = 115 + filler_generated = [] + at_least_one_trap = False + at_least_one_island = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.TRAP in item_table[filler].groups: + at_least_one_trap = True + if Group.GINGER_ISLAND in item_table[filler].groups: + at_least_one_island = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_trap) + self.assertTrue(at_least_one_island) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoIsland(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_medium} + + def test_filler_has_no_island_but_has_traps(self): + max_number_filler = 109 + filler_generated = [] + at_least_one_trap = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.TRAP in item_table[filler].groups: + at_least_one_trap = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_trap) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoTraps(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_no_traps} + + def test_filler_has_no_traps_but_has_island(self): + max_number_filler = 100 + filler_generated = [] + at_least_one_island = False + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.TRAP, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if Group.GINGER_ISLAND in item_table[filler].groups: + at_least_one_island = True + if len(filler_generated) >= max_number_filler: + break + self.assertTrue(at_least_one_island) + self.assertGreaterEqual(len(filler_generated), max_number_filler) + + +class TestItemLinksNoTrapsAndIsland(SVTestBase): + options = {options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_no_traps} + + def test_filler_generated_without_island_or_traps(self): + max_number_filler = 94 + filler_generated = [] + for i in range(0, max_iterations): + filler = self.multiworld.worlds[1].get_filler_item_name() + if filler in filler_generated: + continue + filler_generated.append(filler) + self.assertNotIn(Group.GINGER_ISLAND, item_table[filler].groups) + self.assertNotIn(Group.TRAP, item_table[filler].groups) + self.assertNotIn(Group.MAXIMUM_ONE, item_table[filler].groups) + self.assertNotIn(Group.EXACTLY_TWO, item_table[filler].groups) + if len(filler_generated) >= max_number_filler: + break + self.assertGreaterEqual(len(filler_generated), max_number_filler) From ee76cce1a35a186f09db62a41bcb36b927732ba2 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 22 Nov 2023 10:42:21 -0600 Subject: [PATCH 192/327] Rogue Legacy: Fix a preset including an option that prevents generation. (#2473) --- worlds/rogue_legacy/Presets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py index a4284e9f7d..2dfeee64d8 100644 --- a/worlds/rogue_legacy/Presets.py +++ b/worlds/rogue_legacy/Presets.py @@ -35,7 +35,7 @@ rl_options_presets: Dict[str, Dict[str, Any]] = { "equip_pool": "random", "crit_chance_pool": "random", "crit_damage_pool": "random", - "allow_default_names": False, + "allow_default_names": True, "death_link": "random", }, # A preset I actually use, using some literal values and some from the option itself. From af0d47b444871a4309bcefab04f02e615f8cd348 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 22 Nov 2023 12:13:02 -0500 Subject: [PATCH 193/327] Core: Provide a better error message if only weights.yaml is provided with players: 0 (#2227) --- Generate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Generate.py b/Generate.py index 8113d8a0d7..74244ec231 100644 --- a/Generate.py +++ b/Generate.py @@ -127,6 +127,13 @@ def main(args=None, callback=ERmain): player_id += 1 args.multi = max(player_id - 1, args.multi) + + if args.multi == 0: + raise ValueError( + "No individual player files found and number of players is 0. " + "Provide individual player files or specify the number of players via host.yaml or --multi." + ) + logging.info(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, " f"{seed_name} Seed {seed} with plando: {args.plando}") From b2e7ce2c36df5546510c046a0592e4233c35c79c Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 22 Nov 2023 10:21:15 -0800 Subject: [PATCH 194/327] Pokemon Emerald: Fix using wrong key for extracted constant (#2484) --- worlds/pokemon_emerald/pokemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index b461d006a4..13c92ddc09 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -173,7 +173,7 @@ def get_random_move( # We're either matching types or failed to pick a move above if type_target is None: - possible_moves = [i for i in range(data.constants["MOVE_COUNT"]) if i not in expanded_blacklist] + possible_moves = [i for i in range(data.constants["MOVES_COUNT"]) if i not in expanded_blacklist] else: possible_moves = [move for move in _moves_by_type[type_target[0]] if move not in expanded_blacklist] + \ [move for move in _moves_by_type[type_target[1]] if move not in expanded_blacklist] From 0d38b415404bca2b5bcb7bca4ef4c2c7ca43d0b6 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 06:00:46 -0800 Subject: [PATCH 195/327] BizHawkClient: Add support for multiple concurrent instances (#2475) This allows multiple client/connector pairs to run at the same time. It also includes a few other miscellaneous small changes that accumulated as I went. They can be split if desired - Whatever the `client_socket:send` line (~440) was doing with that missing operator, it's no longer doing. Don't ask me how it was working before. Lua is witchcraft. - Removed the `settimeout(2)` which causes the infamous emulator freeze (and replaced it with a `settimeout(0)` when the server socket is created). It appears to be unnecessary to set a timeout for discovering a client. Maybe at some point in time it was useful to keep the success rate for connecting high, but it seems to not be a problem if the timeout is 0 instead. - Also updated the Emerald setup to remove mention of the freezing. - Connector script now picks the first port that's not in use in a range of 5 ports. - To summarize why I was previously under the impression that multiple running scripts would not detect when a port was in use: 1. Calling `socket.bind` in the existing script will first create an ipv6 socket. 2. A second concurrent script trying to bind to the same port would I think fail to create an ipv6 socket but then succeed in creating an ipv4 socket on the same port. 3. That second socket could never communicate with a client; extra clients would just bounce off the first script. 4. The third concurrent script will then fail on both and actually give an `address already in use` error. - I'm not _really_ sure what's going on there. But forcing one or the other by calling `socket.tcp4()` or `socket.tcp6()` means that only one script will believe it has the port while any others will give `address already in use` as you'd expect. - As a side note, our `socket.lua` is much wonkier than I had previously thought. I understand some parts were added for LADX and when BizHawk 2.9 came out, but as far back as the file's history in this repo, it has provided a strange, modified interface as compared to the file it was originally derived from, to no benefit as far as I can tell. - The connector script closes `server` once it finds a client and opens a new one if the connection drops. I'm not sure this ultimately has an effect, but it seems more proper. - If the connector script's main function returns because of some error or refusal to proceed, the script no longer tries to resume the coroutine it was part of, which would flood the log with irrelevant errors. - Creating `SyncError`s in `guarded_read` and `guarded_write` would raise its own error because the wrong variable was being used in its message. - A call to `_bizhawk.connect` can take a while as the client tries the possible ports. There's a modification that will wait on either the `connect` or the exit event. And if the exit event fires while still looking for a connector script, this cancels the `connect` so the window can close. - Related: It takes 2-3 seconds for a call to `asyncio.open_connection` to come back with any sort of response on my machine, which can be significant now that we're trying multiple ports in sequence. I guess it could fire off 5 tasks at once. Might cause some weirdness if there exist multiple scripts and multiple clients looking for each other at the same time. - Also related: The first time a client attempts to connect to a script, they accept each other and start communicating as expected. The second client to try that port seems to believe it connects and will then time out on the first message. And then all subsequent attempts to connect to that port by any client will be refused (as expected) until the script shuts down or restarts. I haven't been able to explain this behavior. It adds more time to a client's search for a script, but doesn't ultimately cause problems. --- data/lua/connector_bizhawk_generic.lua | 99 ++++++++++++++++--------- worlds/_bizhawk/__init__.py | 36 ++++++--- worlds/_bizhawk/context.py | 13 +++- worlds/pokemon_emerald/docs/setup_en.md | 4 +- 4 files changed, 104 insertions(+), 48 deletions(-) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index b0b06de447..c4e729300d 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -249,6 +249,24 @@ Response: - `err` (`string`): A description of the problem ]] +local bizhawk_version = client.getversion() +local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") +bizhawk_major = tonumber(bizhawk_major) +bizhawk_minor = tonumber(bizhawk_minor) +if bizhawk_patch == "" then + bizhawk_patch = 0 +else + bizhawk_patch = tonumber(bizhawk_patch) +end + +local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") +lua_major = tonumber(lua_major) +lua_minor = tonumber(lua_minor) + +if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then + require("lua_5_3_compat") +end + local base64 = require("base64") local socket = require("socket") local json = require("json") @@ -257,7 +275,9 @@ local json = require("json") -- Will cause lag due to large console output local DEBUG = false -local SOCKET_PORT = 43055 +local SOCKET_PORT_FIRST = 43055 +local SOCKET_PORT_RANGE_SIZE = 5 +local SOCKET_PORT_LAST = SOCKET_PORT_FIRST + SOCKET_PORT_RANGE_SIZE local STATE_NOT_CONNECTED = 0 local STATE_CONNECTED = 1 @@ -277,24 +297,6 @@ local locked = false local rom_hash = nil -local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") -lua_major = tonumber(lua_major) -lua_minor = tonumber(lua_minor) - -if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then - require("lua_5_3_compat") -end - -local bizhawk_version = client.getversion() -local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") -bizhawk_major = tonumber(bizhawk_major) -bizhawk_minor = tonumber(bizhawk_minor) -if bizhawk_patch == "" then - bizhawk_patch = 0 -else - bizhawk_patch = tonumber(bizhawk_patch) -end - function queue_push (self, value) self[self.right] = value self.right = self.right + 1 @@ -435,7 +437,7 @@ function send_receive () end if message == "VERSION" then - local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n") + client_socket:send(tostring(SCRIPT_VERSION).."\n") else local res = {} local data = json.decode(message) @@ -463,14 +465,45 @@ function send_receive () end end -function main () - server, err = socket.bind("localhost", SOCKET_PORT) +function initialize_server () + local err + local port = SOCKET_PORT_FIRST + local res = nil + + server, err = socket.socket.tcp4() + while res == nil and port <= SOCKET_PORT_LAST do + res, err = server:bind("localhost", port) + if res == nil and err ~= "address already in use" then + print(err) + return + end + + if res == nil then + port = port + 1 + end + end + + if port > SOCKET_PORT_LAST then + print("Too many instances of connector script already running. Exiting.") + return + end + + res, err = server:listen(0) + if err ~= nil then print(err) return end + server:settimeout(0) +end + +function main () while true do + if server == nil then + initialize_server() + end + current_time = socket.socket.gettime() timeout_timer = timeout_timer - (current_time - prev_time) message_timer = message_timer - (current_time - prev_time) @@ -482,16 +515,16 @@ function main () end if current_state == STATE_NOT_CONNECTED then - if emu.framecount() % 60 == 0 then - server:settimeout(2) + if emu.framecount() % 30 == 0 then + print("Looking for client...") local client, timeout = server:accept() if timeout == nil then print("Client connected") current_state = STATE_CONNECTED client_socket = client + server:close() + server = nil client_socket:settimeout(0) - else - print("No client found. Trying again...") end end else @@ -527,27 +560,27 @@ else emu.frameadvance() end end - + rom_hash = gameinfo.getromhash() - print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n") + print("Waiting for client to connect. This may take longer the more instances of this script you have open at once.\n") local co = coroutine.create(main) function tick () local status, err = coroutine.resume(co) - - if not status then + + if not status and err ~= "cannot resume dead coroutine" then print("\nERROR: "..err) print("Consider reporting this crash.\n") if server ~= nil then server:close() end - + co = coroutine.create(main) end end - + -- Gambatte has a setting which can cause script execution to become -- misaligned, so for GB and GBC we explicitly set the callback on -- vblank instead. @@ -557,7 +590,7 @@ else else event.onframeend(tick) end - + while true do emu.frameadvance() end diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 3403990832..c3314e18dc 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -12,7 +12,8 @@ import json import typing -BIZHAWK_SOCKET_PORT = 43055 +BIZHAWK_SOCKET_PORT_RANGE_START = 43055 +BIZHAWK_SOCKET_PORT_RANGE_SIZE = 5 class ConnectionStatus(enum.IntEnum): @@ -45,11 +46,13 @@ class BizHawkContext: streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] connection_status: ConnectionStatus _lock: asyncio.Lock + _port: typing.Optional[int] def __init__(self) -> None: self.streams = None self.connection_status = ConnectionStatus.NOT_CONNECTED self._lock = asyncio.Lock() + self._port = None async def _send_message(self, message: str): async with self._lock: @@ -86,15 +89,24 @@ class BizHawkContext: async def connect(ctx: BizHawkContext) -> bool: - """Attempts to establish a connection with the connector script. Returns True if successful.""" - try: - ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT) - ctx.connection_status = ConnectionStatus.TENTATIVE - return True - except (TimeoutError, ConnectionRefusedError): - ctx.streams = None - ctx.connection_status = ConnectionStatus.NOT_CONNECTED - return False + """Attempts to establish a connection with a connector script. Returns True if successful.""" + rotation_steps = 0 if ctx._port is None else ctx._port - BIZHAWK_SOCKET_PORT_RANGE_START + ports = [*range(BIZHAWK_SOCKET_PORT_RANGE_START, BIZHAWK_SOCKET_PORT_RANGE_START + BIZHAWK_SOCKET_PORT_RANGE_SIZE)] + ports = ports[rotation_steps:] + ports[:rotation_steps] + + for port in ports: + try: + ctx.streams = await asyncio.open_connection("localhost", port) + ctx.connection_status = ConnectionStatus.TENTATIVE + ctx._port = port + return True + except (TimeoutError, ConnectionRefusedError): + continue + + # No ports worked + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + return False def disconnect(ctx: BizHawkContext) -> None: @@ -233,7 +245,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[ return None else: if item["type"] != "READ_RESPONSE": - raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}") + raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {item['type']}") ret.append(base64.b64decode(item["value"])) @@ -285,7 +297,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl return False else: if item["type"] != "WRITE_RESPONSE": - raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}") + raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {item['type']}") return True diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index ccf747f15a..2699b0f5f1 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -130,7 +130,18 @@ async def _game_watcher(ctx: BizHawkClientContext): logger.info("Waiting to connect to BizHawk...") showed_connecting_message = True - if not await connect(ctx.bizhawk_ctx): + # Since a call to `connect` can take a while to return, this will cancel connecting + # if the user has decided to close the client. + connect_task = asyncio.create_task(connect(ctx.bizhawk_ctx), name="BizHawkConnect") + exit_task = asyncio.create_task(ctx.exit_event.wait(), name="ExitWait") + await asyncio.wait([connect_task, exit_task], return_when=asyncio.FIRST_COMPLETED) + + if exit_task.done(): + connect_task.cancel() + return + + if not connect_task.result(): + # Failed to connect continue showed_no_handler_message = False diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index 6a1df8e5c3..3c5c8c193a 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -52,8 +52,8 @@ you can re-open it from the launcher. 3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. 4. In the Lua Console window, go to `Script > Open Script…`. 5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. -6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk -Client window should indicate that it connected and recognized Pokemon Emerald. +6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it +connected and recognized Pokemon Emerald. 7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the top text field of the client and click Connect. From a7aed71fbe18e3498ceca73fe96c729a4cb46cec Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 09:55:50 -0800 Subject: [PATCH 196/327] Pokemon Emerald: Fix opponent trainer moves sometimes being MOVE_NONE (#2487) --- worlds/pokemon_emerald/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index d3ced5f3ca..b7730fbdf7 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -677,7 +677,7 @@ class PokemonEmeraldWorld(World): level_up_movepool = list({ move.move_id for move in new_species.learnset - if move.level <= pokemon.level + if move.move_id != 0 and move.level <= pokemon.level }) new_moves = ( From ae8a81c0cbf9ed3002bb0fd630a9a75601fc05b3 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 23 Nov 2023 12:56:55 -0500 Subject: [PATCH 197/327] Lingo: Change docs to link to the client in the Steam Workshop (#2486) --- worlds/lingo/docs/setup_en.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/worlds/lingo/docs/setup_en.md b/worlds/lingo/docs/setup_en.md index 97f3ce5940..0e68c7ed45 100644 --- a/worlds/lingo/docs/setup_en.md +++ b/worlds/lingo/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Lingo](https://store.steampowered.com/app/1814170/Lingo/) -- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md) +- [Lingo Archipelago Randomizer](https://steamcommunity.com/sharedfiles/filedetails/?id=3092505110) ## Optional Software @@ -12,14 +12,12 @@ ## Installation -1. Download the Lingo Archipelago Randomizer from the above link. -2. Open up Lingo, go to settings, and click View Game Data. This should open up - a folder in Windows Explorer. -3. Unzip the contents of the randomizer into the "maps" folder. You may need to - create the "maps" folder if you have not played a custom Lingo map before. -4. Installation complete! You may have to click Return to go back to the main - menu and then click Settings again in order to get the randomizer to show up - in the level selection list. +You can use the above Steam Workshop link to subscribe to the Lingo Archipelago Randomizer. This will automatically +download the client, as well as update it whenever an update is available. + +If you don't want to use Steam Workshop, you can also +[download the randomizer manually](https://code.fourisland.com/lingo-archipelago/about/) using the instructions on the +linked page. ## Joining a Multiworld game From a1759ed7e158a013ce44f0336e581e3dd830e1b9 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:06:57 -0500 Subject: [PATCH 198/327] KH2: Update Game Docs (#2188) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/kh2/docs/en_Kingdom Hearts 2.md | 16 ++++++++++++++++ worlds/kh2/docs/setup_en.md | 19 +++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index 8258a099cc..365ed37cb6 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -63,6 +63,22 @@ For example, if you are fighting Roxas, receive Reflect Element, then die mid-fi - Customize the amount and level of progressive movement (Growth Abilities) you start with. - Customize start inventory, i.e., begin every run with certain items or spells of your choice. +

    What are Lucky Emblems?

    +Lucky Emblems are items that are required to beat the game if your goal is "Lucky Emblem Hunt".
    +You can think of these as requiring X number of Proofs of Nonexistence to open the final door. + +

    What is Hitlist/Bounties?

    +The Hitlist goal adds "bounty" items to select late-game fights and locations, and you need to collect X number of them to win.
    +The list of possible locations that can contain a bounty: + +- Each of the 13 Data Fights +- Max level (7) for each Drive Form +- Sephiroth +- Lingering Will +- Starry Hill +- Transport to Remembrance +- Each of the Goddess of Fate and Paradox Cups +

    Quality of life:

    diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index 17235042e1..e0c8330632 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -66,30 +66,33 @@ Enter `The room's port number` into the top box where the x's are and pr - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. - Set fps limit to 60fps. - Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. +- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed

    Requirement/logic sheet

    Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)

    F.A.Q.

    +- Why is my HP/MP continuously increasing without stopping? + - You do not have `JaredWeakStrike/APCompanion` set up correctly. Make sure it is above the `GoA ROM Mod` in the mod manager. +- Why is my HP/MP continuously increasing without stopping when I have the APCompanion Mod? + - You have a leftover GOA lua script in your `Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\KH2`. +- Why am I missing worlds/portals in the GoA? + - You are missing the required visit-locking item to access the world/portal. +- Why did I not load into the correct visit? + - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item. +- What versions of Kingdom Hearts 2 are supported? + - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`. - Why am I getting wallpapered while going into a world for the first time? - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. - Why am I not getting magic? - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. -- Why am I missing worlds/portals in the GoA? - - You are missing the required visit locking item to access the world/portal. -- What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. - Why did I crash? - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify - Why am I getting dummy items or letters? - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up) -- Why is my HP/MP continuously increasing without stopping? - - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. - Why am I not sending or receiving items? - Make sure you are connected to the KH2 client and the correct room (for more information scroll up) -- Why did I not load in to the correct visit - - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. - Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. - How do I load an auto save? From 286dfd84c096c2cf6e3b594722a82b38a2a4601d Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 23 Nov 2023 19:10:32 +0100 Subject: [PATCH 199/327] sm64ex: Replace old launcher tutorial (#2383) --- worlds/sm64ex/docs/setup_en.md | 81 ++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 38edeb2c4a..2817d3c324 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -2,71 +2,77 @@ ## Required Software -- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported) +- Super Mario 64 US or JP Rom (Europe and Shindou not supported) - Either of - - [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or + - [SM64AP-Launcher](https://github.com/N00byKing/SM64AP-Launcher/releases) or - Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually - Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) -NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex. +NOTE: The above linked launcher is a special version designed to work with the Archipelago build of sm64ex. You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex. ## Installation and Game Start Procedures -### Installation via sm64pclauncher (For Windows) +### Installation via SM64AP-Launcher + +*Windows Preparations* First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES. -Do all steps up to including step 6. -Best use default install directory. -Then follow the steps below +It is extremely encouraged to use the default install directory! +Then continue to `Using the Launcher` -1. Go to the page linked for sm64pclauncher, and press on the topmost entry -3. Scroll down, and download the zip file -4. Unpack the zip file in an empty folder -5. Run the Launcher and press build. -6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox -7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet -8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly -9. Set Build Options and press build. - - Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores). - - Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`). -10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes +*Linux Preparations* -After it's done, the Build list should have another entry titled with what you named the folder in step 7. +You will need to install some dependencies before using the launcher. +The launcher itself needs `qt6`, `patch` and `git`, and building the game requires `sdl2 glew cmake python make` (If you install `jsoncpp` as well, it will be linked dynamically). +Then continue to `Using the Launcher` -NOTE: For some reason first start of the game always crashes the launcher. Just restart it. -If it still crashes, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game") +*Using the Launcher* + +1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry +2. Scroll down, and download the zip file for your OS. +3. Unpack the zip file in an empty folder +4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps. + - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. +5. When finished, use `Compile default SM64AP build` to continue + - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. +6. Press `Download Files` to prepare the build, afterwards `Create Build`. +7. SM64EX will now be compiled. This can take a while. + +After it's done, the build list should have another entry with the name you gave it. + +NOTE: If it does not start when pressing `Play selected build`, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game") ### Manual Compilation (Linux/Windows) -*Windows Instructions* +*Windows Preparations* First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES. -After launching msys2, and update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. +After launching msys2 using a MinGW x64 shell (there should be a start menu entry), update by entering `pacman -Syuu` in the command prompt. Next, install the relevant dependencies by entering `pacman -S unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 mingw-w64-x86_64-cmake`. -After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. +Continue to `Compiling`. -After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want. +*Linux Preparations* -After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder. +Install the relevant dependencies `sdl2 glew cmake python make patch git`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. -*Linux Instructions* +Continue to `Compiling`. -Install the relevant dependencies `sdl2 glew cmake python make`. SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically. +*Compiling* -After this, obtain the code base by cloning the relevant repository manually via `git clone --recursive https://github.com/N00byKing/sm64ex`. Ready your ROM by copying your legally dumped rom into your sm64ex folder. The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. +Obtain the code base by cloning the relevant repository via `git clone --recursive https://github.com/N00byKing/sm64ex`. Copy your legally dumped rom into your sm64ex folder (if you are not sure where your folder is located, do a quick Windows search for sm64ex). The name of the ROM needs to be `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively. -After all these preparatory steps have succeeded, type `make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` where n is the number of cores you want. +After all these preparatory steps have succeeded, type `cd sm64ex && make` in your command prompt and get ready to wait for a bit. If you want to speed up compilation, tell the compiler how many CPU cores to use by using `make -jn` instead, where n is the number of cores you want. After the compliation was successful, there will be a binary in your `sm64ex/build/REGION_pc/` folder. ### Joining a MultiWorld Game To join, set the following launch options: `--sm64ap_name YourName --sm64ap_ip ServerIP:Port`. +For example, if you are hosting a game using the website, `YourName` will be the name from the Settings Page, `ServerIP` is `archipelago.gg` and `Port` the port given on the Archipelago room page. Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requires a password. -The Name in this case is the one specified in your generated .yaml file. -In case you are using the Archipelago Website, the IP should be `archipelago.gg`. +Should your name or password have spaces, enclose it in quotes: `"YourPassword"` and `"YourName"`. Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. @@ -81,7 +87,7 @@ Create a room and download the `.apsm64ex` file, and start the game with the `-- ### Optional: Using Batch Files to play offline and MultiWorld games -As an alternative to launching the game with sm64pclauncher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. +As an alternative to launching the game with SM64AP-Launcher, it is also possible to launch the completed build with the use of Windows batch files. This has the added benefit of streamlining the join process so that manual editing of connection info is not needed for each new game. However, you'll need to be somewhat comfortable with creating and using batch files. IMPORTANT NOTE: The remainder of this section uses copy-and-paste code that assumes you're using the US version. If you instead use the Japanese version, you'll need to edit the EXE name accordingly by changing "sm64.us.f3dex2e.exe" to "sm64.jp.f3dex2e.exe". @@ -91,7 +97,7 @@ Open Notepad. Paste in the following text: `start sm64.us.f3dex2e.exe --sm64ap_f Go to File > Save As... -Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64PCLauncher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. +Navigate to the folder you selected for your SM64 build when you followed the Build guide for SM64AP-Launcher earlier. Once there, navigate further into `build` and then `us_pc`. This folder should be the same folder that `sm64.us.f3dex2e.exe` resides in. Make the file name `"offline.bat"` . THE QUOTE MARKS ARE IMPORTANT! Otherwise, it will create a text file instead ("offline.bat.txt"), which won't work as a batch file. @@ -120,8 +126,8 @@ To use this batch file, double-click it. A window will open. Type the five-digi - The port number is provided on the room page. The game host should share this page with all players. - The slot name is whatever you typed in the "Name" field when creating a config file. All slot names are visible on the room page. -Once you provide those two bits of information, the game will open. If the info is correct, when the game starts, you will see "Connected to Archipelago" on the bottom of your screen, and you will be able to enter the castle. -- If you don't see this text and crash upon entering the castle, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. +Once you provide those two bits of information, the game will open. +- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. ### Addendum - Deleting old saves @@ -170,6 +176,5 @@ Should the problem still be there after about a minute or two, just save and res ### How do I update the Game to a new Build? +When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. The launcher will recognize this, and offer to replace it. When manually compiling just pull in changes and run `make` again. Sometimes it helps to run `make clean` before. - -When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. Then continue as normal. From f840ed3a9416eed6e1ace1cf617021974e96bee8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:17:09 -0500 Subject: [PATCH 200/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20trainer=20r?= =?UTF-8?q?egions=20(#2474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Mt Moon B2F trainer regions * Fix Trainer Party regions --- worlds/pokemon_rb/locations.py | 91 +++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 4f1b55a00d..3fff3b88c1 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -502,8 +502,8 @@ location_data = [ LocationData("Mt Moon 1F", "Lass 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_2_ITEM"], EventFlag(134), inclusion=trainersanity), LocationData("Mt Moon 1F", "Youngster", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_1_ITEM"], EventFlag(135), inclusion=trainersanity), LocationData("Mt Moon 1F", "Hiker", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_1_TRAINER_0_ITEM"], EventFlag(136), inclusion=trainersanity), - LocationData("Mt Moon B2F-NE", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity), - LocationData("Mt Moon B2F-C", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity), + LocationData("Mt Moon B2F-C", "Rocket 1", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_1_ITEM"], EventFlag(127), inclusion=trainersanity), + LocationData("Mt Moon B2F-NE", "Rocket 2", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_2_ITEM"], EventFlag(126), inclusion=trainersanity), LocationData("Mt Moon B2F", "Rocket 3", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_3_ITEM"], EventFlag(125), inclusion=trainersanity), LocationData("Mt Moon B2F", "Rocket 4", None, rom_addresses["Trainersanity_EVENT_BEAT_MT_MOON_3_TRAINER_0_ITEM"], EventFlag(128), inclusion=trainersanity), LocationData("Viridian Forest", "Bug Catcher 1", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM"], EventFlag(139), inclusion=trainersanity), @@ -2310,43 +2310,50 @@ trainer_data = { 'Cerulean Gym': [{'level': 19, 'party': ['Goldeen'], 'party_address': 'Trainer_Party_Cerulean_Gym_JrTrainerF_A'}, {'level': 16, 'party': ['Horsea', 'Shellder'], 'party_address': 'Trainer_Party_Cerulean_Gym_Swimmer_A'}, - {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},], 'Route 10-N': [ ### - {'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'}, + {'level': [18, 21], 'party': ['Staryu', 'Starmie'], 'party_address': 'Trainer_Party_Misty_A'},], + 'Route 10-N': [{'level': 20, 'party': ['Pikachu', 'Clefairy'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_A'}], + 'Route 10-C': [ + {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'}], + 'Route 10-S': [ {'level': 21, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_10_JrTrainerF_B'}, - {'level': 30, 'party': ['Rhyhorn', 'Lickitung'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_A'}, {'level': 20, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Route_10_Pokemaniac_B'}, {'level': 21, 'party': ['Geodude', 'Onix'], 'party_address': 'Trainer_Party_Route_10_Hiker_A'}, {'level': 19, 'party': ['Onix', 'Graveler'], 'party_address': 'Trainer_Party_Route_10_Hiker_B'}], - 'Rock Tunnel B1F-E': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'], ### + 'Rock Tunnel B1F-W': [{'level': 21, 'party': ['Jigglypuff', 'Pidgey', 'Meowth'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_A'}, - {'level': 22, 'party': ['Oddish', 'Bulbasaur'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'}, {'level': 20, 'party': ['Slowpoke', 'Slowpoke', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_A'}, - {'level': 22, 'party': ['Charmander', 'Cubone'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'}, - {'level': 25, 'party': ['Slowpoke'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'}, {'level': 21, 'party': ['Geodude', 'Geodude', 'Graveler'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'}, - {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'}, - {'level': 20, 'party': ['Machop', 'Onix'], - 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}], 'Route 13': [ + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_A'},], + 'Rock Tunnel B1F-E': [ + {'level': 22, 'party': ['Oddish', 'Bulbasaur'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_JrTrainerF_B'}, + {'level': 22, 'party': ['Charmander', 'Cubone'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_B'}, + {'level': 25, 'party': ['Slowpoke'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Pokemaniac_C'}, + {'level': 25, 'party': ['Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_B'}, + {'level': 20, 'party': ['Machop', 'Onix'], + 'party_address': 'Trainer_Party_Rock_Tunnel_B1F_Hiker_D'}], + 'Route 13-E': [ + {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'], + 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'}, + {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'}, {'level': 24, 'party': ['Pidgey', 'Meowth', 'Rattata', 'Pikachu', 'Meowth'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_A'}, + ], + 'Route 13': [ {'level': 30, 'party': ['Poliwag', 'Poliwag'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_B'}, {'level': 27, 'party': ['Pidgey', 'Meowth', 'Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_JrTrainerF_C'}, - {'level': 28, 'party': ['Goldeen', 'Poliwag', 'Horsea'], - 'party_address': 'Trainer_Party_Route_13_JrTrainerF_D'}, {'level': 28, 'party': ['Koffing', 'Koffing', 'Koffing'], 'party_address': 'Trainer_Party_Route_13_Biker_A'}, {'level': 27, 'party': ['Rattata', 'Pikachu', 'Rattata'], 'party_address': 'Trainer_Party_Route_13_Beauty_A'}, {'level': 29, 'party': ['Clefairy', 'Meowth'], 'party_address': 'Trainer_Party_Route_13_Beauty_B'}, - {'level': 29, 'party': ['Pidgey', 'Pidgeotto'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_A'}, {'level': 25, 'party': ['Spearow', 'Pidgey', 'Pidgey', 'Spearow', 'Spearow'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_B'}, {'level': 26, 'party': ['Pidgey', 'Pidgeotto', 'Spearow', 'Fearow'], 'party_address': 'Trainer_Party_Route_13_BirdKeeper_C'}], + 'Route 20-E': [ {'level': 31, 'party': ['Shellder', 'Cloyster'], 'party_address': 'Trainer_Party_Route_20_Swimmer_A'}, {'level': 28, 'party': ['Horsea', 'Horsea', 'Seadra', 'Horsea'], @@ -2354,9 +2361,9 @@ trainer_data = { 'party_address': 'Trainer_Party_Route_20_Swimmer_C'}, {'level': 30, 'party': ['Seadra', 'Horsea', 'Seadra'], - 'party_address': 'Trainer_Party_Route_20_Beauty_E'}], + 'party_address': 'Trainer_Party_Route_20_Beauty_E'}, + {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'},], 'Route 20-W': [ - {'level': 35, 'party': ['Seaking'], 'party_address': 'Trainer_Party_Route_20_Beauty_A'}, {'level': 31, 'party': ['Goldeen', 'Seaking'], 'party_address': 'Trainer_Party_Route_20_JrTrainerF_A'}, {'level': 30, 'party': ['Tentacool', 'Horsea', 'Seel'], 'party_address': 'Trainer_Party_Route_20_JrTrainerF_C'}, @@ -2374,16 +2381,19 @@ trainer_data = { {'level': 20, 'party': ['Meowth', 'Oddish', 'Pidgey'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_B'}, {'level': 19, 'party': ['Pidgey', 'Rattata', 'Rattata', 'Bellsprout'], - 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'}, - {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'}, + 'party_address': 'Trainer_Party_Rock_Tunnel_1F_JrTrainerF_C'}], + 'Rock Tunnel 1F-NE': [ + {'level': 23, 'party': ['Cubone', 'Slowpoke'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Pokemaniac_A'}], + 'Rock Tunnel 1F-NW': [ {'level': 19, 'party': ['Geodude', 'Machop', 'Geodude', 'Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_A'}, {'level': 20, 'party': ['Onix', 'Onix', 'Geodude'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_B'}, {'level': 21, 'party': ['Geodude', 'Graveler'], 'party_address': 'Trainer_Party_Rock_Tunnel_1F_Hiker_C'}], + 'Route 15-N': [ + {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'}], 'Route 15': [ {'level': 28, 'party': ['Gloom', 'Oddish', 'Oddish'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_A'}, {'level': 29, 'party': ['Pikachu', 'Raichu'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_B'}, - {'level': 33, 'party': ['Clefairy'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_C'}, {'level': 29, 'party': ['Bellsprout', 'Oddish', 'Tangela'], 'party_address': 'Trainer_Party_Route_15_JrTrainerF_D'}, {'level': 25, 'party': ['Koffing', 'Koffing', 'Weezing', 'Koffing', 'Grimer'], @@ -2394,15 +2404,16 @@ trainer_data = { {'level': 26, 'party': ['Pidgeotto', 'Farfetchd', 'Doduo', 'Pidgey'], 'party_address': 'Trainer_Party_Route_15_BirdKeeper_A'}, {'level': 28, 'party': ['Dodrio', 'Doduo', 'Doduo'], 'party_address': 'Trainer_Party_Route_15_BirdKeeper_B'}], - 'Victory Road 2F-C': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'], ### - 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'}, - {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'}, - {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'}, - {'level': 44, 'party': ['Persian', 'Golduck'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'}, - {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'], - 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [ + 'Victory Road 2F-NW': [{'level': 40, 'party': ['Charmeleon', 'Lapras', 'Lickitung'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Pokemaniac_A'}], + 'Victory Road 2F-C': [ + {'level': 41, 'party': ['Drowzee', 'Hypno', 'Kadabra', 'Kadabra'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_A'}, + {'level': 48, 'party': ['Mr Mime'], 'party_address': 'Trainer_Party_Victory_Road_2F_Juggler_C'}, + {'level': 44, 'party': ['Persian', 'Golduck'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Tamer_A'}, + {'level': 43, 'party': ['Machoke', 'Machop', 'Machoke'], + 'party_address': 'Trainer_Party_Victory_Road_2F_Blackbelt_A'}], 'Mt Moon B2F': [ {'level': 12, 'party': ['Grimer', 'Voltorb', 'Koffing'], 'party_address': 'Trainer_Party_Mt_Moon_B2F_SuperNerd_A'}, {'level': 13, 'party': ['Rattata', 'Zubat'], 'party_address': 'Trainer_Party_Mt_Moon_B2F_Rocket_A'}, @@ -2585,7 +2596,7 @@ trainer_data = { ['Pidgeotto', 'Abra', 'Rattata', 'Charmander']], 'party_address': ['Trainer_Party_Cerulean_City_Green1_A', 'Trainer_Party_Cerulean_City_Green1_B', 'Trainer_Party_Cerulean_City_Green1_C']}, {'level': 17, 'party': ['Machop', 'Drowzee'], - 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F': [ + 'party_address': 'Trainer_Party_Cerulean_City_Rocket_A'}], 'Pokemon Mansion 1F-SE': [ {'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Mansion_1F_Scientist_A'}], 'Silph Co 2F-SW': [{'level': 26, 'party': ['Grimer', 'Weezing', 'Koffing', 'Weezing'], 'party_address': 'Trainer_Party_Silph_Co_2F_Scientist_A'}], @@ -2595,7 +2606,7 @@ trainer_data = { {'level': 25, 'party': ['Golbat', 'Zubat', 'Zubat', 'Raticate', 'Zubat'], 'party_address': 'Trainer_Party_Silph_Co_2F_Rocket_B'}], 'Silph Co 3F-W': [ {'level': 29, 'party': ['Electrode', 'Weezing'], 'party_address': 'Trainer_Party_Silph_Co_3F_Scientist_A'}], - 'Silph Co 3F': [ {'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'], + 'Silph Co 3F': [{'level': 28, 'party': ['Raticate', 'Hypno', 'Raticate'], 'party_address': 'Trainer_Party_Silph_Co_3F_Rocket_A'}], 'Silph Co 4F-N': [{'level': 33, 'party': ['Electrode'], 'party_address': 'Trainer_Party_Silph_Co_4F_Scientist_A'}], 'Silph Co 4F': [{'level': 29, 'party': ['Machop', 'Drowzee'], @@ -2670,15 +2681,17 @@ trainer_data = { {'level': 26, 'party': ['Koffing', 'Drowzee'], 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_B'}, {'level': 23, 'party': ['Zubat', 'Rattata', 'Raticate', 'Zubat'], - 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}], 'Victory Road 3F': [ - {'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'], + 'party_address': 'Trainer_Party_Pokemon_Tower_7F_Rocket_C'}], + 'Victory Road 3F': [{'level': 43, 'party': ['Exeggutor', 'Cloyster', 'Arcanine'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_A'}, + {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'], + 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}], + 'Victory Road 3F-S': [ {'level': 43, 'party': ['Kingler', 'Tentacruel', 'Blastoise'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerM_B'}, {'level': 43, 'party': ['Bellsprout', 'Weepinbell', 'Victreebel'], 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_A'}, - {'level': 43, 'party': ['Parasect', 'Dewgong', 'Chansey'], - 'party_address': 'Trainer_Party_Victory_Road_3F_CooltrainerF_B'}], 'Victory Road 1F': [ +], 'Victory Road 1F': [ {'level': 42, 'party': ['Ivysaur', 'Wartortle', 'Charmeleon', 'Charizard'], 'party_address': 'Trainer_Party_Victory_Road_1F_CooltrainerM_A'}, {'level': 44, 'party': ['Persian', 'Ninetales'], From 7efec647458bf33a792cdc47d7c80f0723cebbc9 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 23 Nov 2023 11:51:53 -0800 Subject: [PATCH 201/327] BizHawkClient: Restore use of ConnectorErrors (#2480) --- worlds/_bizhawk/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index c3314e18dc..cddfde4ff3 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -9,6 +9,7 @@ import asyncio import base64 import enum import json +import sys import typing @@ -125,7 +126,20 @@ async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[s """Sends a list of requests to the BizHawk connector and returns their responses. It's likely you want to use the wrapper functions instead of this.""" - return json.loads(await ctx._send_message(json.dumps(req_list))) + responses = json.loads(await ctx._send_message(json.dumps(req_list))) + errors: typing.List[ConnectorError] = [] + + for response in responses: + if response["type"] == "ERROR": + errors.append(ConnectorError(response["err"])) + + if errors: + if sys.version_info >= (3, 11, 0): + raise ExceptionGroup("Connector script returned errors", errors) # noqa + else: + raise errors[0] + + return responses async def ping(ctx: BizHawkContext) -> None: From 28ed78660997a8462da6ce44cfb3b1223ae267d9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 Nov 2023 21:36:05 +0100 Subject: [PATCH 202/327] LttP: fix Ganons Tower - Compass Room - Bottom Left being listed twice in Ganons Tower location group and add missing Ganons Tower - Compass Room - Bottom Right (#2490) --- worlds/alttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 2cae70e0ea..32667249f2 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -195,7 +195,7 @@ class ALTTPWorld(World): "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left", "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", "Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right", - "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left", + "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Right", "Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right", "Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right", "Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room", From cb6467cfe61caabb3c8f603245a07b92b5b5c6e6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 23 Nov 2023 21:36:20 +0100 Subject: [PATCH 203/327] Core: update modules, move orjson to core (#2489) --- requirements.txt | 7 ++++--- worlds/factorio/requirements.txt | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f9cddc287..7d93928bb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ colorama>=0.4.5 websockets>=11.0.3 PyYAML>=6.0.1 -jellyfish>=1.0.1 +jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.2.0 bsdiff4>=1.2.4 -platformdirs>=3.9.1 -certifi>=2023.7.22 +platformdirs>=4.0.0 +certifi>=2023.11.17 cython>=3.0.5 cymem>=2.0.8 +orjson>=3.9.10 \ No newline at end of file diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index 8fb74e9330..c45fb771da 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1,2 +1 @@ factorio-rcon-py>=2.0.1 -orjson>=3.9.7 From 9312ad9bfe6e364cc05e011301fbde0a6ed4e566 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:02:20 -0500 Subject: [PATCH 204/327] KH2: Fix grammar to clarify which locations can have a bounty (#2488) --- worlds/kh2/docs/en_Kingdom Hearts 2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index 365ed37cb6..a07f29be54 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -77,7 +77,7 @@ The list of possible locations that can contain a bounty: - Lingering Will - Starry Hill - Transport to Remembrance -- Each of the Goddess of Fate and Paradox Cups +- Godess of Fate cup and Hades Paradox cup

    Quality of life:

    From 5d9896773d34f2beba0bd1e9ef83f7c653a63efe Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Thu, 23 Nov 2023 16:03:56 -0600 Subject: [PATCH 205/327] Generate: Add `--skip_output` flag to bypass assertion and output stages. (#2416) --- Generate.py | 6 +++++- Main.py | 13 +++++++++---- WebHostLib/generate.py | 11 ++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Generate.py b/Generate.py index 74244ec231..e19a7a973f 100644 --- a/Generate.py +++ b/Generate.py @@ -20,7 +20,7 @@ import Options from BaseClasses import seeddigits, get_seed, PlandoOptions from Main import main as ERmain from settings import get_settings -from Utils import parse_yamls, version_tuple, __version__, tuplize_version, user_path +from Utils import parse_yamls, version_tuple, __version__, tuplize_version from worlds.alttp import Options as LttPOptions from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.Text import TextTable @@ -53,6 +53,9 @@ def mystery_argparse(): help='List of options that can be set manually. Can be combined, for example "bosses, items"') parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") + parser.add_argument("--skip_output", action="store_true", + help="Skips generation assertion and output stages and skips multidata and spoiler output. " + "Intended for debugging and testing purposes.") args = parser.parse_args() if not os.path.isabs(args.weights_file_path): args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path) @@ -150,6 +153,7 @@ def main(args=None, callback=ERmain): erargs.outputname = seed_name erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing + erargs.skip_output = args.skip_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) diff --git a/Main.py b/Main.py index 568bf0208f..b64650478b 100644 --- a/Main.py +++ b/Main.py @@ -13,8 +13,8 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items from Options import StartInventoryPool -from settings import get_settings from Utils import __version__, output_path, version_tuple +from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -101,7 +101,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No del item_digits, location_digits, item_count, location_count - AutoWorld.call_stage(world, "assert_generate") + # This assertion method should not be necessary to run if we are not outputting any multidata. + if not args.skip_output: + AutoWorld.call_stage(world, "assert_generate") AutoWorld.call_all(world, "generate_early") @@ -287,11 +289,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No else: logger.info("Progression balancing skipped.") - logger.info(f'Beginning output...') - # we're about to output using multithreading, so we're removing the global random state to prevent accidental use world.random.passthrough = False + if args.skip_output: + logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start) + return world + + logger.info(f'Beginning output...') outfilebase = 'AP_' + world.seed_name output = tempfile.TemporaryDirectory() diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index ddcc5ffb6c..ee1ce591ee 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -1,18 +1,18 @@ +import concurrent.futures import json import os import pickle import random import tempfile import zipfile -import concurrent.futures from collections import Counter -from typing import Dict, Optional, Any, Union, List +from typing import Any, Dict, List, Optional, Union -from flask import request, flash, redirect, url_for, session, render_template +from flask import flash, redirect, render_template, request, session, url_for from pony.orm import commit, db_session -from BaseClasses import seeddigits, get_seed -from Generate import handle_name, PlandoOptions +from BaseClasses import get_seed, seeddigits +from Generate import PlandoOptions, handle_name from Main import main as ERmain from Utils import __version__ from WebHostLib import app @@ -131,6 +131,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options", {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False + erargs.skip_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): From 844481a0027f7e8770be42d0236f59d008e8b8af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Nov 2023 00:35:37 +0100 Subject: [PATCH 206/327] Core: remove duplicate state.item_count (#2463) --- BaseClasses.py | 10 +++++-- docs/world api.md | 4 +-- worlds/alttp/StateHelpers.py | 8 +++--- worlds/checksfinder/Rules.py | 53 +++++++++++++++++------------------- worlds/kh2/logic.py | 2 +- worlds/noita/Rules.py | 4 +-- worlds/overcooked2/Logic.py | 4 +-- worlds/rogue_legacy/Rules.py | 4 +-- worlds/soe/Logic.py | 4 +-- worlds/spire/Rules.py | 4 +-- worlds/zillion/logic.py | 2 +- 11 files changed, 50 insertions(+), 49 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b25d998311..7965eb8b0d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -714,6 +714,7 @@ class CollectionState(): assert isinstance(event.item, Item), "tried to collect Event with no Item" self.collect(event.item, True, event) + # item name related def has(self, item: str, player: int, count: int = 1) -> bool: return self.prog_items[player][item] >= count @@ -728,6 +729,11 @@ class CollectionState(): def count(self, item: str, player: int) -> int: return self.prog_items[player][item] + def item_count(self, item: str, player: int) -> int: + Utils.deprecate("Use count instead.") + return self.count(item, player) + + # item name group related def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: found: int = 0 player_prog_items = self.prog_items[player] @@ -744,9 +750,7 @@ class CollectionState(): found += player_prog_items[item_name] return found - def item_count(self, item: str, player: int) -> int: - return self.prog_items[player][item] - + # Item related def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) diff --git a/docs/world api.md b/docs/world api.md index 4008c9c4dd..71710ac293 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -691,7 +691,7 @@ def generate_basic(self) -> None: ### Setting Rules ```python -from worlds.generic.Rules import add_rule, set_rule, forbid_item +from worlds.generic.Rules import add_rule, set_rule, forbid_item, add_item_rule from .items import get_item_type @@ -718,7 +718,7 @@ def set_rules(self) -> None: # require one item from an item group add_rule(self.multiworld.get_location("Chest3", self.player), lambda state: state.has_group("weapons", self.player)) - # state also has .item_count() for items, .has_any() and .has_all() for sets + # state also has .count() for items, .has_any() and .has_all() for multiple # and .count_group() for groups # set_rule is likely to be a bit faster than add_rule diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 95e31e5ba3..38ce00ef45 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -31,7 +31,7 @@ def can_shoot_arrows(state: CollectionState, player: int) -> bool: def has_triforce_pieces(state: CollectionState, player: int) -> bool: count = state.multiworld.treasure_hunt_count[player] - return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + return state.count('Triforce Piece', player) + state.count('Power Star', player) >= count def has_crystals(state: CollectionState, count: int, player: int) -> bool: @@ -60,9 +60,9 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int: def heart_count(state: CollectionState, player: int) -> int: # Warning: This only considers items that are marked as advancement items diff = state.multiworld.difficulty_requirements[player] - return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \ - + state.item_count('Sanctuary Heart Container', player) \ - + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \ + + state.count('Sanctuary Heart Container', player) \ + + min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + 3 # starting hearts diff --git a/worlds/checksfinder/Rules.py b/worlds/checksfinder/Rules.py index 4e12668798..38d7d77ad3 100644 --- a/worlds/checksfinder/Rules.py +++ b/worlds/checksfinder/Rules.py @@ -1,37 +1,34 @@ -from ..generic.Rules import set_rule, add_rule -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule +from BaseClasses import MultiWorld, CollectionState -class ChecksFinderLogic(LogicMixin): - - def _has_total(self, player: int, total: int): - return (self.item_count('Map Width', player)+self.item_count('Map Height', player)+ - self.item_count('Map Bombs', player)) >= total +def _has_total(state: CollectionState, player: int, total: int): + return (state.count('Map Width', player) + state.count('Map Height', player) + + state.count('Map Bombs', player)) >= total # Sets rules on entrances and advancements that are always applied def set_rules(world: MultiWorld, player: int): - set_rule(world.get_location(("Tile 6"), player), lambda state: state._has_total(player, 1)) - set_rule(world.get_location(("Tile 7"), player), lambda state: state._has_total(player, 2)) - set_rule(world.get_location(("Tile 8"), player), lambda state: state._has_total(player, 3)) - set_rule(world.get_location(("Tile 9"), player), lambda state: state._has_total(player, 4)) - set_rule(world.get_location(("Tile 10"), player), lambda state: state._has_total(player, 5)) - set_rule(world.get_location(("Tile 11"), player), lambda state: state._has_total(player, 6)) - set_rule(world.get_location(("Tile 12"), player), lambda state: state._has_total(player, 7)) - set_rule(world.get_location(("Tile 13"), player), lambda state: state._has_total(player, 8)) - set_rule(world.get_location(("Tile 14"), player), lambda state: state._has_total(player, 9)) - set_rule(world.get_location(("Tile 15"), player), lambda state: state._has_total(player, 10)) - set_rule(world.get_location(("Tile 16"), player), lambda state: state._has_total(player, 11)) - set_rule(world.get_location(("Tile 17"), player), lambda state: state._has_total(player, 12)) - set_rule(world.get_location(("Tile 18"), player), lambda state: state._has_total(player, 13)) - set_rule(world.get_location(("Tile 19"), player), lambda state: state._has_total(player, 14)) - set_rule(world.get_location(("Tile 20"), player), lambda state: state._has_total(player, 15)) - set_rule(world.get_location(("Tile 21"), player), lambda state: state._has_total(player, 16)) - set_rule(world.get_location(("Tile 22"), player), lambda state: state._has_total(player, 17)) - set_rule(world.get_location(("Tile 23"), player), lambda state: state._has_total(player, 18)) - set_rule(world.get_location(("Tile 24"), player), lambda state: state._has_total(player, 19)) - set_rule(world.get_location(("Tile 25"), player), lambda state: state._has_total(player, 20)) + set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1)) + set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2)) + set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3)) + set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4)) + set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5)) + set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6)) + set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7)) + set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8)) + set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9)) + set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10)) + set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11)) + set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12)) + set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13)) + set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14)) + set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15)) + set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16)) + set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17)) + set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18)) + set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19)) + set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20)) # Sets rules on completion condition diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py index 1c5883f5ce..10af4144a7 100644 --- a/worlds/kh2/logic.py +++ b/worlds/kh2/logic.py @@ -63,7 +63,7 @@ class KH2Logic(LogicMixin): ItemName.MembershipCard, ItemName.IceCream, ItemName.WaytotheDawn, ItemName.IdentityDisk, ItemName.NamineSketches}: - visit += self.item_count(item, player) + visit += self.count(item, player) return visit >= amount def kh_three_proof_unlocked(self, player): diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 808dd3a200..8190b80dc7 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -57,11 +57,11 @@ perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: - return sum(state.item_count(perk, player) for perk in perk_list) >= amount + return sum(state.count(perk, player) for perk in perk_list) >= amount def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: - return state.item_count("Orb", player) >= amount + return state.count("Orb", player) >= amount def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int): diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index d8468cb59a..20111aa01d 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -18,7 +18,7 @@ def has_requirements_for_level_access(state: CollectionState, level_name: str, p return state.has(level_name, player) # Must have enough stars to purchase level - star_count = state.item_count("Star", player) + state.item_count("Bonus Star", player) + star_count = state.count("Star", player) + state.count("Bonus Star", player) if star_count < required_star_count: return False @@ -64,7 +64,7 @@ def meets_requirements(state: CollectionState, name: str, stars: int, player: in total: float = 0.0 for (item_name, weight) in additive_reqs: - for _ in range(0, state.item_count(item_name, player)): + for _ in range(0, state.count(item_name, player)): total += weight if total >= 0.99: # be nice to rounding errors :) return True diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 90f6cc08b1..2fac8d5613 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -7,8 +7,8 @@ def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: def get_upgrade_count(state: CollectionState, player: int) -> int: - return state.item_count("Health Up", player) + state.item_count("Mana Up", player) + \ - state.item_count("Attack Up", player) + state.item_count("Magic Damage Up", player) + return state.count("Health Up", player) + state.count("Mana Up", player) + \ + state.count("Attack Up", player) + state.count("Magic Damage Up", player) def has_vendors(state: CollectionState, player: int) -> bool: diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index e464b7fd3b..fe5339c955 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -18,7 +18,7 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_ class LogicProtocol(Protocol): def has(self, name: str, player: int) -> bool: ... - def item_count(self, name: str, player: int) -> int: ... + def count(self, name: str, player: int) -> int: ... def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... @@ -35,7 +35,7 @@ class SecretOfEvermoreLogic(LogicMixin): for pvd in item.provides: if pvd[1] == progress: if self.has(item.name, player): - n += self.item_count(item.name, player) * pvd[0] + n += self.count(item.name, player) * pvd[0] if n >= max_count > 0: return n for rule in rules: diff --git a/worlds/spire/Rules.py b/worlds/spire/Rules.py index 7c8c1c0f3d..3c6f09b34d 100644 --- a/worlds/spire/Rules.py +++ b/worlds/spire/Rules.py @@ -5,11 +5,11 @@ from ..generic.Rules import set_rule class SpireLogic(LogicMixin): def _spire_has_relics(self, player: int, amount: int) -> bool: - count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player) + count: int = self.count("Relic", player) + self.count("Boss Relic", player) return count >= amount def _spire_has_cards(self, player: int, amount: int) -> bool: - count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player) + count = self.count("Card Draw", player) + self.count("Rare Card Draw", player) return count >= amount diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index e99867c742..12f1875b40 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -38,7 +38,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: ((item_name, count), (item_name, count), ...) """ - return tuple((item_name, cs.item_count(item_name, p)) for item_name in item_name_to_id) + return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] From 2f6b6838cd230c5b97b809ca8e420dd7d2657822 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 23 Nov 2023 17:38:57 -0600 Subject: [PATCH 207/327] The Messenger: more optimizations (#2451) More speed optimizations for The Messenger. Moves Figurines into their own region, so their complicated access rule only needs to be calculated once when doing a sweep. Removes a redundant loop for shop locations by just directly assigning the access rule in the class instead of retroactively. Reduces slot_data to only information that can't be derived, and removes some additional extraneous data. Removes some unused sets and lists. Removes a redundant event location, and increments the required_client_version to prevent clients that don't expect the new slot_data. Drops data version since it's going away soon anyways, to remove conflicts. --- worlds/messenger/__init__.py | 18 +++------- worlds/messenger/regions.py | 4 ++- worlds/messenger/rules.py | 52 ++++++++++++++--------------- worlds/messenger/subclasses.py | 14 +++----- worlds/messenger/test/test_logic.py | 1 - worlds/messenger/test/test_shop.py | 1 - 6 files changed, 38 insertions(+), 52 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 304b43cf53..f12687361b 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -62,8 +62,7 @@ class MessengerWorld(World): "Money Wrench", ], base_offset)} - data_version = 3 - required_client_version = (0, 4, 0) + required_client_version = (0, 4, 1) web = MessengerWeb() @@ -148,19 +147,12 @@ class MessengerWorld(World): MessengerOOBRules(self).set_messenger_rules() def fill_slot_data(self) -> Dict[str, Any]: - shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()} - figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()} - return { - "deathlink": self.options.death_link.value, - "goal": self.options.goal.current_key, - "music_box": self.options.music_box.value, - "required_seals": self.required_seals, - "mega_shards": self.options.shuffle_shards.value, - "logic": self.options.logic_level.current_key, - "shop": shop_prices, - "figures": figure_prices, + "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, + "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, "max_price": self.total_shards, + "required_seals": self.required_seals, + **self.options.as_dict("music_box", "death_link", "logic_level"), } def get_filler_item_name(self) -> str: diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 3a6c95bff5..43de4dd1f6 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -4,6 +4,7 @@ REGIONS: Dict[str, List[str]] = { "Menu": [], "Tower HQ": [], "The Shop": [], + "The Craftsman's Corner": [], "Tower of Time": [], "Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"], "Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"], @@ -82,7 +83,8 @@ MEGA_SHARDS: Dict[str, List[str]] = { REGION_CONNECTIONS: Dict[str, Set[str]] = { "Menu": {"Tower HQ"}, "Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", - "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"}, + "Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", + "The Craftsman's Corner", "Music Box"}, "Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"}, "Forlorn Temple": {"Catacombs", "Bamboo Creek"}, "Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"}, diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 876acd42c1..793de50afb 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,26 +1,32 @@ -from typing import Callable, Dict, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING from BaseClasses import CollectionState -from worlds.generic.Rules import add_rule, allow_self_locking_items +from worlds.generic.Rules import add_rule, allow_self_locking_items, CollectionRule from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility if TYPE_CHECKING: from . import MessengerWorld -else: - MessengerWorld = object class MessengerRules: player: int - world: MessengerWorld - region_rules: Dict[str, Callable[[CollectionState], bool]] - location_rules: Dict[str, Callable[[CollectionState], bool]] + world: "MessengerWorld" + region_rules: Dict[str, CollectionRule] + location_rules: Dict[str, CollectionRule] + maximum_price: int + required_seals: int - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.player = world.player self.world = world + # these locations are at the top of the shop tree, and the entire shop tree needs to be purchased + maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost + + world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost) + self.maximum_price = min(maximum_price, world.total_shards) + self.required_seals = max(1, world.required_seals) + self.region_rules = { "Ninja Village": self.has_wingsuit, "Autumn Hills": self.has_wingsuit, @@ -36,9 +42,9 @@ class MessengerRules: "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player) and self.can_dboost(state), "Glacial Peak": self.has_vertical, "Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state), - "Music Box": lambda state: (state.has_all(set(NOTES), self.player) - or state.has("Power Seal", self.player, max(1, self.world.required_seals))) - and self.has_dart(state), + "Music Box": lambda state: (state.has_all(NOTES, self.player) + or self.has_enough_seals(state)) and self.has_dart(state), + "The Craftsman's Corner": lambda state: state.has("Money Wrench", self.player) and self.can_shop(state), } self.location_rules = { @@ -110,7 +116,7 @@ class MessengerRules: return self.has_wingsuit(state) or self.has_dart(state) def has_enough_seals(self, state: CollectionState) -> bool: - return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals) + return state.has("Power Seal", self.player, self.required_seals) def can_destroy_projectiles(self, state: CollectionState) -> bool: return state.has("Strike of the Ninja", self.player) @@ -127,9 +133,7 @@ class MessengerRules: return True def can_shop(self, state: CollectionState) -> bool: - prices = self.world.shop_prices - most_expensive_loc = max(prices, key=prices.get) - return state.can_reach(f"The Shop - {most_expensive_loc}", "Location", self.player) + return state.has("Shards", self.player, self.maximum_price) def set_messenger_rules(self) -> None: multiworld = self.world.multiworld @@ -141,9 +145,6 @@ class MessengerRules: for loc in region.locations: if loc.name in self.location_rules: loc.access_rule = self.location_rules[loc.name] - if region.name == "The Shop": - for loc in region.locations: - loc.access_rule = loc.can_afford multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) if multiworld.accessibility[self.player]: # not locations accessibility @@ -151,9 +152,9 @@ class MessengerRules: class MessengerHardRules(MessengerRules): - extra_rules: Dict[str, Callable[[CollectionState], bool]] + extra_rules: Dict[str, CollectionRule] - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: super().__init__(world) self.region_rules.update({ @@ -162,7 +163,7 @@ class MessengerHardRules(MessengerRules): "Catacombs": self.has_vertical, "Bamboo Creek": self.has_vertical, "Riviere Turquoise": self.true, - "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player), + "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(PHOBEKINS, self.player), "Searing Crags Upper": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state) or self.has_vertical(state), "Glacial Peak": lambda state: self.can_destroy_projectiles(state) or self.has_windmill(state) @@ -215,14 +216,15 @@ class MessengerHardRules(MessengerRules): class MessengerOOBRules(MessengerRules): - def __init__(self, world: MessengerWorld) -> None: + def __init__(self, world: "MessengerWorld") -> None: self.world = world self.player = world.player + self.required_seals = max(1, world.required_seals) self.region_rules = { "Elemental Skylands": lambda state: state.has_any({"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player), - "Music Box": lambda state: state.has_all(set(NOTES), self.player) + "Music Box": lambda state: state.has_all(set(NOTES), self.player) or self.has_enough_seals(state), } self.location_rules = { @@ -238,16 +240,14 @@ class MessengerOOBRules(MessengerRules): "Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"}, self.player), "Tower of Time Seal - Time Waster": self.has_dart, - "Shop Chest": self.has_enough_seals } def set_messenger_rules(self) -> None: super().set_messenger_rules() - self.world.multiworld.completion_condition[self.player] = lambda state: True self.world.options.accessibility.value = MessengerAccessibility.option_minimal -def set_self_locking_items(world: MessengerWorld, player: int) -> None: +def set_self_locking_items(world: "MessengerWorld", player: int) -> None: multiworld = world.multiworld # do the ones for seal shuffle on and off first diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 0c04bc015c..b6a0b80b21 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -3,7 +3,6 @@ from typing import Optional, TYPE_CHECKING, cast from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS -from .options import Goal from .regions import MEGA_SHARDS, REGIONS, SEALS from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS @@ -19,8 +18,10 @@ class MessengerRegion(Region): if self.name == "The Shop": shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] for shop_loc in SHOP_ITEMS} - shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}) self.add_locations(shop_locations, MessengerShopLocation) + elif self.name == "The Craftsman's Corner": + self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES}, + MessengerLocation) elif self.name == "Tower HQ": locations.append("Money Wrench") if world.options.shuffle_seals and self.name in SEALS: @@ -46,10 +47,6 @@ class MessengerShopLocation(MessengerLocation): def cost(self) -> int: name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player]) - # short circuit figurines which all require demon's bane be purchased, but nothing else - if "Figurine" in name: - return world.figurine_prices[name] +\ - cast(MessengerShopLocation, world.multiworld.get_location("The Shop - Demon's Bane", self.player)).cost shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: prereq_cost = 0 @@ -65,12 +62,9 @@ class MessengerShopLocation(MessengerLocation): return world.shop_prices[name] + prereq_cost return world.shop_prices[name] - def can_afford(self, state: CollectionState) -> bool: + def access_rule(self, state: CollectionState) -> bool: world = cast("MessengerWorld", state.multiworld.worlds[self.player]) can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards)) - if "Figurine" in self.name: - can_afford = state.has("Money Wrench", self.player) and can_afford\ - and state.can_reach("Money Wrench", "Location", self.player) return can_afford diff --git a/worlds/messenger/test/test_logic.py b/worlds/messenger/test/test_logic.py index 53ea929922..15df89b920 100644 --- a/worlds/messenger/test/test_logic.py +++ b/worlds/messenger/test/test_logic.py @@ -111,4 +111,3 @@ class NoLogicTest(MessengerTestBase): for loc in all_locations: with self.subTest("Default unreachables", location=loc): self.assertFalse(self.can_reach_location(loc)) - self.assertBeatable(True) diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index bfd3b417a8..afb1b32b88 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -106,6 +106,5 @@ class PlandoTest(MessengerTestBase): elif loc == "Demon Hive Figurine": self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"]) - self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) From 205c6acb495bdc6c213abfe77e134c52f5ca2339 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:59:41 +0100 Subject: [PATCH 208/327] lufia2ac: fix client behavior at max blue chests combined with party member or capsule monster shuffle (#2478) When option combinations at (or near) the maximum location count were used, the client could trip over a wrongly coded limit and stop sending checks. --- worlds/lufia2ac/Client.py | 2 +- worlds/lufia2ac/Locations.py | 2 +- worlds/lufia2ac/Options.py | 1 + worlds/lufia2ac/__init__.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index bc0cb6a7d8..ac0de19bfd 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -113,7 +113,7 @@ class L2ACSNIClient(SNIClient): }], }]) - total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end) + total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.overall_max) snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little")) location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)] diff --git a/worlds/lufia2ac/Locations.py b/worlds/lufia2ac/Locations.py index 2f433f72e2..510ecbbbf7 100644 --- a/worlds/lufia2ac/Locations.py +++ b/worlds/lufia2ac/Locations.py @@ -6,7 +6,7 @@ from .Options import BlueChestCount start_id: int = 0xAC0000 l2ac_location_name_to_id: Dict[str, int] = { - **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)}, + **{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.overall_max)}, **{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)}, "Boss": start_id + 0x01C2, } diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 783da8e407..419532cded 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -121,6 +121,7 @@ class BlueChestCount(Range): range_start = 10 range_end = 100 default = 25 + overall_max = range_end + 7 + 6 # Have to account for capsule monster and party member items class Boss(RandomGroupsChoice): diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 8f9b8d5823..9bd436fa0d 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -66,7 +66,7 @@ class L2ACWorld(World): "Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER}, } data_version: ClassVar[int] = 2 - required_client_version: Tuple[int, int, int] = (0, 4, 2) + required_client_version: Tuple[int, int, int] = (0, 4, 4) # L2ACWorld specific properties rom_name: bytearray From e93842a52cf853b04f759b1138db7813b233ab62 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:27:03 +0100 Subject: [PATCH 209/327] =?UTF-8?q?The=20Witness:=20Big=E2=84=A2=20new?= =?UTF-8?q?=E2=84=A2=20content=20update=E2=84=A2=20(#2114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blastron Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/Options.py | 126 +++-- worlds/witness/WitnessItems.txt | 115 +++- worlds/witness/WitnessLogic.txt | 182 +++--- worlds/witness/WitnessLogicExpert.txt | 198 ++++--- worlds/witness/WitnessLogicVanilla.txt | 180 +++--- worlds/witness/__init__.py | 227 +++++--- worlds/witness/hints.py | 158 ++++-- worlds/witness/items.py | 68 ++- worlds/witness/locations.py | 277 +++++----- worlds/witness/player_logic.py | 517 +++++++++--------- worlds/witness/regions.py | 136 +++-- worlds/witness/rules.py | 383 +++++++------ .../witness/settings/Door_Panel_Shuffle.txt | 31 -- worlds/witness/settings/Door_Shuffle/Boat.txt | 2 + .../Complex_Additional_Panels.txt | 25 + .../Door_Shuffle/Complex_Door_Panels.txt | 38 ++ .../Complex_Doors.txt} | 21 +- .../Door_Shuffle/Elevators_Come_To_You.txt | 11 + .../Door_Shuffle/Simple_Additional_Panels.txt | 11 + .../Simple_Doors.txt} | 57 +- .../settings/Door_Shuffle/Simple_Panels.txt | 22 + worlds/witness/settings/Doors_Max.txt | 211 ------- worlds/witness/settings/EP_Shuffle/EP_All.txt | 270 ++++----- .../witness/settings/EP_Shuffle/EP_Easy.txt | 31 +- .../settings/EP_Shuffle/EP_NoCavesEPs.txt | 5 - .../settings/EP_Shuffle/EP_NoEclipse.txt | 4 +- .../settings/EP_Shuffle/EP_NoMountainEPs.txt | 4 - .../witness/settings/EP_Shuffle/EP_Sides.txt | 67 +-- .../witness/settings/EP_Shuffle/EP_Videos.txt | 6 - worlds/witness/settings/Early_Caves.txt | 6 + .../{Early_UTM.txt => Early_Caves_Start.txt} | 4 +- .../{ => Exclusions}/Disable_Unrandomized.txt | 62 ++- .../witness/settings/Exclusions/Discards.txt | 15 + worlds/witness/settings/Exclusions/Vaults.txt | 31 ++ .../settings/Postgame/Beyond_Challenge.txt | 4 + .../Postgame/Bottom_Floor_Discard.txt | 2 + .../Bottom_Floor_Discard_NonDoors.txt | 6 + worlds/witness/settings/Postgame/Caves.txt | 65 +++ .../settings/Postgame/Challenge_Vault_Box.txt | 3 + .../settings/Postgame/Mountain_Lower.txt | 27 + .../settings/Postgame/Mountain_Upper.txt | 41 ++ .../settings/Postgame/Path_To_Challenge.txt | 30 + worlds/witness/static_logic.py | 78 +-- worlds/witness/utils.py | 148 +++-- 44 files changed, 2223 insertions(+), 1682 deletions(-) delete mode 100644 worlds/witness/settings/Door_Panel_Shuffle.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Boat.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt rename worlds/witness/settings/{Doors_Complex.txt => Door_Shuffle/Complex_Doors.txt} (94%) create mode 100644 worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt create mode 100644 worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt rename worlds/witness/settings/{Doors_Simple.txt => Door_Shuffle/Simple_Doors.txt} (74%) create mode 100644 worlds/witness/settings/Door_Shuffle/Simple_Panels.txt delete mode 100644 worlds/witness/settings/Doors_Max.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt delete mode 100644 worlds/witness/settings/EP_Shuffle/EP_Videos.txt create mode 100644 worlds/witness/settings/Early_Caves.txt rename worlds/witness/settings/{Early_UTM.txt => Early_Caves_Start.txt} (65%) rename worlds/witness/settings/{ => Exclusions}/Disable_Unrandomized.txt (70%) create mode 100644 worlds/witness/settings/Exclusions/Discards.txt create mode 100644 worlds/witness/settings/Exclusions/Vaults.txt create mode 100644 worlds/witness/settings/Postgame/Beyond_Challenge.txt create mode 100644 worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt create mode 100644 worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt create mode 100644 worlds/witness/settings/Postgame/Caves.txt create mode 100644 worlds/witness/settings/Postgame/Challenge_Vault_Box.txt create mode 100644 worlds/witness/settings/Postgame/Mountain_Lower.txt create mode 100644 worlds/witness/settings/Postgame/Mountain_Upper.txt create mode 100644 worlds/witness/settings/Postgame/Path_To_Challenge.txt diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index b7364b5e70..4c4b4f7626 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -1,23 +1,25 @@ -from typing import Dict, Union -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, Range, Choice +from dataclasses import dataclass +from Options import Toggle, DefaultOnToggle, Range, Choice, PerGameCommonOptions -# class HardMode(Toggle): -# "Play the randomizer in hardmode" -# display_name = "Hard Mode" - class DisableNonRandomizedPuzzles(Toggle): """Disables puzzles that cannot be randomized. This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. - The lasers for those areas will be activated as you solve optional puzzles throughout the island.""" + The lasers for those areas will activate as you solve optional puzzles, such as Discarded Panels. + Additionally, the panels activating Monastery Laser and Jungle Popup Wall will be on from the start.""" display_name = "Disable non randomized puzzles" -class EarlySecretArea(Toggle): - """Opens the Mountainside shortcut to the Caves from the start. - (Otherwise known as "UTM", "Caves" or the "Challenge Area")""" +class EarlyCaves(Choice): + """Adds an item that opens the Caves Shortcuts to Swamp and Mountain, + allowing early access to the Caves even if you are not playing a remote Door Shuffle mode. + You can either add this item to the pool to be found on one of your randomized checks, + or you can outright start with it and have immediate access to the Caves. + If you choose "add_to_pool" and you are already playing a remote Door Shuffle mode, this setting will do nothing.""" display_name = "Early Caves" + option_off = 0 + option_add_to_pool = 1 + option_starting_inventory = 2 class ShuffleSymbols(DefaultOnToggle): @@ -34,27 +36,41 @@ class ShuffleLasers(Toggle): class ShuffleDoors(Choice): - """If on, opening doors will require their respective "keys". - If set to "panels", those keys will unlock the panels on doors. - In "doors_simple" and "doors_complex", the doors will magically open by themselves upon receiving the key. - The last option, "max", is a combination of "doors_complex" and "panels".""" + """If on, opening doors, moving bridges etc. will require a "key". + If set to "panels", the panel on the door will be locked until receiving its corresponding key. + If set to "doors", the door will open immediately upon receiving its key. Door panels are added as location checks. + "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".""" display_name = "Shuffle Doors" - option_none = 0 + option_off = 0 option_panels = 1 - option_doors_simple = 2 - option_doors_complex = 3 - option_max = 4 + option_doors = 2 + option_mixed = 3 + + +class DoorGroupings(Choice): + """If set to "none", there will be one key for every door, resulting in up to 120 keys being added to the item pool. + If set to "regional", all doors in the same general region will open at once with a single key, + reducing the amount of door items and complexity.""" + display_name = "Door Groupings" + option_off = 0 + option_regional = 1 + + +class ShuffleBoat(DefaultOnToggle): + """If set, adds a "Boat" item to the item pool. Before receiving this item, you will not be able to use the boat.""" + display_name = "Shuffle Boat" class ShuffleDiscardedPanels(Toggle): """Add Discarded Panels into the location pool. - Solving certain Discarded Panels may still be necessary to beat the game, even if this is off.""" + Solving certain Discarded Panels may still be necessary to beat the game, even if this is off - The main example + of this being the alternate activation triggers in disable_non_randomized.""" display_name = "Shuffle Discarded Panels" class ShuffleVaultBoxes(Toggle): - """Vault Boxes will have items on them.""" + """Add Vault Boxes to the location pool.""" display_name = "Shuffle Vault Boxes" @@ -132,6 +148,12 @@ class ChallengeLasers(Range): default = 11 +class ElevatorsComeToYou(Toggle): + """If true, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. + This does actually affect logic as it allows unintended backwards / early access into these areas.""" + display_name = "All Bridges & Elevators come to you" + + class TrapPercentage(Range): """Replaces junk items with traps, at the specified rate.""" display_name = "Trap Percentage" @@ -150,8 +172,8 @@ class PuzzleSkipAmount(Range): class HintAmount(Range): - """Adds hints to Audio Logs. Hints will have the same number of duplicates, as many as will fit. Remaining Audio - Logs will have junk hints.""" + """Adds hints to Audio Logs. If set to a low amount, up to 2 additional duplicates of each hint will be added. + Remaining Audio Logs will have junk hints.""" display_name = "Hints on Audio Logs" range_start = 0 range_end = 49 @@ -164,38 +186,26 @@ class DeathLink(Toggle): display_name = "Death Link" -the_witness_options: Dict[str, type] = { - "puzzle_randomization": PuzzleRandomization, - "shuffle_symbols": ShuffleSymbols, - "shuffle_doors": ShuffleDoors, - "shuffle_lasers": ShuffleLasers, - "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, - "shuffle_discarded_panels": ShuffleDiscardedPanels, - "shuffle_vault_boxes": ShuffleVaultBoxes, - "shuffle_EPs": ShuffleEnvironmentalPuzzles, - "EP_difficulty": EnvironmentalPuzzlesDifficulty, - "shuffle_postgame": ShufflePostgame, - "victory_condition": VictoryCondition, - "mountain_lasers": MountainLasers, - "challenge_lasers": ChallengeLasers, - "early_secret_area": EarlySecretArea, - "trap_percentage": TrapPercentage, - "puzzle_skip_amount": PuzzleSkipAmount, - "hint_amount": HintAmount, - "death_link": DeathLink, -} - - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[bool, int]: - option = getattr(world, name, None) - - if option is None: - return 0 - - if issubclass(the_witness_options[name], Toggle) or issubclass(the_witness_options[name], DefaultOnToggle): - return bool(option[player].value) - return option[player].value +@dataclass +class TheWitnessOptions(PerGameCommonOptions): + puzzle_randomization: PuzzleRandomization + shuffle_symbols: ShuffleSymbols + shuffle_doors: ShuffleDoors + door_groupings: DoorGroupings + shuffle_boat: ShuffleBoat + shuffle_lasers: ShuffleLasers + disable_non_randomized_puzzles: DisableNonRandomizedPuzzles + shuffle_discarded_panels: ShuffleDiscardedPanels + shuffle_vault_boxes: ShuffleVaultBoxes + shuffle_EPs: ShuffleEnvironmentalPuzzles + EP_difficulty: EnvironmentalPuzzlesDifficulty + shuffle_postgame: ShufflePostgame + victory_condition: VictoryCondition + mountain_lasers: MountainLasers + challenge_lasers: ChallengeLasers + early_caves: EarlyCaves + elevators_come_to_you: ElevatorsComeToYou + trap_percentage: TrapPercentage + puzzle_skip_amount: PuzzleSkipAmount + hint_amount: HintAmount + death_link: DeathLink diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 71ffe276a6..750d6bd4eb 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -37,22 +37,42 @@ Jokes: Doors: 1100 - Glass Factory Entry (Panel) - 0x01A54 +1101 - Tutorial Outpost Entry (Panel) - 0x0A171 +1102 - Tutorial Outpost Exit (Panel) - 0x04CA4 1105 - Symmetry Island Lower (Panel) - 0x000B0 1107 - Symmetry Island Upper (Panel) - 0x1C349 1110 - Desert Light Room Entry (Panel) - 0x0C339 1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +1112 - Desert Light Control (Panel) - 0x09FAA +1113 - Desert Flood Room Entry (Panel) - 0x0A249 +1115 - Quarry Elevator Control (Panel) - 0x17CC4 +1117 - Quarry Entry 1 (Panel) - 0x09E57 +1118 - Quarry Entry 2 (Panel) - 0x17C09 1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59 1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676 1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 +1129 - Quarry Boathouse Hook Control (Panel) - 0x275FA 1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC +1140 - Keep Hedge Maze 1 (Panel) - 0x00139 +1142 - Keep Hedge Maze 2 (Panel) - 0x019DC +1144 - Keep Hedge Maze 3 (Panel) - 0x019E7 +1146 - Keep Hedge Maze 4 (Panel) - 0x01A0F 1150 - Monastery Entry Left (Panel) - 0x00B10 1151 - Monastery Entry Right (Panel) - 0x00C92 -1162 - Town Tinted Glass Door (Panel) - 0x28998 +1156 - Monastery Shutters Control (Panel) - 0x09D9B +1162 - Town RGB House Entry (Panel) - 0x28998 1163 - Town Church Entry (Panel) - 0x28A0D -1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 -1169 - Windmill Entry (Panel) - 0x17F5F +1164 - Town RGB Control (Panel) - 0x334D8 +1166 - Town Maze Stairs (Panel) - 0x28A79 +1167 - Town Maze Rooftop Bridge (Panel) - 0x2896A +1169 - Town Windmill Entry (Panel) - 0x17F5F +1172 - Town Cargo Box Entry (Panel) - 0x0A0C8 +1182 - Windmill Turn Control (Panel) - 0x17D02 +1184 - Theater Entry (Panel) - 0x17F89 +1185 - Theater Video Input (Panel) - 0x00815 +1189 - Theater Exit (Panel) - 0x0A168,0x33AB2 1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886 1202 - Treehouse Third Door (Panel) - 0x0A182 1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC @@ -61,10 +81,24 @@ Doors: 1180 - Bunker Entry (Panel) - 0x17C2E 1183 - Bunker Tinted Glass Door (Panel) - 0x0A099 1186 - Bunker Elevator Control (Panel) - 0x0A079 +1188 - Bunker Drop-Down Door Controls (Panel) - 0x34BC5,0x34BC6 1190 - Swamp Entry (Panel) - 0x0056E 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 +1194 - Swamp Platform Shortcut (Panel) - 0x17C0D 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 -1197 - Swamp Maze Control (Panel) - 0x17C0A,0x17E07 +1196 - Swamp Long Bridge (Panel) - 0x17E2B +1197 - Swamp Maze Controls (Panel) - 0x17C0A,0x17E07 +1220 - Mountain Floor 1 Light Bridge (Panel) - 0x09E39 +1225 - Mountain Floor 2 Light Bridge Near (Panel) - 0x09E86 +1230 - Mountain Floor 2 Light Bridge Far (Panel) - 0x09ED8 +1235 - Mountain Floor 2 Elevator Control (Panel) - 0x09EEB +1240 - Caves Entry (Panel) - 0x00FF8 +1242 - Caves Elevator Controls (Panel) - 0x335AB,0x335AC,0x3369D +1245 - Challenge Entry (Panel) - 0x0A16E +1250 - Tunnels Entry (Panel) - 0x039B4 +1255 - Tunnels Town Shortcut (Panel) - 0x09E85 + + 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 1400 - Caves Mountain Shortcut (Door) - 0x2D73F @@ -82,6 +116,7 @@ Doors: 1624 - Desert Pond Room Entry (Door) - 0x0C2C3 1627 - Desert Flood Room Entry (Door) - 0x0A24B 1630 - Desert Elevator Room Entry (Door) - 0x0C316 +1631 - Desert Elevator (Door) - 0x01317 1633 - Quarry Entry 1 (Door) - 0x09D6F 1636 - Quarry Entry 2 (Door) - 0x17C07 1639 - Quarry Stoneworks Entry (Door) - 0x02010 @@ -109,13 +144,13 @@ Doors: 1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40 1702 - Keep Shadows Shortcut (Door) - 0x09E3D 1705 - Keep Tower Shortcut (Door) - 0x04F8F -1708 - Monastery Shortcut (Door) - 0x0364E +1708 - Monastery Laser Shortcut (Door) - 0x0364E 1711 - Monastery Entry Inner (Door) - 0x0C128 1714 - Monastery Entry Outer (Door) - 0x0C153 1717 - Monastery Garden Entry (Door) - 0x03750 1718 - Town Cargo Box Entry (Door) - 0x0A0C9 1720 - Town Wooden Roof Stairs (Door) - 0x034F5 -1723 - Town Tinted Glass Door - 0x28A61 +1723 - Town RGB House Entry (Door) - 0x28A61 1726 - Town Church Entry (Door) - 0x03BB0 1729 - Town Maze Stairs (Door) - 0x28AA2 1732 - Town Windmill Entry (Door) - 0x1845B @@ -129,7 +164,7 @@ Doors: 1756 - Theater Exit Right (Door) - 0x3CCDF 1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B 1760 - Jungle Popup Wall (Door) - 0x1475B -1762 - River Monastery Shortcut (Door) - 0x0CF2A +1762 - River Monastery Garden Shortcut (Door) - 0x0CF2A 1765 - Bunker Entry (Door) - 0x0C2A4 1768 - Bunker Tinted Glass Door - 0x17C79 1771 - Bunker UV Room Entry (Door) - 0x0C2A3 @@ -166,36 +201,66 @@ Doors: 1870 - Tunnels Town Shortcut (Door) - 0x09E87 1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 +1904 - Glass Factory Doors - 0x0D7ED,0x01A29 1906 - Symmetry Island Doors - 0x17F3E,0x18269 1909 - Orchard Gates - 0x03313,0x03307 -1912 - Desert Doors - 0x09FEE,0x0C2C3,0x0A24B,0x0C316 -1915 - Quarry Main Entry - 0x09D6F,0x17C07 -1918 - Quarry Stoneworks Shortcuts - 0x17CE8,0x0368A,0x275FF -1921 - Quarry Boathouse Barriers - 0x17C50,0x3865F -1924 - Shadows Laser Room Door - 0x194B2,0x19665 -1927 - Shadows Barriers - 0x19865,0x0A2DF,0x1855B,0x19ADE +1912 - Desert Doors & Elevator - 0x09FEE,0x0C2C3,0x0A24B,0x0C316,0x01317 +1915 - Quarry Entry Doors - 0x09D6F,0x17C07 +1918 - Quarry Stoneworks Doors - 0x02010,0x275FF,0x17CE8,0x0368A +1921 - Quarry Boathouse Doors - 0x17C50,0x3865F,0x2769B,0x27163 +1924 - Shadows Laser Room Doors - 0x194B2,0x19665 +1927 - Shadows Lower Doors - 0x19865,0x0A2DF,0x1855B,0x19ADE,0x19B24 1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E 1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 1936 - Keep Shortcuts - 0x09E3D,0x04F8F -1939 - Monastery Entry - 0x0C128,0x0C153 -1942 - Monastery Shortcuts - 0x0364E,0x03750 -1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B +1939 - Monastery Entry Doors - 0x0C128,0x0C153 +1942 - Monastery Shortcuts - 0x0364E,0x03750,0x0CF2A +1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x2897B 1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C -1951 - Theater Exit - 0x0A16D,0x3CCDF -1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A +1951 - Windmill & Theater Doors - 0x0A16D,0x3CCDF,0x1845B,0x17F88 +1954 - Jungle Doors - 0x3873B,0x1475B 1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D -1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 +1960 - Swamp Doors - 0x00C1C,0x184B7,0x18507 +1961 - Swamp Shortcuts - 0x38AE6,0x2D880 1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 -1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 -1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D -1981 - Caves Doors to Challenge - 0x019A5,0x0A19A -1984 - Caves Exits to Main Island - 0x2D859,0x2D73F -1987 - Tunnels Doors - 0x27739,0x27263,0x09E87 +1969 - Treehouse Upper Doors - 0x0C323,0x0C32D +1975 - Mountain Floor 1 & 2 Doors - 0x09E54,0x09FFB,0x09EDD,0x09E07 +1978 - Mountain Bottom Floor Doors - 0x0C141,0x17F33,0x09F89 +1981 - Caves Doors - 0x019A5,0x0A19A,0x2D77D +1984 - Caves Shortcuts - 0x2D859,0x2D73F +1987 - Tunnels Doors - 0x27739,0x27263,0x09E87,0x0348A + +2000 - Desert Control Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +2005 - Quarry Stoneworks Control Panels - 0x03678,0x03676,0x03679,0x03675 +2010 - Quarry Boathouse Control Panels - 0x03852,0x03858,0x275FA +2015 - Town Control Panels - 0x2896A,0x334D8 +2020 - Windmill & Theater Control Panels - 0x17D02,0x00815 +2025 - Bunker Control Panels - 0x34BC5,0x34BC6,0x0A079 +2030 - Swamp Control Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07 +2035 - Mountain & Caves Control Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB,0x335AB,0x335AC,0x3369D + +2100 - Symmetry Island Panels - 0x1C349,0x000B0 +2101 - Tutorial Outpost Panels - 0x0A171,0x04CA4 +2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249 +2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4 +2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675 +2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA +2122 - Keep Hedge Maze Panels - 0x00139,0x019DC,0x019E7,0x01A0F +2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 +2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8 +2135 - Town Maze Panels - 0x2896A,0x28A79 +2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2 +2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF +2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E +2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E +2160 - Mountain Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB +2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC +2170 - Tunnels Panels - 0x09E85,0x039B4 Lasers: 1500 - Symmetry Laser - 0x00509 -1501 - Desert Laser - 0x012FB,0x01317 +1501 - Desert Laser - 0x012FB 1502 - Quarry Laser - 0x01539 1503 - Shadows Laser - 0x181B3 1504 - Keep Laser - 0x014BB diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index dffdc1a701..acfbe8c14e 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 159516 - 0x334A3 (Path EP) - True - True 159500 - 0x035C7 (Tractor EP) - True - True +Outside Tutorial Vault (Outside Tutorial): +158651 - 0x03481 (Vault Box) - True - True + Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: 158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Triangles Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots +158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots 158700 - 0x0360D (Laser Panel) - 0x00A68 - True Laser - 0x00509 (Laser) - 0x0360D 159001 - 0x03367 (Glass Factory Black Line EP) - True - True @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares 158603 - 0x17CF0 (Discard) - True - Triangles 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares 158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A -Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay: 158123 - 0x275ED (Side Exit Panel) - True - True Door - 0x275FF (Side Exit) - 0x275ED 158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser 158145 - 0x17CAC (Roof Exit Panel) - True - True Door - 0x17CE8 (Roof Exit) - 0x17CAC -Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay: 158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser 158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser 158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser 158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser 158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots & Eraser 158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser + +Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679: 158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A: 158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser 158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser 158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser -158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: +Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6: Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: 158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665: 158170 - 0x334DB (Door Timer Outside) - True - True -Door - 0x19B24 (Timed Door) - 0x334DB +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: 158187 - 0x334DC (Door Timer Inside) - True - True 158188 - 0x198B5 (Intro 1) - True - True 158189 - 0x198BD (Intro 2) - 0x198B5 - True -158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True +158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True Door - 0x19865 (Quarry Barrier) - 0x198BF Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF 158191 - 0x19771 (Intro 4) - 0x198BF - True @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4: +158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots +Door - 0x17BB4 (Vault Door) - 0x00AFB 158605 - 0x17D28 (Discard) - True - Triangles 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots 158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers -Door - 0x28A61 (Tinted Glass Door) - 0x28998 +158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (RGB House Entry) - 0x28998 158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars Door - 0x03BB0 (Church Entry) - 0x28A0D -158228 - 0x28A79 (Maze Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River Vault - 0x15287: +158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True +Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots +Door - 0x15287 (Vault Door) - 0x15ADD 159110 - 0x03AC5 (Green Leaf Moss EP) - True - True 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares +158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares Door - 0x0C2A3 (UV Room Entry) - 0x0A01F Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers Door - 0x184B7 (Between Bridges First Door) - 0x00990 158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers -158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True -Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B: +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay: Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A Swamp Purple Underwater (Swamp): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: Door - 0x0A181 (Third Door) - 0x0A182 Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: -158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True +158356 - 0x2700B (Laser House Door Timer Outside) - True - True Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: 158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ Treehouse Green Bridge Left House (Treehouse): 159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: -Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Triangles @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Triangles -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares & Dots -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots +Door - 0x00085 (Vault Door) - 0x002A6 159301 - 0x335AE (Cloud Cycle EP) - True - True 159325 - 0x33505 (Bush EP) - True - True 159335 - 0x03C07 (Apparent River EP) - True - True +Mountainside Vault (Mountainside): +158666 - 0x03542 (Vault Box) - True - True + Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Dots @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots 158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers + +Mountain Top Layer At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): 158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser -Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8: 158432 - 0x09FCC (Far Row 1) - True - Dots 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay: 158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser 158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser 158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True +159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True Door - 0x09F89 (Exit) - 0x09FDA -Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Triangles & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,7 +1046,7 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75: 158499 - 0x0A332 (Start Timer) - 11 Lasers - True 158500 - 0x0088E (Small Basic) - 0x0A332 - True 158501 - 0x00BAF (Big Basic) - 0x0088E - True @@ -1041,11 +1066,14 @@ Challenge (Challenge) - Tunnels - 0x0348A: 158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles 158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry 158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: 159042 - 0x22106 (Desert EP) - True - True 159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True 159231 - 0x28B29 (Shipwreck Green EP) - True - True @@ -1093,34 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True - -Lasers (Lasers) - Entry - True: +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/WitnessLogicExpert.txt b/worlds/witness/WitnessLogicExpert.txt index 581167cc45..b1d9b8e30e 100644 --- a/worlds/witness/WitnessLogicExpert.txt +++ b/worlds/witness/WitnessLogicExpert.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - True: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Full Dots & Squares & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Full Dots & Squares & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots & Full Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots & Full Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots & Full Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 159516 - 0x334A3 (Path EP) - True - True 159500 - 0x035C7 (Tractor EP) - True - True +Outside Tutorial Vault (Outside Tutorial): +158651 - 0x03481 (Vault Box) - True - True + Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: 158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots & Triangles Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Arrows Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots +158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots 158700 - 0x0360D (Laser Panel) - 0x00A68 - True Laser - 0x00509 (Laser) - 0x0360D 159001 - 0x03367 (Glass Factory Black Line EP) - True - True @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Full Dots & Stars & Stars + Same Colored Symbol & Eraser & Triangles & Shapers & Negative Shapers & Colored Squares +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Arrows 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Squares & Black/White Squares & Triangles 158603 - 0x17CF0 (Discard) - True - Arrows 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Triangles & Stars & Stars + Same Colored Symbol @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Triangles Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Squares & Black/White Squares & Stars & Stars + Same Colored Symbol 158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Triangles Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A -Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay: 158123 - 0x275ED (Side Exit Panel) - True - True Door - 0x275FF (Side Exit) - 0x275ED 158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser 158145 - 0x17CAC (Roof Exit Panel) - True - True Door - 0x17CE8 (Roof Exit) - 0x17CAC -Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay: 158125 - 0x00E0C (Lower Row 1) - True - Triangles & Eraser 158126 - 0x01489 (Lower Row 2) - 0x00E0C - Triangles & Eraser 158127 - 0x0148A (Lower Row 3) - 0x01489 - Triangles & Eraser 158128 - 0x014D9 (Lower Row 4) - 0x0148A - Triangles & Eraser 158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Triangles & Eraser 158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Triangles & Eraser + +Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679: 158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A: 158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser 158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser 158134 - 0x00557 (Upper Row 1) - True - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Squares & Colored Squares & Eraser & Stars & Stars + Same Colored Symbol -158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Squares & Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Squares & Black/White Squares & Dots & Full Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Squares & Colored Squares & Triangles & Eraser & Stars & Stars + Same Colored Symbol @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: +Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6: Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: 158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser & Negative Shapers @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers & Negative Shapers & Stars + Same Colored Symbol 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665: 158170 - 0x334DB (Door Timer Outside) - True - True -Door - 0x19B24 (Timed Door) - 0x334DB +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: 158187 - 0x334DC (Door Timer Inside) - True - True 158188 - 0x198B5 (Intro 1) - True - True 158189 - 0x198BD (Intro 2) - 0x198B5 - True -158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True +158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True Door - 0x19865 (Quarry Barrier) - 0x198BF Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF 158191 - 0x19771 (Intro 4) - 0x198BF - True @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4: +158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots +Door - 0x17BB4 (Vault Door) - 0x00AFB 158605 - 0x17D28 (Discard) - True - Arrows 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Squares & Black/White Squares & Shapers & Triangles Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Triangles & Dots & Full Dots 158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Triangles & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol -Door - 0x28A61 (Tinted Glass Door) - 0x28A0D +158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers & Stars + Same Colored Symbol +Door - 0x28A61 (RGB House Entry) - 0x28A0D 158226 - 0x28A0D (Church Entry Panel) - 0x28998 - Stars Door - 0x03BB0 (Church Entry) - 0x03C08 -158228 - 0x28A79 (Maze Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -484,7 +500,7 @@ Door - 0x1845B (Windmill Entry) - 0x17F5F 159541 - 0x03412 (Tower Underside Fourth EP) - True - True 159542 - 0x038A6 (Tower Underside First EP) - True - True 159543 - 0x038AA (Tower Underside Second EP) - True - True -159545 - 0x03E40 (RGB House Green EP) - 0x334D8 & 0x03C0C & 0x03C08 - True +159545 - 0x03E40 (RGB House Green EP) - 0x334D8 - True 159546 - 0x28B8E (Maze Bridge Underside EP) - 0x2896A - True 159552 - 0x03BCF (Black Line Redirect EP) - True - True 159800 - 0xFFF80 (Pet the Dog) - True - True @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Arrows 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River Vault - 0x15287: +158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True +Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots +Door - 0x15287 (Vault Door) - 0x15ADD 159110 - 0x03AC5 (Green Leaf Moss EP) - True - True 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Squares & Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Squares & Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares +158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Squares & Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Squares & Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Squares & Colored Squares & Black/White Squares Door - 0x0C2A3 (UV Room Entry) - 0x0A01F Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 158316 - 0x00990 (Platform Row 4) - 0x0098F - Rotated Shapers Door - 0x184B7 (Between Bridges First Door) - 0x00990 158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Rotated Shapers -158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Rotated Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Rotated Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True -Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Shapers & Dots & Full Dots 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Shapers & Dots & Full Dots -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B: +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay: Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A Swamp Purple Underwater (Swamp): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Stars & Stars + Same Colored Symbol Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: Door - 0x0A181 (Third Door) - 0x0A182 Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: -158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True +158356 - 0x2700B (Laser House Door Timer Outside) - True - True Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: 158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots & Full Dots @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Triangles Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ Treehouse Green Bridge Left House (Treehouse): 159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: -Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DEC | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Arrows @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Arrows -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol +Door - 0x00085 (Vault Door) - 0x002A6 159301 - 0x335AE (Cloud Cycle EP) - True - True 159325 - 0x33505 (Bush EP) - True - True 159335 - 0x03C07 (Apparent River EP) - True - True +Mountainside Vault (Mountainside): +158666 - 0x03542 (Vault Box) - True - True + Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Eraser & Triangles -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Triangles 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Stars & Shapers & Stars + Same Colored Symbol 158424 - 0x09EAD (Trash Pillar 1) - True - Rotated Shapers & Stars 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Rotated Shapers & Triangles + +Mountain Top Layer At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): 158431 - 0x09E86 (Light Bridge Controller Near) - True - Shapers & Dots -Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8: 158432 - 0x09FCC (Far Row 1) - True - Triangles 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares & Stars & Stars + Same Colored Symbol 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars & Triangles & Stars + Same Colored Symbol @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay: 158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser & Negative Shapers 158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser & Negative Shapers 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser & Negative Shapers 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser & Negative Shapers 158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True +159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True Door - 0x09F89 (Exit) - 0x09FDA -Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Squares & Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Arrows & Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,31 +1046,34 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Arrows & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75: 158499 - 0x0A332 (Start Timer) - 11 Lasers - True 158500 - 0x0088E (Small Basic) - 0x0A332 - True 158501 - 0x00BAF (Big Basic) - 0x0088E - True -158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares +158502 - 0x00BF3 (Square) - 0x00BAF - Black/White Squares 158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots 158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots 158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots 158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers 158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True -158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares -158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares -158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares -158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Black/White Squares +158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Black/White Squares +158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Black/White Squares +158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares 158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles 158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles 158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry -158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Arrows Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: 159042 - 0x22106 (Desert EP) - True - True 159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True 159231 - 0x28B29 (Shipwreck Green EP) - True - True @@ -1093,32 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 84e73e68a5..719eae6c4e 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -1,3 +1,5 @@ +Menu (Menu) - Entry - True: + Entry (Entry): First Hallway (First Hallway) - Entry - True - First Hallway Room - 0x00064: @@ -21,9 +23,9 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True 159517 - 0x3352F (Gate EP) - 0x03505 - True -Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares -158651 - 0x03481 (Vault Box) - 0x033D4 - True +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares +Door - 0x033D0 (Vault Door) - 0x033D4 158013 - 0x0005D (Shed Row 1) - True - Dots 158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots 158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots @@ -44,6 +46,9 @@ Door - 0x03BA2 (Outpost Path) - 0x0A3B5 159516 - 0x334A3 (Path EP) - True - True 159500 - 0x035C7 (Tractor EP) - True - True +Outside Tutorial Vault (Outside Tutorial): +158651 - 0x03481 (Vault Box) - True - True + Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: 158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots Door - 0x0A170 (Outpost Entry) - 0x0A171 @@ -54,6 +59,7 @@ Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Triangles Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True 159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: @@ -76,7 +82,7 @@ Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry Door - 0x0D7ED (Back Wall) - 0x0005C -Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: @@ -112,12 +118,12 @@ Door - 0x18269 (Upper) - 0x1C349 159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True Symmetry Island Upper (Symmetry Island): -158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots -158066 - 0x00A57 (Yellow 2) - 0x00A52 - Symmetry & Colored Dots -158067 - 0x00A5B (Yellow 3) - 0x00A57 - Symmetry & Colored Dots -158068 - 0x00A61 (Blue 1) - 0x00A52 - Symmetry & Colored Dots -158069 - 0x00A64 (Blue 2) - 0x00A61 & 0x00A52 - Symmetry & Colored Dots -158070 - 0x00A68 (Blue 3) - 0x00A64 & 0x00A57 - Symmetry & Colored Dots +158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots +158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots 158700 - 0x0360D (Laser Panel) - 0x00A68 - True Laser - 0x00509 (Laser) - 0x0360D 159001 - 0x03367 (Glass Factory Black Line EP) - True - True @@ -135,9 +141,9 @@ Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): -Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: -158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots -158653 - 0x0339E (Vault Box) - 0x0CC7B - True +Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Shapers & Rotated Shapers & Negative Shapers & Full Dots +Door - 0x03444 (Vault Door) - 0x0CC7B 158602 - 0x17CE7 (Discard) - True - Triangles 158076 - 0x00698 (Surface 1) - True - True 158077 - 0x0048F (Surface 2) - 0x00698 - True @@ -163,6 +169,9 @@ Laser - 0x012FB (Laser) - 0x03608 159040 - 0x334B9 (Shore EP) - True - True 159041 - 0x334BC (Island EP) - True - True +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True 158088 - 0x00422 (Light Room 1) - 0x09FAA - True @@ -199,18 +208,19 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: Door - 0x0C316 (Elevator Room Entry) - 0x18076 159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True -Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: +Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x01317: 158111 - 0x17C31 (Final Transparent) - True - True 158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True 158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True 158115 - 0x0A15C (Final Bent 1) - True - True 158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True 158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True -159035 - 0x037BB (Elevator EP) - 0x012FB - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - TrueOneWay: +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: 158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares 158603 - 0x17CF0 (Discard) - True - Triangles 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers @@ -222,7 +232,7 @@ Door - 0x09D6F (Entry 1) - 0x09E57 159420 - 0x289CF (Rock Line EP) - True - True 159421 - 0x289D1 (Rock Line Reflection EP) - True - True -Quarry Elevator (Quarry): +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True @@ -230,28 +240,31 @@ Quarry Between Entrys (Quarry) - Quarry - 0x17C07: 158119 - 0x17C09 (Entry 2 Panel) - True - Shapers Door - 0x17C07 (Entry 2) - 0x17C09 -Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010 - Quarry Elevator - 0x17CC4: +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True 158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Black/White Squares 158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Dots Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A -Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: +Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay: 158123 - 0x275ED (Side Exit Panel) - True - True Door - 0x275FF (Side Exit) - 0x275ED 158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser 158145 - 0x17CAC (Roof Exit Panel) - True - True Door - 0x17CE8 (Roof Exit) - 0x17CAC -Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Ground Floor - 0x03675 - Quarry Stoneworks Upper Floor - 0x03679: +Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay: 158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser 158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser 158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser 158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser 158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots 158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser + +Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679: 158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser -Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03676 & 0x03679 - Quarry Stoneworks Ground Floor - 0x0368A: +Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A: 158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser 158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser 158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser @@ -262,7 +275,7 @@ Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Middle Flo 158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser 158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser 158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser -158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser +158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser 158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser @@ -277,7 +290,7 @@ Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Fro Door - 0x2769B (Dock) - 0x17CA6 Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 -Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: +Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6: Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: 158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser @@ -309,9 +322,9 @@ Door - 0x3865F (Second Barrier) - 0x38663 158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers 159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True -Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665: 158170 - 0x334DB (Door Timer Outside) - True - True -Door - 0x19B24 (Timed Door) - 0x334DB +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC 158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True 158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True 158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True @@ -336,7 +349,7 @@ Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: 158187 - 0x334DC (Door Timer Inside) - True - True 158188 - 0x198B5 (Intro 1) - True - True 158189 - 0x198BD (Intro 2) - 0x198B5 - True -158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True +158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True Door - 0x19865 (Quarry Barrier) - 0x198BF Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF 158191 - 0x19771 (Intro 4) - 0x198BF - True @@ -345,7 +358,7 @@ Door - 0x1855B (Ledge Barrier) - 0x0A8DC Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - True +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True Laser - 0x181B3 (Laser) - 0x19650 Treehouse Beach (Treehouse Beach) - Main Island - True: @@ -395,9 +408,9 @@ Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158205 - 0x09E49 (Shadows Shortcut Panel) - True - True Door - 0x09E3D (Shadows Shortcut) - 0x09E49 -Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots -158655 - 0x03535 (Vault Box) - 0x00AFB - True +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4: +158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots +Door - 0x17BB4 (Vault Door) - 0x00AFB 158605 - 0x17D28 (Discard) - True - Triangles 159220 - 0x03B22 (Circle Far EP) - True - True 159221 - 0x03B23 (Circle Left EP) - True - True @@ -407,6 +420,9 @@ Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: 159226 - 0x28ABE (Rope Outer EP) - True - True 159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B @@ -422,8 +438,8 @@ Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159251 - 0x3348F (Hedges EP) - True - True Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Panel) - True - True -Door - 0x0364E (Shortcut) - 0x03713 +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 158208 - 0x00B10 (Entry Left) - True - True 158209 - 0x00C92 (Entry Right) - True - True Door - 0x0C128 (Entry Inner) - 0x00B10 @@ -454,7 +470,7 @@ Inside Monastery (Monastery): Monastery Garden (Monastery): -Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat 158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 @@ -469,11 +485,11 @@ Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots 158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 -158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers -Door - 0x28A61 (Tinted Glass Door) - 0x28998 +158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (RGB House Entry) - 0x28998 158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars Door - 0x03BB0 (Church Entry) - 0x28A0D -158228 - 0x28A79 (Maze Stair Control) - True - True +158228 - 0x28A79 (Maze Panel) - True - True Door - 0x28AA2 (Maze Stairs) - 0x28A79 158241 - 0x17F5F (Windmill Entry Panel) - True - Dots Door - 0x1845B (Windmill Entry) - 0x17F5F @@ -557,7 +573,7 @@ Door - 0x3CCDF (Exit Right) - 0x33AB2 159556 - 0x33A2A (Door EP) - 0x03553 - True 159558 - 0x33B06 (Church EP) - 0x0354E - True -Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles 158252 - 0x002C4 (First Row 1) - True - True @@ -588,18 +604,21 @@ Door - 0x3873B (Laser Shortcut) - 0x337FA 159350 - 0x035CB (Bamboo CCW EP) - True - True 159351 - 0x035CF (Bamboo CW EP) - True - True -Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Monastery Shortcut Panel) - True - True -Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots -158664 - 0x03702 (Vault Box) - 0x15ADD - True +Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A - River Vault - 0x15287: +158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True +Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots +Door - 0x15287 (Vault Door) - 0x15ADD 159110 - 0x03AC5 (Green Leaf Moss EP) - True - True 159120 - 0x03BE2 (Monastery Garden Left EP) - 0x03750 - True 159121 - 0x03BE3 (Monastery Garden Right EP) - True - True 159122 - 0x0A409 (Monastery Wall EP) - True - True +River Vault (River): +158664 - 0x03702 (Vault Box) - True - True + Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: -158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares Door - 0x0C2A4 (Entry) - 0x17C2E Bunker (Bunker) - Bunker Glass Room - 0x17C79: @@ -616,9 +635,9 @@ Bunker (Bunker) - Bunker Glass Room - 0x17C79: Door - 0x17C79 (Tinted Glass Door) - 0x0A099 Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Glass Room 1) - True - Colored Squares -158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares -158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares +158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares Door - 0x0C2A3 (UV Room Entry) - 0x0A01F Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: @@ -631,7 +650,7 @@ Door - 0x0A08D (Elevator Room Entry) - 0x17E67 Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: 159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True -Bunker Elevator (Bunker) - Bunker Laser Platform - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: 158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: @@ -676,7 +695,7 @@ Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat 158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers Door - 0x184B7 (Between Bridges First Door) - 0x00990 158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers -158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 @@ -715,18 +734,21 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True -Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers 158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers -158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True -Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6: +Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B: +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay: Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A Swamp Purple Underwater (Swamp): @@ -752,7 +774,7 @@ Laser - 0x00BF6 (Laser) - 0x03615 158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers Door - 0x2D880 (Laser Shortcut) - 0x17C02 -Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: +Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309 - The Ocean - 0x17C95: 158343 - 0x17C95 (Boat Spawn) - True - Boat 158344 - 0x0288C (First Door Panel) - True - Stars Door - 0x0C309 (First Door) - 0x0288C @@ -778,7 +800,7 @@ Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: Door - 0x0A181 (Third Door) - 0x0A182 Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: -158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True +158356 - 0x2700B (Laser House Door Timer Outside) - True - True Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: 158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots @@ -802,7 +824,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: -158404 - 0x037FF (Bridge Control) - True - Stars +158404 - 0x037FF (Drawbridge Panel) - True - Stars Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: @@ -847,7 +869,7 @@ Treehouse Green Bridge Left House (Treehouse): 159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: -Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC Treehouse Laser Room Back Platform (Treehouse): 158611 - 0x17FA0 (Laser Discard) - True - Triangles @@ -860,19 +882,22 @@ Treehouse Laser Room (Treehouse): 158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: 158612 - 0x17C42 (Discard) - True - Triangles -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares +Door - 0x00085 (Vault Door) - 0x002A6 159301 - 0x335AE (Cloud Cycle EP) - True - True 159325 - 0x33505 (Bush EP) - True - True 159335 - 0x03C07 (Apparent River EP) - True - True +Mountainside Vault (Mountainside): +158666 - 0x03542 (Vault Box) - True - True + Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Black/White Squares -158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Black/White Squares +158800 - 0xFFF00 (Box Long) - 11 Lasers & 0x17C34 - True 159300 - 0x001A3 (River Shape EP) - True - True 159320 - 0x3370E (Arch Black EP) - True - True 159324 - 0x336C8 (Arch White Right EP) - True - True @@ -881,7 +906,7 @@ Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: 158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Rotated Shapers -Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - TrueOneWay: 158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots 158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots 158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers @@ -899,6 +924,8 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots 158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers + +Mountain Top Layer At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86 - Mountain Pink Bridge EP - TrueOneWay: @@ -917,7 +944,7 @@ Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): 158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Black/White Squares -Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8: 158432 - 0x09FCC (Far Row 1) - True - Dots 158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares 158434 - 0x09FCF (Far Row 3) - 0x09FCE - Shapers @@ -935,29 +962,27 @@ Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay: 158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser 158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Rotated Shapers & Eraser 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser 158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True +159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True Door - 0x09F89 (Exit) - 0x09FDA -Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141 - Mountain Pink Bridge EP - TrueOneWay: +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Final Room - 0x0C141: 158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles 158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars 158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True -159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 Mountain Pink Bridge EP (Mountain Floor 2): 159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True -Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: -Door - 0x17F33 (Rock Open) - True - True - -Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: 158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True @@ -1021,7 +1046,7 @@ Path to Challenge (Caves) - Challenge - 0x0A19A: 158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Tunnels - 0x0348A: +Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75: 158499 - 0x0A332 (Start Timer) - 11 Lasers - True 158500 - 0x0088E (Small Basic) - 0x0A332 - True 158501 - 0x00BAF (Big Basic) - 0x0088E - True @@ -1041,11 +1066,14 @@ Challenge (Challenge) - Tunnels - 0x0348A: 158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles 158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry 158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry -158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles Door - 0x0348A (Tunnels Entry) - 0x039B4 159530 - 0x28B30 (Water EP) - True - True +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True @@ -1075,7 +1103,7 @@ Elevator (Mountain Final Room): 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True -Boat (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: 159042 - 0x22106 (Desert EP) - True - True 159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True 159231 - 0x28B29 (Shipwreck Green EP) - True - True @@ -1093,32 +1121,38 @@ Obelisks (EPs) - Entry - True: 159702 - 0xFFE02 (Desert Obelisk Side 3) - 0x3351D - True 159703 - 0xFFE03 (Desert Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True 159704 - 0xFFE04 (Desert Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Desert Obelisk) - True - True 159710 - 0xFFE10 (Monastery Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True 159711 - 0xFFE11 (Monastery Obelisk Side 2) - 0x03AC5 - True 159712 - 0xFFE12 (Monastery Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True 159713 - 0xFFE13 (Monastery Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True 159714 - 0xFFE14 (Monastery Obelisk Side 5) - 0x03E01 - True 159715 - 0xFFE15 (Monastery Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Monastery Obelisk) - True - True 159720 - 0xFFE20 (Treehouse Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True 159721 - 0xFFE21 (Treehouse Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True 159722 - 0xFFE22 (Treehouse Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True 159723 - 0xFFE23 (Treehouse Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True 159724 - 0xFFE24 (Treehouse Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True 159725 - 0xFFE25 (Treehouse Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Treehouse Obelisk) - True - True 159730 - 0xFFE30 (River Obelisk Side 1) - 0x001A3 & 0x335AE - True 159731 - 0xFFE31 (River Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True 159732 - 0xFFE32 (River Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True 159733 - 0xFFE33 (River Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True 159734 - 0xFFE34 (River Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True 159735 - 0xFFE35 (River Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (River Obelisk) - True - True 159740 - 0xFFE40 (Quarry Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True 159741 - 0xFFE41 (Quarry Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True 159742 - 0xFFE42 (Quarry Obelisk Side 3) - 0x289CF & 0x289D1 - True 159743 - 0xFFE43 (Quarry Obelisk Side 4) - 0x33692 - True 159744 - 0xFFE44 (Quarry Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Quarry Obelisk) - True - True 159750 - 0xFFE50 (Town Obelisk Side 1) - 0x035C7 - True 159751 - 0xFFE51 (Town Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True 159752 - 0xFFE52 (Town Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True 159753 - 0xFFE53 (Town Obelisk Side 4) - 0x28B30 & 0x035C9 - True 159754 - 0xFFE54 (Town Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True 159755 - 0xFFE55 (Town Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True +159759 - 0x0A16C (Town Obelisk) - True - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 28eaba6404..c2d2311c15 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -1,9 +1,11 @@ """ Archipelago init file for The Witness """ +import dataclasses from typing import Dict, Optional -from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial +from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState +from Options import PerGameCommonOptions, Toggle from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ get_priority_hint_items, make_hints, generate_joke_hints from worlds.AutoWorld import World, WebWorld @@ -11,9 +13,9 @@ from .player_logic import WitnessPlayerLogic from .static_logic import StaticWitnessLogic from .locations import WitnessPlayerLocations, StaticWitnessLocations from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData -from .rules import set_rules from .regions import WitnessRegions -from .Options import is_option_enabled, the_witness_options, get_option_value +from .rules import set_rules +from .Options import TheWitnessOptions from .utils import get_audio_logs from logging import warning, error @@ -38,13 +40,15 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 13 + data_version = 14 StaticWitnessLogic() StaticWitnessLocations() StaticWitnessItems() web = WitnessWebWorld() - option_definitions = the_witness_options + + options_dataclass = TheWitnessOptions + options: TheWitnessOptions item_name_to_id = { name: data.ap_code for name, data in StaticWitnessItems.item_data.items() @@ -52,7 +56,7 @@ class WitnessWorld(World): location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID item_name_groups = StaticWitnessItems.item_groups - required_client_version = (0, 3, 9) + required_client_version = (0, 4, 4) def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) @@ -64,6 +68,9 @@ class WitnessWorld(World): self.log_ids_to_hints = None + self.items_placed_early = [] + self.own_itempool = [] + def _get_slot_data(self): return { 'seed': self.random.randrange(0, 1000000), @@ -72,12 +79,11 @@ class WitnessWorld(World): 'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(), 'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(), 'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(), - 'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS), + 'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], 'log_ids_to_hints': self.log_ids_to_hints, 'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(), 'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES, - 'precompleted_puzzles': [int(h, 16) for h in - self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS], + 'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], 'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME, } @@ -85,36 +91,125 @@ class WitnessWorld(World): disabled_locations = self.multiworld.exclude_locations[self.player].value self.player_logic = WitnessPlayerLogic( - self.multiworld, self.player, disabled_locations, self.multiworld.start_inventory[self.player].value + self, disabled_locations, self.multiworld.start_inventory[self.player].value ) - self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self.multiworld, self.player, self.player_logic) - self.items: WitnessPlayerItems = WitnessPlayerItems(self.multiworld, self.player, self.player_logic, self.locat) - self.regio: WitnessRegions = WitnessRegions(self.locat) + self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) + self.items: WitnessPlayerItems = WitnessPlayerItems( + self, self.player_logic, self.locat + ) + self.regio: WitnessRegions = WitnessRegions(self.locat, self) self.log_ids_to_hints = dict() - if not (is_option_enabled(self.multiworld, self.player, "shuffle_symbols") - or get_option_value(self.multiworld, self.player, "shuffle_doors") - or is_option_enabled(self.multiworld, self.player, "shuffle_lasers")): + if not (self.options.shuffle_symbols or self.options.shuffle_doors or self.options.shuffle_lasers): if self.multiworld.players == 1: - warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" - " Shuffle or Laser Shuffle if that doesn't seem right.") + warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't" + f" seem right.") else: - raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle," - " Door Shuffle or Laser Shuffle.") + raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any" + f" progression items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle.") def create_regions(self): - self.regio.create_regions(self.multiworld, self.player, self.player_logic) + self.regio.create_regions(self, self.player_logic) + + # Set rules early so extra locations can be created based on the results of exploring collection states + + set_rules(self) + + # Add event items and tie them to event locations (e.g. laser activations). + + event_locations = [] + + for event_location in self.locat.EVENT_LOCATION_TABLE: + item_obj = self.create_item( + self.player_logic.EVENT_ITEM_PAIRS[event_location] + ) + location_obj = self.multiworld.get_location(event_location, self.player) + location_obj.place_locked_item(item_obj) + self.own_itempool.append(item_obj) + + event_locations.append(location_obj) + + # Place other locked items + dog_puzzle_skip = self.create_item("Puzzle Skip") + self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip) + + self.own_itempool.append(dog_puzzle_skip) + + self.items_placed_early.append("Puzzle Skip") + + # Pick an early item to place on the tutorial gate. + early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] + if early_items: + random_early_item = self.multiworld.random.choice(early_items) + if self.options.puzzle_randomization == 1: + # In Expert, only tag the item as early, rather than forcing it onto the gate. + self.multiworld.local_early_items[self.player][random_early_item] = 1 + else: + # Force the item onto the tutorial gate check and remove it from our random pool. + gate_item = self.create_item(random_early_item) + self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item) + self.own_itempool.append(gate_item) + self.items_placed_early.append(random_early_item) + + # There are some really restrictive settings in The Witness. + # They are rarely played, but when they are, we add some extra sphere 1 locations. + # This is done both to prevent generation failures, but also to make the early game less linear. + # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. + + state = CollectionState(self.multiworld) + state.sweep_for_events(locations=event_locations) + + num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) + + # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items + + needed_size = 3 + needed_size += self.options.puzzle_randomization == 1 + needed_size += self.options.shuffle_symbols + needed_size += self.options.shuffle_doors > 0 + + # Then, add checks in order until the required amount of sphere 1 checks is met. + + extra_checks = [ + ("First Hallway Room", "First Hallway Bend"), + ("First Hallway", "First Hallway Straight"), + ("Desert Outside", "Desert Surface 3"), + ] + + for i in range(num_early_locs, needed_size): + if not extra_checks: + break + + region, loc = extra_checks.pop(0) + self.locat.add_location_late(loc) + self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]}) + + player = self.multiworld.get_player_name(self.player) + + warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") def create_items(self): - - # Determine pool size. Note that the dog location is included in the location list, so this needs to be -1. - pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1 + # Determine pool size. + pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) # Fill mandatory items and remove precollected and/or starting items from the pool. item_pool: Dict[str, int] = self.items.get_mandatory_items() + # Remove one copy of each item that was placed early + for already_placed in self.items_placed_early: + pool_size -= 1 + + if already_placed not in item_pool: + continue + + if item_pool[already_placed] == 1: + item_pool.pop(already_placed) + else: + item_pool[already_placed] -= 1 + for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]: if precollected_item_name in item_pool: if item_pool[precollected_item_name] == 1: @@ -131,17 +226,18 @@ class WitnessWorld(World): self.multiworld.push_precollected(self.create_item(inventory_item_name)) if len(item_pool) > pool_size: - error_string = "The Witness world has too few locations ({num_loc}) to place its necessary items " \ - "({num_item})." - error(error_string.format(num_loc=pool_size, num_item=len(item_pool))) + error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})" + f" to place its necessary items ({len(item_pool)}).") return remaining_item_slots = pool_size - sum(item_pool.values()) # Add puzzle skips. - num_puzzle_skips = get_option_value(self.multiworld, self.player, "puzzle_skip_amount") + num_puzzle_skips = self.options.puzzle_skip_amount + if num_puzzle_skips > remaining_item_slots: - warning(f"The Witness world has insufficient locations to place all requested puzzle skips.") + warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" + f" to place all requested puzzle skips.") num_puzzle_skips = remaining_item_slots item_pool["Puzzle Skip"] = num_puzzle_skips remaining_item_slots -= num_puzzle_skips @@ -150,45 +246,17 @@ class WitnessWorld(World): if remaining_item_slots > 0: item_pool.update(self.items.get_filler_items(remaining_item_slots)) - # Add event items and tie them to event locations (e.g. laser activations). - for event_location in self.locat.EVENT_LOCATION_TABLE: - item_obj = self.create_item( - self.player_logic.EVENT_ITEM_PAIRS[event_location] - ) - location_obj = self.multiworld.get_location(event_location, self.player) - location_obj.place_locked_item(item_obj) - - # BAD DOG GET BACK HERE WITH THAT PUZZLE SKIP YOU'RE POLLUTING THE ITEM POOL - self.multiworld.get_location("Town Pet the Dog", self.player)\ - .place_locked_item(self.create_item("Puzzle Skip")) - - # Pick an early item to place on the tutorial gate. - early_items = [item for item in self.items.get_early_items() if item in item_pool] - if early_items: - random_early_item = self.multiworld.random.choice(early_items) - if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1: - # In Expert, only tag the item as early, rather than forcing it onto the gate. - self.multiworld.local_early_items[self.player][random_early_item] = 1 - else: - # Force the item onto the tutorial gate check and remove it from our random pool. - self.multiworld.get_location("Tutorial Gate Open", self.player)\ - .place_locked_item(self.create_item(random_early_item)) - if item_pool[random_early_item] == 1: - item_pool.pop(random_early_item) - else: - item_pool[random_early_item] -= 1 - # Generate the actual items. for item_name, quantity in sorted(item_pool.items()): - self.multiworld.itempool += [self.create_item(item_name) for _ in range(0, quantity)] + new_items = [self.create_item(item_name) for _ in range(0, quantity)] + + self.own_itempool += new_items + self.multiworld.itempool += new_items if self.items.item_data[item_name].local_only: self.multiworld.local_items[self.player].value.add(item_name) - def set_rules(self): - set_rules(self.multiworld, self.player, self.player_logic, self.locat) - def fill_slot_data(self) -> dict: - hint_amount = get_option_value(self.multiworld, self.player, "hint_amount") + hint_amount = self.options.hint_amount.value credits_hint = ( "This Randomizer is brought to you by", @@ -199,9 +267,9 @@ class WitnessWorld(World): audio_logs = get_audio_logs().copy() if hint_amount != 0: - generated_hints = make_hints(self.multiworld, self.player, hint_amount) + generated_hints = make_hints(self, hint_amount, self.own_itempool) - self.multiworld.per_slot_randoms[self.player].shuffle(audio_logs) + self.random.shuffle(audio_logs) duplicates = min(3, len(audio_logs) // hint_amount) @@ -216,7 +284,7 @@ class WitnessWorld(World): audio_log = audio_logs.pop() self.log_ids_to_hints[int(audio_log, 16)] = credits_hint - joke_hints = generate_joke_hints(self.multiworld, self.player, len(audio_logs)) + joke_hints = generate_joke_hints(self, len(audio_logs)) while audio_logs: audio_log = audio_logs.pop() @@ -226,10 +294,10 @@ class WitnessWorld(World): slot_data = self._get_slot_data() - for option_name in the_witness_options: - slot_data[option_name] = get_option_value( - self.multiworld, self.player, option_name - ) + for option_name in (attr.name for attr in dataclasses.fields(TheWitnessOptions) + if attr not in dataclasses.fields(PerGameCommonOptions)): + option = getattr(self.options, option_name) + slot_data[option_name] = bool(option.value) if isinstance(option, Toggle) else option.value return slot_data @@ -257,36 +325,35 @@ class WitnessLocation(Location): Archipelago Location for The Witness """ game: str = "The Witness" - check_hex: int = -1 + entity_hex: int = -1 def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1): super().__init__(player, name, address, parent) - self.check_hex = ch_hex + self.entity_hex = ch_hex -def create_region(world: MultiWorld, player: int, name: str, - locat: WitnessPlayerLocations, region_locations=None, exits=None): +def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None): """ Create an Archipelago Region for The Witness """ - ret = Region(name, player, world) + ret = Region(name, world.player, world.multiworld) if region_locations: for location in region_locations: loc_id = locat.CHECK_LOCATION_TABLE[location] - check_hex = -1 - if location in StaticWitnessLogic.CHECKS_BY_NAME: - check_hex = int( - StaticWitnessLogic.CHECKS_BY_NAME[location]["checkHex"], 0 + entity_hex = -1 + if location in StaticWitnessLogic.ENTITIES_BY_NAME: + entity_hex = int( + StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) location = WitnessLocation( - player, location, loc_id, ret, check_hex + world.player, location, loc_id, ret, entity_hex ) ret.locations.append(location) if exits: for single_exit in exits: - ret.exits.append(Entrance(player, single_exit, ret)) + ret.exits.append(Entrance(world.player, single_exit, ret)) return ret diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 8a9dab54bc..24302f0c67 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,5 +1,9 @@ -from BaseClasses import MultiWorld -from .Options import is_option_enabled, get_option_value +from typing import Tuple, List, TYPE_CHECKING + +from BaseClasses import Item + +if TYPE_CHECKING: + from . import WitnessWorld joke_hints = [ "Quaternions break my brain", @@ -113,16 +117,16 @@ joke_hints = [ ] -def get_always_hint_items(multiworld: MultiWorld, player: int): +def get_always_hint_items(world: "WitnessWorld"): always = [ "Boat", - "Caves Exits to Main Island", + "Caves Shortcuts", "Progressive Dots", ] - difficulty = get_option_value(multiworld, player, "puzzle_randomization") - discards = is_option_enabled(multiworld, player, "shuffle_discarded_panels") - wincon = get_option_value(multiworld, player, "victory_condition") + difficulty = world.options.puzzle_randomization + discards = world.options.shuffle_discarded_panels + wincon = world.options.victory_condition if discards: if difficulty == 1: @@ -131,12 +135,15 @@ def get_always_hint_items(multiworld: MultiWorld, player: int): always.append("Triangles") if wincon == 0: - always.append("Mountain Bottom Floor Final Room Entry (Door)") + always += ["Mountain Bottom Floor Final Room Entry (Door)", "Mountain Bottom Floor Doors"] + + if wincon == 1: + always += ["Challenge Entry (Panel)", "Caves Panels"] return always -def get_always_hint_locations(multiworld: MultiWorld, player: int): +def get_always_hint_locations(_: "WitnessWorld"): return { "Challenge Vault Box", "Mountain Bottom Floor Discard", @@ -146,19 +153,34 @@ def get_always_hint_locations(multiworld: MultiWorld, player: int): } -def get_priority_hint_items(multiworld: MultiWorld, player: int): +def get_priority_hint_items(world: "WitnessWorld"): priority = { "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", - "Negative Shapers", - "Sound Dots", - "Colored Dots", - "Stars + Same Colored Symbol", "Swamp Entry (Panel)", "Swamp Laser Shortcut (Door)", } - if is_option_enabled(multiworld, player, "shuffle_lasers"): + if world.options.shuffle_symbols: + symbols = [ + "Progressive Dots", + "Progressive Stars", + "Shapers", + "Rotated Shapers", + "Negative Shapers", + "Arrows", + "Triangles", + "Eraser", + "Black/White Squares", + "Colored Squares", + "Colored Dots", + "Sound Dots", + "Symmetry" + ] + + priority.update(world.random.sample(symbols, 5)) + + if world.options.shuffle_lasers: lasers = [ "Symmetry Laser", "Town Laser", @@ -172,18 +194,18 @@ def get_priority_hint_items(multiworld: MultiWorld, player: int): "Shadows Laser", ] - if get_option_value(multiworld, player, "shuffle_doors") >= 2: + if world.options.shuffle_doors >= 2: priority.add("Desert Laser") - priority.update(multiworld.per_slot_randoms[player].sample(lasers, 5)) + priority.update(world.random.sample(lasers, 5)) else: lasers.append("Desert Laser") - priority.update(multiworld.per_slot_randoms[player].sample(lasers, 6)) + priority.update(world.random.sample(lasers, 6)) return priority -def get_priority_hint_locations(multiworld: MultiWorld, player: int): +def get_priority_hint_locations(_: "WitnessWorld"): return { "Swamp Purple Underwater", "Shipwreck Vault Box", @@ -201,89 +223,100 @@ def get_priority_hint_locations(multiworld: MultiWorld, player: int): } -def make_hint_from_item(multiworld: MultiWorld, player: int, item: str): - location_obj = multiworld.find_item(item, player).item.location +def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): + locations = [item.location for item in own_itempool if item.name == item_name and item.location] + + if not locations: + return None + + location_obj = world.random.choice(locations) location_name = location_obj.name - if location_obj.player != player: - location_name += " (" + multiworld.get_player_name(location_obj.player) + ")" - return location_name, item, location_obj.address if (location_obj.player == player) else -1 + if location_obj.player != world.player: + location_name += " (" + world.multiworld.get_player_name(location_obj.player) + ")" + + return location_name, item_name, location_obj.address if (location_obj.player == world.player) else -1 -def make_hint_from_location(multiworld: MultiWorld, player: int, location: str): - location_obj = multiworld.get_location(location, player) - item_obj = multiworld.get_location(location, player).item +def make_hint_from_location(world: "WitnessWorld", location: str): + location_obj = world.multiworld.get_location(location, world.player) + item_obj = world.multiworld.get_location(location, world.player).item item_name = item_obj.name - if item_obj.player != player: - item_name += " (" + multiworld.get_player_name(item_obj.player) + ")" + if item_obj.player != world.player: + item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" - return location, item_name, location_obj.address if (location_obj.player == player) else -1 + return location, item_name, location_obj.address if (location_obj.player == world.player) else -1 -def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): +def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item]): hints = list() prog_items_in_this_world = { - item.name for item in multiworld.get_items() - if item.player == player and item.code and item.advancement + item.name for item in own_itempool if item.advancement and item.code and item.location } loc_in_this_world = { - location.name for location in multiworld.get_locations(player) - if location.address + location.name for location in world.multiworld.get_locations(world.player) if location.address } always_locations = [ - location for location in get_always_hint_locations(multiworld, player) + location for location in get_always_hint_locations(world) if location in loc_in_this_world ] always_items = [ - item for item in get_always_hint_items(multiworld, player) + item for item in get_always_hint_items(world) if item in prog_items_in_this_world ] priority_locations = [ - location for location in get_priority_hint_locations(multiworld, player) + location for location in get_priority_hint_locations(world) if location in loc_in_this_world ] priority_items = [ - item for item in get_priority_hint_items(multiworld, player) + item for item in get_priority_hint_items(world) if item in prog_items_in_this_world ] always_hint_pairs = dict() for item in always_items: - hint_pair = make_hint_from_item(multiworld, player, item) + hint_pair = make_hint_from_item(world, item, own_itempool) - if hint_pair[2] == 158007: # Tutorial Gate Open + if not hint_pair or hint_pair[2] == 158007: # Tutorial Gate Open continue always_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) for location in always_locations: - hint_pair = make_hint_from_location(multiworld, player, location) + hint_pair = make_hint_from_location(world, location) always_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) priority_hint_pairs = dict() for item in priority_items: - hint_pair = make_hint_from_item(multiworld, player, item) + hint_pair = make_hint_from_item(world, item, own_itempool) - if hint_pair[2] == 158007: # Tutorial Gate Open + if not hint_pair or hint_pair[2] == 158007: # Tutorial Gate Open continue priority_hint_pairs[hint_pair[0]] = (hint_pair[1], True, hint_pair[2]) for location in priority_locations: - hint_pair = make_hint_from_location(multiworld, player, location) + hint_pair = make_hint_from_location(world, location) priority_hint_pairs[hint_pair[0]] = (hint_pair[1], False, hint_pair[2]) + already_hinted_locations = set() + for loc, item in always_hint_pairs.items(): + if loc in already_hinted_locations: + continue + if item[1]: hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: hints.append((f"{loc} contains {item[0]}.", item[2])) - multiworld.per_slot_randoms[player].shuffle(hints) # shuffle always hint order in case of low hint amount + already_hinted_locations.add(loc) + + world.random.shuffle(hints) # shuffle always hint order in case of low hint amount remaining_hints = hint_amount - len(hints) priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) @@ -291,22 +324,27 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): prog_items_in_this_world = sorted(list(prog_items_in_this_world)) locations_in_this_world = sorted(list(loc_in_this_world)) - multiworld.per_slot_randoms[player].shuffle(prog_items_in_this_world) - multiworld.per_slot_randoms[player].shuffle(locations_in_this_world) + world.random.shuffle(prog_items_in_this_world) + world.random.shuffle(locations_in_this_world) priority_hint_list = list(priority_hint_pairs.items()) - multiworld.per_slot_randoms[player].shuffle(priority_hint_list) + world.random.shuffle(priority_hint_list) for _ in range(0, priority_hint_amount): next_priority_hint = priority_hint_list.pop() loc = next_priority_hint[0] item = next_priority_hint[1] + if loc in already_hinted_locations: + continue + if item[1]: hints.append((f"{item[0]} can be found at {loc}.", item[2])) else: hints.append((f"{loc} contains {item[0]}.", item[2])) - next_random_hint_is_item = multiworld.per_slot_randoms[player].randrange(0, 2) # Moving this to the new system is in the bigger refactoring PR + already_hinted_locations.add(loc) + + next_random_hint_is_item = world.random.randrange(0, 2) while len(hints) < hint_amount: if next_random_hint_is_item: @@ -314,16 +352,28 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): next_random_hint_is_item = not next_random_hint_is_item continue - hint = make_hint_from_item(multiworld, player, prog_items_in_this_world.pop()) + hint = make_hint_from_item(world, prog_items_in_this_world.pop(), own_itempool) + + if not hint or hint[0] in already_hinted_locations: + continue + hints.append((f"{hint[1]} can be found at {hint[0]}.", hint[2])) + + already_hinted_locations.add(hint[0]) else: - hint = make_hint_from_location(multiworld, player, locations_in_this_world.pop()) + hint = make_hint_from_location(world, locations_in_this_world.pop()) + + if hint[0] in already_hinted_locations: + continue + hints.append((f"{hint[0]} contains {hint[1]}.", hint[2])) + already_hinted_locations.add(hint[0]) + next_random_hint_is_item = not next_random_hint_is_item return hints -def generate_joke_hints(multiworld: MultiWorld, player: int, amount: int): - return [(x, -1) for x in multiworld.per_slot_randoms[player].sample(joke_hints, amount)] +def generate_joke_hints(world: "WitnessWorld", amount: int) -> List[Tuple[str, int]]: + return [(x, -1) for x in world.random.sample(joke_hints, amount)] diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 82c79047f3..15c693b25d 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -2,18 +2,20 @@ Defines progression, junk and event items for The Witness """ import copy + from dataclasses import dataclass -from typing import Optional, Dict, List, Set +from typing import Optional, Dict, List, Set, TYPE_CHECKING from BaseClasses import Item, MultiWorld, ItemClassification -from .Options import get_option_value, is_option_enabled, the_witness_options - from .locations import ID_START, WitnessPlayerLocations from .player_logic import WitnessPlayerLogic from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \ StaticWitnessLogic, WeightedItemDefinition from .utils import build_weighted_int_list +if TYPE_CHECKING: + from . import WitnessWorld + NUM_ENERGY_UPGRADES = 4 @@ -59,7 +61,7 @@ class StaticWitnessItems: classification = ItemClassification.progression StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name) elif definition.category is ItemCategory.LASER: - classification = ItemClassification.progression + classification = ItemClassification.progression_skip_balancing StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name) elif definition.category is ItemCategory.USEFUL: classification = ItemClassification.useful @@ -90,11 +92,12 @@ class WitnessPlayerItems: Class that defines Items for a single world """ - def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): + def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): """Adds event items after logic changes due to options""" - self._world: MultiWorld = multiworld - self._player_id: int = player + self._world: "WitnessWorld" = world + self._multiworld: MultiWorld = world.multiworld + self._player_id: int = world.player self._logic: WitnessPlayerLogic = logic self._locations: WitnessPlayerLocations = locat @@ -102,19 +105,33 @@ class WitnessPlayerItems: self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data) # Remove all progression items that aren't actually in the game. - self.item_data = {name: data for (name, data) in self.item_data.items() - if data.classification is not ItemClassification.progression or - name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME} + self.item_data = { + name: data for (name, data) in self.item_data.items() + if data.classification not in + {ItemClassification.progression, ItemClassification.progression_skip_balancing} + or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME + } # Adjust item classifications based on game settings. - eps_shuffled = get_option_value(self._world, self._player_id, "shuffle_EPs") != 0 + eps_shuffled = self._world.options.shuffle_EPs + come_to_you = self._world.options.elevators_come_to_you for item_name, item_data in self.item_data.items(): - if not eps_shuffled and item_name in ["Monastery Garden Entry (Door)", "Monastery Shortcuts"]: + if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", + "Monastery Shortcuts", + "Quarry Boathouse Hook Control (Panel)", + "Windmill Turn Control (Panel)"}: # Downgrade doors that only gate progress in EP shuffle. item_data.classification = ItemClassification.useful - elif item_name in ["River Monastery Shortcut (Door)", "Jungle & River Shortcuts", - "Monastery Shortcut (Door)", - "Orchard Second Gate (Door)"]: + elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)", + "Swamp Long Bridge (Panel)"}: + # These Bridges/Elevators are not logical access because they may leave you stuck. + item_data.classification = ItemClassification.useful + elif item_name in {"River Monastery Garden Shortcut (Door)", + "Monastery Laser Shortcut (Door)", + "Orchard Second Gate (Door)", + "Jungle Bamboo Laser Shortcut (Door)", + "Keep Pressure Plates 2 Exit (Door)", + "Caves Elevator Controls (Panel)"}: # Downgrade doors that don't gate progress. item_data.classification = ItemClassification.useful @@ -122,8 +139,11 @@ class WitnessPlayerItems: self._mandatory_items: Dict[str, int] = {} # Add progression items to the mandatory item list. - for item_name, item_data in {name: data for (name, data) in self.item_data.items() - if data.classification == ItemClassification.progression}.items(): + progression_dict = { + name: data for (name, data) in self.item_data.items() + if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing} + } + for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): num_progression = len(self._logic.MULTI_LISTS[item_name]) self._mandatory_items[item_name] = num_progression @@ -170,7 +190,7 @@ class WitnessPlayerItems: remaining_quantity -= len(output) # Read trap configuration data. - trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100 + trap_weight = self._world.options.trap_percentage / 100 filler_weight = 1 - trap_weight # Add filler items to the list. @@ -198,15 +218,14 @@ class WitnessPlayerItems: Returns items that are ideal for placing on extremely early checks, like the tutorial gate. """ output: Set[str] = set() - if "shuffle_symbols" not in the_witness_options.keys() \ - or is_option_enabled(self._world, self._player_id, "shuffle_symbols"): - if get_option_value(self._world, self._player_id, "shuffle_doors") > 0: + if self._world.options.shuffle_symbols: + if self._world.options.shuffle_doors: output = {"Dots", "Black/White Squares", "Symmetry"} else: output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"} - if is_option_enabled(self._world, self._player_id, "shuffle_discarded_panels"): - if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1: + if self._world.options.shuffle_discarded_panels: + if self._world.options.puzzle_randomization == 1: output.add("Arrows") else: output.add("Triangles") @@ -217,7 +236,7 @@ class WitnessPlayerItems: # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved # before create_items so that we'll be able to check placed items instead of just removing all items mentioned # regardless of whether or not they actually wind up being manually placed. - for plando_setting in self._world.plando_items[self._player_id]: + for plando_setting in self._multiworld.plando_items[self._player_id]: if plando_setting.get("from_pool", True): for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]: if type(plando_setting[item_setting_key]) is str: @@ -243,6 +262,7 @@ class WitnessPlayerItems: for item_name, item_data in {name: data for name, data in self.item_data.items() if isinstance(data.definition, DoorItemDefinition)}.items(): output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] + return output def get_symbol_ids_not_in_pool(self) -> List[int]: diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index b33e276e3a..d20be27940 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -1,11 +1,14 @@ """ Defines constants for different types of locations in the game """ +from typing import TYPE_CHECKING -from .Options import is_option_enabled, get_option_value from .player_logic import WitnessPlayerLogic from .static_logic import StaticWitnessLogic +if TYPE_CHECKING: + from . import WitnessWorld + ID_START = 158000 @@ -19,22 +22,29 @@ class StaticWitnessLocations: "Tutorial Front Left", "Tutorial Back Left", "Tutorial Back Right", + "Tutorial Patio Floor", "Tutorial Gate Open", "Outside Tutorial Vault Box", "Outside Tutorial Discard", "Outside Tutorial Shed Row 5", "Outside Tutorial Tree Row 9", + "Outside Tutorial Outpost Entry Panel", + "Outside Tutorial Outpost Exit Panel", "Glass Factory Discard", "Glass Factory Back Wall 5", "Glass Factory Front 3", "Glass Factory Melting 3", + "Symmetry Island Lower Panel", "Symmetry Island Right 5", "Symmetry Island Back 6", "Symmetry Island Left 7", + "Symmetry Island Upper Panel", "Symmetry Island Scenery Outlines 5", + "Symmetry Island Laser Yellow 3", + "Symmetry Island Laser Blue 3", "Symmetry Island Laser Panel", "Orchard Apple Tree 5", @@ -49,9 +59,15 @@ class StaticWitnessLocations: "Desert Final Bent 3", "Desert Laser Panel", + "Quarry Entry 1 Panel", + "Quarry Entry 2 Panel", + "Quarry Stoneworks Entry Left Panel", + "Quarry Stoneworks Entry Right Panel", "Quarry Stoneworks Lower Row 6", "Quarry Stoneworks Upper Row 8", + "Quarry Stoneworks Control Room Left", "Quarry Stoneworks Control Room Right", + "Quarry Stoneworks Stairs Panel", "Quarry Boathouse Intro Right", "Quarry Boathouse Intro Left", "Quarry Boathouse Front Row 5", @@ -84,15 +100,32 @@ class StaticWitnessLocations: "Monastery Inside 4", "Monastery Laser Panel", + "Town Cargo Box Entry Panel", "Town Cargo Box Discard", "Town Tall Hexagonal", + "Town Church Entry Panel", "Town Church Lattice", + "Town Maze Panel", "Town Rooftop Discard", "Town Red Rooftop 5", "Town Wooden Roof Lower Row 5", "Town Wooden Rooftop", + "Town Windmill Entry Panel", + "Town RGB House Entry Panel", "Town Laser Panel", + "Town RGB Room Left", + "Town RGB Room Right", + "Town Sound Room Right", + + "Windmill Theater Entry Panel", + "Theater Exit Left Panel", + "Theater Exit Right Panel", + "Theater Tutorial Video", + "Theater Desert Video", + "Theater Jungle Video", + "Theater Shipwreck Video", + "Theater Mountain Video", "Theater Discard", "Jungle Discard", @@ -102,24 +135,33 @@ class StaticWitnessLocations: "Jungle Laser Panel", "River Vault Box", + "River Monastery Garden Shortcut Panel", + "Bunker Entry Panel", "Bunker Intro Left 5", "Bunker Intro Back 4", "Bunker Glass Room 3", "Bunker UV Room 2", "Bunker Laser Panel", + "Swamp Entry Panel", "Swamp Intro Front 6", "Swamp Intro Back 8", "Swamp Between Bridges Near Row 4", "Swamp Cyan Underwater 5", "Swamp Platform Row 4", + "Swamp Platform Shortcut Right Panel", "Swamp Between Bridges Far Row 4", "Swamp Red Underwater 4", + "Swamp Purple Underwater", "Swamp Beyond Rotating Bridge 4", "Swamp Blue Underwater 5", "Swamp Laser Panel", + "Swamp Laser Shortcut Right Panel", + "Treehouse First Door Panel", + "Treehouse Second Door Panel", + "Treehouse Third Door Panel", "Treehouse Yellow Bridge 9", "Treehouse First Purple Bridge 5", "Treehouse Second Purple Bridge 7", @@ -129,22 +171,11 @@ class StaticWitnessLocations: "Treehouse Laser Discard", "Treehouse Right Orange Bridge 12", "Treehouse Laser Panel", + "Treehouse Drawbridge Panel", "Mountainside Discard", "Mountainside Vault Box", - "Mountaintop River Shape", - "Tutorial Patio Floor", - "Quarry Stoneworks Control Room Left", - "Theater Tutorial Video", - "Theater Desert Video", - "Theater Jungle Video", - "Theater Shipwreck Video", - "Theater Mountain Video", - "Town RGB Room Left", - "Town RGB Room Right", - "Town Sound Room Right", - "Swamp Purple Underwater", "First Hallway EP", "Tutorial Cloud EP", @@ -316,6 +347,77 @@ class StaticWitnessLocations: "Town Obelisk Side 4", "Town Obelisk Side 5", "Town Obelisk Side 6", + + "Caves Mountain Shortcut Panel", + "Caves Swamp Shortcut Panel", + + "Caves Blue Tunnel Right First 4", + "Caves Blue Tunnel Left First 1", + "Caves Blue Tunnel Left Second 5", + "Caves Blue Tunnel Right Second 5", + "Caves Blue Tunnel Right Third 1", + "Caves Blue Tunnel Left Fourth 1", + "Caves Blue Tunnel Left Third 1", + + "Caves First Floor Middle", + "Caves First Floor Right", + "Caves First Floor Left", + "Caves First Floor Grounded", + "Caves Lone Pillar", + "Caves First Wooden Beam", + "Caves Second Wooden Beam", + "Caves Third Wooden Beam", + "Caves Fourth Wooden Beam", + "Caves Right Upstairs Left Row 8", + "Caves Right Upstairs Right Row 3", + "Caves Left Upstairs Single", + "Caves Left Upstairs Left Row 5", + + "Caves Challenge Entry Panel", + "Challenge Tunnels Entry Panel", + + "Tunnels Vault Box", + "Theater Challenge Video", + + "Tunnels Town Shortcut Panel", + + "Caves Skylight EP", + "Challenge Water EP", + "Tunnels Theater Flowers EP", + "Tutorial Gate EP", + + "Mountaintop Mountain Entry Panel", + + "Mountain Floor 1 Light Bridge Controller", + + "Mountain Floor 1 Right Row 5", + "Mountain Floor 1 Left Row 7", + "Mountain Floor 1 Back Row 3", + "Mountain Floor 1 Trash Pillar 2", + "Mountain Floor 2 Near Row 5", + "Mountain Floor 2 Far Row 6", + + "Mountain Floor 2 Light Bridge Controller Near", + "Mountain Floor 2 Light Bridge Controller Far", + + "Mountain Bottom Floor Yellow Bridge EP", + "Mountain Bottom Floor Blue Bridge EP", + "Mountain Floor 2 Pink Bridge EP", + + "Mountain Floor 2 Elevator Discard", + "Mountain Bottom Floor Giant Puzzle", + + "Mountain Bottom Floor Final Room Entry Left", + "Mountain Bottom Floor Final Room Entry Right", + + "Mountain Bottom Floor Caves Entry Panel", + + "Mountain Final Room Left Pillar 4", + "Mountain Final Room Right Pillar 4", + + "Challenge Vault Box", + "Theater Challenge Video", + "Mountain Bottom Floor Discard", } OBELISK_SIDES = { @@ -355,94 +457,30 @@ class StaticWitnessLocations: "Town Obelisk Side 6", } - CAVES_LOCATIONS = { - "Caves Blue Tunnel Right First 4", - "Caves Blue Tunnel Left First 1", - "Caves Blue Tunnel Left Second 5", - "Caves Blue Tunnel Right Second 5", - "Caves Blue Tunnel Right Third 1", - "Caves Blue Tunnel Left Fourth 1", - "Caves Blue Tunnel Left Third 1", - - "Caves First Floor Middle", - "Caves First Floor Right", - "Caves First Floor Left", - "Caves First Floor Grounded", - "Caves Lone Pillar", - "Caves First Wooden Beam", - "Caves Second Wooden Beam", - "Caves Third Wooden Beam", - "Caves Fourth Wooden Beam", - "Caves Right Upstairs Left Row 8", - "Caves Right Upstairs Right Row 3", - "Caves Left Upstairs Single", - "Caves Left Upstairs Left Row 5", - - "Tunnels Vault Box", - "Theater Challenge Video", - - "Caves Skylight EP", - "Challenge Water EP", - "Tunnels Theater Flowers EP", - "Tutorial Gate EP", - } - - MOUNTAIN_UNREACHABLE_FROM_BEHIND = { - "Mountaintop Trap Door Triple Exit", - - "Mountain Floor 1 Right Row 5", - "Mountain Floor 1 Left Row 7", - "Mountain Floor 1 Back Row 3", - "Mountain Floor 1 Trash Pillar 2", - "Mountain Floor 2 Near Row 5", - "Mountain Floor 2 Far Row 6", - - "Mountain Floor 2 Light Bridge Controller Near", - "Mountain Floor 2 Light Bridge Controller Far", - - "Mountain Bottom Floor Yellow Bridge EP", - "Mountain Bottom Floor Blue Bridge EP", - "Mountain Floor 2 Pink Bridge EP", - } - - MOUNTAIN_REACHABLE_FROM_BEHIND = { - "Mountain Floor 2 Elevator Discard", - "Mountain Bottom Floor Giant Puzzle", - - "Mountain Final Room Left Pillar 4", - "Mountain Final Room Right Pillar 4", - } - - MOUNTAIN_EXTRAS = { - "Challenge Vault Box", - "Theater Challenge Video", - "Mountain Bottom Floor Discard" - } - ALL_LOCATIONS_TO_ID = dict() @staticmethod - def get_id(chex): + def get_id(chex: str): """ Calculates the location ID for any given location """ - return StaticWitnessLogic.CHECKS_BY_HEX[chex]["id"] + return StaticWitnessLogic.ENTITIES_BY_HEX[chex]["id"] @staticmethod - def get_event_name(panel_hex): + def get_event_name(panel_hex: str): """ Returns the event name of any given panel. """ - action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["panelType"] == "Door" else " Solved" + action = " Opened" if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] == "Door" else " Solved" - return StaticWitnessLogic.CHECKS_BY_HEX[panel_hex]["checkName"] + action + return StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["checkName"] + action def __init__(self): all_loc_to_id = { panel_obj["checkName"]: self.get_id(chex) - for chex, panel_obj in StaticWitnessLogic.CHECKS_BY_HEX.items() + for chex, panel_obj in StaticWitnessLogic.ENTITIES_BY_HEX.items() if panel_obj["id"] } @@ -459,84 +497,44 @@ class WitnessPlayerLocations: Class that defines locations for a single player """ - def __init__(self, world, player, player_logic: WitnessPlayerLogic): + def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): """Defines locations AFTER logic changes due to options""" self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy() - doors = get_option_value(world, player, "shuffle_doors") >= 2 - earlyutm = is_option_enabled(world, player, "early_secret_area") - victory = get_option_value(world, player, "victory_condition") - mount_lasers = get_option_value(world, player, "mountain_lasers") - chal_lasers = get_option_value(world, player, "challenge_lasers") - # laser_shuffle = get_option_value(world, player, "shuffle_lasers") - - postgame = set() - postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS - postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND - postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND - postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS - - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame - - mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers) - - if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently) - postgame -= StaticWitnessLocations.CAVES_LOCATIONS - - if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)): - postgame -= {"Challenge Vault Box", "Theater Challenge Video"} - - if doors or mountain_enterable_from_top: - postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND - - if mountain_enterable_from_top: - postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND - - if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors): - postgame -= {"Mountain Bottom Floor Discard"} - - if is_option_enabled(world, player, "shuffle_discarded_panels"): + if world.options.shuffle_discarded_panels: self.PANEL_TYPES_TO_SHUFFLE.add("Discard") - if is_option_enabled(world, player, "shuffle_vault_boxes"): + if world.options.shuffle_vault_boxes: self.PANEL_TYPES_TO_SHUFFLE.add("Vault") - if get_option_value(world, player, "shuffle_EPs") == 1: + if world.options.shuffle_EPs == 1: self.PANEL_TYPES_TO_SHUFFLE.add("EP") - elif get_option_value(world, player, "shuffle_EPs") == 2: + elif world.options.shuffle_EPs == 2: self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side") for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES: - obelisk_loc_hex = StaticWitnessLogic.CHECKS_BY_NAME[obelisk_loc]["checkHex"] + obelisk_loc_hex = StaticWitnessLogic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"] if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}): self.CHECK_LOCATIONS.discard(obelisk_loc) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS - if not is_option_enabled(world, player, "shuffle_postgame"): - self.CHECK_LOCATIONS -= postgame - - self.CHECK_LOCATIONS -= { - StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] - for panel in player_logic.PRECOMPLETED_LOCATIONS - } - - self.CHECK_LOCATIONS.discard(StaticWitnessLogic.CHECKS_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) + self.CHECK_LOCATIONS.discard(StaticWitnessLogic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { - StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] - for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS + StaticWitnessLogic.ENTITIES_BY_HEX[entity_hex]["checkName"] + for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS } self.CHECK_PANELHEX_TO_ID = { - StaticWitnessLogic.CHECKS_BY_NAME[ch]["checkHex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch] + StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entity_hex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch] for ch in self.CHECK_LOCATIONS - if StaticWitnessLogic.CHECKS_BY_NAME[ch]["panelType"] in self.PANEL_TYPES_TO_SHUFFLE + if StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = StaticWitnessLogic.CHECKS_BY_NAME["Town Pet the Dog"]["checkHex"] + dog_hex = StaticWitnessLogic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id @@ -554,9 +552,14 @@ class WitnessPlayerLocations: } check_dict = { - StaticWitnessLogic.CHECKS_BY_HEX[location]["checkName"]: - StaticWitnessLocations.get_id(StaticWitnessLogic.CHECKS_BY_HEX[location]["checkHex"]) + StaticWitnessLogic.ENTITIES_BY_HEX[location]["checkName"]: + StaticWitnessLocations.get_id(StaticWitnessLogic.ENTITIES_BY_HEX[location]["entity_hex"]) for location in self.CHECK_PANELHEX_TO_ID } self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict} + + def add_location_late(self, entity_name: str): + entity_hex = StaticWitnessLogic.ENTITIES_BY_NAME[entity_name]["entity_hex"] + self.CHECK_LOCATION_TABLE[entity_hex] = entity_name + self.CHECK_PANELHEX_TO_ID[entity_hex] = StaticWitnessLocations.get_id(entity_hex) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index be1a34aedf..cfd36c09be 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -16,22 +16,22 @@ When the world has parsed its options, a second function is called to finalize t """ import copy -from typing import Set, Dict, cast, List +from collections import defaultdict +from typing import cast, TYPE_CHECKING from logging import warning -from BaseClasses import MultiWorld from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition -from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \ - get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \ - get_doors_simple_list, get_laser_shuffle, get_ep_all_individual, get_ep_obelisks, get_ep_easy, get_ep_no_eclipse, \ - get_ep_no_caves, get_ep_no_mountain, get_ep_no_videos -from .Options import is_option_enabled, get_option_value, the_witness_options +from .utils import * + +if TYPE_CHECKING: + from . import WitnessWorld class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" - def reduce_req_within_region(self, panel_hex): + @lru_cache(maxsize=None) + def reduce_req_within_region(self, panel_hex: str) -> FrozenSet[FrozenSet[str]]: """ Panels in this game often only turn on when other panels are solved. Those other panels may have different item requirements. @@ -40,14 +40,14 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ - if panel_hex in self.COMPLETELY_DISABLED_CHECKS or panel_hex in self.PRECOMPLETED_LOCATIONS: + if panel_hex in self.COMPLETELY_DISABLED_ENTITIES or panel_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: return frozenset() - check_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel_hex] + entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) - if check_obj["id"]: + if entity_obj["id"]: these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"] these_items = frozenset({ @@ -58,6 +58,8 @@ class WitnessPlayerLogic: for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] + if panel_hex in self.DOOR_ITEMS_BY_ID: door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) @@ -68,14 +70,21 @@ class WitnessPlayerLogic: for items_option in these_items: all_options.add(items_option.union(dependentItem)) + # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved... if panel_hex != "0x28A0D": return frozenset(all_options) - else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved - these_items = all_options + # ...except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. + # In the future, it would be wise to make a distinction between "power dependencies" and other dependencies. + if any("0x28998" in option for option in these_panels): + return frozenset(all_options) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] + these_items = all_options - these_panels = frozenset({panels - self.PRECOMPLETED_LOCATIONS for panels in these_panels}) + disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES + if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"} + + these_panels = frozenset({panels - disabled_eps + for panels in these_panels}) if these_panels == frozenset({frozenset()}): return these_items @@ -85,40 +94,30 @@ class WitnessPlayerLogic: for option in these_panels: dependent_items_for_option = frozenset({frozenset()}) - for option_panel in option: - dep_obj = self.REFERENCE_LOGIC.CHECKS_BY_HEX.get(option_panel) + for option_entity in option: + dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) - if option_panel in self.COMPLETELY_DISABLED_CHECKS: - new_items = frozenset() - elif option_panel in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: - new_items = frozenset({frozenset([option_panel])}) - # If a panel turns on when a panel in a different region turns on, - # the latter panel will be an "event panel", unless it ends up being - # a location itself. This prevents generation failures. - elif dep_obj["region"]["name"] != check_obj["region"]["name"]: - new_items = frozenset({frozenset([option_panel])}) - self.EVENT_PANELS_FROM_PANELS.add(option_panel) - elif option_panel in self.ALWAYS_EVENT_NAMES_BY_HEX.keys(): - new_items = frozenset({frozenset([option_panel])}) - self.EVENT_PANELS_FROM_PANELS.add(option_panel) + if option_entity in self.EVENT_NAMES_BY_HEX: + new_items = frozenset({frozenset([option_entity])}) + elif option_entity in {"7 Lasers", "11 Lasers", "PP2 Weirdness", "Theater to Tunnels"}: + new_items = frozenset({frozenset([option_entity])}) else: - new_items = self.reduce_req_within_region(option_panel) + new_items = self.reduce_req_within_region(option_entity) + if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]: + new_items = frozenset( + frozenset(possibility | {dep_obj["region"]["name"]}) + for possibility in new_items + ) - updated_items = set() - - for items_option in dependent_items_for_option: - for items_option2 in new_items: - updated_items.add(items_option.union(items_option2)) - - dependent_items_for_option = updated_items + dependent_items_for_option = dnf_and([dependent_items_for_option, new_items]) for items_option in these_items: for dependentItem in dependent_items_for_option: all_options.add(items_option.union(dependentItem)) - return frozenset(all_options) + return dnf_remove_redundancies(frozenset(all_options)) - def make_single_adjustment(self, adj_type, line): + def make_single_adjustment(self, adj_type: str, line: str): from . import StaticWitnessItems """Makes a single logic adjustment based on additional logic file""" @@ -148,9 +147,9 @@ class WitnessPlayerLogic: self.THEORETICAL_ITEMS.discard(item_name) if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI\ - .difference_update(cast(ProgressiveItemDefinition, - StaticWitnessLogic.all_items[item_name]).child_item_names) + self.THEORETICAL_ITEMS_NO_MULTI.difference_update( + cast(ProgressiveItemDefinition, StaticWitnessLogic.all_items[item_name]).child_item_names + ) else: self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) @@ -165,25 +164,15 @@ class WitnessPlayerLogic: if adj_type == "Event Items": line_split = line.split(" - ") + new_event_name = line_split[0] hex_set = line_split[1].split(",") + for entity, event_name in self.EVENT_NAMES_BY_HEX.items(): + if event_name == new_event_name: + self.DONT_MAKE_EVENTS.add(entity) + for hex_code in hex_set: - self.ALWAYS_EVENT_NAMES_BY_HEX[hex_code] = line_split[0] - - """ - Should probably do this differently... - Events right now depend on a panel. - That seems bad. - """ - - to_remove = set() - - for hex_code, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): - if hex_code not in hex_set and event_name == line_split[0]: - to_remove.add(hex_code) - - for remove in to_remove: - del self.ALWAYS_EVENT_NAMES_BY_HEX[remove] + self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name return @@ -196,9 +185,10 @@ class WitnessPlayerLogic: if len(line_split) > 2: required_items = parse_lambda(line_split[2]) - items_actually_in_the_game = [item_name for item_name, item_definition - in StaticWitnessLogic.all_items.items() - if item_definition.category is ItemCategory.SYMBOL] + items_actually_in_the_game = [ + item_name for item_name, item_definition in StaticWitnessLogic.all_items.items() + if item_definition.category is ItemCategory.SYMBOL + ] required_items = frozenset( subset.intersection(items_actually_in_the_game) for subset in required_items @@ -213,116 +203,204 @@ class WitnessPlayerLogic: if adj_type == "Disabled Locations": panel_hex = line[:7] - self.COMPLETELY_DISABLED_CHECKS.add(panel_hex) + self.COMPLETELY_DISABLED_ENTITIES.add(panel_hex) + + return + + if adj_type == "Irrelevant Locations": + panel_hex = line[:7] + + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(panel_hex) return if adj_type == "Region Changes": new_region_and_options = define_new_region(line + ":") - + self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1] return + if adj_type == "New Connections": + line_split = line.split(" - ") + source_region = line_split[0] + target_region = line_split[1] + panel_set_string = line_split[2] + + for connection in self.CONNECTIONS_BY_REGION_NAME[source_region]: + if connection[0] == target_region: + self.CONNECTIONS_BY_REGION_NAME[source_region].remove(connection) + + if panel_set_string == "TrueOneWay": + self.CONNECTIONS_BY_REGION_NAME[source_region].add( + (target_region, frozenset({frozenset(["TrueOneWay"])})) + ) + else: + new_lambda = connection[1] | parse_lambda(panel_set_string) + self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda)) + break + else: # Execute if loop did not break. TIL this is a thing you can do! + new_conn = (target_region, parse_lambda(panel_set_string)) + self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn) + if adj_type == "Added Locations": if "0x" in line: - line = StaticWitnessLogic.CHECKS_BY_HEX[line]["checkName"] + line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"] self.ADDED_CHECKS.add(line) - if adj_type == "Precompleted Locations": - self.PRECOMPLETED_LOCATIONS.add(line) - - def make_options_adjustments(self, world, player): + def make_options_adjustments(self, world: "WitnessWorld"): """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] - if get_option_value(world, player, "victory_condition") == 0: + # Postgame + + doors = world.options.shuffle_doors >= 2 + lasers = world.options.shuffle_lasers + early_caves = world.options.early_caves > 0 + victory = world.options.victory_condition + mnt_lasers = world.options.mountain_lasers + chal_lasers = world.options.challenge_lasers + + mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mnt_lasers) + + if not world.options.shuffle_postgame: + if not (early_caves or doors): + adjustment_linesets_in_order.append(get_caves_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_path_to_challenge_exclusion_list()) + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) + + if not ((doors or early_caves) and (victory == 0 or (victory == 2 and mnt_lasers > chal_lasers))): + adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + + if not (doors or mountain_enterable_from_top): + adjustment_linesets_in_order.append(get_mountain_lower_exclusion_list()) + + if not mountain_enterable_from_top: + adjustment_linesets_in_order.append(get_mountain_upper_exclusion_list()) + + if not ((victory == 0 and doors) or victory == 1 or (victory == 2 and mnt_lasers > chal_lasers and doors)): + if doors: + adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) + else: + adjustment_linesets_in_order.append(get_bottom_floor_discard_nondoors_exclusion_list()) + + if victory == 2 and chal_lasers >= mnt_lasers: + adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"]) + + # Exclude Discards / Vaults + + if not world.options.shuffle_discarded_panels: + # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both + # (remote) doors and lasers are shuffled. + if not world.options.disable_non_randomized_puzzles or (doors and lasers): + adjustment_linesets_in_order.append(get_discard_exclusion_list()) + + if doors: + adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list()) + + if not world.options.shuffle_vault_boxes: + adjustment_linesets_in_order.append(get_vault_exclusion_list()) + if not victory == 1: + adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list()) + + # Victory Condition + + if victory == 0: self.VICTORY_LOCATION = "0x3D9A9" - elif get_option_value(world, player, "victory_condition") == 1: + elif victory == 1: self.VICTORY_LOCATION = "0x0356B" - elif get_option_value(world, player, "victory_condition") == 2: + elif victory == 2: self.VICTORY_LOCATION = "0x09F7F" - elif get_option_value(world, player, "victory_condition") == 3: + elif victory == 3: self.VICTORY_LOCATION = "0xFFF00" - if get_option_value(world, player, "challenge_lasers") <= 7: + if chal_lasers <= 7: adjustment_linesets_in_order.append([ "Requirement Changes:", "0xFFF00 - 11 Lasers - True", ]) - if is_option_enabled(world, player, "disable_non_randomized_puzzles"): + if world.options.disable_non_randomized_puzzles: adjustment_linesets_in_order.append(get_disable_unrandomized_list()) - if is_option_enabled(world, player, "shuffle_symbols") or "shuffle_symbols" not in the_witness_options.keys(): + if world.options.shuffle_symbols: adjustment_linesets_in_order.append(get_symbol_shuffle_list()) - if get_option_value(world, player, "EP_difficulty") == 0: + if world.options.EP_difficulty == 0: adjustment_linesets_in_order.append(get_ep_easy()) - elif get_option_value(world, player, "EP_difficulty") == 1: + elif world.options.EP_difficulty == 1: adjustment_linesets_in_order.append(get_ep_no_eclipse()) - if not is_option_enabled(world, player, "shuffle_vault_boxes"): - adjustment_linesets_in_order.append(get_ep_no_videos()) + if world.options.door_groupings == 1: + if world.options.shuffle_doors == 1: + adjustment_linesets_in_order.append(get_simple_panels()) + elif world.options.shuffle_doors == 2: + adjustment_linesets_in_order.append(get_simple_doors()) + elif world.options.shuffle_doors == 3: + adjustment_linesets_in_order.append(get_simple_doors()) + adjustment_linesets_in_order.append(get_simple_additional_panels()) + else: + if world.options.shuffle_doors == 1: + adjustment_linesets_in_order.append(get_complex_door_panels()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) + elif world.options.shuffle_doors == 2: + adjustment_linesets_in_order.append(get_complex_doors()) + elif world.options.shuffle_doors == 3: + adjustment_linesets_in_order.append(get_complex_doors()) + adjustment_linesets_in_order.append(get_complex_additional_panels()) - doors = get_option_value(world, player, "shuffle_doors") >= 2 - earlyutm = is_option_enabled(world, player, "early_secret_area") - victory = get_option_value(world, player, "victory_condition") - mount_lasers = get_option_value(world, player, "mountain_lasers") - chal_lasers = get_option_value(world, player, "challenge_lasers") + if world.options.shuffle_boat: + adjustment_linesets_in_order.append(get_boat()) - excluse_postgame = not is_option_enabled(world, player, "shuffle_postgame") + if world.options.early_caves == 2: + adjustment_linesets_in_order.append(get_early_caves_start_list()) - if excluse_postgame and not (earlyutm or doors): - adjustment_linesets_in_order.append(get_ep_no_caves()) + if world.options.early_caves == 1 and not doors: + adjustment_linesets_in_order.append(get_early_caves_list()) - mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers) - if excluse_postgame and not mountain_enterable_from_top: - adjustment_linesets_in_order.append(get_ep_no_mountain()) - - if get_option_value(world, player, "shuffle_doors") == 1: - adjustment_linesets_in_order.append(get_door_panel_shuffle_list()) - - if get_option_value(world, player, "shuffle_doors") == 2: - adjustment_linesets_in_order.append(get_doors_simple_list()) - - if get_option_value(world, player, "shuffle_doors") == 3: - adjustment_linesets_in_order.append(get_doors_complex_list()) - - if get_option_value(world, player, "shuffle_doors") == 4: - adjustment_linesets_in_order.append(get_doors_max_list()) - - if is_option_enabled(world, player, "early_secret_area"): - adjustment_linesets_in_order.append(get_early_utm_list()) + if world.options.elevators_come_to_you: + adjustment_linesets_in_order.append(get_elevators_come_to_you()) for item in self.YAML_ADDED_ITEMS: adjustment_linesets_in_order.append(["Items:", item]) - if is_option_enabled(world, player, "shuffle_lasers"): + if lasers: adjustment_linesets_in_order.append(get_laser_shuffle()) - if get_option_value(world, player, "shuffle_EPs") == 0: # No EP Shuffle - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:]) + if world.options.shuffle_EPs: + ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items() + if ep_obj["entityType"] == "EP") + + for ep_hex, ep_obj in ep_gen: + obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]] + obelisk_name = obelisk["checkName"] + ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] + self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" + else: adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) - elif get_option_value(world, player, "shuffle_EPs") == 1: # Individual EPs - adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) + if world.options.shuffle_EPs == 0: + adjustment_linesets_in_order.append(["Irrelevant Locations:"] + get_ep_all_individual()[1:]) yaml_disabled_eps = [] for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS: - if yaml_disabled_location not in StaticWitnessLogic.CHECKS_BY_NAME: + if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: continue - loc_obj = StaticWitnessLogic.CHECKS_BY_NAME[yaml_disabled_location] + loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location] - if loc_obj["panelType"] == "EP" and get_option_value(world, player, "shuffle_EPs") == 2: - yaml_disabled_eps.append(loc_obj["checkHex"]) + if loc_obj["entityType"] == "EP" and world.options.shuffle_EPs != 0: + yaml_disabled_eps.append(loc_obj["entity_hex"]) - if loc_obj["panelType"] in {"EP", "General"}: - self.EXCLUDED_LOCATIONS.add(loc_obj["checkHex"]) + if loc_obj["entityType"] in {"EP", "General", "Vault", "Discard"}: + self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"]) - adjustment_linesets_in_order.append(["Precompleted Locations:"] + yaml_disabled_eps) + adjustment_linesets_in_order.append(["Disabled Locations:"] + yaml_disabled_eps) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -337,15 +415,19 @@ class WitnessPlayerLogic: self.make_single_adjustment(current_adjustment_type, line) + for entity_id in self.COMPLETELY_DISABLED_ENTITIES: + if entity_id in self.DOOR_ITEMS_BY_ID: + del self.DOOR_ITEMS_BY_ID[entity_id] + def make_dependency_reduced_checklist(self): """ Turns dependent check set into semi-independent check set """ - for check_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): - indep_requirement = self.reduce_req_within_region(check_hex) + for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): + indep_requirement = self.reduce_req_within_region(entity_hex) - self.REQUIREMENTS_BY_HEX[check_hex] = indep_requirement + self.REQUIREMENTS_BY_HEX[entity_hex] = indep_requirement for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: if item not in self.THEORETICAL_ITEMS: @@ -360,71 +442,76 @@ class WitnessPlayerLogic: else: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) - def make_event_item_pair(self, panel): + for region, connections in self.CONNECTIONS_BY_REGION_NAME.items(): + new_connections = [] + + for connection in connections: + overall_requirement = frozenset() + + for option in connection[1]: + individual_entity_requirements = [] + for entity in option: + if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: + individual_entity_requirements.append(frozenset({frozenset({entity})})) + else: + entity_req = self.reduce_req_within_region(entity) + + if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: + region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] + entity_req = dnf_and([entity_req, frozenset({frozenset({region_name})})]) + + individual_entity_requirements.append(entity_req) + + overall_requirement |= dnf_and(individual_entity_requirements) + + new_connections.append((connection[0], overall_requirement)) + + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + + def make_event_item_pair(self, panel: str): """ Makes a pair of an event panel and its event item """ - action = " Opened" if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "Door" else " Solved" + action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" - name = StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] + action - if panel not in self.EVENT_ITEM_NAMES: - if StaticWitnessLogic.CHECKS_BY_HEX[panel]["panelType"] == "EP": - obelisk = StaticWitnessLogic.CHECKS_BY_HEX[StaticWitnessLogic.EP_TO_OBELISK_SIDE[panel]]["checkName"] - - self.EVENT_ITEM_NAMES[panel] = obelisk + " - " + StaticWitnessLogic.CHECKS_BY_HEX[panel]["checkName"] - - else: - warning("Panel \"" + name + "\" does not have an associated event name.") - self.EVENT_ITEM_NAMES[panel] = name + " Event" - pair = (name, self.EVENT_ITEM_NAMES[panel]) + name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action + if panel not in self.EVENT_NAMES_BY_HEX: + warning("Panel \"" + name + "\" does not have an associated event name.") + self.EVENT_NAMES_BY_HEX[panel] = name + " Event" + pair = (name, self.EVENT_NAMES_BY_HEX[panel]) return pair def make_event_panel_lists(self): - """ - Special event panel data structures - """ + self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - - for region_name, connections in self.CONNECTIONS_BY_REGION_NAME.items(): - for connection in connections: - for panel_req in connection[1]: - for panel in panel_req: - if panel == "TrueOneWay": - continue - - if self.REFERENCE_LOGIC.CHECKS_BY_HEX[panel]["region"]["name"] != region_name: - self.EVENT_PANELS_FROM_REGIONS.add(panel) - - self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_PANELS) - self.EVENT_PANELS.update(self.EVENT_PANELS_FROM_REGIONS) - - for always_hex, always_item in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): - self.ALWAYS_EVENT_HEX_CODES.add(always_hex) - self.EVENT_PANELS.add(always_hex) - self.EVENT_ITEM_NAMES[always_hex] = always_item + for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): + if event_hex in self.COMPLETELY_DISABLED_ENTITIES: + continue + self.EVENT_PANELS.add(event_hex) for panel in self.EVENT_PANELS: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - def __init__(self, world: MultiWorld, player: int, disabled_locations: Set[str], start_inv: Dict[str, int]): + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]): self.YAML_DISABLED_LOCATIONS = disabled_locations self.YAML_ADDED_ITEMS = start_inv self.EVENT_PANELS_FROM_PANELS = set() self.EVENT_PANELS_FROM_REGIONS = set() + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() + self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS_NO_MULTI = set() - self.MULTI_AMOUNTS = dict() + self.MULTI_AMOUNTS = defaultdict(lambda: 1) self.MULTI_LISTS = dict() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_ITEMS_BY_ID: Dict[str, List[int]] = {} + self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} self.STARTING_INVENTORY = set() - self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization") + self.DIFFICULTY = world.options.puzzle_randomization.value if self.DIFFICULTY == 0: self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal @@ -441,106 +528,30 @@ class WitnessPlayerLogic: # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() - self.ALWAYS_EVENT_HEX_CODES = set() - self.COMPLETELY_DISABLED_CHECKS = set() + self.DONT_MAKE_EVENTS = set() + self.COMPLETELY_DISABLED_ENTITIES = set() self.PRECOMPLETED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set() self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" - self.EVENT_ITEM_NAMES = { - "0x09D9B": "Monastery Shutters Open", - "0x193A6": "Monastery Laser Panel Activates", - "0x00037": "Monastery Branch Panels Activate", - "0x0A079": "Access to Bunker Laser", - "0x0A3B5": "Door to Tutorial Discard Opens", - "0x00139": "Keep Hedges 1 Knowledge", - "0x019DC": "Keep Hedges 2 Knowledge", - "0x019E7": "Keep Hedges 3 Knowledge", - "0x01A0F": "Keep Hedges 4 Knowledge", - "0x033EA": "Pressure Plates 1 Knowledge", - "0x01BE9": "Pressure Plates 2 Knowledge", - "0x01CD3": "Pressure Plates 3 Knowledge", - "0x01D3F": "Pressure Plates 4 Knowledge", - "0x09F7F": "Mountain Access", - "0x0367C": "Quarry Laser Stoneworks Requirement Met", - "0x009A1": "Swamp Between Bridges Far 1 Activates", - "0x00006": "Swamp Cyan Water Drains", - "0x00990": "Swamp Between Bridges Near Row 1 Activates", - "0x0A8DC": "Intro 6 Activates", - "0x0000A": "Swamp Beyond Rotating Bridge 1 Access", - "0x09E86": "Mountain Floor 2 Blue Bridge Access", - "0x09ED8": "Mountain Floor 2 Yellow Bridge Access", - "0x0A3D0": "Quarry Laser Boathouse Requirement Met", - "0x00596": "Swamp Red Water Drains", - "0x00E3A": "Swamp Purple Water Drains", - "0x0343A": "Door to Symmetry Island Powers On", - "0xFFF00": "Mountain Bottom Floor Discard Turns On", - "0x17CA6": "All Boat Panels Turn On", - "0x17CDF": "All Boat Panels Turn On", - "0x09DB8": "All Boat Panels Turn On", - "0x17C95": "All Boat Panels Turn On", - "0x0A054": "Couch EP solvable", - "0x03BB0": "Town Church Lattice Vision From Outside", - "0x28AC1": "Town Wooden Rooftop Turns On", - "0x28A69": "Town Tower 1st Door Opens", - "0x28ACC": "Town Tower 2nd Door Opens", - "0x28AD9": "Town Tower 3rd Door Opens", - "0x28B39": "Town Tower 4th Door Opens", - "0x03675": "Quarry Stoneworks Ramp Activation From Above", - "0x03679": "Quarry Stoneworks Lift Lowering While Standing On It", - "0x2FAF6": "Tutorial Gate Secret Solution Knowledge", - "0x079DF": "Town Tall Hexagonal Turns On", - "0x17DA2": "Right Orange Bridge Fully Extended", - "0x19B24": "Shadows Intro Patterns Visible", - "0x2700B": "Open Door to Treehouse Laser House", - "0x00055": "Orchard Apple Trees 4 Turns On", - "0x17DDB": "Left Orange Bridge Fully Extended", - "0x03535": "Shipwreck Video Pattern Knowledge", - "0x03542": "Mountain Video Pattern Knowledge", - "0x0339E": "Desert Video Pattern Knowledge", - "0x03481": "Tutorial Video Pattern Knowledge", - "0x03702": "Jungle Video Pattern Knowledge", - "0x0356B": "Challenge Video Pattern Knowledge", - "0x0A15F": "Desert Laser Panel Shutters Open (1)", - "0x012D7": "Desert Laser Panel Shutters Open (2)", - "0x03613": "Treehouse Orange Bridge 13 Turns On", - "0x17DEC": "Treehouse Laser House Access Requirement", - "0x03C08": "Town Church Entry Opens", - "0x17D02": "Windmill Blades Spinning", - "0x0A0C9": "Cargo Box EP completable", - "0x09E39": "Pink Light Bridge Extended", - "0x17CC4": "Rails EP available", - "0x2896A": "Bridge Underside EP available", - "0x00064": "First Tunnel EP visible", - "0x03553": "Tutorial Video EPs availble", - "0x17C79": "Bunker Door EP available", - "0x275FF": "Stoneworks Light EPs available", - "0x17E2B": "Remaining Purple Sand EPs available", - "0x03852": "Ramp EPs requirement", - "0x334D8": "RGB panels & EPs solvable", - "0x03750": "Left Garden EP available", - "0x03C0C": "RGB Flowers EP requirement", - "0x01CD5": "Pressure Plates 3 EP requirement", - "0x3865F": "Ramp EPs access requirement", - } - self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "Symmetry Laser Activation", - "0x012FB": "Desert Laser Activation", + self.EVENT_NAMES_BY_HEX = { + "0x00509": "+1 Laser (Symmetry Laser)", + "0x012FB": "+1 Laser (Desert Laser)", "0x09F98": "Desert Laser Redirection", - "0x01539": "Quarry Laser Activation", - "0x181B3": "Shadows Laser Activation", - "0x014BB": "Keep Laser Activation", - "0x17C65": "Monastery Laser Activation", - "0x032F9": "Town Laser Activation", - "0x00274": "Jungle Laser Activation", - "0x0C2B2": "Bunker Laser Activation", - "0x00BF6": "Swamp Laser Activation", - "0x028A4": "Treehouse Laser Activation", - "0x09F7F": "Mountaintop Trap Door Turns On", - "0x17C34": "Mountain Access", + "0x01539": "+1 Laser (Quarry Laser)", + "0x181B3": "+1 Laser (Shadows Laser)", + "0x014BB": "+1 Laser (Keep Laser)", + "0x17C65": "+1 Laser (Monastery Laser)", + "0x032F9": "+1 Laser (Town Laser)", + "0x00274": "+1 Laser (Jungle Laser)", + "0x0C2B2": "+1 Laser (Bunker Laser)", + "0x00BF6": "+1 Laser (Swamp Laser)", + "0x028A4": "+1 Laser (Treehouse Laser)", + "0x09F7F": "Mountain Entry", + "0xFFF00": "Bottom Floor Discard Turns On", } - self.make_options_adjustments(world, player) + self.make_options_adjustments(world) self.make_dependency_reduced_checklist() self.make_event_panel_lists() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 0e15cafe10..2187010bac 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -2,13 +2,17 @@ Defines Region for The Witness, assigns locations to them, and connects them with the proper requirements """ +from typing import FrozenSet, TYPE_CHECKING, Dict, Tuple, List -from BaseClasses import MultiWorld, Entrance +from BaseClasses import Entrance, Region +from Utils import KeyedDefaultDict from .static_logic import StaticWitnessLogic -from .Options import get_option_value from .locations import WitnessPlayerLocations, StaticWitnessLocations from .player_logic import WitnessPlayerLogic +if TYPE_CHECKING: + from . import WitnessWorld + class WitnessRegions: """Class that defines Witness Regions""" @@ -16,62 +20,81 @@ class WitnessRegions: locat = None logic = None - def make_lambda(self, panel_hex_to_solve_set, world, player, player_logic): + @staticmethod + def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld"): + from .rules import _meets_item_requirements + """ Lambdas are made in a for loop, so the values have to be captured This function is for that purpose """ - return lambda state: state._witness_can_solve_panels( - panel_hex_to_solve_set, world, player, player_logic, self.locat - ) + return _meets_item_requirements(item_requirement, world) - def connect(self, world: MultiWorld, player: int, source: str, target: str, player_logic: WitnessPlayerLogic, - panel_hex_to_solve_set=frozenset({frozenset()}), backwards: bool = False): + def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: FrozenSet[FrozenSet[str]], + regions_by_name: Dict[str, Region], backwards: bool = False): """ connect two regions and set the corresponding requirement """ - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) + + # Remove any possibilities where being in the target region would be required anyway. + real_requirement = frozenset({option for option in req if target not in option}) + + # There are some connections that should only be done one way. If this is a backwards connection, check for that + if backwards: + real_requirement = frozenset({option for option in real_requirement if "TrueOneWay" not in option}) + + # Dissolve any "True" or "TrueOneWay" + real_requirement = frozenset({option - {"True", "TrueOneWay"} for option in real_requirement}) + + # If there is no way to actually use this connection, don't even bother making it. + if not real_requirement: + return + + # We don't need to check for the accessibility of the source region. + final_requirement = frozenset({option - frozenset({source}) for option in real_requirement}) + + source_region = regions_by_name[source] + target_region = regions_by_name[target] backwards = " Backwards" if backwards else "" + connection_name = source + " to " + target + backwards connection = Entrance( - player, - source + " to " + target + backwards, + world.player, + connection_name, source_region ) - connection.access_rule = self.make_lambda(panel_hex_to_solve_set, world, player, player_logic) + connection.access_rule = self.make_lambda(final_requirement, world) source_region.exits.append(connection) connection.connect(target_region) - def create_regions(self, world, player: int, player_logic: WitnessPlayerLogic): + self.created_entrances[(source, target)].append(connection) + + # Register any necessary indirect connections + mentioned_regions = { + single_unlock for option in final_requirement for single_unlock in option + if single_unlock in self.reference_logic.ALL_REGIONS_BY_NAME + } + + for dependent_region in mentioned_regions: + world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection) + + def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic): """ Creates all the regions for The Witness """ from . import create_region - world.regions += [ - create_region(world, player, 'Menu', self.locat, None, ["The Splashscreen?"]), - ] - - difficulty = get_option_value(world, player, "puzzle_randomization") - - if difficulty == 1: - reference_logic = StaticWitnessLogic.sigma_expert - elif difficulty == 0: - reference_logic = StaticWitnessLogic.sigma_normal - else: - reference_logic = StaticWitnessLogic.vanilla - all_locations = set() + regions_by_name = dict() - for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): + for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): locations_for_this_region = [ - reference_logic.CHECKS_BY_HEX[panel]["checkName"] for panel in region["panels"] - if reference_logic.CHECKS_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE + self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"] + if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE ] locations_for_this_region += [ StaticWitnessLocations.get_event_name(panel) for panel in region["panels"] @@ -80,34 +103,45 @@ class WitnessRegions: all_locations = all_locations | set(locations_for_this_region) - world.regions += [ - create_region(world, player, region_name, self.locat, locations_for_this_region) - ] + new_region = create_region(world, region_name, self.locat, locations_for_this_region) - for region_name, region in reference_logic.ALL_REGIONS_BY_NAME.items(): + regions_by_name[region_name] = new_region + + for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: - if connection[1] == frozenset({frozenset(["TrueOneWay"])}): - self.connect(world, player, region_name, connection[0], player_logic, frozenset({frozenset()})) + self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) + self.connect_if_possible(world, connection[0], region_name, connection[1], regions_by_name, True) + + # find regions that are completely disconnected from the start node and remove them + regions_to_check = {"Menu"} + reachable_regions = {"Menu"} + + while regions_to_check: + next_region = regions_to_check.pop() + region_obj = regions_by_name[next_region] + + for exit in region_obj.exits: + target = exit.connected_region + + if target.name in reachable_regions: continue - backwards_connections = set() + regions_to_check.add(target.name) + reachable_regions.add(target.name) - for subset in connection[1]: - if all({panel in player_logic.DOOR_ITEMS_BY_ID for panel in subset}): - if all({reference_logic.CHECKS_BY_HEX[panel]["id"] is None for panel in subset}): - backwards_connections.add(subset) + final_regions_list = [v for k, v in regions_by_name.items() if k in reachable_regions] - if backwards_connections: - self.connect( - world, player, connection[0], region_name, player_logic, - frozenset(backwards_connections), True - ) + world.multiworld.regions += final_regions_list - self.connect(world, player, region_name, connection[0], player_logic, connection[1]) + def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"): + difficulty = world.options.puzzle_randomization.value - world.get_entrance("The Splashscreen?", player).connect( - world.get_region('First Hallway', player) - ) + if difficulty == 0: + self.reference_logic = StaticWitnessLogic.sigma_normal + elif difficulty == 1: + self.reference_logic = StaticWitnessLogic.sigma_expert + elif difficulty == 2: + self.reference_logic = StaticWitnessLogic.vanilla - def __init__(self, locat: WitnessPlayerLocations): self.locat = locat + self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: []) diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 4cf3054af6..07fea23b14 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -3,223 +3,218 @@ Defines the rules by which locations can be accessed, depending on the items received """ -# pylint: disable=E1101 +from typing import TYPE_CHECKING, Callable, FrozenSet -from BaseClasses import MultiWorld +from BaseClasses import CollectionState from .player_logic import WitnessPlayerLogic -from .Options import is_option_enabled, get_option_value from .locations import WitnessPlayerLocations -from . import StaticWitnessLogic -from worlds.AutoWorld import LogicMixin +from . import StaticWitnessLogic, WitnessRegions from worlds.generic.Rules import set_rule +if TYPE_CHECKING: + from . import WitnessWorld -class WitnessLogic(LogicMixin): +laser_hexes = [ + "0x028A4", + "0x00274", + "0x032F9", + "0x01539", + "0x181B3", + "0x0C2B2", + "0x00509", + "0x00BF6", + "0x014BB", + "0x012FB", + "0x17C65", +] + + +def _has_laser(laser_hex: str, world: "WitnessWorld", player: int) -> Callable[[CollectionState], bool]: + if laser_hex == "0x012FB": + return lambda state: ( + _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state) + and state.has("Desert Laser Redirection", player) + ) + else: + return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat) + + +def _has_lasers(amount: int, world: "WitnessWorld") -> Callable[[CollectionState], bool]: + laser_lambdas = [] + + for laser_hex in laser_hexes: + has_laser_lambda = _has_laser(laser_hex, world, world.player) + + laser_lambdas.append(has_laser_lambda) + + return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount + + +def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, + locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: """ - Logic macros that get reused + Determines whether a panel can be solved """ - def _witness_has_lasers(self, world, player: int, amount: int) -> bool: - regular_lasers = not is_option_enabled(world, player, "shuffle_lasers") + panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel] + entity_name = panel_obj["checkName"] - lasers = 0 - - place_names = [ - "Symmetry", "Desert", "Town", "Monastery", "Keep", - "Quarry", "Treehouse", "Jungle", "Bunker", "Swamp", "Shadows" - ] - - for place in place_names: - has_laser = self.has(place + " Laser", player) - - has_laser = has_laser or (regular_lasers and self.has(place + " Laser Activation", player)) - - if place == "Desert": - has_laser = has_laser and self.has("Desert Laser Redirection", player) - - lasers += int(has_laser) - - return lasers >= amount - - def _witness_can_solve_panel(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Determines whether a panel can be solved - """ - - panel_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel] - check_name = panel_obj["checkName"] - - if (check_name + " Solved" in locat.EVENT_LOCATION_TABLE - and not self.has(player_logic.EVENT_ITEM_PAIRS[check_name + " Solved"], player)): - return False - if (check_name + " Solved" not in locat.EVENT_LOCATION_TABLE - and not self._witness_meets_item_requirements(panel, world, player, player_logic, locat)): - return False - return True - - def _witness_meets_item_requirements(self, panel, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Checks whether item and panel requirements are met for - a panel - """ - - panel_req = player_logic.REQUIREMENTS_BY_HEX[panel] - - for option in panel_req: - if len(option) == 0: - return True - - valid_option = True - - for item in option: - if item == "7 Lasers": - laser_req = get_option_value(world, player, "mountain_lasers") - - if not self._witness_has_lasers(world, player, laser_req): - valid_option = False - break - elif item == "11 Lasers": - laser_req = get_option_value(world, player, "challenge_lasers") - - if not self._witness_has_lasers(world, player, laser_req): - valid_option = False - break - elif item == "PP2 Weirdness": - hedge_2_access = ( - self.can_reach("Keep 2nd Maze to Keep", "Entrance", player) - or self.can_reach("Keep to Keep 2nd Maze", "Entrance", player) - ) - - hedge_3_access = ( - self.can_reach("Keep 3rd Maze to Keep", "Entrance", player) - or self.can_reach("Keep 2nd Maze to Keep 3rd Maze", "Entrance", player) - and hedge_2_access - ) - - hedge_4_access = ( - self.can_reach("Keep 4th Maze to Keep", "Entrance", player) - or self.can_reach("Keep 3rd Maze to Keep 4th Maze", "Entrance", player) - and hedge_3_access - ) - - hedge_access = ( - self.can_reach("Keep 4th Maze to Keep Tower", "Entrance", player) - and self.can_reach("Keep", "Region", player) - and hedge_4_access - ) - - backwards_to_fourth = ( - self.can_reach("Keep", "Region", player) - and self.can_reach("Keep 4th Pressure Plate to Keep Tower", "Entrance", player) - and ( - self.can_reach("Keep Tower to Keep", "Entrance", player) - or hedge_access - ) - ) - - shadows_shortcut = ( - self.can_reach("Main Island", "Region", player) - and self.can_reach("Keep 4th Pressure Plate to Shadows", "Entrance", player) - ) - - backwards_access = ( - self.can_reach("Keep 3rd Pressure Plate to Keep 4th Pressure Plate", "Entrance", player) - and (backwards_to_fourth or shadows_shortcut) - ) - - front_access = ( - self.can_reach("Keep to Keep 2nd Pressure Plate", 'Entrance', player) - and self.can_reach("Keep", "Region", player) - ) - - if not (front_access and backwards_access): - valid_option = False - break - elif item == "Theater to Tunnels": - direct_access = ( - self.can_reach("Tunnels to Windmill Interior", "Entrance", player) - and self.can_reach("Windmill Interior to Theater", "Entrance", player) - ) - - theater_from_town = ( - self.can_reach("Town to Windmill Interior", "Entrance", player) - and self.can_reach("Windmill Interior to Theater", "Entrance", player) - or self.can_reach("Theater to Town", "Entrance", player) - ) - - tunnels_from_town = ( - self.can_reach("Tunnels to Windmill Interior", "Entrance", player) - and self.can_reach("Town to Windmill Interior", "Entrance", player) - or self.can_reach("Tunnels to Town", "Entrance", player) - ) - - if not (direct_access or theater_from_town and tunnels_from_town): - valid_option = False - break - elif item in player_logic.EVENT_PANELS: - if not self._witness_can_solve_panel(item, world, player, player_logic, locat): - valid_option = False - break - elif not self.has(item, player): - # The player doesn't have the item. Check to see if it's part of a progressive item and, if so, the - # player has enough of that. - prog_item = StaticWitnessLogic.get_parent_progressive_item(item) - if prog_item is item or not self.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]): - valid_option = False - break - - if valid_option: - return True - - return False - - def _witness_can_solve_panels(self, panel_hex_to_solve_set, world, player, player_logic: WitnessPlayerLogic, locat): - """ - Checks whether a set of panels can be solved. - """ - - for option in panel_hex_to_solve_set: - if len(option) == 0: - return True - - valid_option = True - - for panel in option: - if not self._witness_can_solve_panel(panel, world, player, player_logic, locat): - valid_option = False - break - - if valid_option: - return True - return False + if entity_name + " Solved" in locat.EVENT_LOCATION_TABLE: + return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) + else: + return make_lambda(panel, world) -def make_lambda(check_hex, world, player, player_logic, locat): - """ - Lambdas are created in a for loop so values need to be captured - """ - return lambda state: state._witness_meets_item_requirements( - check_hex, world, player, player_logic, locat +def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool: + entrance_forward = regio.created_entrances[(source, target)] + entrance_backward = regio.created_entrances[(source, target)] + + return ( + any(entrance.can_reach(state) for entrance in entrance_forward) + or + any(entrance.can_reach(state) for entrance in entrance_backward) ) -def set_rules(world: MultiWorld, player: int, player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations): +def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: + player = world.player + + hedge_2_access = ( + _can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.regio) + ) + + hedge_3_access = ( + _can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio) + or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.regio) + and hedge_2_access + ) + + hedge_4_access = ( + _can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio) + or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.regio) + and hedge_3_access + ) + + hedge_access = ( + _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio) + and state.can_reach("Keep", "Region", player) + and hedge_4_access + ) + + backwards_to_fourth = ( + state.can_reach("Keep", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.regio) + and ( + _can_move_either_direction(state, "Keep", "Keep Tower", world.regio) + or hedge_access + ) + ) + + shadows_shortcut = ( + state.can_reach("Main Island", "Region", player) + and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio) + ) + + backwards_access = ( + _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio) + and (backwards_to_fourth or shadows_shortcut) + ) + + front_access = ( + _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio) + and state.can_reach("Keep", "Region", player) + ) + + return front_access and backwards_access + + +def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: + direct_access = ( + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) + ) + + theater_from_town = ( + _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) + or _can_move_either_direction(state, "Town", "Theater", world.regio) + ) + + tunnels_from_town = ( + _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) + and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) + or _can_move_either_direction(state, "Tunnels", "Town", world.regio) + ) + + return direct_access or theater_from_town and tunnels_from_town + + +def _has_item(item: str, world: "WitnessWorld", player: int, + player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]: + if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: + return lambda state: state.can_reach(item, "Region", player) + if item == "7 Lasers": + laser_req = world.options.mountain_lasers.value + return _has_lasers(laser_req, world) + if item == "11 Lasers": + laser_req = world.options.challenge_lasers.value + return _has_lasers(laser_req, world) + elif item == "PP2 Weirdness": + return lambda state: _can_do_expert_pp2(state, world) + elif item == "Theater to Tunnels": + return lambda state: _can_do_theater_to_tunnels(state, world) + if item in player_logic.EVENT_PANELS: + return _can_solve_panel(item, world, player, player_logic, locat) + + prog_item = StaticWitnessLogic.get_parent_progressive_item(item) + return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) + + +def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], + world: "WitnessWorld") -> Callable[[CollectionState], bool]: + """ + Checks whether item and panel requirements are met for + a panel + """ + + lambda_conversion = [ + [_has_item(item, world, world.player, world.player_logic, world.locat) for item in subset] + for subset in requirements + ] + + return lambda state: any( + all(condition(state) for condition in sub_requirement) + for sub_requirement in lambda_conversion + ) + + +def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionState], bool]: + """ + Lambdas are created in a for loop so values need to be captured + """ + entity_req = world.player_logic.REQUIREMENTS_BY_HEX[entity_hex] + + return _meets_item_requirements(entity_req, world) + + +def set_rules(world: "WitnessWorld"): """ Sets all rules for all locations """ - for location in locat.CHECK_LOCATION_TABLE: + for location in world.locat.CHECK_LOCATION_TABLE: real_location = location - if location in locat.EVENT_LOCATION_TABLE: + if location in world.locat.EVENT_LOCATION_TABLE: real_location = location[:-7] - panel = StaticWitnessLogic.CHECKS_BY_NAME[real_location] - check_hex = panel["checkHex"] + associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] + entity_hex = associated_entity["entity_hex"] - rule = make_lambda(check_hex, world, player, player_logic, locat) + rule = make_lambda(entity_hex, world) - set_rule(world.get_location(location, player), rule) + location = world.multiworld.get_location(location, world.player) - world.completion_condition[player] = \ - lambda state: state.has('Victory', player) + set_rule(location, rule) + + world.multiworld.completion_condition[world.player] = lambda state: state.has('Victory', world.player) diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt deleted file mode 100644 index 80195cfb99..0000000000 --- a/worlds/witness/settings/Door_Panel_Shuffle.txt +++ /dev/null @@ -1,31 +0,0 @@ -Items: -Glass Factory Entry (Panel) -Symmetry Island Lower (Panel) -Symmetry Island Upper (Panel) -Desert Light Room Entry (Panel) -Desert Flood Controls (Panel) -Quarry Stoneworks Entry (Panel) -Quarry Stoneworks Ramp Controls (Panel) -Quarry Stoneworks Lift Controls (Panel) -Quarry Boathouse Ramp Height Control (Panel) -Quarry Boathouse Ramp Horizontal Control (Panel) -Shadows Door Timer (Panel) -Monastery Entry Left (Panel) -Monastery Entry Right (Panel) -Town Tinted Glass Door (Panel) -Town Church Entry (Panel) -Town Maze Panel (Drop-Down Staircase) (Panel) -Windmill Entry (Panel) -Treehouse First & Second Doors (Panel) -Treehouse Third Door (Panel) -Treehouse Laser House Door Timer (Panel) -Treehouse Drawbridge (Panel) -Jungle Popup Wall (Panel) -Bunker Entry (Panel) -Bunker Tinted Glass Door (Panel) -Bunker Elevator Control (Panel) -Swamp Entry (Panel) -Swamp Sliding Bridge (Panel) -Swamp Rotating Bridge (Panel) -Swamp Maze Control (Panel) -Boat \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Boat.txt b/worlds/witness/settings/Door_Shuffle/Boat.txt new file mode 100644 index 0000000000..6494b455cf --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Boat.txt @@ -0,0 +1,2 @@ +Items: +Boat \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt new file mode 100644 index 0000000000..79bda7ea22 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Complex_Additional_Panels.txt @@ -0,0 +1,25 @@ +Items: +Desert Flood Controls (Panel) +Desert Light Control (Panel) +Quarry Elevator Control (Panel) +Quarry Stoneworks Ramp Controls (Panel) +Quarry Stoneworks Lift Controls (Panel) +Quarry Boathouse Ramp Height Control (Panel) +Quarry Boathouse Ramp Horizontal Control (Panel) +Quarry Boathouse Hook Control (Panel) +Monastery Shutters Control (Panel) +Town Maze Rooftop Bridge (Panel) +Town RGB Control (Panel) +Windmill Turn Control (Panel) +Theater Video Input (Panel) +Bunker Drop-Down Door Controls (Panel) +Bunker Elevator Control (Panel) +Swamp Sliding Bridge (Panel) +Swamp Rotating Bridge (Panel) +Swamp Long Bridge (Panel) +Swamp Maze Controls (Panel) +Mountain Floor 1 Light Bridge (Panel) +Mountain Floor 2 Light Bridge Near (Panel) +Mountain Floor 2 Light Bridge Far (Panel) +Mountain Floor 2 Elevator Control (Panel) +Caves Elevator Controls (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt new file mode 100644 index 0000000000..4724039620 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Complex_Door_Panels.txt @@ -0,0 +1,38 @@ +Items: +Glass Factory Entry (Panel) +Tutorial Outpost Entry (Panel) +Tutorial Outpost Exit (Panel) +Symmetry Island Lower (Panel) +Symmetry Island Upper (Panel) +Desert Light Room Entry (Panel) +Desert Flood Room Entry (Panel) +Quarry Entry 1 (Panel) +Quarry Entry 2 (Panel) +Quarry Stoneworks Entry (Panel) +Shadows Door Timer (Panel) +Keep Hedge Maze 1 (Panel) +Keep Hedge Maze 2 (Panel) +Keep Hedge Maze 3 (Panel) +Keep Hedge Maze 4 (Panel) +Monastery Entry Left (Panel) +Monastery Entry Right (Panel) +Town RGB House Entry (Panel) +Town Church Entry (Panel) +Town Maze Stairs (Panel) +Town Windmill Entry (Panel) +Town Cargo Box Entry (Panel) +Theater Entry (Panel) +Theater Exit (Panel) +Treehouse First & Second Doors (Panel) +Treehouse Third Door (Panel) +Treehouse Laser House Door Timer (Panel) +Treehouse Drawbridge (Panel) +Jungle Popup Wall (Panel) +Bunker Entry (Panel) +Bunker Tinted Glass Door (Panel) +Swamp Entry (Panel) +Swamp Platform Shortcut (Panel) +Caves Entry (Panel) +Challenge Entry (Panel) +Tunnels Entry (Panel) +Tunnels Town Shortcut (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt similarity index 94% rename from worlds/witness/settings/Doors_Complex.txt rename to worlds/witness/settings/Door_Shuffle/Complex_Doors.txt index d8da6783b0..2f2b321710 100644 --- a/worlds/witness/settings/Doors_Complex.txt +++ b/worlds/witness/settings/Door_Shuffle/Complex_Doors.txt @@ -12,6 +12,7 @@ Desert Light Room Entry (Door) Desert Pond Room Entry (Door) Desert Flood Room Entry (Door) Desert Elevator Room Entry (Door) +Desert Elevator (Door) Quarry Entry 1 (Door) Quarry Entry 2 (Door) Quarry Stoneworks Entry (Door) @@ -39,13 +40,13 @@ Keep Pressure Plates 3 Exit (Door) Keep Pressure Plates 4 Exit (Door) Keep Shadows Shortcut (Door) Keep Tower Shortcut (Door) -Monastery Shortcut (Door) +Monastery Laser Shortcut (Door) Monastery Entry Inner (Door) Monastery Entry Outer (Door) Monastery Garden Entry (Door) Town Cargo Box Entry (Door) Town Wooden Roof Stairs (Door) -Town Tinted Glass Door +Town RGB House Entry (Door) Town Church Entry (Door) Town Maze Stairs (Door) Town Windmill Entry (Door) @@ -59,7 +60,7 @@ Theater Exit Left (Door) Theater Exit Right (Door) Jungle Bamboo Laser Shortcut (Door) Jungle Popup Wall (Door) -River Monastery Shortcut (Door) +River Monastery Garden Shortcut (Door) Bunker Entry (Door) Bunker Tinted Glass Door Bunker UV Room Entry (Door) @@ -115,7 +116,7 @@ Quarry Stoneworks Entry Right Panel Quarry Stoneworks Entry Left Panel Quarry Stoneworks Side Exit Panel Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control +Quarry Stoneworks Stairs Panel Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside @@ -133,15 +134,15 @@ Keep Pressure Plates 3 Keep Pressure Plates 4 Keep Shadows Shortcut Panel Keep Tower Shortcut Panel -Monastery Shortcut Panel +Monastery Laser Shortcut Panel Monastery Entry Left Monastery Entry Right Monastery Outside 3 Town Cargo Box Entry Panel Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel +Town RGB House Entry Panel Town Church Entry Panel -Town Maze Stair Control +Town Maze Panel Town Windmill Entry Panel Town Sound Room Right Town Red Rooftop 5 @@ -153,7 +154,7 @@ Theater Exit Left Panel Theater Exit Right Panel Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Monastery Shortcut Panel +River Monastery Garden Shortcut Panel Bunker Entry Panel Bunker Tinted Glass Door Panel Bunker Glass Room 3 @@ -171,10 +172,10 @@ Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel Treehouse Third Door Panel -Treehouse Bridge Control +Treehouse Drawbridge Panel Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Outside Treehouse Laser House Door Timer Inside Mountain Floor 1 Left Row 7 Mountain Floor 1 Right Row 5 diff --git a/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt new file mode 100644 index 0000000000..78d245f9f0 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Elevators_Come_To_You.txt @@ -0,0 +1,11 @@ +New Connections: +Quarry - Quarry Elevator - TrueOneWay +Outside Quarry - Quarry Elevator - TrueOneWay +Outside Bunker - Bunker Elevator - TrueOneWay +Outside Swamp - Swamp Long Bridge - TrueOneWay +Swamp Near Boat - Swamp Long Bridge - TrueOneWay +Town Red Rooftop - Town Maze Rooftop - TrueOneWay + + +Requirement Changes: +0x035DE - 0x17E2B - True \ No newline at end of file diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt new file mode 100644 index 0000000000..c16ce73762 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Simple_Additional_Panels.txt @@ -0,0 +1,11 @@ +Items: +Desert Control Panels +Quarry Elevator Control (Panel) +Quarry Stoneworks Control Panels +Quarry Boathouse Control Panels +Monastery Shutters Control (Panel) +Town Control Panels +Windmill & Theater Control Panels +Bunker Control Panels +Swamp Control Panels +Mountain & Caves Control Panels \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Door_Shuffle/Simple_Doors.txt similarity index 74% rename from worlds/witness/settings/Doors_Simple.txt rename to worlds/witness/settings/Door_Shuffle/Simple_Doors.txt index 309874b131..91a7132ec1 100644 --- a/worlds/witness/settings/Doors_Simple.txt +++ b/worlds/witness/settings/Door_Shuffle/Simple_Doors.txt @@ -1,44 +1,33 @@ Items: -Glass Factory Back Wall (Door) -Quarry Boathouse Dock (Door) Outside Tutorial Outpost Doors -Glass Factory Entry (Door) +Glass Factory Doors Symmetry Island Doors Orchard Gates -Desert Doors -Quarry Main Entry -Quarry Stoneworks Entry (Door) -Quarry Stoneworks Shortcuts -Quarry Boathouse Barriers -Shadows Timed Door -Shadows Laser Room Door -Shadows Barriers +Desert Doors & Elevator +Quarry Entry Doors +Quarry Stoneworks Doors +Quarry Boathouse Doors +Shadows Laser Room Doors +Shadows Lower Doors Keep Hedge Maze Doors Keep Pressure Plates Doors Keep Shortcuts -Monastery Entry +Monastery Entry Doors Monastery Shortcuts Town Doors Town Tower Doors -Theater Entry (Door) -Theater Exit -Jungle & River Shortcuts -Jungle Popup Wall (Door) +Windmill & Theater Doors +Jungle Doors Bunker Doors Swamp Doors -Swamp Laser Shortcut (Door) +Swamp Shortcuts Swamp Water Pumps Treehouse Entry Doors -Treehouse Drawbridge (Door) -Treehouse Laser House Entry (Door) -Mountain Floor 1 Exit (Door) -Mountain Floor 2 Stairs & Doors -Mountain Bottom Floor Giant Puzzle Exit (Door) -Mountain Bottom Floor Final Room Entry (Door) -Mountain Bottom Floor Doors to Caves -Caves Doors to Challenge -Caves Exits to Main Island -Challenge Tunnels Entry (Door) +Treehouse Upper Doors +Mountain Floor 1 & 2 Doors +Mountain Bottom Floor Doors +Caves Doors +Caves Shortcuts Tunnels Doors Added Locations: @@ -60,7 +49,7 @@ Quarry Stoneworks Entry Right Panel Quarry Stoneworks Entry Left Panel Quarry Stoneworks Side Exit Panel Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control +Quarry Stoneworks Stairs Panel Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside @@ -78,15 +67,15 @@ Keep Pressure Plates 3 Keep Pressure Plates 4 Keep Shadows Shortcut Panel Keep Tower Shortcut Panel -Monastery Shortcut Panel +Monastery Laser Shortcut Panel Monastery Entry Left Monastery Entry Right Monastery Outside 3 Town Cargo Box Entry Panel Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel +Town RGB House Entry Panel Town Church Entry Panel -Town Maze Stair Control +Town Maze Panel Town Windmill Entry Panel Town Sound Room Right Town Red Rooftop 5 @@ -98,7 +87,7 @@ Theater Exit Left Panel Theater Exit Right Panel Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Monastery Shortcut Panel +River Monastery Garden Shortcut Panel Bunker Entry Panel Bunker Tinted Glass Door Panel Bunker Glass Room 3 @@ -116,10 +105,10 @@ Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel Treehouse Third Door Panel -Treehouse Bridge Control +Treehouse Drawbridge Panel Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control +Treehouse Laser House Door Timer Outside Treehouse Laser House Door Timer Inside Mountain Floor 1 Left Row 7 Mountain Floor 1 Right Row 5 diff --git a/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt new file mode 100644 index 0000000000..79da154491 --- /dev/null +++ b/worlds/witness/settings/Door_Shuffle/Simple_Panels.txt @@ -0,0 +1,22 @@ +Items: +Symmetry Island Panels +Tutorial Outpost Panels +Desert Panels +Quarry Outside Panels +Quarry Stoneworks Panels +Quarry Boathouse Panels +Keep Hedge Maze Panels +Monastery Panels +Town Church & RGB House Panels +Town Maze Panels +Windmill & Theater Panels +Town Cargo Box Entry (Panel) +Treehouse Panels +Bunker Panels +Swamp Panels +Mountain Panels +Caves Panels +Tunnels Panels +Glass Factory Entry (Panel) +Shadows Door Timer (Panel) +Jungle Popup Wall (Panel) \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt deleted file mode 100644 index e722b61ca0..0000000000 --- a/worlds/witness/settings/Doors_Max.txt +++ /dev/null @@ -1,211 +0,0 @@ -Items: -Outside Tutorial Outpost Path (Door) -Outside Tutorial Outpost Entry (Door) -Outside Tutorial Outpost Exit (Door) -Glass Factory Entry (Door) -Glass Factory Back Wall (Door) -Symmetry Island Lower (Door) -Symmetry Island Upper (Door) -Orchard First Gate (Door) -Orchard Second Gate (Door) -Desert Light Room Entry (Door) -Desert Pond Room Entry (Door) -Desert Flood Room Entry (Door) -Desert Elevator Room Entry (Door) -Quarry Entry 1 (Door) -Quarry Entry 2 (Door) -Quarry Stoneworks Entry (Door) -Quarry Stoneworks Side Exit (Door) -Quarry Stoneworks Roof Exit (Door) -Quarry Stoneworks Stairs (Door) -Quarry Boathouse Dock (Door) -Quarry Boathouse First Barrier (Door) -Quarry Boathouse Second Barrier (Door) -Shadows Timed Door -Shadows Laser Entry Right (Door) -Shadows Laser Entry Left (Door) -Shadows Quarry Barrier (Door) -Shadows Ledge Barrier (Door) -Keep Hedge Maze 1 Exit (Door) -Keep Pressure Plates 1 Exit (Door) -Keep Hedge Maze 2 Shortcut (Door) -Keep Hedge Maze 2 Exit (Door) -Keep Hedge Maze 3 Shortcut (Door) -Keep Hedge Maze 3 Exit (Door) -Keep Hedge Maze 4 Shortcut (Door) -Keep Hedge Maze 4 Exit (Door) -Keep Pressure Plates 2 Exit (Door) -Keep Pressure Plates 3 Exit (Door) -Keep Pressure Plates 4 Exit (Door) -Keep Shadows Shortcut (Door) -Keep Tower Shortcut (Door) -Monastery Shortcut (Door) -Monastery Entry Inner (Door) -Monastery Entry Outer (Door) -Monastery Garden Entry (Door) -Town Cargo Box Entry (Door) -Town Wooden Roof Stairs (Door) -Town Tinted Glass Door -Town Church Entry (Door) -Town Maze Stairs (Door) -Town Windmill Entry (Door) -Town RGB House Stairs (Door) -Town Tower Second (Door) -Town Tower First (Door) -Town Tower Fourth (Door) -Town Tower Third (Door) -Theater Entry (Door) -Theater Exit Left (Door) -Theater Exit Right (Door) -Jungle Bamboo Laser Shortcut (Door) -Jungle Popup Wall (Door) -River Monastery Shortcut (Door) -Bunker Entry (Door) -Bunker Tinted Glass Door -Bunker UV Room Entry (Door) -Bunker Elevator Room Entry (Door) -Swamp Entry (Door) -Swamp Between Bridges First Door -Swamp Platform Shortcut Door -Swamp Cyan Water Pump (Door) -Swamp Between Bridges Second Door -Swamp Red Water Pump (Door) -Swamp Red Underwater Exit (Door) -Swamp Blue Water Pump (Door) -Swamp Purple Water Pump (Door) -Swamp Laser Shortcut (Door) -Treehouse First (Door) -Treehouse Second (Door) -Treehouse Third (Door) -Treehouse Drawbridge (Door) -Treehouse Laser House Entry (Door) -Mountain Floor 1 Exit (Door) -Mountain Floor 2 Staircase Near (Door) -Mountain Floor 2 Exit (Door) -Mountain Floor 2 Staircase Far (Door) -Mountain Bottom Floor Giant Puzzle Exit (Door) -Mountain Bottom Floor Final Room Entry (Door) -Mountain Bottom Floor Rock (Door) -Caves Entry (Door) -Caves Pillar Door -Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) -Challenge Entry (Door) -Challenge Tunnels Entry (Door) -Tunnels Theater Shortcut (Door) -Tunnels Desert Shortcut (Door) -Tunnels Town Shortcut (Door) - -Desert Flood Controls (Panel) -Quarry Stoneworks Ramp Controls (Panel) -Quarry Stoneworks Lift Controls (Panel) -Quarry Boathouse Ramp Height Control (Panel) -Quarry Boathouse Ramp Horizontal Control (Panel) -Bunker Elevator Control (Panel) -Swamp Sliding Bridge (Panel) -Swamp Rotating Bridge (Panel) -Swamp Maze Control (Panel) -Boat - -Added Locations: -Outside Tutorial Outpost Entry Panel -Outside Tutorial Outpost Exit Panel -Glass Factory Entry Panel -Glass Factory Back Wall 5 -Symmetry Island Lower Panel -Symmetry Island Upper Panel -Orchard Apple Tree 3 -Orchard Apple Tree 5 -Desert Light Room Entry Panel -Desert Light Room 3 -Desert Flood Room Entry Panel -Desert Flood Room 6 -Quarry Entry 1 Panel -Quarry Entry 2 Panel -Quarry Stoneworks Entry Right Panel -Quarry Stoneworks Entry Left Panel -Quarry Stoneworks Side Exit Panel -Quarry Stoneworks Roof Exit Panel -Quarry Stoneworks Stair Control -Quarry Boathouse Second Barrier Panel -Shadows Door Timer Inside -Shadows Door Timer Outside -Shadows Far 8 -Shadows Near 5 -Shadows Intro 3 -Shadows Intro 5 -Keep Hedge Maze 1 -Keep Pressure Plates 1 -Keep Hedge Maze 2 -Keep Hedge Maze 3 -Keep Hedge Maze 4 -Keep Pressure Plates 2 -Keep Pressure Plates 3 -Keep Pressure Plates 4 -Keep Shadows Shortcut Panel -Keep Tower Shortcut Panel -Monastery Shortcut Panel -Monastery Entry Left -Monastery Entry Right -Monastery Outside 3 -Town Cargo Box Entry Panel -Town Wooden Roof Lower Row 5 -Town Tinted Glass Door Panel -Town Church Entry Panel -Town Maze Stair Control -Town Windmill Entry Panel -Town Sound Room Right -Town Red Rooftop 5 -Town Church Lattice -Town Tall Hexagonal -Town Wooden Rooftop -Windmill Theater Entry Panel -Theater Exit Left Panel -Theater Exit Right Panel -Jungle Laser Shortcut Panel -Jungle Popup Wall Control -River Monastery Shortcut Panel -Bunker Entry Panel -Bunker Tinted Glass Door Panel -Bunker Glass Room 3 -Bunker UV Room 2 -Swamp Entry Panel -Swamp Platform Row 4 -Swamp Platform Shortcut Right Panel -Swamp Blue Underwater 5 -Swamp Between Bridges Near Row 4 -Swamp Cyan Underwater 5 -Swamp Red Underwater 4 -Swamp Beyond Rotating Bridge 4 -Swamp Beyond Rotating Bridge 4 -Swamp Laser Shortcut Right Panel -Treehouse First Door Panel -Treehouse Second Door Panel -Treehouse Third Door Panel -Treehouse Bridge Control -Treehouse Left Orange Bridge 15 -Treehouse Right Orange Bridge 12 -Treehouse Laser House Door Timer Outside Control -Treehouse Laser House Door Timer Inside -Mountain Floor 1 Left Row 7 -Mountain Floor 1 Right Row 5 -Mountain Floor 1 Back Row 3 -Mountain Floor 1 Trash Pillar 2 -Mountain Floor 2 Near Row 5 -Mountain Floor 2 Light Bridge Controller Near -Mountain Floor 2 Light Bridge Controller Far -Mountain Floor 2 Far Row 6 -Mountain Bottom Floor Giant Puzzle -Mountain Bottom Floor Final Room Entry Left -Mountain Bottom Floor Final Room Entry Right -Mountain Bottom Floor Discard -Mountain Bottom Floor Rock Control -Mountain Bottom Floor Caves Entry Panel -Caves Lone Pillar -Caves Mountain Shortcut Panel -Caves Swamp Shortcut Panel -Caves Challenge Entry Panel -Challenge Tunnels Entry Panel -Tunnels Theater Shortcut Panel -Tunnels Desert Shortcut Panel -Tunnels Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_All.txt b/worlds/witness/settings/EP_Shuffle/EP_All.txt index 51af5e3850..939adc36e8 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_All.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_All.txt @@ -1,136 +1,136 @@ Added Locations: -0x0332B -0x03367 -0x28B8A -0x037B6 -0x037B2 -0x000F7 -0x3351D -0x0053C -0x00771 -0x335C8 -0x335C9 -0x337F8 -0x037BB -0x220E4 -0x220E5 -0x334B9 -0x334BC -0x22106 -0x0A14C -0x0A14D -0x03ABC -0x03ABE -0x03AC0 -0x03AC4 -0x03AC5 -0x03BE2 -0x03BE3 -0x0A409 -0x006E5 -0x006E6 -0x006E7 -0x034A7 -0x034AD -0x034AF -0x03DAB -0x03DAC -0x03DAD -0x03E01 -0x289F4 -0x289F5 -0x0053D -0x0053E -0x00769 -0x33721 -0x220A7 -0x220BD -0x03B22 -0x03B23 -0x03B24 -0x03B25 -0x03A79 -0x28ABD -0x28ABE -0x3388F -0x28B29 -0x28B2A -0x018B6 -0x033BE -0x033BF -0x033DD -0x033E5 -0x28AE9 -0x3348F -0x001A3 -0x335AE -0x000D3 -0x035F5 -0x09D5D -0x09D5E -0x09D63 -0x3370E -0x035DE -0x03601 -0x03603 -0x03D0D -0x3369A -0x336C8 -0x33505 -0x03A9E -0x016B2 -0x3365F -0x03731 -0x036CE -0x03C07 -0x03A93 -0x03AA6 -0x3397C -0x0105D -0x0A304 -0x035CB -0x035CF -0x28A7B -0x005F6 -0x00859 -0x17CB9 -0x28A4A -0x334B6 -0x0069D -0x00614 -0x28A4C -0x289CF -0x289D1 -0x33692 -0x03E77 -0x03E7C -0x035C7 -0x01848 -0x03D06 -0x33530 -0x33600 -0x28A2F -0x28A37 -0x334A3 -0x3352F -0x33857 -0x33879 -0x03C19 -0x28B30 -0x035C9 -0x03335 -0x03412 -0x038A6 -0x038AA -0x03E3F -0x03E40 -0x28B8E -0x28B91 -0x03BCE -0x03BCF -0x03BD1 -0x339B6 -0x33A20 -0x33A29 -0x33A2A -0x33B06 \ No newline at end of file +0x0332B (Glass Factory Black Line Reflection EP) +0x03367 (Glass Factory Black Line EP) +0x28B8A (Vase EP) +0x037B6 (Windmill First Blade EP) +0x037B2 (Windmill Second Blade EP) +0x000F7 (Windmill Third Blade EP) +0x3351D (Sand Snake EP) +0x0053C (Facade Right EP) +0x00771 (Facade Left EP) +0x335C8 (Stairs Left EP) +0x335C9 (Stairs Right EP) +0x337F8 (Flood Room EP) +0x037BB (Elevator EP) +0x220E4 (Broken Wall Straight EP) +0x220E5 (Broken Wall Bend EP) +0x334B9 (Shore EP) +0x334BC (Island EP) +0x22106 (Desert EP) +0x0A14C (Pond Room Near Reflection EP) +0x0A14D (Pond Room Far Reflection EP) +0x03ABC (Long Arch Moss EP) +0x03ABE (Straight Left Moss EP) +0x03AC0 (Pop-up Wall Moss EP) +0x03AC4 (Short Arch Moss EP) +0x03AC5 (Green Leaf Moss EP) +0x03BE2 (Monastery Garden Left EP) +0x03BE3 (Monastery Garden Right EP) +0x0A409 (Monastery Wall EP) +0x006E5 (Facade Left Near EP) +0x006E6 (Facade Left Far Short EP) +0x006E7 (Facade Left Far Long EP) +0x034A7 (Left Shutter EP) +0x034AD (Middle Shutter EP) +0x034AF (Right Shutter EP) +0x03DAB (Facade Right Near EP) +0x03DAC (Facade Left Stairs EP) +0x03DAD (Facade Right Stairs EP) +0x03E01 (Grass Stairs EP) +0x289F4 (Entrance EP) +0x289F5 (Tree Halo EP) +0x0053D (Rock Shadow EP) +0x0053E (Sand Shadow EP) +0x00769 (Burned House Beach EP) +0x33721 (Buoy EP) +0x220A7 (Right Orange Bridge EP) +0x220BD (Both Orange Bridges EP) +0x03B22 (Circle Far EP) +0x03B23 (Circle Left EP) +0x03B24 (Circle Near EP) +0x03B25 (Shipwreck CCW Underside EP) +0x03A79 (Stern EP) +0x28ABD (Rope Inner EP) +0x28ABE (Rope Outer EP) +0x3388F (Couch EP) +0x28B29 (Shipwreck Green EP) +0x28B2A (Shipwreck CW Underside EP) +0x018B6 (Pressure Plates 4 Right Exit EP) +0x033BE (Pressure Plates 1 EP) +0x033BF (Pressure Plates 2 EP) +0x033DD (Pressure Plates 3 EP) +0x033E5 (Pressure Plates 4 Left Exit EP) +0x28AE9 (Path EP) +0x3348F (Hedges EP) +0x001A3 (River Shape EP) +0x335AE (Cloud Cycle EP) +0x000D3 (Green Room Flowers EP) +0x035F5 (Tinted Door EP) +0x09D5D (Yellow Bridge EP) +0x09D5E (Blue Bridge EP) +0x09D63 (Pink Bridge EP) +0x3370E (Arch Black EP) +0x035DE (Purple Sand Bottom EP) +0x03601 (Purple Sand Top EP) +0x03603 (Purple Sand Middle EP) +0x03D0D (Bunker Yellow Line EP) +0x3369A (Arch White Left EP) +0x336C8 (Arch White Right EP) +0x33505 (Bush EP) +0x03A9E (Purple Underwater Right EP) +0x016B2 (Rotating Bridge CCW EP) +0x3365F (Boat EP) +0x03731 (Long Bridge Side EP) +0x036CE (Rotating Bridge CW EP) +0x03C07 (Apparent River EP) +0x03A93 (Purple Underwater Left EP) +0x03AA6 (Cyan Underwater Sliding Bridge EP) +0x3397C (Skylight EP) +0x0105D (Sliding Bridge Left EP) +0x0A304 (Sliding Bridge Right EP) +0x035CB (Bamboo CCW EP) +0x035CF (Bamboo CW EP) +0x28A7B (Quarry Stoneworks Rooftop Vent EP) +0x005F6 (Hook EP) +0x00859 (Moving Ramp EP) +0x17CB9 (Railroad EP) +0x28A4A (Shore EP) +0x334B6 (Entrance Pipe EP) +0x0069D (Ramp EP) +0x00614 (Lift EP) +0x28A4C (Sand Pile EP) +0x289CF (Rock Line EP) +0x289D1 (Rock Line Reflection EP) +0x33692 (Brown Bridge EP) +0x03E77 (Red Flowers EP) +0x03E7C (Purple Flowers EP) +0x035C7 (Tractor EP) +0x01848 (EP) +0x03D06 (Garden EP) +0x33530 (Cloud EP) +0x33600 (Patio Flowers EP) +0x28A2F (Town Sewer EP) +0x28A37 (Town Long Sewer EP) +0x334A3 (Path EP) +0x3352F (Gate EP) +0x33857 (Tutorial EP) +0x33879 (Tutorial Reflection EP) +0x03C19 (Tutorial Moss EP) +0x28B30 (Water EP) +0x035C9 (Cargo Box EP) +0x03335 (Tower Underside Third EP) +0x03412 (Tower Underside Fourth EP) +0x038A6 (Tower Underside First EP) +0x038AA (Tower Underside Second EP) +0x03E3F (RGB House Red EP) +0x03E40 (RGB House Green EP) +0x28B8E (Maze Bridge Underside EP) +0x28B91 (Thundercloud EP) +0x03BCE (Black Line Tower EP) +0x03BCF (Black Line Redirect EP) +0x03BD1 (Black Line Church EP) +0x339B6 (Eclipse EP) +0x33A20 (Theater Flowers EP) +0x33A29 (Window EP) +0x33A2A (Door EP) +0x33B06 (Church EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt index 939055169a..6f9c80fc0a 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Easy.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Easy.txt @@ -1,14 +1,17 @@ -Precompleted Locations: -0x339B6 -0x335AE -0x3388F -0x33A20 -0x037B2 -0x000F7 -0x28B29 -0x33857 -0x33879 -0x016B2 -0x036CE -0x03B25 -0x28B2A \ No newline at end of file +Disabled Locations: +0x339B6 (Eclipse EP) +0x335AE (Cloud Cycle EP) +0x3388F (Couch EP) +0x33A20 (Theater Flowers EP) +0x037B2 (Windmill Second Blade EP) +0x000F7 (Windmill Third Blade EP) +0x28B29 (Shipwreck Green EP) +0x33857 (Tutorial EP) +0x33879 (Tutorial Reflection EP) +0x016B2 (Rotating Bridge CCW EP) +0x036CE (Rotating Bridge CW EP) +0x03B25 (Shipwreck CCW Underside EP) +0x28B2A (Shipwreck CW Underside EP) +0x09D63 (Mountain Pink Bridge EP) +0x09D5E (Mountain Blue Bridge EP) +0x09D5D (Mountain Yellow Bridge EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt deleted file mode 100644 index 3bb318a69e..0000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_NoCavesEPs.txt +++ /dev/null @@ -1,5 +0,0 @@ -Precompleted Locations: -0x3397C -0x33A20 -0x3352F -0x28B30 \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt b/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt index d41badfa18..f241957c82 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_NoEclipse.txt @@ -1,2 +1,2 @@ -Precompleted Locations: -0x339B6 \ No newline at end of file +Disabled Locations: +0x339B6 (Eclipse EP) diff --git a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt b/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt deleted file mode 100644 index 3558d77ad8..0000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_NoMountainEPs.txt +++ /dev/null @@ -1,4 +0,0 @@ -Precompleted Locations: -0x09D63 -0x09D5D -0x09D5E \ No newline at end of file diff --git a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt b/worlds/witness/settings/EP_Shuffle/EP_Sides.txt index 1da52ffb89..82ab633295 100644 --- a/worlds/witness/settings/EP_Shuffle/EP_Sides.txt +++ b/worlds/witness/settings/EP_Shuffle/EP_Sides.txt @@ -1,34 +1,35 @@ Added Locations: -0xFFE00 -0xFFE01 -0xFFE02 -0xFFE03 -0xFFE04 -0xFFE10 -0xFFE11 -0xFFE12 -0xFFE13 -0xFFE14 -0xFFE15 -0xFFE20 -0xFFE21 -0xFFE22 -0xFFE23 -0xFFE24 -0xFFE25 -0xFFE30 -0xFFE31 -0xFFE32 -0xFFE33 -0xFFE34 -0xFFE35 -0xFFE40 -0xFFE41 -0xFFE42 -0xFFE43 -0xFFE50 -0xFFE51 -0xFFE52 -0xFFE53 -0xFFE54 -0xFFE55 \ No newline at end of file +0xFFE00 (Desert Obelisk Side 1) +0xFFE01 (Desert Obelisk Side 2) +0xFFE02 (Desert Obelisk Side 3) +0xFFE03 (Desert Obelisk Side 4) +0xFFE04 (Desert Obelisk Side 5) +0xFFE10 (Monastery Obelisk Side 1) +0xFFE11 (Monastery Obelisk Side 2) +0xFFE12 (Monastery Obelisk Side 3) +0xFFE13 (Monastery Obelisk Side 4) +0xFFE14 (Monastery Obelisk Side 5) +0xFFE15 (Monastery Obelisk Side 6) +0xFFE20 (Treehouse Obelisk Side 1) +0xFFE21 (Treehouse Obelisk Side 2) +0xFFE22 (Treehouse Obelisk Side 3) +0xFFE23 (Treehouse Obelisk Side 4) +0xFFE24 (Treehouse Obelisk Side 5) +0xFFE25 (Treehouse Obelisk Side 6) +0xFFE30 (River Obelisk Side 1) +0xFFE31 (River Obelisk Side 2) +0xFFE32 (River Obelisk Side 3) +0xFFE33 (River Obelisk Side 4) +0xFFE34 (River Obelisk Side 5) +0xFFE35 (River Obelisk Side 6) +0xFFE40 (Quarry Obelisk Side 1) +0xFFE41 (Quarry Obelisk Side 2) +0xFFE42 (Quarry Obelisk Side 3) +0xFFE43 (Quarry Obelisk Side 4) +0xFFE44 (Quarry Obelisk Side 5) +0xFFE50 (Town Obelisk Side 1) +0xFFE51 (Town Obelisk Side 2) +0xFFE52 (Town Obelisk Side 3) +0xFFE53 (Town Obelisk Side 4) +0xFFE54 (Town Obelisk Side 5) +0xFFE55 (Town Obelisk Side 6) diff --git a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt b/worlds/witness/settings/EP_Shuffle/EP_Videos.txt deleted file mode 100644 index c4aaca13a6..0000000000 --- a/worlds/witness/settings/EP_Shuffle/EP_Videos.txt +++ /dev/null @@ -1,6 +0,0 @@ -Precompleted Locations: -0x339B6 -0x33A29 -0x33A2A -0x33B06 -0x33A20 \ No newline at end of file diff --git a/worlds/witness/settings/Early_Caves.txt b/worlds/witness/settings/Early_Caves.txt new file mode 100644 index 0000000000..48c8056bc7 --- /dev/null +++ b/worlds/witness/settings/Early_Caves.txt @@ -0,0 +1,6 @@ +Items: +Caves Shortcuts + +Remove Items: +Caves Mountain Shortcut (Door) +Caves Swamp Shortcut (Door) \ No newline at end of file diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_Caves_Start.txt similarity index 65% rename from worlds/witness/settings/Early_UTM.txt rename to worlds/witness/settings/Early_Caves_Start.txt index b04aa3d339..a16a6d02bb 100644 --- a/worlds/witness/settings/Early_UTM.txt +++ b/worlds/witness/settings/Early_Caves_Start.txt @@ -1,8 +1,8 @@ Items: -Caves Exits to Main Island +Caves Shortcuts Starting Inventory: -Caves Exits to Main Island +Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt similarity index 70% rename from worlds/witness/settings/Disable_Unrandomized.txt rename to worlds/witness/settings/Exclusions/Disable_Unrandomized.txt index 3cd7ec1fb5..2419bde06c 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt @@ -2,16 +2,20 @@ Event Items: Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 Bunker Laser Activation - 0x00061,0x17D01,0x17C42 Shadows Laser Activation - 0x00021,0x17D28,0x17C71 +Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7 +Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB Requirement Changes: 0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 -0x28B39 - True - Reflection 0x17CAB - True - True +0x17CA4 - True - True +0x1475B - 0x17FA0 | 0x17D27 | 0x17F9B | 0x17CAB 0x2779A - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: +0x28B39 (Town Tall Hexagonal) 0x03505 (Tutorial Gate Close) 0x0C335 (Tutorial Pillar) 0x0C373 (Tutorial Patio Floor) @@ -25,6 +29,11 @@ Disabled Locations: 0x00055 (Orchard Apple Tree 3) 0x032F7 (Orchard Apple Tree 4) 0x032FF (Orchard Apple Tree 5) +0x334DB (Door Timer Outside) +0x334DC (Door Timer Inside) +0x19B24 (Timed Door) - 0x334DB +0x194B2 (Laser Entry Right) +0x19665 (Laser Entry Left) 0x198B5 (Shadows Intro 1) 0x198BD (Shadows Intro 2) 0x198BF (Shadows Intro 3) @@ -47,11 +56,22 @@ Disabled Locations: 0x197E8 (Shadows Near 4) 0x197E5 (Shadows Near 5) 0x19650 (Shadows Laser) +0x19865 (Quarry Barrier) +0x0A2DF (Quarry Barrier 2) +0x1855B (Ledge Barrier) +0x19ADE (Ledge Barrier 2) 0x00139 (Keep Hedge Maze 1) 0x019DC (Keep Hedge Maze 2) 0x019E7 (Keep Hedge Maze 3) 0x01A0F (Keep Hedge Maze 4) 0x0360E (Laser Hedges) +0x01954 (Hedge 1 Exit) +0x018CE (Hedge 2 Shortcut) +0x019D8 (Hedge 2 Exit) +0x019B5 (Hedge 3 Shortcut) +0x019E6 (Hedge 3 Exit) +0x0199A (Hedge 4 Shortcut) +0x01A0E (Hedge 4 Exit) 0x03307 (First Gate) 0x03313 (Second Gate) 0x0C128 (Entry Inner) @@ -61,16 +81,20 @@ Disabled Locations: 0x00290 (Monastery Outside 1) 0x00038 (Monastery Outside 2) 0x00037 (Monastery Outside 3) +0x03750 (Garden Entry) +0x09D9B (Monastery Shutters Control) 0x193A7 (Monastery Inside 1) 0x193AA (Monastery Inside 2) 0x193AB (Monastery Inside 3) 0x193A6 (Monastery Inside 4) -0x17CA4 (Monastery Laser) +0x17CA4 (Monastery Laser Panel) +0x0364E (Monastery Laser Shortcut Door) +0x03713 (Monastery Laser Shortcut Panel) 0x18590 (Transparent) - True - Symmetry & Environment 0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment 0x28938 (Apple Tree) - 0x28AE3 - Environment 0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection -0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection +0x00815 (Theater Video Input) 0x03553 (Theater Tutorial Video) 0x03552 (Theater Desert Video) 0x0354E (Theater Jungle Video) @@ -84,7 +108,8 @@ Disabled Locations: 0x0070F (Second Row 2) 0x0087D (Second Row 3) 0x002C7 (Second Row 4) -0x17CAA (Monastery Shortcut Panel) +0x17CAA (Monastery Garden Shortcut Panel) +0x0CF2A (Monastery Garden Shortcut) 0x0C2A4 (Bunker Entry) 0x17C79 (Tinted Glass Door) 0x0C2A3 (UV Room Entry) @@ -110,19 +135,16 @@ Disabled Locations: 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) -0x17CAA (River Garden Entry Panel) - -Precompleted Locations: -0x034A7 -0x034AD -0x034AF -0x339B6 -0x33A29 -0x33A2A -0x33B06 -0x3352F -0x33600 -0x035F5 -0x000D3 -0x33A20 -0x03BE2 +0x034A7 (Monastery Left Shutter EP) +0x034AD (Monastery Middle Shutter EP) +0x034AF (Monastery Right Shutter EP) +0x339B6 (Theater Eclipse EP) +0x33A29 (Theater Window EP) +0x33A2A (Theater Door EP) +0x33B06 (Theater Church EP) +0x3352F (Tutorial Gate EP) +0x33600 (Tutorial Patio Flowers EP) +0x035F5 (Bunker Tinted Door EP) +0x000D3 (Bunker Green Room Flowers EP) +0x33A20 (Theater Flowers EP) +0x03BE2 (Monastery Garden Left EP) diff --git a/worlds/witness/settings/Exclusions/Discards.txt b/worlds/witness/settings/Exclusions/Discards.txt new file mode 100644 index 0000000000..e46d1dd82b --- /dev/null +++ b/worlds/witness/settings/Exclusions/Discards.txt @@ -0,0 +1,15 @@ +Disabled Locations: +0x17CFB (Outside Tutorial Discard) +0x3C12B (Glass Factory Discard) +0x17CE7 (Desert Discard) +0x17CF0 (Quarry Discard) +0x17D27 (Keep Discard) +0x17D28 (Shipwreck Discard) +0x17D01 (Town Cargo Box Discard) +0x17C71 (Town Rooftop Discard) +0x17CF7 (Theater Discard) +0x17F9B (Jungle Discard) +0x17FA9 (Treehouse Green Bridge Discard) +0x17C42 (Mountainside Discard) +0x17F93 (Mountain Floor 2 Elevator Discard) +0x17FA0 (Treehouse Laser Discard) diff --git a/worlds/witness/settings/Exclusions/Vaults.txt b/worlds/witness/settings/Exclusions/Vaults.txt new file mode 100644 index 0000000000..f23a131833 --- /dev/null +++ b/worlds/witness/settings/Exclusions/Vaults.txt @@ -0,0 +1,31 @@ +Disabled Locations: +0x033D4 (Outside Tutorial Vault) +0x03481 (Outside Tutorial Vault Box) +0x033D0 (Outside Tutorial Vault Door) +0x0CC7B (Desert Vault) +0x0339E (Desert Vault Box) +0x03444 (Desert Vault Door) +0x00AFB (Shipwreck Vault) +0x03535 (Shipwreck Vault Box) +0x17BB4 (Shipwreck Vault Door) +0x15ADD (River Vault) +0x03702 (River Vault Box) +0x15287 (River Vault Door) +0x002A6 (Mountainside Vault) +0x03542 (Mountainside Vault Box) +0x00085 (Mountainside Vault Door) +0x2FAF6 (Tunnels Vault Box) +0x00815 (Theater Video Input) +0x03553 (Theater Tutorial Video) +0x03552 (Theater Desert Video) +0x0354E (Theater Jungle Video) +0x03549 (Theater Challenge Video) +0x0354F (Theater Shipwreck Video) +0x03545 (Theater Mountain Video) +0x03505 (Tutorial Gate Close) +0x339B6 (Theater clipse EP) +0x33A29 (Theater Window EP) +0x33A2A (Theater Door EP) +0x33B06 (Theater Church EP) +0x33A20 (Theater Flowers EP) +0x3352F (Tutorial Gate EP) diff --git a/worlds/witness/settings/Postgame/Beyond_Challenge.txt b/worlds/witness/settings/Postgame/Beyond_Challenge.txt new file mode 100644 index 0000000000..5cd20b6a5e --- /dev/null +++ b/worlds/witness/settings/Postgame/Beyond_Challenge.txt @@ -0,0 +1,4 @@ +Disabled Locations: +0x03549 (Challenge Video) + +0x339B6 (Eclipse EP) diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt b/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt new file mode 100644 index 0000000000..8f7d6a257a --- /dev/null +++ b/worlds/witness/settings/Postgame/Bottom_Floor_Discard.txt @@ -0,0 +1,2 @@ +Disabled Locations: +0x17FA2 (Mountain Bottom Floor Discard) diff --git a/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt b/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt new file mode 100644 index 0000000000..5ea7c578d8 --- /dev/null +++ b/worlds/witness/settings/Postgame/Bottom_Floor_Discard_NonDoors.txt @@ -0,0 +1,6 @@ +Disabled Locations: +0x17FA2 (Mountain Bottom Floor Discard) +0x17F33 (Rock Open Door) +0x00FF8 (Caves Entry Panel) +0x334E1 (Rock Control) +0x2D77D (Caves Entry Door) diff --git a/worlds/witness/settings/Postgame/Caves.txt b/worlds/witness/settings/Postgame/Caves.txt new file mode 100644 index 0000000000..aadb4c3f96 --- /dev/null +++ b/worlds/witness/settings/Postgame/Caves.txt @@ -0,0 +1,65 @@ +Disabled Locations: +0x335AB (Elevator Inside Control) +0x335AC (Elevator Upper Outside Control) +0x3369D (Elevator Lower Outside Control) +0x00190 (Blue Tunnel Right First 1) +0x00558 (Blue Tunnel Right First 2) +0x00567 (Blue Tunnel Right First 3) +0x006FE (Blue Tunnel Right First 4) +0x01A0D (Blue Tunnel Left First 1) +0x008B8 (Blue Tunnel Left Second 1) +0x00973 (Blue Tunnel Left Second 2) +0x0097B (Blue Tunnel Left Second 3) +0x0097D (Blue Tunnel Left Second 4) +0x0097E (Blue Tunnel Left Second 5) +0x00994 (Blue Tunnel Right Second 1) +0x334D5 (Blue Tunnel Right Second 2) +0x00995 (Blue Tunnel Right Second 3) +0x00996 (Blue Tunnel Right Second 4) +0x00998 (Blue Tunnel Right Second 5) +0x009A4 (Blue Tunnel Left Third 1) +0x018A0 (Blue Tunnel Right Third 1) +0x00A72 (Blue Tunnel Left Fourth 1) +0x32962 (First Floor Left) +0x32966 (First Floor Grounded) +0x01A31 (First Floor Middle) +0x00B71 (First Floor Right) +0x288EA (First Wooden Beam) +0x288FC (Second Wooden Beam) +0x289E7 (Third Wooden Beam) +0x288AA (Fourth Wooden Beam) +0x17FB9 (Left Upstairs Single) +0x0A16B (Left Upstairs Left Row 1) +0x0A2CE (Left Upstairs Left Row 2) +0x0A2D7 (Left Upstairs Left Row 3) +0x0A2DD (Left Upstairs Left Row 4) +0x0A2EA (Left Upstairs Left Row 5) +0x0008F (Right Upstairs Left Row 1) +0x0006B (Right Upstairs Left Row 2) +0x0008B (Right Upstairs Left Row 3) +0x0008C (Right Upstairs Left Row 4) +0x0008A (Right Upstairs Left Row 5) +0x00089 (Right Upstairs Left Row 6) +0x0006A (Right Upstairs Left Row 7) +0x0006C (Right Upstairs Left Row 8) +0x00027 (Right Upstairs Right Row 1) +0x00028 (Right Upstairs Right Row 2) +0x00029 (Right Upstairs Right Row 3) +0x021D7 (Mountain Shortcut Panel) +0x2D73F (Mountain Shortcut Door) +0x17CF2 (Swamp Shortcut Panel) +0x2D859 (Swamp Shortcut Door) +0x039B4 (Tunnels Entry Panel) +0x0348A (Tunnels Entry Door) +0x2FAF6 (Vault Box) +0x27732 (Tunnels Theater Shortcut Panel) +0x27739 (Tunnels Theater Shortcut Door) +0x2773D (Tunnels Desert Shortcut Panel) +0x27263 (Tunnels Desert Shortcut Door) +0x09E85 (Tunnels Town Shortcut Panel) +0x09E87 (Tunnels Town Shortcut Door) + +0x3397C (Skylight EP) +0x28B30 (Water EP) +0x33A20 (Theater Flowers EP) +0x3352F (Gate EP) diff --git a/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt new file mode 100644 index 0000000000..d65900418c --- /dev/null +++ b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt @@ -0,0 +1,3 @@ +Disabled Locations: +0x0356B (Challenge Vault Box) +0x04D75 (Vault Door) diff --git a/worlds/witness/settings/Postgame/Mountain_Lower.txt b/worlds/witness/settings/Postgame/Mountain_Lower.txt new file mode 100644 index 0000000000..354e3feb82 --- /dev/null +++ b/worlds/witness/settings/Postgame/Mountain_Lower.txt @@ -0,0 +1,27 @@ +Disabled Locations: +0x17F93 (Elevator Discard) +0x09EEB (Elevator Control Panel) +0x09FC1 (Giant Puzzle Bottom Left) +0x09F8E (Giant Puzzle Bottom Right) +0x09F01 (Giant Puzzle Top Right) +0x09EFF (Giant Puzzle Top Left) +0x09FDA (Giant Puzzle) +0x09F89 (Exit Door) +0x01983 (Final Room Entry Left) +0x01987 (Final Room Entry Right) +0x0C141 (Final Room Entry Door) +0x0383A (Right Pillar 1) +0x09E56 (Right Pillar 2) +0x09E5A (Right Pillar 3) +0x33961 (Right Pillar 4) +0x0383D (Left Pillar 1) +0x0383F (Left Pillar 2) +0x03859 (Left Pillar 3) +0x339BB (Left Pillar 4) +0x3D9A6 (Elevator Door Closer Left) +0x3D9A7 (Elevator Door Close Right) +0x3C113 (Elevator Entry Left) +0x3C114 (Elevator Entry Right) +0x3D9AA (Back Wall Left) +0x3D9A8 (Back Wall Right) +0x3D9A9 (Elevator Start) diff --git a/worlds/witness/settings/Postgame/Mountain_Upper.txt b/worlds/witness/settings/Postgame/Mountain_Upper.txt new file mode 100644 index 0000000000..e2b0765f53 --- /dev/null +++ b/worlds/witness/settings/Postgame/Mountain_Upper.txt @@ -0,0 +1,41 @@ +Disabled Locations: +0x17C34 (Mountain Entry Panel) +0x09E39 (Light Bridge Controller) +0x09E7A (Right Row 1) +0x09E71 (Right Row 2) +0x09E72 (Right Row 3) +0x09E69 (Right Row 4) +0x09E7B (Right Row 5) +0x09E73 (Left Row 1) +0x09E75 (Left Row 2) +0x09E78 (Left Row 3) +0x09E79 (Left Row 4) +0x09E6C (Left Row 5) +0x09E6F (Left Row 6) +0x09E6B (Left Row 7) +0x33AF5 (Back Row 1) +0x33AF7 (Back Row 2) +0x09F6E (Back Row 3) +0x09EAD (Trash Pillar 1) +0x09EAF (Trash Pillar 2) +0x09E54 (Mountain Floor 1 Exit Door) +0x09FD3 (Near Row 1) +0x09FD4 (Near Row 2) +0x09FD6 (Near Row 3) +0x09FD7 (Near Row 4) +0x09FD8 (Near Row 5) +0x09FFB (Staircase Near Door) +0x09EDD (Elevator Room Entry Door) +0x09E86 (Light Bridge Controller Near) +0x09FCC (Far Row 1) +0x09FCE (Far Row 2) +0x09FCF (Far Row 3) +0x09FD0 (Far Row 4) +0x09FD1 (Far Row 5) +0x09FD2 (Far Row 6) +0x09E07 (Staircase Far Door) +0x09ED8 (Light Bridge Controller Far) + +0x09D63 (Pink Bridge EP) +0x09D5D (Yellow Bridge EP) +0x09D5E (Blue Bridge EP) diff --git a/worlds/witness/settings/Postgame/Path_To_Challenge.txt b/worlds/witness/settings/Postgame/Path_To_Challenge.txt new file mode 100644 index 0000000000..3f9239cc48 --- /dev/null +++ b/worlds/witness/settings/Postgame/Path_To_Challenge.txt @@ -0,0 +1,30 @@ +Disabled Locations: +0x0356B (Vault Box) +0x04D75 (Vault Door) +0x17F33 (Rock Open Door) +0x00FF8 (Caves Entry Panel) +0x334E1 (Rock Control) +0x2D77D (Caves Entry Door) +0x09DD5 (Lone Pillar) +0x019A5 (Caves Pillar Door) +0x0A16E (Challenge Entry Panel) +0x0A19A (Challenge Entry Door) +0x0A332 (Start Timer) +0x0088E (Small Basic) +0x00BAF (Big Basic) +0x00BF3 (Square) +0x00C09 (Maze Map) +0x00CDB (Stars and Dots) +0x0051F (Symmetry) +0x00524 (Stars and Shapers) +0x00CD4 (Big Basic 2) +0x00CB9 (Choice Squares Right) +0x00CA1 (Choice Squares Middle) +0x00C80 (Choice Squares Left) +0x00C68 (Choice Squares 2 Right) +0x00C59 (Choice Squares 2 Middle) +0x00C22 (Choice Squares 2 Left) +0x034F4 (Maze Hidden 1) +0x034EC (Maze Hidden 2) +0x1C31A (Dots Pillar) +0x1C319 (Squares Pillar) diff --git a/worlds/witness/static_logic.py b/worlds/witness/static_logic.py index 8dc2a05de5..29c171d45c 100644 --- a/worlds/witness/static_logic.py +++ b/worlds/witness/static_logic.py @@ -73,31 +73,34 @@ class StaticWitnessLogicObj: location_id = line_split.pop(0) - check_name_full = line_split.pop(0) + entity_name_full = line_split.pop(0) - check_hex = check_name_full[0:7] - check_name = check_name_full[9:-1] + entity_hex = entity_name_full[0:7] + entity_name = entity_name_full[9:-1] required_panel_lambda = line_split.pop(0) - full_check_name = current_region["shortName"] + " " + check_name + full_entity_name = current_region["shortName"] + " " + entity_name if location_id == "Door" or location_id == "Laser": - self.CHECKS_BY_HEX[check_hex] = { - "checkName": full_check_name, - "checkHex": check_hex, - "region": current_region, + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, + "region": None, "id": None, - "panelType": location_id + "entityType": location_id } - self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = { + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = { "panels": parse_lambda(required_panel_lambda) } - current_region["panels"].append(check_hex) + # Lasers and Doors exist in a region, but don't have a regional *requirement* + # If a laser is activated, you don't need to physically walk up to it for it to count + # As such, logically, they behave more as if they were part of the "Entry" region + self.ALL_REGIONS_BY_NAME["Entry"]["panels"].append(entity_hex) continue required_item_lambda = line_split.pop(0) @@ -108,18 +111,18 @@ class StaticWitnessLogicObj: "Laser Pressure Plates", "Desert Laser Redirect" } - is_vault_or_video = "Vault" in check_name or "Video" in check_name + is_vault_or_video = "Vault" in entity_name or "Video" in entity_name - if "Discard" in check_name: + if "Discard" in entity_name: location_type = "Discard" - elif is_vault_or_video or check_name == "Tutorial Gate Close": + elif is_vault_or_video or entity_name == "Tutorial Gate Close": location_type = "Vault" - elif check_name in laser_names: + elif entity_name in laser_names: location_type = "Laser" - elif "Obelisk Side" in check_name: + elif "Obelisk Side" in entity_name: location_type = "Obelisk Side" - full_check_name = check_name - elif "EP" in check_name: + full_entity_name = entity_name + elif "EP" in entity_name: location_type = "EP" else: location_type = "General" @@ -140,32 +143,35 @@ class StaticWitnessLogicObj: eps_ints = {int(h, 16) for h in eps} - self.OBELISK_SIDE_ID_TO_EP_HEXES[int(check_hex, 16)] = eps_ints + self.OBELISK_SIDE_ID_TO_EP_HEXES[int(entity_hex, 16)] = eps_ints for ep_hex in eps: - self.EP_TO_OBELISK_SIDE[ep_hex] = check_hex + self.EP_TO_OBELISK_SIDE[ep_hex] = entity_hex - self.CHECKS_BY_HEX[check_hex] = { - "checkName": full_check_name, - "checkHex": check_hex, + self.ENTITIES_BY_HEX[entity_hex] = { + "checkName": full_entity_name, + "entity_hex": entity_hex, "region": current_region, "id": int(location_id), - "panelType": location_type + "entityType": location_type } - self.ENTITY_ID_TO_NAME[check_hex] = full_check_name + self.ENTITY_ID_TO_NAME[entity_hex] = full_entity_name - self.CHECKS_BY_NAME[self.CHECKS_BY_HEX[check_hex]["checkName"]] = self.CHECKS_BY_HEX[check_hex] - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[check_hex] = requirement + self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex] + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement - current_region["panels"].append(check_hex) + current_region["panels"].append(entity_hex) + + def __init__(self, lines=None): + if lines is None: + lines = get_sigma_normal_logic() - def __init__(self, lines=get_sigma_normal_logic()): # All regions with a list of panels in them and the connections to other regions, before logic adjustments self.ALL_REGIONS_BY_NAME = dict() self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() - self.CHECKS_BY_HEX = dict() - self.CHECKS_BY_NAME = dict() + self.ENTITIES_BY_HEX = dict() + self.ENTITIES_BY_NAME = dict() self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() self.OBELISK_SIDE_ID_TO_EP_HEXES = dict() @@ -187,8 +193,8 @@ class StaticWitnessLogic: OBELISK_SIDE_ID_TO_EP_HEXES = dict() - CHECKS_BY_HEX = dict() - CHECKS_BY_NAME = dict() + ENTITIES_BY_HEX = dict() + ENTITIES_BY_NAME = dict() STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() EP_TO_OBELISK_SIDE = dict() @@ -262,8 +268,8 @@ class StaticWitnessLogic: self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME) self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME) - self.CHECKS_BY_HEX.update(self.sigma_normal.CHECKS_BY_HEX) - self.CHECKS_BY_NAME.update(self.sigma_normal.CHECKS_BY_NAME) + self.ENTITIES_BY_HEX.update(self.sigma_normal.ENTITIES_BY_HEX) + self.ENTITIES_BY_NAME.update(self.sigma_normal.ENTITIES_BY_NAME) self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.OBELISK_SIDE_ID_TO_EP_HEXES.update(self.sigma_normal.OBELISK_SIDE_ID_TO_EP_HEXES) diff --git a/worlds/witness/utils.py b/worlds/witness/utils.py index 72dc10fd06..fbb670fd08 100644 --- a/worlds/witness/utils.py +++ b/worlds/witness/utils.py @@ -1,6 +1,6 @@ from functools import lru_cache from math import floor -from typing import List, Collection +from typing import List, Collection, FrozenSet, Tuple, Dict, Any, Set from pkgutil import get_data @@ -33,7 +33,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]: return rounded_output -def define_new_region(region_string): +def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str, FrozenSet[FrozenSet[str]]]]]: """ Returns a region object by parsing a line in the logic file """ @@ -66,7 +66,7 @@ def define_new_region(region_string): return region_obj, options -def parse_lambda(lambda_string): +def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -97,86 +97,168 @@ class lazy(object): @lru_cache(maxsize=None) -def get_adjustment_file(adjustment_file): +def get_adjustment_file(adjustment_file: str) -> List[str]: data = get_data(__name__, adjustment_file).decode('utf-8') return [line.strip() for line in data.split("\n")] -def get_disable_unrandomized_list(): - return get_adjustment_file("settings/Disable_Unrandomized.txt") +def get_disable_unrandomized_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Disable_Unrandomized.txt") -def get_early_utm_list(): - return get_adjustment_file("settings/Early_UTM.txt") +def get_early_caves_list() -> List[str]: + return get_adjustment_file("settings/Early_Caves.txt") -def get_symbol_shuffle_list(): +def get_early_caves_start_list() -> List[str]: + return get_adjustment_file("settings/Early_Caves_Start.txt") + + +def get_symbol_shuffle_list() -> List[str]: return get_adjustment_file("settings/Symbol_Shuffle.txt") -def get_door_panel_shuffle_list(): - return get_adjustment_file("settings/Door_Panel_Shuffle.txt") +def get_complex_doors() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Doors.txt") -def get_doors_simple_list(): - return get_adjustment_file("settings/Doors_Simple.txt") +def get_simple_doors() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Doors.txt") -def get_doors_complex_list(): - return get_adjustment_file("settings/Doors_Complex.txt") +def get_complex_door_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Door_Panels.txt") -def get_doors_max_list(): - return get_adjustment_file("settings/Doors_Max.txt") +def get_complex_additional_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Complex_Additional_Panels.txt") -def get_laser_shuffle(): +def get_simple_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Panels.txt") + + +def get_simple_additional_panels() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Simple_Additional_Panels.txt") + + +def get_boat() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Boat.txt") + + +def get_laser_shuffle() -> List[str]: return get_adjustment_file("settings/Laser_Shuffle.txt") -def get_audio_logs(): +def get_audio_logs() -> List[str]: return get_adjustment_file("settings/Audio_Logs.txt") -def get_ep_all_individual(): +def get_ep_all_individual() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_All.txt") -def get_ep_obelisks(): +def get_ep_obelisks() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_Sides.txt") -def get_ep_easy(): +def get_ep_easy() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_Easy.txt") -def get_ep_no_eclipse(): +def get_ep_no_eclipse() -> List[str]: return get_adjustment_file("settings/EP_Shuffle/EP_NoEclipse.txt") -def get_ep_no_caves(): - return get_adjustment_file("settings/EP_Shuffle/EP_NoCavesEPs.txt") +def get_vault_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Vaults.txt") -def get_ep_no_mountain(): - return get_adjustment_file("settings/EP_Shuffle/EP_NoMountainEPs.txt") +def get_discard_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Exclusions/Discards.txt") -def get_ep_no_videos(): - return get_adjustment_file("settings/EP_Shuffle/EP_Videos.txt") +def get_caves_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Caves.txt") -def get_sigma_normal_logic(): +def get_beyond_challenge_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Beyond_Challenge.txt") + + +def get_bottom_floor_discard_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard.txt") + + +def get_bottom_floor_discard_nondoors_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Bottom_Floor_Discard_NonDoors.txt") + + +def get_mountain_upper_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Mountain_Upper.txt") + + +def get_challenge_vault_box_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Challenge_Vault_Box.txt") + + +def get_path_to_challenge_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Path_To_Challenge.txt") + + +def get_mountain_lower_exclusion_list() -> List[str]: + return get_adjustment_file("settings/Postgame/Mountain_Lower.txt") + + +def get_elevators_come_to_you() -> List[str]: + return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt") + + +def get_sigma_normal_logic() -> List[str]: return get_adjustment_file("WitnessLogic.txt") -def get_sigma_expert_logic(): +def get_sigma_expert_logic() -> List[str]: return get_adjustment_file("WitnessLogicExpert.txt") -def get_vanilla_logic(): +def get_vanilla_logic() -> List[str]: return get_adjustment_file("WitnessLogicVanilla.txt") -def get_items(): +def get_items() -> List[str]: return get_adjustment_file("WitnessItems.txt") + + +def dnf_remove_redundancies(dnf_requirement: FrozenSet[FrozenSet[str]]) -> FrozenSet[FrozenSet[str]]: + """Removes any redundant terms from a logical formula in disjunctive normal form. + This means removing any terms that are a superset of any other term get removed. + This is possible because of the boolean absorption law: a | (a & b) = a""" + to_remove = set() + + for option1 in dnf_requirement: + for option2 in dnf_requirement: + if option2 < option1: + to_remove.add(option1) + + return dnf_requirement - to_remove + + +def dnf_and(dnf_requirements: List[FrozenSet[FrozenSet[str]]]) -> FrozenSet[FrozenSet[str]]: + """ + performs the "and" operator on a list of logical formula in disjunctive normal form, represented as a set of sets. + A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". + These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b. + """ + current_overall_requirement = frozenset({frozenset()}) + + for next_dnf_requirement in dnf_requirements: + new_requirement: Set[FrozenSet[str]] = set() + + for option1 in current_overall_requirement: + for option2 in next_dnf_requirement: + new_requirement.add(option1 | option2) + + current_overall_requirement = frozenset(new_requirement) + + return dnf_remove_redundancies(current_overall_requirement) From 1ff8ed396b409a3fc5f5e3791a7e5aa1fee6df73 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 11:30:15 -0500 Subject: [PATCH 210/327] Lingo: Demote warpless painting items to filler (#2481) --- worlds/lingo/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 1f426c92f2..3d98ae9183 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -1,7 +1,7 @@ """ Archipelago init file for Lingo """ -from BaseClasses import Item, Tutorial +from BaseClasses import Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .items import ALL_ITEM_TABLE, LingoItem from .locations import ALL_LOCATION_TABLE @@ -90,7 +90,16 @@ class LingoWorld(World): def create_item(self, name: str) -> Item: item = ALL_ITEM_TABLE[name] - return LingoItem(name, item.classification, item.code, self.player) + + classification = item.classification + if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ + and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING + for painting_id in item.painting_ids): + # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings + # go nowhere, then this item should not be progression. + classification = ItemClassification.filler + + return LingoItem(name, classification, item.code, self.player) def set_rules(self): self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) From a8e03420ec93af0b3e30e7fb320daaab94dea914 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 24 Nov 2023 08:33:59 -0800 Subject: [PATCH 211/327] Fill: Fix plando removing Usefuls first (#2445) Co-authored-by: blastron Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 9fdbcc3843..e89db1bd43 100644 --- a/Fill.py +++ b/Fill.py @@ -471,7 +471,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None: raise FillError( f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") - restitempool = usefulitempool + filleritempool + restitempool = filleritempool + usefulitempool remaining_fill(world, defaultlocations, restitempool) From d892622ab1e22ae314c09c5e562e66794feba8d0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 24 Nov 2023 17:41:56 +0100 Subject: [PATCH 212/327] Plando: verify from_pool type (#2200) --- Fill.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Fill.py b/Fill.py index e89db1bd43..342c155079 100644 --- a/Fill.py +++ b/Fill.py @@ -792,6 +792,9 @@ def distribute_planned(world: MultiWorld) -> None: block['force'] = 'silent' if 'from_pool' not in block: block['from_pool'] = True + elif not isinstance(block['from_pool'], bool): + from_pool_type = type(block['from_pool']) + raise Exception(f'Plando "from_pool" has to be boolean, not {from_pool_type} for player {player}.') if 'world' not in block: target_world = False else: From 530e792c3c97ae25b5277d8a4742316a788d9c22 Mon Sep 17 00:00:00 2001 From: Ishigh1 Date: Fri, 24 Nov 2023 17:42:22 +0100 Subject: [PATCH 213/327] Core: Floor and ceil in datastorage (#2448) --- MultiServer.py | 17 +++++++++++------ docs/network protocol.md | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8be8d64132..bd9d2446af 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -10,6 +10,7 @@ import hashlib import inspect import itertools import logging +import math import operator import pickle import random @@ -67,21 +68,25 @@ def update_dict(dictionary, entries): # functions callable on storable data on the server by clients modify_functions = { - "add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append) - "mul": operator.mul, - "mod": operator.mod, - "max": max, - "min": min, + # generic: "replace": lambda old, new: new, "default": lambda old, new: old, + # numeric: + "add": operator.add, # add together two objects, using python's "+" operator (works on strings and lists as append) + "mul": operator.mul, "pow": operator.pow, + "mod": operator.mod, + "floor": lambda value, _: math.floor(value), + "ceil": lambda value, _: math.ceil(value), + "max": max, + "min": min, # bitwise: "xor": operator.xor, "or": operator.or_, "and": operator.and_, "left_shift": operator.lshift, "right_shift": operator.rshift, - # lists/dicts + # lists/dicts: "remove": remove_from_list, "pop": pop_from_container, "update": update_dict, diff --git a/docs/network protocol.md b/docs/network protocol.md index d461cebce1..c17cc74a8a 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -415,6 +415,8 @@ The following operations can be applied to a datastorage key | mul | Multiplies the current value of the key by `value`. | | pow | Multiplies the current value of the key to the power of `value`. | | mod | Sets the current value of the key to the remainder after division by `value`. | +| floor | Floors the current value (`value` is ignored). | +| ceil | Ceils the current value (`value` is ignored). | | max | Sets the current value of the key to `value` if `value` is bigger. | | min | Sets the current value of the key to `value` if `value` is lower. | | and | Applies a bitwise AND to the current value of the key with `value`. | From c5b0330223eedcf49b3c4299e424f28045970d58 Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Fri, 24 Nov 2023 12:08:02 -0500 Subject: [PATCH 214/327] DOOM II: implement new game (#2255) --- README.md | 2 + worlds/doom_ii/Items.py | 1071 +++++++++ worlds/doom_ii/Locations.py | 3442 +++++++++++++++++++++++++++++ worlds/doom_ii/Maps.py | 39 + worlds/doom_ii/Options.py | 150 ++ worlds/doom_ii/Regions.py | 502 +++++ worlds/doom_ii/Rules.py | 501 +++++ worlds/doom_ii/__init__.py | 267 +++ worlds/doom_ii/docs/en_DOOM II.md | 23 + worlds/doom_ii/docs/setup_en.md | 51 + 10 files changed, 6048 insertions(+) create mode 100644 worlds/doom_ii/Items.py create mode 100644 worlds/doom_ii/Locations.py create mode 100644 worlds/doom_ii/Maps.py create mode 100644 worlds/doom_ii/Options.py create mode 100644 worlds/doom_ii/Regions.py create mode 100644 worlds/doom_ii/Rules.py create mode 100644 worlds/doom_ii/__init__.py create mode 100644 worlds/doom_ii/docs/en_DOOM II.md create mode 100644 worlds/doom_ii/docs/setup_en.md diff --git a/README.md b/README.md index a6a482942e..a57f0f9802 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Currently, the following games are supported: * Terraria * Lingo * Pokémon Emerald +* DOOM II + 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/worlds/doom_ii/Items.py b/worlds/doom_ii/Items.py new file mode 100644 index 0000000000..fc426cc883 --- /dev/null +++ b/worlds/doom_ii/Items.py @@ -0,0 +1,1071 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set + + +class ItemDict(TypedDict, total=False): + classification: ItemClassification + count: int + name: str + doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item. + episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup. + map: int + + +item_table: Dict[int, ItemDict] = { + 360000: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shotgun', + 'doom_type': 2001, + 'episode': -1, + 'map': -1}, + 360001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rocket launcher', + 'doom_type': 2003, + 'episode': -1, + 'map': -1}, + 360002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Plasma gun', + 'doom_type': 2004, + 'episode': -1, + 'map': -1}, + 360003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Chainsaw', + 'doom_type': 2005, + 'episode': -1, + 'map': -1}, + 360004: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Chaingun', + 'doom_type': 2002, + 'episode': -1, + 'map': -1}, + 360005: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'BFG9000', + 'doom_type': 2006, + 'episode': -1, + 'map': -1}, + 360006: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Super Shotgun', + 'doom_type': 82, + 'episode': -1, + 'map': -1}, + 360007: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Backpack', + 'doom_type': 8, + 'episode': -1, + 'map': -1}, + 360008: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Armor', + 'doom_type': 2018, + 'episode': -1, + 'map': -1}, + 360009: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Mega Armor', + 'doom_type': 2019, + 'episode': -1, + 'map': -1}, + 360010: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Berserk', + 'doom_type': 2023, + 'episode': -1, + 'map': -1}, + 360011: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Invulnerability', + 'doom_type': 2022, + 'episode': -1, + 'map': -1}, + 360012: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Partial invisibility', + 'doom_type': 2024, + 'episode': -1, + 'map': -1}, + 360013: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Supercharge', + 'doom_type': 2013, + 'episode': -1, + 'map': -1}, + 360014: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Megasphere', + 'doom_type': 83, + 'episode': -1, + 'map': -1}, + 360015: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Medikit', + 'doom_type': 2012, + 'episode': -1, + 'map': -1}, + 360016: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of bullets', + 'doom_type': 2048, + 'episode': -1, + 'map': -1}, + 360017: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of rockets', + 'doom_type': 2046, + 'episode': -1, + 'map': -1}, + 360018: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Box of shotgun shells', + 'doom_type': 2049, + 'episode': -1, + 'map': -1}, + 360019: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Energy cell pack', + 'doom_type': 17, + 'episode': -1, + 'map': -1}, + 360200: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 2}, + 360201: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 2}, + 360202: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 3}, + 360203: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 3}, + 360204: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 4}, + 360205: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 4}, + 360206: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 4}, + 360207: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 5}, + 360208: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 5}, + 360209: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 5}, + 360210: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 6}, + 360211: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 6}, + 360212: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 6}, + 360213: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Yellow skull key', + 'doom_type': 39, + 'episode': 1, + 'map': 8}, + 360214: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Red skull key', + 'doom_type': 38, + 'episode': 1, + 'map': 8}, + 360215: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 9}, + 360216: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 9}, + 360217: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 10}, + 360218: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Yellow keycard', + 'doom_type': 6, + 'episode': 1, + 'map': 10}, + 360219: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Red keycard', + 'doom_type': 13, + 'episode': 1, + 'map': 11}, + 360220: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Blue keycard', + 'doom_type': 5, + 'episode': 1, + 'map': 11}, + 360221: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 1}, + 360222: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 1}, + 360223: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 2}, + 360224: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 2}, + 360225: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 2}, + 360226: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 3}, + 360227: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 3}, + 360228: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Yellow keycard', + 'doom_type': 6, + 'episode': 2, + 'map': 4}, + 360229: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 4}, + 360230: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 4}, + 360231: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 5}, + 360232: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 5}, + 360233: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Red keycard', + 'doom_type': 13, + 'episode': 2, + 'map': 6}, + 360234: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Blue keycard', + 'doom_type': 5, + 'episode': 2, + 'map': 6}, + 360235: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 6}, + 360236: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 7}, + 360237: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 7}, + 360238: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Blue skull key', + 'doom_type': 40, + 'episode': 2, + 'map': 8}, + 360239: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Red skull key', + 'doom_type': 38, + 'episode': 2, + 'map': 8}, + 360240: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Yellow skull key', + 'doom_type': 39, + 'episode': 2, + 'map': 8}, + 360241: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 1}, + 360242: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 1}, + 360243: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 1}, + 360244: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 2}, + 360245: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 2}, + 360246: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 3}, + 360247: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Blue keycard', + 'doom_type': 5, + 'episode': 3, + 'map': 4}, + 360248: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Red keycard', + 'doom_type': 13, + 'episode': 3, + 'map': 4}, + 360249: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 5}, + 360250: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Blue keycard', + 'doom_type': 5, + 'episode': 3, + 'map': 6}, + 360251: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Red keycard', + 'doom_type': 13, + 'episode': 3, + 'map': 6}, + 360252: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Yellow keycard', + 'doom_type': 6, + 'episode': 3, + 'map': 6}, + 360253: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 7}, + 360254: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 7}, + 360255: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Blue skull key', + 'doom_type': 40, + 'episode': 3, + 'map': 7}, + 360256: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Yellow skull key', + 'doom_type': 39, + 'episode': 3, + 'map': 8}, + 360257: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Red skull key', + 'doom_type': 38, + 'episode': 3, + 'map': 8}, + 360400: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Entryway (MAP01)', + 'doom_type': -1, + 'episode': 1, + 'map': 1}, + 360401: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Entryway (MAP01) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 1}, + 360402: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Entryway (MAP01) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 1}, + 360403: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02)', + 'doom_type': -1, + 'episode': 1, + 'map': 2}, + 360404: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Underhalls (MAP02) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 2}, + 360405: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Underhalls (MAP02) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 2}, + 360406: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03)', + 'doom_type': -1, + 'episode': 1, + 'map': 3}, + 360407: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 3}, + 360408: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Gantlet (MAP03) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 3}, + 360409: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04)', + 'doom_type': -1, + 'episode': 1, + 'map': 4}, + 360410: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Focus (MAP04) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 4}, + 360411: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Focus (MAP04) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 4}, + 360412: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05)', + 'doom_type': -1, + 'episode': 1, + 'map': 5}, + 360413: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 5}, + 360414: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Waste Tunnels (MAP05) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 5}, + 360415: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06)', + 'doom_type': -1, + 'episode': 1, + 'map': 6}, + 360416: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crusher (MAP06) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 6}, + 360417: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crusher (MAP06) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 6}, + 360418: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dead Simple (MAP07)', + 'doom_type': -1, + 'episode': 1, + 'map': 7}, + 360419: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dead Simple (MAP07) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 7}, + 360420: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Dead Simple (MAP07) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 7}, + 360421: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08)', + 'doom_type': -1, + 'episode': 1, + 'map': 8}, + 360422: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 8}, + 360423: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Tricks and Traps (MAP08) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 8}, + 360424: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09)', + 'doom_type': -1, + 'episode': 1, + 'map': 9}, + 360425: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Pit (MAP09) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 9}, + 360426: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Pit (MAP09) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 9}, + 360427: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10)', + 'doom_type': -1, + 'episode': 1, + 'map': 10}, + 360428: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 10}, + 360429: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Refueling Base (MAP10) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 10}, + 360430: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11)', + 'doom_type': -1, + 'episode': 1, + 'map': 11}, + 360431: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 11}, + 360432: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Circle of Death (MAP11) - Computer area map', + 'doom_type': 2026, + 'episode': 1, + 'map': 11}, + 360433: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12)', + 'doom_type': -1, + 'episode': 2, + 'map': 1}, + 360434: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Factory (MAP12) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 1}, + 360435: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Factory (MAP12) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 1}, + 360436: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13)', + 'doom_type': -1, + 'episode': 2, + 'map': 2}, + 360437: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Downtown (MAP13) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 2}, + 360438: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Downtown (MAP13) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 2}, + 360439: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14)', + 'doom_type': -1, + 'episode': 2, + 'map': 3}, + 360440: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 3}, + 360441: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Inmost Dens (MAP14) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 3}, + 360442: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15)', + 'doom_type': -1, + 'episode': 2, + 'map': 4}, + 360443: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 4}, + 360444: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Industrial Zone (MAP15) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 4}, + 360445: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16)', + 'doom_type': -1, + 'episode': 2, + 'map': 5}, + 360446: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Suburbs (MAP16) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 5}, + 360447: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Suburbs (MAP16) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 5}, + 360448: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17)', + 'doom_type': -1, + 'episode': 2, + 'map': 6}, + 360449: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Tenements (MAP17) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 6}, + 360450: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Tenements (MAP17) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 6}, + 360451: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18)', + 'doom_type': -1, + 'episode': 2, + 'map': 7}, + 360452: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 7}, + 360453: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Courtyard (MAP18) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 7}, + 360454: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19)', + 'doom_type': -1, + 'episode': 2, + 'map': 8}, + 360455: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (MAP19) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 8}, + 360456: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Citadel (MAP19) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 8}, + 360457: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gotcha! (MAP20)', + 'doom_type': -1, + 'episode': 2, + 'map': 9}, + 360458: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gotcha! (MAP20) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 9}, + 360459: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Gotcha! (MAP20) - Computer area map', + 'doom_type': 2026, + 'episode': 2, + 'map': 9}, + 360460: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21)', + 'doom_type': -1, + 'episode': 3, + 'map': 1}, + 360461: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Nirvana (MAP21) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 1}, + 360462: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Nirvana (MAP21) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 1}, + 360463: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22)', + 'doom_type': -1, + 'episode': 3, + 'map': 2}, + 360464: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 2}, + 360465: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Catacombs (MAP22) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 2}, + 360466: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23)', + 'doom_type': -1, + 'episode': 3, + 'map': 3}, + 360467: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 3}, + 360468: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Barrels o Fun (MAP23) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 3}, + 360469: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24)', + 'doom_type': -1, + 'episode': 3, + 'map': 4}, + 360470: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (MAP24) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 4}, + 360471: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Chasm (MAP24) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 4}, + 360472: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25)', + 'doom_type': -1, + 'episode': 3, + 'map': 5}, + 360473: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 5}, + 360474: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Bloodfalls (MAP25) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 5}, + 360475: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26)', + 'doom_type': -1, + 'episode': 3, + 'map': 6}, + 360476: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 6}, + 360477: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Abandoned Mines (MAP26) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 6}, + 360478: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27)', + 'doom_type': -1, + 'episode': 3, + 'map': 7}, + 360479: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 7}, + 360480: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Monster Condo (MAP27) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 7}, + 360481: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28)', + 'doom_type': -1, + 'episode': 3, + 'map': 8}, + 360482: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 8}, + 360483: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Spirit World (MAP28) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 8}, + 360484: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Living End (MAP29)', + 'doom_type': -1, + 'episode': 3, + 'map': 9}, + 360485: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Living End (MAP29) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 9}, + 360486: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Living End (MAP29) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 9}, + 360487: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Icon of Sin (MAP30)', + 'doom_type': -1, + 'episode': 3, + 'map': 10}, + 360488: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Icon of Sin (MAP30) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 10}, + 360489: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Icon of Sin (MAP30) - Computer area map', + 'doom_type': 2026, + 'episode': 3, + 'map': 10}, + 360490: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31)', + 'doom_type': -1, + 'episode': 4, + 'map': 1}, + 360491: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 1}, + 360492: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Wolfenstein2 (MAP31) - Computer area map', + 'doom_type': 2026, + 'episode': 4, + 'map': 1}, + 360493: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Grosse2 (MAP32)', + 'doom_type': -1, + 'episode': 4, + 'map': 2}, + 360494: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Grosse2 (MAP32) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 2}, + 360495: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Grosse2 (MAP32) - Computer area map', + 'doom_type': 2026, + 'episode': 4, + 'map': 2}, +} + + +item_name_groups: Dict[str, Set[str]] = { + 'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', }, + 'Computer area maps': {'Barrels o Fun (MAP23) - Computer area map', 'Bloodfalls (MAP25) - Computer area map', 'Circle of Death (MAP11) - Computer area map', 'Dead Simple (MAP07) - Computer area map', 'Downtown (MAP13) - Computer area map', 'Entryway (MAP01) - Computer area map', 'Gotcha! (MAP20) - Computer area map', 'Grosse2 (MAP32) - Computer area map', 'Icon of Sin (MAP30) - Computer area map', 'Industrial Zone (MAP15) - Computer area map', 'Monster Condo (MAP27) - Computer area map', 'Nirvana (MAP21) - Computer area map', 'Refueling Base (MAP10) - Computer area map', 'Suburbs (MAP16) - Computer area map', 'Tenements (MAP17) - Computer area map', 'The Abandoned Mines (MAP26) - Computer area map', 'The Catacombs (MAP22) - Computer area map', 'The Chasm (MAP24) - Computer area map', 'The Citadel (MAP19) - Computer area map', 'The Courtyard (MAP18) - Computer area map', 'The Crusher (MAP06) - Computer area map', 'The Factory (MAP12) - Computer area map', 'The Focus (MAP04) - Computer area map', 'The Gantlet (MAP03) - Computer area map', 'The Inmost Dens (MAP14) - Computer area map', 'The Living End (MAP29) - Computer area map', 'The Pit (MAP09) - Computer area map', 'The Spirit World (MAP28) - Computer area map', 'The Waste Tunnels (MAP05) - Computer area map', 'Tricks and Traps (MAP08) - Computer area map', 'Underhalls (MAP02) - Computer area map', 'Wolfenstein2 (MAP31) - Computer area map', }, + 'Keys': {'Barrels o Fun (MAP23) - Yellow skull key', 'Bloodfalls (MAP25) - Blue skull key', 'Circle of Death (MAP11) - Blue keycard', 'Circle of Death (MAP11) - Red keycard', 'Downtown (MAP13) - Blue keycard', 'Downtown (MAP13) - Red keycard', 'Downtown (MAP13) - Yellow keycard', 'Industrial Zone (MAP15) - Blue keycard', 'Industrial Zone (MAP15) - Red keycard', 'Industrial Zone (MAP15) - Yellow keycard', 'Monster Condo (MAP27) - Blue skull key', 'Monster Condo (MAP27) - Red skull key', 'Monster Condo (MAP27) - Yellow skull key', 'Nirvana (MAP21) - Blue skull key', 'Nirvana (MAP21) - Red skull key', 'Nirvana (MAP21) - Yellow skull key', 'Refueling Base (MAP10) - Blue keycard', 'Refueling Base (MAP10) - Yellow keycard', 'Suburbs (MAP16) - Blue skull key', 'Suburbs (MAP16) - Red skull key', 'Tenements (MAP17) - Blue keycard', 'Tenements (MAP17) - Red keycard', 'Tenements (MAP17) - Yellow skull key', 'The Abandoned Mines (MAP26) - Blue keycard', 'The Abandoned Mines (MAP26) - Red keycard', 'The Abandoned Mines (MAP26) - Yellow keycard', 'The Catacombs (MAP22) - Blue skull key', 'The Catacombs (MAP22) - Red skull key', 'The Chasm (MAP24) - Blue keycard', 'The Chasm (MAP24) - Red keycard', 'The Citadel (MAP19) - Blue skull key', 'The Citadel (MAP19) - Red skull key', 'The Citadel (MAP19) - Yellow skull key', 'The Courtyard (MAP18) - Blue skull key', 'The Courtyard (MAP18) - Yellow skull key', 'The Crusher (MAP06) - Blue keycard', 'The Crusher (MAP06) - Red keycard', 'The Crusher (MAP06) - Yellow keycard', 'The Factory (MAP12) - Blue keycard', 'The Factory (MAP12) - Yellow keycard', 'The Focus (MAP04) - Blue keycard', 'The Focus (MAP04) - Red keycard', 'The Focus (MAP04) - Yellow keycard', 'The Gantlet (MAP03) - Blue keycard', 'The Gantlet (MAP03) - Red keycard', 'The Inmost Dens (MAP14) - Blue skull key', 'The Inmost Dens (MAP14) - Red skull key', 'The Pit (MAP09) - Blue keycard', 'The Pit (MAP09) - Yellow keycard', 'The Spirit World (MAP28) - Red skull key', 'The Spirit World (MAP28) - Yellow skull key', 'The Waste Tunnels (MAP05) - Blue keycard', 'The Waste Tunnels (MAP05) - Red keycard', 'The Waste Tunnels (MAP05) - Yellow keycard', 'Tricks and Traps (MAP08) - Red skull key', 'Tricks and Traps (MAP08) - Yellow skull key', 'Underhalls (MAP02) - Blue keycard', 'Underhalls (MAP02) - Red keycard', }, + 'Levels': {'Barrels o Fun (MAP23)', 'Bloodfalls (MAP25)', 'Circle of Death (MAP11)', 'Dead Simple (MAP07)', 'Downtown (MAP13)', 'Entryway (MAP01)', 'Gotcha! (MAP20)', 'Grosse2 (MAP32)', 'Icon of Sin (MAP30)', 'Industrial Zone (MAP15)', 'Monster Condo (MAP27)', 'Nirvana (MAP21)', 'Refueling Base (MAP10)', 'Suburbs (MAP16)', 'Tenements (MAP17)', 'The Abandoned Mines (MAP26)', 'The Catacombs (MAP22)', 'The Chasm (MAP24)', 'The Citadel (MAP19)', 'The Courtyard (MAP18)', 'The Crusher (MAP06)', 'The Factory (MAP12)', 'The Focus (MAP04)', 'The Gantlet (MAP03)', 'The Inmost Dens (MAP14)', 'The Living End (MAP29)', 'The Pit (MAP09)', 'The Spirit World (MAP28)', 'The Waste Tunnels (MAP05)', 'Tricks and Traps (MAP08)', 'Underhalls (MAP02)', 'Wolfenstein2 (MAP31)', }, + 'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Megasphere', 'Partial invisibility', 'Supercharge', }, + 'Weapons': {'BFG9000', 'Chaingun', 'Chainsaw', 'Plasma gun', 'Rocket launcher', 'Shotgun', 'Super Shotgun', }, +} diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py new file mode 100644 index 0000000000..3ce87b8a66 --- /dev/null +++ b/worlds/doom_ii/Locations.py @@ -0,0 +1,3442 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import Dict, TypedDict, List, Set + + +class LocationDict(TypedDict, total=False): + name: str + episode: int + map: int + index: int # Thing index as it is stored in the wad file. + doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things. + region: str + + +location_table: Dict[int, LocationDict] = { + 361000: {'name': 'Entryway (MAP01) - Armor', + 'episode': 1, + 'map': 1, + 'index': 17, + 'doom_type': 2018, + 'region': "Entryway (MAP01) Main"}, + 361001: {'name': 'Entryway (MAP01) - Shotgun', + 'episode': 1, + 'map': 1, + 'index': 37, + 'doom_type': 2001, + 'region': "Entryway (MAP01) Main"}, + 361002: {'name': 'Entryway (MAP01) - Rocket launcher', + 'episode': 1, + 'map': 1, + 'index': 52, + 'doom_type': 2003, + 'region': "Entryway (MAP01) Main"}, + 361003: {'name': 'Entryway (MAP01) - Chainsaw', + 'episode': 1, + 'map': 1, + 'index': 68, + 'doom_type': 2005, + 'region': "Entryway (MAP01) Main"}, + 361004: {'name': 'Entryway (MAP01) - Exit', + 'episode': 1, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Entryway (MAP01) Main"}, + 361005: {'name': 'Underhalls (MAP02) - Red keycard', + 'episode': 1, + 'map': 2, + 'index': 31, + 'doom_type': 13, + 'region': "Underhalls (MAP02) Main"}, + 361006: {'name': 'Underhalls (MAP02) - Blue keycard', + 'episode': 1, + 'map': 2, + 'index': 44, + 'doom_type': 5, + 'region': "Underhalls (MAP02) Red"}, + 361007: {'name': 'Underhalls (MAP02) - Mega Armor', + 'episode': 1, + 'map': 2, + 'index': 116, + 'doom_type': 2019, + 'region': "Underhalls (MAP02) Main"}, + 361008: {'name': 'Underhalls (MAP02) - Super Shotgun', + 'episode': 1, + 'map': 2, + 'index': 127, + 'doom_type': 82, + 'region': "Underhalls (MAP02) Main"}, + 361009: {'name': 'Underhalls (MAP02) - Exit', + 'episode': 1, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Underhalls (MAP02) Blue"}, + 361010: {'name': 'The Gantlet (MAP03) - Mega Armor', + 'episode': 1, + 'map': 3, + 'index': 5, + 'doom_type': 2019, + 'region': "The Gantlet (MAP03) Main"}, + 361011: {'name': 'The Gantlet (MAP03) - Shotgun', + 'episode': 1, + 'map': 3, + 'index': 6, + 'doom_type': 2001, + 'region': "The Gantlet (MAP03) Main"}, + 361012: {'name': 'The Gantlet (MAP03) - Blue keycard', + 'episode': 1, + 'map': 3, + 'index': 85, + 'doom_type': 5, + 'region': "The Gantlet (MAP03) Main"}, + 361013: {'name': 'The Gantlet (MAP03) - Rocket launcher', + 'episode': 1, + 'map': 3, + 'index': 86, + 'doom_type': 2003, + 'region': "The Gantlet (MAP03) Main"}, + 361014: {'name': 'The Gantlet (MAP03) - Partial invisibility', + 'episode': 1, + 'map': 3, + 'index': 96, + 'doom_type': 2024, + 'region': "The Gantlet (MAP03) Main"}, + 361015: {'name': 'The Gantlet (MAP03) - Supercharge', + 'episode': 1, + 'map': 3, + 'index': 97, + 'doom_type': 2013, + 'region': "The Gantlet (MAP03) Main"}, + 361016: {'name': 'The Gantlet (MAP03) - Mega Armor 2', + 'episode': 1, + 'map': 3, + 'index': 98, + 'doom_type': 2019, + 'region': "The Gantlet (MAP03) Main"}, + 361017: {'name': 'The Gantlet (MAP03) - Red keycard', + 'episode': 1, + 'map': 3, + 'index': 104, + 'doom_type': 13, + 'region': "The Gantlet (MAP03) Blue"}, + 361018: {'name': 'The Gantlet (MAP03) - Chaingun', + 'episode': 1, + 'map': 3, + 'index': 122, + 'doom_type': 2002, + 'region': "The Gantlet (MAP03) Main"}, + 361019: {'name': 'The Gantlet (MAP03) - Backpack', + 'episode': 1, + 'map': 3, + 'index': 146, + 'doom_type': 8, + 'region': "The Gantlet (MAP03) Blue"}, + 361020: {'name': 'The Gantlet (MAP03) - Exit', + 'episode': 1, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Gantlet (MAP03) Red"}, + 361021: {'name': 'The Focus (MAP04) - Super Shotgun', + 'episode': 1, + 'map': 4, + 'index': 4, + 'doom_type': 82, + 'region': "The Focus (MAP04) Main"}, + 361022: {'name': 'The Focus (MAP04) - Blue keycard', + 'episode': 1, + 'map': 4, + 'index': 21, + 'doom_type': 5, + 'region': "The Focus (MAP04) Main"}, + 361023: {'name': 'The Focus (MAP04) - Red keycard', + 'episode': 1, + 'map': 4, + 'index': 32, + 'doom_type': 13, + 'region': "The Focus (MAP04) Blue"}, + 361024: {'name': 'The Focus (MAP04) - Yellow keycard', + 'episode': 1, + 'map': 4, + 'index': 59, + 'doom_type': 6, + 'region': "The Focus (MAP04) Red"}, + 361025: {'name': 'The Focus (MAP04) - Exit', + 'episode': 1, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Focus (MAP04) Yellow"}, + 361026: {'name': 'The Waste Tunnels (MAP05) - Rocket launcher', + 'episode': 1, + 'map': 5, + 'index': 45, + 'doom_type': 2003, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361027: {'name': 'The Waste Tunnels (MAP05) - Super Shotgun', + 'episode': 1, + 'map': 5, + 'index': 46, + 'doom_type': 82, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361028: {'name': 'The Waste Tunnels (MAP05) - Blue keycard', + 'episode': 1, + 'map': 5, + 'index': 50, + 'doom_type': 5, + 'region': "The Waste Tunnels (MAP05) Red"}, + 361029: {'name': 'The Waste Tunnels (MAP05) - Plasma gun', + 'episode': 1, + 'map': 5, + 'index': 53, + 'doom_type': 2004, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361030: {'name': 'The Waste Tunnels (MAP05) - Red keycard', + 'episode': 1, + 'map': 5, + 'index': 55, + 'doom_type': 13, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361031: {'name': 'The Waste Tunnels (MAP05) - Supercharge', + 'episode': 1, + 'map': 5, + 'index': 56, + 'doom_type': 2013, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361032: {'name': 'The Waste Tunnels (MAP05) - Mega Armor', + 'episode': 1, + 'map': 5, + 'index': 57, + 'doom_type': 2019, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361033: {'name': 'The Waste Tunnels (MAP05) - Yellow keycard', + 'episode': 1, + 'map': 5, + 'index': 78, + 'doom_type': 6, + 'region': "The Waste Tunnels (MAP05) Blue"}, + 361034: {'name': 'The Waste Tunnels (MAP05) - Armor', + 'episode': 1, + 'map': 5, + 'index': 151, + 'doom_type': 2018, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361035: {'name': 'The Waste Tunnels (MAP05) - Supercharge 2', + 'episode': 1, + 'map': 5, + 'index': 170, + 'doom_type': 2013, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361036: {'name': 'The Waste Tunnels (MAP05) - Shotgun', + 'episode': 1, + 'map': 5, + 'index': 202, + 'doom_type': 2001, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361037: {'name': 'The Waste Tunnels (MAP05) - Berserk', + 'episode': 1, + 'map': 5, + 'index': 215, + 'doom_type': 2023, + 'region': "The Waste Tunnels (MAP05) Main"}, + 361038: {'name': 'The Waste Tunnels (MAP05) - Exit', + 'episode': 1, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Waste Tunnels (MAP05) Yellow"}, + 361039: {'name': 'The Crusher (MAP06) - Red keycard', + 'episode': 1, + 'map': 6, + 'index': 0, + 'doom_type': 13, + 'region': "The Crusher (MAP06) Blue"}, + 361040: {'name': 'The Crusher (MAP06) - Yellow keycard', + 'episode': 1, + 'map': 6, + 'index': 1, + 'doom_type': 6, + 'region': "The Crusher (MAP06) Red"}, + 361041: {'name': 'The Crusher (MAP06) - Blue keycard', + 'episode': 1, + 'map': 6, + 'index': 36, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361042: {'name': 'The Crusher (MAP06) - Supercharge', + 'episode': 1, + 'map': 6, + 'index': 55, + 'doom_type': 2013, + 'region': "The Crusher (MAP06) Main"}, + 361043: {'name': 'The Crusher (MAP06) - Plasma gun', + 'episode': 1, + 'map': 6, + 'index': 59, + 'doom_type': 2004, + 'region': "The Crusher (MAP06) Main"}, + 361044: {'name': 'The Crusher (MAP06) - Blue keycard 2', + 'episode': 1, + 'map': 6, + 'index': 74, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361045: {'name': 'The Crusher (MAP06) - Blue keycard 3', + 'episode': 1, + 'map': 6, + 'index': 75, + 'doom_type': 5, + 'region': "The Crusher (MAP06) Main"}, + 361046: {'name': 'The Crusher (MAP06) - Megasphere', + 'episode': 1, + 'map': 6, + 'index': 94, + 'doom_type': 83, + 'region': "The Crusher (MAP06) Main"}, + 361047: {'name': 'The Crusher (MAP06) - Armor', + 'episode': 1, + 'map': 6, + 'index': 130, + 'doom_type': 2018, + 'region': "The Crusher (MAP06) Main"}, + 361048: {'name': 'The Crusher (MAP06) - Super Shotgun', + 'episode': 1, + 'map': 6, + 'index': 134, + 'doom_type': 82, + 'region': "The Crusher (MAP06) Blue"}, + 361049: {'name': 'The Crusher (MAP06) - Mega Armor', + 'episode': 1, + 'map': 6, + 'index': 222, + 'doom_type': 2019, + 'region': "The Crusher (MAP06) Blue"}, + 361050: {'name': 'The Crusher (MAP06) - Rocket launcher', + 'episode': 1, + 'map': 6, + 'index': 223, + 'doom_type': 2003, + 'region': "The Crusher (MAP06) Blue"}, + 361051: {'name': 'The Crusher (MAP06) - Backpack', + 'episode': 1, + 'map': 6, + 'index': 225, + 'doom_type': 8, + 'region': "The Crusher (MAP06) Blue"}, + 361052: {'name': 'The Crusher (MAP06) - Megasphere 2', + 'episode': 1, + 'map': 6, + 'index': 246, + 'doom_type': 83, + 'region': "The Crusher (MAP06) Blue"}, + 361053: {'name': 'The Crusher (MAP06) - Exit', + 'episode': 1, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Crusher (MAP06) Yellow"}, + 361054: {'name': 'Dead Simple (MAP07) - Megasphere', + 'episode': 1, + 'map': 7, + 'index': 4, + 'doom_type': 83, + 'region': "Dead Simple (MAP07) Main"}, + 361055: {'name': 'Dead Simple (MAP07) - Rocket launcher', + 'episode': 1, + 'map': 7, + 'index': 5, + 'doom_type': 2003, + 'region': "Dead Simple (MAP07) Main"}, + 361056: {'name': 'Dead Simple (MAP07) - Partial invisibility', + 'episode': 1, + 'map': 7, + 'index': 7, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361057: {'name': 'Dead Simple (MAP07) - Super Shotgun', + 'episode': 1, + 'map': 7, + 'index': 8, + 'doom_type': 82, + 'region': "Dead Simple (MAP07) Main"}, + 361058: {'name': 'Dead Simple (MAP07) - Chaingun', + 'episode': 1, + 'map': 7, + 'index': 9, + 'doom_type': 2002, + 'region': "Dead Simple (MAP07) Main"}, + 361059: {'name': 'Dead Simple (MAP07) - Plasma gun', + 'episode': 1, + 'map': 7, + 'index': 10, + 'doom_type': 2004, + 'region': "Dead Simple (MAP07) Main"}, + 361060: {'name': 'Dead Simple (MAP07) - Backpack', + 'episode': 1, + 'map': 7, + 'index': 43, + 'doom_type': 8, + 'region': "Dead Simple (MAP07) Main"}, + 361061: {'name': 'Dead Simple (MAP07) - Berserk', + 'episode': 1, + 'map': 7, + 'index': 44, + 'doom_type': 2023, + 'region': "Dead Simple (MAP07) Main"}, + 361062: {'name': 'Dead Simple (MAP07) - Partial invisibility 2', + 'episode': 1, + 'map': 7, + 'index': 60, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361063: {'name': 'Dead Simple (MAP07) - Partial invisibility 3', + 'episode': 1, + 'map': 7, + 'index': 73, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361064: {'name': 'Dead Simple (MAP07) - Partial invisibility 4', + 'episode': 1, + 'map': 7, + 'index': 74, + 'doom_type': 2024, + 'region': "Dead Simple (MAP07) Main"}, + 361065: {'name': 'Dead Simple (MAP07) - Exit', + 'episode': 1, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Dead Simple (MAP07) Main"}, + 361066: {'name': 'Tricks and Traps (MAP08) - Plasma gun', + 'episode': 1, + 'map': 8, + 'index': 14, + 'doom_type': 2004, + 'region': "Tricks and Traps (MAP08) Main"}, + 361067: {'name': 'Tricks and Traps (MAP08) - Rocket launcher', + 'episode': 1, + 'map': 8, + 'index': 17, + 'doom_type': 2003, + 'region': "Tricks and Traps (MAP08) Main"}, + 361068: {'name': 'Tricks and Traps (MAP08) - Armor', + 'episode': 1, + 'map': 8, + 'index': 36, + 'doom_type': 2018, + 'region': "Tricks and Traps (MAP08) Main"}, + 361069: {'name': 'Tricks and Traps (MAP08) - Chaingun', + 'episode': 1, + 'map': 8, + 'index': 48, + 'doom_type': 2002, + 'region': "Tricks and Traps (MAP08) Main"}, + 361070: {'name': 'Tricks and Traps (MAP08) - Shotgun', + 'episode': 1, + 'map': 8, + 'index': 87, + 'doom_type': 2001, + 'region': "Tricks and Traps (MAP08) Main"}, + 361071: {'name': 'Tricks and Traps (MAP08) - Supercharge', + 'episode': 1, + 'map': 8, + 'index': 119, + 'doom_type': 2013, + 'region': "Tricks and Traps (MAP08) Main"}, + 361072: {'name': 'Tricks and Traps (MAP08) - Invulnerability', + 'episode': 1, + 'map': 8, + 'index': 120, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361073: {'name': 'Tricks and Traps (MAP08) - Invulnerability 2', + 'episode': 1, + 'map': 8, + 'index': 122, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361074: {'name': 'Tricks and Traps (MAP08) - Yellow skull key', + 'episode': 1, + 'map': 8, + 'index': 123, + 'doom_type': 39, + 'region': "Tricks and Traps (MAP08) Main"}, + 361075: {'name': 'Tricks and Traps (MAP08) - Backpack', + 'episode': 1, + 'map': 8, + 'index': 133, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361076: {'name': 'Tricks and Traps (MAP08) - Backpack 2', + 'episode': 1, + 'map': 8, + 'index': 134, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361077: {'name': 'Tricks and Traps (MAP08) - Invulnerability 3', + 'episode': 1, + 'map': 8, + 'index': 135, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361078: {'name': 'Tricks and Traps (MAP08) - Invulnerability 4', + 'episode': 1, + 'map': 8, + 'index': 136, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Main"}, + 361079: {'name': 'Tricks and Traps (MAP08) - BFG9000', + 'episode': 1, + 'map': 8, + 'index': 161, + 'doom_type': 2006, + 'region': "Tricks and Traps (MAP08) Main"}, + 361080: {'name': 'Tricks and Traps (MAP08) - Supercharge 2', + 'episode': 1, + 'map': 8, + 'index': 162, + 'doom_type': 2013, + 'region': "Tricks and Traps (MAP08) Main"}, + 361081: {'name': 'Tricks and Traps (MAP08) - Backpack 3', + 'episode': 1, + 'map': 8, + 'index': 163, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361082: {'name': 'Tricks and Traps (MAP08) - Backpack 4', + 'episode': 1, + 'map': 8, + 'index': 164, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361083: {'name': 'Tricks and Traps (MAP08) - Chainsaw', + 'episode': 1, + 'map': 8, + 'index': 168, + 'doom_type': 2005, + 'region': "Tricks and Traps (MAP08) Main"}, + 361084: {'name': 'Tricks and Traps (MAP08) - Red skull key', + 'episode': 1, + 'map': 8, + 'index': 176, + 'doom_type': 38, + 'region': "Tricks and Traps (MAP08) Yellow"}, + 361085: {'name': 'Tricks and Traps (MAP08) - Invulnerability 5', + 'episode': 1, + 'map': 8, + 'index': 202, + 'doom_type': 2022, + 'region': "Tricks and Traps (MAP08) Yellow"}, + 361086: {'name': 'Tricks and Traps (MAP08) - Armor 2', + 'episode': 1, + 'map': 8, + 'index': 220, + 'doom_type': 2018, + 'region': "Tricks and Traps (MAP08) Main"}, + 361087: {'name': 'Tricks and Traps (MAP08) - Backpack 5', + 'episode': 1, + 'map': 8, + 'index': 226, + 'doom_type': 8, + 'region': "Tricks and Traps (MAP08) Main"}, + 361088: {'name': 'Tricks and Traps (MAP08) - Partial invisibility', + 'episode': 1, + 'map': 8, + 'index': 235, + 'doom_type': 2024, + 'region': "Tricks and Traps (MAP08) Main"}, + 361089: {'name': 'Tricks and Traps (MAP08) - Exit', + 'episode': 1, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Tricks and Traps (MAP08) Red"}, + 361090: {'name': 'The Pit (MAP09) - Berserk', + 'episode': 1, + 'map': 9, + 'index': 5, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Main"}, + 361091: {'name': 'The Pit (MAP09) - Shotgun', + 'episode': 1, + 'map': 9, + 'index': 21, + 'doom_type': 2001, + 'region': "The Pit (MAP09) Main"}, + 361092: {'name': 'The Pit (MAP09) - Mega Armor', + 'episode': 1, + 'map': 9, + 'index': 26, + 'doom_type': 2019, + 'region': "The Pit (MAP09) Main"}, + 361093: {'name': 'The Pit (MAP09) - Supercharge', + 'episode': 1, + 'map': 9, + 'index': 78, + 'doom_type': 2013, + 'region': "The Pit (MAP09) Main"}, + 361094: {'name': 'The Pit (MAP09) - Berserk 2', + 'episode': 1, + 'map': 9, + 'index': 90, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Main"}, + 361095: {'name': 'The Pit (MAP09) - Rocket launcher', + 'episode': 1, + 'map': 9, + 'index': 92, + 'doom_type': 2003, + 'region': "The Pit (MAP09) Main"}, + 361096: {'name': 'The Pit (MAP09) - BFG9000', + 'episode': 1, + 'map': 9, + 'index': 184, + 'doom_type': 2006, + 'region': "The Pit (MAP09) Main"}, + 361097: {'name': 'The Pit (MAP09) - Blue keycard', + 'episode': 1, + 'map': 9, + 'index': 185, + 'doom_type': 5, + 'region': "The Pit (MAP09) Main"}, + 361098: {'name': 'The Pit (MAP09) - Yellow keycard', + 'episode': 1, + 'map': 9, + 'index': 226, + 'doom_type': 6, + 'region': "The Pit (MAP09) Blue"}, + 361099: {'name': 'The Pit (MAP09) - Backpack', + 'episode': 1, + 'map': 9, + 'index': 244, + 'doom_type': 8, + 'region': "The Pit (MAP09) Blue"}, + 361100: {'name': 'The Pit (MAP09) - Computer area map', + 'episode': 1, + 'map': 9, + 'index': 245, + 'doom_type': 2026, + 'region': "The Pit (MAP09) Blue"}, + 361101: {'name': 'The Pit (MAP09) - Supercharge 2', + 'episode': 1, + 'map': 9, + 'index': 250, + 'doom_type': 2013, + 'region': "The Pit (MAP09) Blue"}, + 361102: {'name': 'The Pit (MAP09) - Mega Armor 2', + 'episode': 1, + 'map': 9, + 'index': 251, + 'doom_type': 2019, + 'region': "The Pit (MAP09) Blue"}, + 361103: {'name': 'The Pit (MAP09) - Berserk 3', + 'episode': 1, + 'map': 9, + 'index': 309, + 'doom_type': 2023, + 'region': "The Pit (MAP09) Blue"}, + 361104: {'name': 'The Pit (MAP09) - Armor', + 'episode': 1, + 'map': 9, + 'index': 348, + 'doom_type': 2018, + 'region': "The Pit (MAP09) Main"}, + 361105: {'name': 'The Pit (MAP09) - Exit', + 'episode': 1, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Pit (MAP09) Yellow"}, + 361106: {'name': 'Refueling Base (MAP10) - BFG9000', + 'episode': 1, + 'map': 10, + 'index': 17, + 'doom_type': 2006, + 'region': "Refueling Base (MAP10) Main"}, + 361107: {'name': 'Refueling Base (MAP10) - Supercharge', + 'episode': 1, + 'map': 10, + 'index': 28, + 'doom_type': 2013, + 'region': "Refueling Base (MAP10) Main"}, + 361108: {'name': 'Refueling Base (MAP10) - Plasma gun', + 'episode': 1, + 'map': 10, + 'index': 29, + 'doom_type': 2004, + 'region': "Refueling Base (MAP10) Main"}, + 361109: {'name': 'Refueling Base (MAP10) - Blue keycard', + 'episode': 1, + 'map': 10, + 'index': 50, + 'doom_type': 5, + 'region': "Refueling Base (MAP10) Main"}, + 361110: {'name': 'Refueling Base (MAP10) - Shotgun', + 'episode': 1, + 'map': 10, + 'index': 99, + 'doom_type': 2001, + 'region': "Refueling Base (MAP10) Main"}, + 361111: {'name': 'Refueling Base (MAP10) - Chaingun', + 'episode': 1, + 'map': 10, + 'index': 158, + 'doom_type': 2002, + 'region': "Refueling Base (MAP10) Main"}, + 361112: {'name': 'Refueling Base (MAP10) - Armor', + 'episode': 1, + 'map': 10, + 'index': 172, + 'doom_type': 2018, + 'region': "Refueling Base (MAP10) Main"}, + 361113: {'name': 'Refueling Base (MAP10) - Rocket launcher', + 'episode': 1, + 'map': 10, + 'index': 291, + 'doom_type': 2003, + 'region': "Refueling Base (MAP10) Main"}, + 361114: {'name': 'Refueling Base (MAP10) - Supercharge 2', + 'episode': 1, + 'map': 10, + 'index': 359, + 'doom_type': 2013, + 'region': "Refueling Base (MAP10) Main"}, + 361115: {'name': 'Refueling Base (MAP10) - Backpack', + 'episode': 1, + 'map': 10, + 'index': 368, + 'doom_type': 8, + 'region': "Refueling Base (MAP10) Main"}, + 361116: {'name': 'Refueling Base (MAP10) - Berserk', + 'episode': 1, + 'map': 10, + 'index': 392, + 'doom_type': 2023, + 'region': "Refueling Base (MAP10) Main"}, + 361117: {'name': 'Refueling Base (MAP10) - Mega Armor', + 'episode': 1, + 'map': 10, + 'index': 395, + 'doom_type': 2019, + 'region': "Refueling Base (MAP10) Main"}, + 361118: {'name': 'Refueling Base (MAP10) - Invulnerability', + 'episode': 1, + 'map': 10, + 'index': 396, + 'doom_type': 2022, + 'region': "Refueling Base (MAP10) Main"}, + 361119: {'name': 'Refueling Base (MAP10) - Invulnerability 2', + 'episode': 1, + 'map': 10, + 'index': 398, + 'doom_type': 2022, + 'region': "Refueling Base (MAP10) Main"}, + 361120: {'name': 'Refueling Base (MAP10) - Armor 2', + 'episode': 1, + 'map': 10, + 'index': 400, + 'doom_type': 2018, + 'region': "Refueling Base (MAP10) Main"}, + 361121: {'name': 'Refueling Base (MAP10) - Berserk 2', + 'episode': 1, + 'map': 10, + 'index': 441, + 'doom_type': 2023, + 'region': "Refueling Base (MAP10) Main"}, + 361122: {'name': 'Refueling Base (MAP10) - Partial invisibility', + 'episode': 1, + 'map': 10, + 'index': 470, + 'doom_type': 2024, + 'region': "Refueling Base (MAP10) Main"}, + 361123: {'name': 'Refueling Base (MAP10) - Chainsaw', + 'episode': 1, + 'map': 10, + 'index': 472, + 'doom_type': 2005, + 'region': "Refueling Base (MAP10) Main"}, + 361124: {'name': 'Refueling Base (MAP10) - Yellow keycard', + 'episode': 1, + 'map': 10, + 'index': 473, + 'doom_type': 6, + 'region': "Refueling Base (MAP10) Main"}, + 361125: {'name': 'Refueling Base (MAP10) - Megasphere', + 'episode': 1, + 'map': 10, + 'index': 507, + 'doom_type': 83, + 'region': "Refueling Base (MAP10) Main"}, + 361126: {'name': 'Refueling Base (MAP10) - Exit', + 'episode': 1, + 'map': 10, + 'index': -1, + 'doom_type': -1, + 'region': "Refueling Base (MAP10) Yellow Blue"}, + 361127: {'name': 'Circle of Death (MAP11) - Red keycard', + 'episode': 1, + 'map': 11, + 'index': 1, + 'doom_type': 13, + 'region': "Circle of Death (MAP11) Main"}, + 361128: {'name': 'Circle of Death (MAP11) - Chaingun', + 'episode': 1, + 'map': 11, + 'index': 14, + 'doom_type': 2002, + 'region': "Circle of Death (MAP11) Main"}, + 361129: {'name': 'Circle of Death (MAP11) - Supercharge', + 'episode': 1, + 'map': 11, + 'index': 23, + 'doom_type': 2013, + 'region': "Circle of Death (MAP11) Main"}, + 361130: {'name': 'Circle of Death (MAP11) - Plasma gun', + 'episode': 1, + 'map': 11, + 'index': 30, + 'doom_type': 2004, + 'region': "Circle of Death (MAP11) Main"}, + 361131: {'name': 'Circle of Death (MAP11) - Blue keycard', + 'episode': 1, + 'map': 11, + 'index': 40, + 'doom_type': 5, + 'region': "Circle of Death (MAP11) Main"}, + 361132: {'name': 'Circle of Death (MAP11) - Armor', + 'episode': 1, + 'map': 11, + 'index': 42, + 'doom_type': 2018, + 'region': "Circle of Death (MAP11) Main"}, + 361133: {'name': 'Circle of Death (MAP11) - Shotgun', + 'episode': 1, + 'map': 11, + 'index': 50, + 'doom_type': 2001, + 'region': "Circle of Death (MAP11) Main"}, + 361134: {'name': 'Circle of Death (MAP11) - Mega Armor', + 'episode': 1, + 'map': 11, + 'index': 58, + 'doom_type': 2019, + 'region': "Circle of Death (MAP11) Blue"}, + 361135: {'name': 'Circle of Death (MAP11) - Partial invisibility', + 'episode': 1, + 'map': 11, + 'index': 70, + 'doom_type': 2024, + 'region': "Circle of Death (MAP11) Main"}, + 361136: {'name': 'Circle of Death (MAP11) - Invulnerability', + 'episode': 1, + 'map': 11, + 'index': 83, + 'doom_type': 2022, + 'region': "Circle of Death (MAP11) Red"}, + 361137: {'name': 'Circle of Death (MAP11) - Rocket launcher', + 'episode': 1, + 'map': 11, + 'index': 86, + 'doom_type': 2003, + 'region': "Circle of Death (MAP11) Red"}, + 361138: {'name': 'Circle of Death (MAP11) - Backpack', + 'episode': 1, + 'map': 11, + 'index': 88, + 'doom_type': 8, + 'region': "Circle of Death (MAP11) Red"}, + 361139: {'name': 'Circle of Death (MAP11) - Supercharge 2', + 'episode': 1, + 'map': 11, + 'index': 108, + 'doom_type': 2013, + 'region': "Circle of Death (MAP11) Red"}, + 361140: {'name': 'Circle of Death (MAP11) - BFG9000', + 'episode': 1, + 'map': 11, + 'index': 110, + 'doom_type': 2006, + 'region': "Circle of Death (MAP11) Red"}, + 361141: {'name': 'Circle of Death (MAP11) - Exit', + 'episode': 1, + 'map': 11, + 'index': -1, + 'doom_type': -1, + 'region': "Circle of Death (MAP11) Red"}, + 361142: {'name': 'The Factory (MAP12) - Shotgun', + 'episode': 2, + 'map': 1, + 'index': 14, + 'doom_type': 2001, + 'region': "The Factory (MAP12) Main"}, + 361143: {'name': 'The Factory (MAP12) - Berserk', + 'episode': 2, + 'map': 1, + 'index': 35, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361144: {'name': 'The Factory (MAP12) - Chaingun', + 'episode': 2, + 'map': 1, + 'index': 38, + 'doom_type': 2002, + 'region': "The Factory (MAP12) Main"}, + 361145: {'name': 'The Factory (MAP12) - Supercharge', + 'episode': 2, + 'map': 1, + 'index': 52, + 'doom_type': 2013, + 'region': "The Factory (MAP12) Main"}, + 361146: {'name': 'The Factory (MAP12) - Blue keycard', + 'episode': 2, + 'map': 1, + 'index': 54, + 'doom_type': 5, + 'region': "The Factory (MAP12) Main"}, + 361147: {'name': 'The Factory (MAP12) - Armor', + 'episode': 2, + 'map': 1, + 'index': 63, + 'doom_type': 2018, + 'region': "The Factory (MAP12) Blue"}, + 361148: {'name': 'The Factory (MAP12) - Backpack', + 'episode': 2, + 'map': 1, + 'index': 70, + 'doom_type': 8, + 'region': "The Factory (MAP12) Blue"}, + 361149: {'name': 'The Factory (MAP12) - Supercharge 2', + 'episode': 2, + 'map': 1, + 'index': 83, + 'doom_type': 2013, + 'region': "The Factory (MAP12) Main"}, + 361150: {'name': 'The Factory (MAP12) - Armor 2', + 'episode': 2, + 'map': 1, + 'index': 92, + 'doom_type': 2018, + 'region': "The Factory (MAP12) Main"}, + 361151: {'name': 'The Factory (MAP12) - Partial invisibility', + 'episode': 2, + 'map': 1, + 'index': 93, + 'doom_type': 2024, + 'region': "The Factory (MAP12) Main"}, + 361152: {'name': 'The Factory (MAP12) - Berserk 2', + 'episode': 2, + 'map': 1, + 'index': 107, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361153: {'name': 'The Factory (MAP12) - Yellow keycard', + 'episode': 2, + 'map': 1, + 'index': 123, + 'doom_type': 6, + 'region': "The Factory (MAP12) Main"}, + 361154: {'name': 'The Factory (MAP12) - BFG9000', + 'episode': 2, + 'map': 1, + 'index': 135, + 'doom_type': 2006, + 'region': "The Factory (MAP12) Blue"}, + 361155: {'name': 'The Factory (MAP12) - Berserk 3', + 'episode': 2, + 'map': 1, + 'index': 189, + 'doom_type': 2023, + 'region': "The Factory (MAP12) Main"}, + 361156: {'name': 'The Factory (MAP12) - Super Shotgun', + 'episode': 2, + 'map': 1, + 'index': 192, + 'doom_type': 82, + 'region': "The Factory (MAP12) Main"}, + 361157: {'name': 'The Factory (MAP12) - Exit', + 'episode': 2, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Factory (MAP12) Yellow"}, + 361158: {'name': 'Downtown (MAP13) - Rocket launcher', + 'episode': 2, + 'map': 2, + 'index': 4, + 'doom_type': 2003, + 'region': "Downtown (MAP13) Main"}, + 361159: {'name': 'Downtown (MAP13) - Shotgun', + 'episode': 2, + 'map': 2, + 'index': 42, + 'doom_type': 2001, + 'region': "Downtown (MAP13) Main"}, + 361160: {'name': 'Downtown (MAP13) - Supercharge', + 'episode': 2, + 'map': 2, + 'index': 73, + 'doom_type': 2013, + 'region': "Downtown (MAP13) Main"}, + 361161: {'name': 'Downtown (MAP13) - Berserk', + 'episode': 2, + 'map': 2, + 'index': 131, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Main"}, + 361162: {'name': 'Downtown (MAP13) - Mega Armor', + 'episode': 2, + 'map': 2, + 'index': 158, + 'doom_type': 2019, + 'region': "Downtown (MAP13) Main"}, + 361163: {'name': 'Downtown (MAP13) - Chaingun', + 'episode': 2, + 'map': 2, + 'index': 183, + 'doom_type': 2002, + 'region': "Downtown (MAP13) Main"}, + 361164: {'name': 'Downtown (MAP13) - Blue keycard', + 'episode': 2, + 'map': 2, + 'index': 195, + 'doom_type': 5, + 'region': "Downtown (MAP13) Main"}, + 361165: {'name': 'Downtown (MAP13) - Yellow keycard', + 'episode': 2, + 'map': 2, + 'index': 201, + 'doom_type': 6, + 'region': "Downtown (MAP13) Red"}, + 361166: {'name': 'Downtown (MAP13) - Berserk 2', + 'episode': 2, + 'map': 2, + 'index': 207, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Red"}, + 361167: {'name': 'Downtown (MAP13) - Plasma gun', + 'episode': 2, + 'map': 2, + 'index': 231, + 'doom_type': 2004, + 'region': "Downtown (MAP13) Main"}, + 361168: {'name': 'Downtown (MAP13) - Partial invisibility', + 'episode': 2, + 'map': 2, + 'index': 249, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361169: {'name': 'Downtown (MAP13) - Backpack', + 'episode': 2, + 'map': 2, + 'index': 250, + 'doom_type': 8, + 'region': "Downtown (MAP13) Main"}, + 361170: {'name': 'Downtown (MAP13) - Chainsaw', + 'episode': 2, + 'map': 2, + 'index': 257, + 'doom_type': 2005, + 'region': "Downtown (MAP13) Blue"}, + 361171: {'name': 'Downtown (MAP13) - BFG9000', + 'episode': 2, + 'map': 2, + 'index': 258, + 'doom_type': 2006, + 'region': "Downtown (MAP13) Main"}, + 361172: {'name': 'Downtown (MAP13) - Invulnerability', + 'episode': 2, + 'map': 2, + 'index': 269, + 'doom_type': 2022, + 'region': "Downtown (MAP13) Blue"}, + 361173: {'name': 'Downtown (MAP13) - Invulnerability 2', + 'episode': 2, + 'map': 2, + 'index': 280, + 'doom_type': 2022, + 'region': "Downtown (MAP13) Main"}, + 361174: {'name': 'Downtown (MAP13) - Partial invisibility 2', + 'episode': 2, + 'map': 2, + 'index': 281, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361175: {'name': 'Downtown (MAP13) - Partial invisibility 3', + 'episode': 2, + 'map': 2, + 'index': 282, + 'doom_type': 2024, + 'region': "Downtown (MAP13) Main"}, + 361176: {'name': 'Downtown (MAP13) - Red keycard', + 'episode': 2, + 'map': 2, + 'index': 283, + 'doom_type': 13, + 'region': "Downtown (MAP13) Blue"}, + 361177: {'name': 'Downtown (MAP13) - Berserk 3', + 'episode': 2, + 'map': 2, + 'index': 296, + 'doom_type': 2023, + 'region': "Downtown (MAP13) Yellow"}, + 361178: {'name': 'Downtown (MAP13) - Computer area map', + 'episode': 2, + 'map': 2, + 'index': 298, + 'doom_type': 2026, + 'region': "Downtown (MAP13) Main"}, + 361179: {'name': 'Downtown (MAP13) - Exit', + 'episode': 2, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Downtown (MAP13) Yellow"}, + 361180: {'name': 'The Inmost Dens (MAP14) - Shotgun', + 'episode': 2, + 'map': 3, + 'index': 13, + 'doom_type': 2001, + 'region': "The Inmost Dens (MAP14) Main"}, + 361181: {'name': 'The Inmost Dens (MAP14) - Supercharge', + 'episode': 2, + 'map': 3, + 'index': 16, + 'doom_type': 2013, + 'region': "The Inmost Dens (MAP14) Main"}, + 361182: {'name': 'The Inmost Dens (MAP14) - Mega Armor', + 'episode': 2, + 'map': 3, + 'index': 22, + 'doom_type': 2019, + 'region': "The Inmost Dens (MAP14) Main"}, + 361183: {'name': 'The Inmost Dens (MAP14) - Berserk', + 'episode': 2, + 'map': 3, + 'index': 78, + 'doom_type': 2023, + 'region': "The Inmost Dens (MAP14) Main"}, + 361184: {'name': 'The Inmost Dens (MAP14) - Chaingun', + 'episode': 2, + 'map': 3, + 'index': 80, + 'doom_type': 2002, + 'region': "The Inmost Dens (MAP14) Main"}, + 361185: {'name': 'The Inmost Dens (MAP14) - Plasma gun', + 'episode': 2, + 'map': 3, + 'index': 81, + 'doom_type': 2004, + 'region': "The Inmost Dens (MAP14) Main"}, + 361186: {'name': 'The Inmost Dens (MAP14) - Red skull key', + 'episode': 2, + 'map': 3, + 'index': 119, + 'doom_type': 38, + 'region': "The Inmost Dens (MAP14) Main"}, + 361187: {'name': 'The Inmost Dens (MAP14) - Rocket launcher', + 'episode': 2, + 'map': 3, + 'index': 123, + 'doom_type': 2003, + 'region': "The Inmost Dens (MAP14) Main"}, + 361188: {'name': 'The Inmost Dens (MAP14) - Blue skull key', + 'episode': 2, + 'map': 3, + 'index': 130, + 'doom_type': 40, + 'region': "The Inmost Dens (MAP14) Red South"}, + 361189: {'name': 'The Inmost Dens (MAP14) - Partial invisibility', + 'episode': 2, + 'map': 3, + 'index': 138, + 'doom_type': 2024, + 'region': "The Inmost Dens (MAP14) Red South"}, + 361190: {'name': 'The Inmost Dens (MAP14) - Exit', + 'episode': 2, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Inmost Dens (MAP14) Blue"}, + 361191: {'name': 'Industrial Zone (MAP15) - Berserk', + 'episode': 2, + 'map': 4, + 'index': 4, + 'doom_type': 2023, + 'region': "Industrial Zone (MAP15) Main"}, + 361192: {'name': 'Industrial Zone (MAP15) - Rocket launcher', + 'episode': 2, + 'map': 4, + 'index': 11, + 'doom_type': 2003, + 'region': "Industrial Zone (MAP15) Main"}, + 361193: {'name': 'Industrial Zone (MAP15) - Shotgun', + 'episode': 2, + 'map': 4, + 'index': 13, + 'doom_type': 2001, + 'region': "Industrial Zone (MAP15) Main"}, + 361194: {'name': 'Industrial Zone (MAP15) - Partial invisibility', + 'episode': 2, + 'map': 4, + 'index': 14, + 'doom_type': 2024, + 'region': "Industrial Zone (MAP15) Main"}, + 361195: {'name': 'Industrial Zone (MAP15) - Backpack', + 'episode': 2, + 'map': 4, + 'index': 24, + 'doom_type': 8, + 'region': "Industrial Zone (MAP15) Main"}, + 361196: {'name': 'Industrial Zone (MAP15) - BFG9000', + 'episode': 2, + 'map': 4, + 'index': 48, + 'doom_type': 2006, + 'region': "Industrial Zone (MAP15) Main"}, + 361197: {'name': 'Industrial Zone (MAP15) - Supercharge', + 'episode': 2, + 'map': 4, + 'index': 56, + 'doom_type': 2013, + 'region': "Industrial Zone (MAP15) Main"}, + 361198: {'name': 'Industrial Zone (MAP15) - Mega Armor', + 'episode': 2, + 'map': 4, + 'index': 57, + 'doom_type': 2019, + 'region': "Industrial Zone (MAP15) Main"}, + 361199: {'name': 'Industrial Zone (MAP15) - Armor', + 'episode': 2, + 'map': 4, + 'index': 59, + 'doom_type': 2018, + 'region': "Industrial Zone (MAP15) Main"}, + 361200: {'name': 'Industrial Zone (MAP15) - Yellow keycard', + 'episode': 2, + 'map': 4, + 'index': 71, + 'doom_type': 6, + 'region': "Industrial Zone (MAP15) Main"}, + 361201: {'name': 'Industrial Zone (MAP15) - Chaingun', + 'episode': 2, + 'map': 4, + 'index': 74, + 'doom_type': 2002, + 'region': "Industrial Zone (MAP15) Main"}, + 361202: {'name': 'Industrial Zone (MAP15) - Plasma gun', + 'episode': 2, + 'map': 4, + 'index': 86, + 'doom_type': 2004, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361203: {'name': 'Industrial Zone (MAP15) - Partial invisibility 2', + 'episode': 2, + 'map': 4, + 'index': 91, + 'doom_type': 2024, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361204: {'name': 'Industrial Zone (MAP15) - Computer area map', + 'episode': 2, + 'map': 4, + 'index': 93, + 'doom_type': 2026, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361205: {'name': 'Industrial Zone (MAP15) - Invulnerability', + 'episode': 2, + 'map': 4, + 'index': 94, + 'doom_type': 2022, + 'region': "Industrial Zone (MAP15) Main"}, + 361206: {'name': 'Industrial Zone (MAP15) - Red keycard', + 'episode': 2, + 'map': 4, + 'index': 100, + 'doom_type': 13, + 'region': "Industrial Zone (MAP15) Main"}, + 361207: {'name': 'Industrial Zone (MAP15) - Backpack 2', + 'episode': 2, + 'map': 4, + 'index': 103, + 'doom_type': 8, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361208: {'name': 'Industrial Zone (MAP15) - Chainsaw', + 'episode': 2, + 'map': 4, + 'index': 113, + 'doom_type': 2005, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361209: {'name': 'Industrial Zone (MAP15) - Megasphere', + 'episode': 2, + 'map': 4, + 'index': 125, + 'doom_type': 83, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361210: {'name': 'Industrial Zone (MAP15) - Berserk 2', + 'episode': 2, + 'map': 4, + 'index': 178, + 'doom_type': 2023, + 'region': "Industrial Zone (MAP15) Yellow East"}, + 361211: {'name': 'Industrial Zone (MAP15) - Blue keycard', + 'episode': 2, + 'map': 4, + 'index': 337, + 'doom_type': 5, + 'region': "Industrial Zone (MAP15) Yellow West"}, + 361212: {'name': 'Industrial Zone (MAP15) - Mega Armor 2', + 'episode': 2, + 'map': 4, + 'index': 361, + 'doom_type': 2019, + 'region': "Industrial Zone (MAP15) Main"}, + 361213: {'name': 'Industrial Zone (MAP15) - Exit', + 'episode': 2, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Industrial Zone (MAP15) Blue"}, + 361214: {'name': 'Suburbs (MAP16) - Megasphere', + 'episode': 2, + 'map': 5, + 'index': 7, + 'doom_type': 83, + 'region': "Suburbs (MAP16) Main"}, + 361215: {'name': 'Suburbs (MAP16) - Super Shotgun', + 'episode': 2, + 'map': 5, + 'index': 11, + 'doom_type': 82, + 'region': "Suburbs (MAP16) Main"}, + 361216: {'name': 'Suburbs (MAP16) - Chaingun', + 'episode': 2, + 'map': 5, + 'index': 15, + 'doom_type': 2002, + 'region': "Suburbs (MAP16) Main"}, + 361217: {'name': 'Suburbs (MAP16) - Backpack', + 'episode': 2, + 'map': 5, + 'index': 53, + 'doom_type': 8, + 'region': "Suburbs (MAP16) Main"}, + 361218: {'name': 'Suburbs (MAP16) - Rocket launcher', + 'episode': 2, + 'map': 5, + 'index': 59, + 'doom_type': 2003, + 'region': "Suburbs (MAP16) Main"}, + 361219: {'name': 'Suburbs (MAP16) - Berserk', + 'episode': 2, + 'map': 5, + 'index': 60, + 'doom_type': 2023, + 'region': "Suburbs (MAP16) Main"}, + 361220: {'name': 'Suburbs (MAP16) - Plasma gun', + 'episode': 2, + 'map': 5, + 'index': 62, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361221: {'name': 'Suburbs (MAP16) - Plasma gun 2', + 'episode': 2, + 'map': 5, + 'index': 63, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361222: {'name': 'Suburbs (MAP16) - Plasma gun 3', + 'episode': 2, + 'map': 5, + 'index': 64, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361223: {'name': 'Suburbs (MAP16) - Plasma gun 4', + 'episode': 2, + 'map': 5, + 'index': 65, + 'doom_type': 2004, + 'region': "Suburbs (MAP16) Blue"}, + 361224: {'name': 'Suburbs (MAP16) - BFG9000', + 'episode': 2, + 'map': 5, + 'index': 169, + 'doom_type': 2006, + 'region': "Suburbs (MAP16) Main"}, + 361225: {'name': 'Suburbs (MAP16) - Shotgun', + 'episode': 2, + 'map': 5, + 'index': 182, + 'doom_type': 2001, + 'region': "Suburbs (MAP16) Main"}, + 361226: {'name': 'Suburbs (MAP16) - Supercharge', + 'episode': 2, + 'map': 5, + 'index': 185, + 'doom_type': 2013, + 'region': "Suburbs (MAP16) Main"}, + 361227: {'name': 'Suburbs (MAP16) - Blue skull key', + 'episode': 2, + 'map': 5, + 'index': 186, + 'doom_type': 40, + 'region': "Suburbs (MAP16) Main"}, + 361228: {'name': 'Suburbs (MAP16) - Invulnerability', + 'episode': 2, + 'map': 5, + 'index': 221, + 'doom_type': 2022, + 'region': "Suburbs (MAP16) Main"}, + 361229: {'name': 'Suburbs (MAP16) - Partial invisibility', + 'episode': 2, + 'map': 5, + 'index': 231, + 'doom_type': 2024, + 'region': "Suburbs (MAP16) Main"}, + 361230: {'name': 'Suburbs (MAP16) - Red skull key', + 'episode': 2, + 'map': 5, + 'index': 236, + 'doom_type': 38, + 'region': "Suburbs (MAP16) Blue"}, + 361231: {'name': 'Suburbs (MAP16) - Exit', + 'episode': 2, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Suburbs (MAP16) Red"}, + 361232: {'name': 'Tenements (MAP17) - Armor', + 'episode': 2, + 'map': 6, + 'index': 1, + 'doom_type': 2018, + 'region': "Tenements (MAP17) Red"}, + 361233: {'name': 'Tenements (MAP17) - Supercharge', + 'episode': 2, + 'map': 6, + 'index': 7, + 'doom_type': 2013, + 'region': "Tenements (MAP17) Yellow"}, + 361234: {'name': 'Tenements (MAP17) - Shotgun', + 'episode': 2, + 'map': 6, + 'index': 18, + 'doom_type': 2001, + 'region': "Tenements (MAP17) Main"}, + 361235: {'name': 'Tenements (MAP17) - Red keycard', + 'episode': 2, + 'map': 6, + 'index': 34, + 'doom_type': 13, + 'region': "Tenements (MAP17) Main"}, + 361236: {'name': 'Tenements (MAP17) - Blue keycard', + 'episode': 2, + 'map': 6, + 'index': 69, + 'doom_type': 5, + 'region': "Tenements (MAP17) Red"}, + 361237: {'name': 'Tenements (MAP17) - Supercharge 2', + 'episode': 2, + 'map': 6, + 'index': 75, + 'doom_type': 2013, + 'region': "Tenements (MAP17) Blue"}, + 361238: {'name': 'Tenements (MAP17) - Yellow skull key', + 'episode': 2, + 'map': 6, + 'index': 76, + 'doom_type': 39, + 'region': "Tenements (MAP17) Blue"}, + 361239: {'name': 'Tenements (MAP17) - Rocket launcher', + 'episode': 2, + 'map': 6, + 'index': 77, + 'doom_type': 2003, + 'region': "Tenements (MAP17) Blue"}, + 361240: {'name': 'Tenements (MAP17) - Partial invisibility', + 'episode': 2, + 'map': 6, + 'index': 81, + 'doom_type': 2024, + 'region': "Tenements (MAP17) Blue"}, + 361241: {'name': 'Tenements (MAP17) - Chaingun', + 'episode': 2, + 'map': 6, + 'index': 92, + 'doom_type': 2002, + 'region': "Tenements (MAP17) Red"}, + 361242: {'name': 'Tenements (MAP17) - BFG9000', + 'episode': 2, + 'map': 6, + 'index': 102, + 'doom_type': 2006, + 'region': "Tenements (MAP17) Main"}, + 361243: {'name': 'Tenements (MAP17) - Plasma gun', + 'episode': 2, + 'map': 6, + 'index': 114, + 'doom_type': 2004, + 'region': "Tenements (MAP17) Yellow"}, + 361244: {'name': 'Tenements (MAP17) - Mega Armor', + 'episode': 2, + 'map': 6, + 'index': 168, + 'doom_type': 2019, + 'region': "Tenements (MAP17) Red"}, + 361245: {'name': 'Tenements (MAP17) - Armor 2', + 'episode': 2, + 'map': 6, + 'index': 179, + 'doom_type': 2018, + 'region': "Tenements (MAP17) Red"}, + 361246: {'name': 'Tenements (MAP17) - Berserk', + 'episode': 2, + 'map': 6, + 'index': 218, + 'doom_type': 2023, + 'region': "Tenements (MAP17) Red"}, + 361247: {'name': 'Tenements (MAP17) - Backpack', + 'episode': 2, + 'map': 6, + 'index': 261, + 'doom_type': 8, + 'region': "Tenements (MAP17) Blue"}, + 361248: {'name': 'Tenements (MAP17) - Megasphere', + 'episode': 2, + 'map': 6, + 'index': 419, + 'doom_type': 83, + 'region': "Tenements (MAP17) Yellow"}, + 361249: {'name': 'Tenements (MAP17) - Exit', + 'episode': 2, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Tenements (MAP17) Yellow"}, + 361250: {'name': 'The Courtyard (MAP18) - Shotgun', + 'episode': 2, + 'map': 7, + 'index': 12, + 'doom_type': 2001, + 'region': "The Courtyard (MAP18) Main"}, + 361251: {'name': 'The Courtyard (MAP18) - Plasma gun', + 'episode': 2, + 'map': 7, + 'index': 36, + 'doom_type': 2004, + 'region': "The Courtyard (MAP18) Main"}, + 361252: {'name': 'The Courtyard (MAP18) - Armor', + 'episode': 2, + 'map': 7, + 'index': 48, + 'doom_type': 2018, + 'region': "The Courtyard (MAP18) Main"}, + 361253: {'name': 'The Courtyard (MAP18) - Berserk', + 'episode': 2, + 'map': 7, + 'index': 52, + 'doom_type': 2023, + 'region': "The Courtyard (MAP18) Main"}, + 361254: {'name': 'The Courtyard (MAP18) - Chaingun', + 'episode': 2, + 'map': 7, + 'index': 95, + 'doom_type': 2002, + 'region': "The Courtyard (MAP18) Main"}, + 361255: {'name': 'The Courtyard (MAP18) - Rocket launcher', + 'episode': 2, + 'map': 7, + 'index': 130, + 'doom_type': 2003, + 'region': "The Courtyard (MAP18) Main"}, + 361256: {'name': 'The Courtyard (MAP18) - Partial invisibility', + 'episode': 2, + 'map': 7, + 'index': 170, + 'doom_type': 2024, + 'region': "The Courtyard (MAP18) Main"}, + 361257: {'name': 'The Courtyard (MAP18) - Partial invisibility 2', + 'episode': 2, + 'map': 7, + 'index': 171, + 'doom_type': 2024, + 'region': "The Courtyard (MAP18) Main"}, + 361258: {'name': 'The Courtyard (MAP18) - Backpack', + 'episode': 2, + 'map': 7, + 'index': 198, + 'doom_type': 8, + 'region': "The Courtyard (MAP18) Main"}, + 361259: {'name': 'The Courtyard (MAP18) - Supercharge', + 'episode': 2, + 'map': 7, + 'index': 218, + 'doom_type': 2013, + 'region': "The Courtyard (MAP18) Main"}, + 361260: {'name': 'The Courtyard (MAP18) - Invulnerability', + 'episode': 2, + 'map': 7, + 'index': 228, + 'doom_type': 2022, + 'region': "The Courtyard (MAP18) Main"}, + 361261: {'name': 'The Courtyard (MAP18) - Invulnerability 2', + 'episode': 2, + 'map': 7, + 'index': 229, + 'doom_type': 2022, + 'region': "The Courtyard (MAP18) Main"}, + 361262: {'name': 'The Courtyard (MAP18) - Yellow skull key', + 'episode': 2, + 'map': 7, + 'index': 254, + 'doom_type': 39, + 'region': "The Courtyard (MAP18) Main"}, + 361263: {'name': 'The Courtyard (MAP18) - Blue skull key', + 'episode': 2, + 'map': 7, + 'index': 268, + 'doom_type': 40, + 'region': "The Courtyard (MAP18) Yellow"}, + 361264: {'name': 'The Courtyard (MAP18) - BFG9000', + 'episode': 2, + 'map': 7, + 'index': 400, + 'doom_type': 2006, + 'region': "The Courtyard (MAP18) Main"}, + 361265: {'name': 'The Courtyard (MAP18) - Computer area map', + 'episode': 2, + 'map': 7, + 'index': 458, + 'doom_type': 2026, + 'region': "The Courtyard (MAP18) Main"}, + 361266: {'name': 'The Courtyard (MAP18) - Super Shotgun', + 'episode': 2, + 'map': 7, + 'index': 461, + 'doom_type': 82, + 'region': "The Courtyard (MAP18) Main"}, + 361267: {'name': 'The Courtyard (MAP18) - Exit', + 'episode': 2, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Courtyard (MAP18) Blue"}, + 361268: {'name': 'The Citadel (MAP19) - Armor', + 'episode': 2, + 'map': 8, + 'index': 64, + 'doom_type': 2018, + 'region': "The Citadel (MAP19) Main"}, + 361269: {'name': 'The Citadel (MAP19) - Chaingun', + 'episode': 2, + 'map': 8, + 'index': 99, + 'doom_type': 2002, + 'region': "The Citadel (MAP19) Main"}, + 361270: {'name': 'The Citadel (MAP19) - Berserk', + 'episode': 2, + 'map': 8, + 'index': 116, + 'doom_type': 2023, + 'region': "The Citadel (MAP19) Main"}, + 361271: {'name': 'The Citadel (MAP19) - Mega Armor', + 'episode': 2, + 'map': 8, + 'index': 127, + 'doom_type': 2019, + 'region': "The Citadel (MAP19) Main"}, + 361272: {'name': 'The Citadel (MAP19) - Supercharge', + 'episode': 2, + 'map': 8, + 'index': 174, + 'doom_type': 2013, + 'region': "The Citadel (MAP19) Main"}, + 361273: {'name': 'The Citadel (MAP19) - Armor 2', + 'episode': 2, + 'map': 8, + 'index': 223, + 'doom_type': 2018, + 'region': "The Citadel (MAP19) Main"}, + 361274: {'name': 'The Citadel (MAP19) - Backpack', + 'episode': 2, + 'map': 8, + 'index': 232, + 'doom_type': 8, + 'region': "The Citadel (MAP19) Main"}, + 361275: {'name': 'The Citadel (MAP19) - Invulnerability', + 'episode': 2, + 'map': 8, + 'index': 315, + 'doom_type': 2022, + 'region': "The Citadel (MAP19) Main"}, + 361276: {'name': 'The Citadel (MAP19) - Blue skull key', + 'episode': 2, + 'map': 8, + 'index': 370, + 'doom_type': 40, + 'region': "The Citadel (MAP19) Main"}, + 361277: {'name': 'The Citadel (MAP19) - Partial invisibility', + 'episode': 2, + 'map': 8, + 'index': 403, + 'doom_type': 2024, + 'region': "The Citadel (MAP19) Main"}, + 361278: {'name': 'The Citadel (MAP19) - Red skull key', + 'episode': 2, + 'map': 8, + 'index': 404, + 'doom_type': 38, + 'region': "The Citadel (MAP19) Main"}, + 361279: {'name': 'The Citadel (MAP19) - Yellow skull key', + 'episode': 2, + 'map': 8, + 'index': 405, + 'doom_type': 39, + 'region': "The Citadel (MAP19) Main"}, + 361280: {'name': 'The Citadel (MAP19) - Computer area map', + 'episode': 2, + 'map': 8, + 'index': 415, + 'doom_type': 2026, + 'region': "The Citadel (MAP19) Main"}, + 361281: {'name': 'The Citadel (MAP19) - Rocket launcher', + 'episode': 2, + 'map': 8, + 'index': 416, + 'doom_type': 2003, + 'region': "The Citadel (MAP19) Main"}, + 361282: {'name': 'The Citadel (MAP19) - Super Shotgun', + 'episode': 2, + 'map': 8, + 'index': 431, + 'doom_type': 82, + 'region': "The Citadel (MAP19) Main"}, + 361283: {'name': 'The Citadel (MAP19) - Exit', + 'episode': 2, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Citadel (MAP19) Red"}, + 361284: {'name': 'Gotcha! (MAP20) - Mega Armor', + 'episode': 2, + 'map': 9, + 'index': 9, + 'doom_type': 2019, + 'region': "Gotcha! (MAP20) Main"}, + 361285: {'name': 'Gotcha! (MAP20) - Rocket launcher', + 'episode': 2, + 'map': 9, + 'index': 10, + 'doom_type': 2003, + 'region': "Gotcha! (MAP20) Main"}, + 361286: {'name': 'Gotcha! (MAP20) - Supercharge', + 'episode': 2, + 'map': 9, + 'index': 12, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361287: {'name': 'Gotcha! (MAP20) - Armor', + 'episode': 2, + 'map': 9, + 'index': 33, + 'doom_type': 2018, + 'region': "Gotcha! (MAP20) Main"}, + 361288: {'name': 'Gotcha! (MAP20) - Megasphere', + 'episode': 2, + 'map': 9, + 'index': 43, + 'doom_type': 83, + 'region': "Gotcha! (MAP20) Main"}, + 361289: {'name': 'Gotcha! (MAP20) - Armor 2', + 'episode': 2, + 'map': 9, + 'index': 47, + 'doom_type': 2018, + 'region': "Gotcha! (MAP20) Main"}, + 361290: {'name': 'Gotcha! (MAP20) - Super Shotgun', + 'episode': 2, + 'map': 9, + 'index': 54, + 'doom_type': 82, + 'region': "Gotcha! (MAP20) Main"}, + 361291: {'name': 'Gotcha! (MAP20) - Plasma gun', + 'episode': 2, + 'map': 9, + 'index': 70, + 'doom_type': 2004, + 'region': "Gotcha! (MAP20) Main"}, + 361292: {'name': 'Gotcha! (MAP20) - Mega Armor 2', + 'episode': 2, + 'map': 9, + 'index': 96, + 'doom_type': 2019, + 'region': "Gotcha! (MAP20) Main"}, + 361293: {'name': 'Gotcha! (MAP20) - Berserk', + 'episode': 2, + 'map': 9, + 'index': 109, + 'doom_type': 2023, + 'region': "Gotcha! (MAP20) Main"}, + 361294: {'name': 'Gotcha! (MAP20) - Supercharge 2', + 'episode': 2, + 'map': 9, + 'index': 119, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361295: {'name': 'Gotcha! (MAP20) - Supercharge 3', + 'episode': 2, + 'map': 9, + 'index': 122, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361296: {'name': 'Gotcha! (MAP20) - BFG9000', + 'episode': 2, + 'map': 9, + 'index': 142, + 'doom_type': 2006, + 'region': "Gotcha! (MAP20) Main"}, + 361297: {'name': 'Gotcha! (MAP20) - Supercharge 4', + 'episode': 2, + 'map': 9, + 'index': 145, + 'doom_type': 2013, + 'region': "Gotcha! (MAP20) Main"}, + 361298: {'name': 'Gotcha! (MAP20) - Exit', + 'episode': 2, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Gotcha! (MAP20) Main"}, + 361299: {'name': 'Nirvana (MAP21) - Super Shotgun', + 'episode': 3, + 'map': 1, + 'index': 70, + 'doom_type': 82, + 'region': "Nirvana (MAP21) Main"}, + 361300: {'name': 'Nirvana (MAP21) - Rocket launcher', + 'episode': 3, + 'map': 1, + 'index': 76, + 'doom_type': 2003, + 'region': "Nirvana (MAP21) Main"}, + 361301: {'name': 'Nirvana (MAP21) - Yellow skull key', + 'episode': 3, + 'map': 1, + 'index': 108, + 'doom_type': 39, + 'region': "Nirvana (MAP21) Main"}, + 361302: {'name': 'Nirvana (MAP21) - Backpack', + 'episode': 3, + 'map': 1, + 'index': 109, + 'doom_type': 8, + 'region': "Nirvana (MAP21) Main"}, + 361303: {'name': 'Nirvana (MAP21) - Megasphere', + 'episode': 3, + 'map': 1, + 'index': 112, + 'doom_type': 83, + 'region': "Nirvana (MAP21) Main"}, + 361304: {'name': 'Nirvana (MAP21) - Invulnerability', + 'episode': 3, + 'map': 1, + 'index': 194, + 'doom_type': 2022, + 'region': "Nirvana (MAP21) Yellow"}, + 361305: {'name': 'Nirvana (MAP21) - Blue skull key', + 'episode': 3, + 'map': 1, + 'index': 199, + 'doom_type': 40, + 'region': "Nirvana (MAP21) Yellow"}, + 361306: {'name': 'Nirvana (MAP21) - Red skull key', + 'episode': 3, + 'map': 1, + 'index': 215, + 'doom_type': 38, + 'region': "Nirvana (MAP21) Yellow"}, + 361307: {'name': 'Nirvana (MAP21) - Exit', + 'episode': 3, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Nirvana (MAP21) Magenta"}, + 361308: {'name': 'The Catacombs (MAP22) - Rocket launcher', + 'episode': 3, + 'map': 2, + 'index': 4, + 'doom_type': 2003, + 'region': "The Catacombs (MAP22) Main"}, + 361309: {'name': 'The Catacombs (MAP22) - Blue skull key', + 'episode': 3, + 'map': 2, + 'index': 5, + 'doom_type': 40, + 'region': "The Catacombs (MAP22) Main"}, + 361310: {'name': 'The Catacombs (MAP22) - Red skull key', + 'episode': 3, + 'map': 2, + 'index': 12, + 'doom_type': 38, + 'region': "The Catacombs (MAP22) Blue"}, + 361311: {'name': 'The Catacombs (MAP22) - Shotgun', + 'episode': 3, + 'map': 2, + 'index': 28, + 'doom_type': 2001, + 'region': "The Catacombs (MAP22) Main"}, + 361312: {'name': 'The Catacombs (MAP22) - Berserk', + 'episode': 3, + 'map': 2, + 'index': 45, + 'doom_type': 2023, + 'region': "The Catacombs (MAP22) Main"}, + 361313: {'name': 'The Catacombs (MAP22) - Plasma gun', + 'episode': 3, + 'map': 2, + 'index': 83, + 'doom_type': 2004, + 'region': "The Catacombs (MAP22) Main"}, + 361314: {'name': 'The Catacombs (MAP22) - Supercharge', + 'episode': 3, + 'map': 2, + 'index': 118, + 'doom_type': 2013, + 'region': "The Catacombs (MAP22) Main"}, + 361315: {'name': 'The Catacombs (MAP22) - Armor', + 'episode': 3, + 'map': 2, + 'index': 119, + 'doom_type': 2018, + 'region': "The Catacombs (MAP22) Main"}, + 361316: {'name': 'The Catacombs (MAP22) - Exit', + 'episode': 3, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Catacombs (MAP22) Red"}, + 361317: {'name': 'Barrels o Fun (MAP23) - Shotgun', + 'episode': 3, + 'map': 3, + 'index': 136, + 'doom_type': 2001, + 'region': "Barrels o Fun (MAP23) Main"}, + 361318: {'name': 'Barrels o Fun (MAP23) - Berserk', + 'episode': 3, + 'map': 3, + 'index': 222, + 'doom_type': 2023, + 'region': "Barrels o Fun (MAP23) Main"}, + 361319: {'name': 'Barrels o Fun (MAP23) - Backpack', + 'episode': 3, + 'map': 3, + 'index': 223, + 'doom_type': 8, + 'region': "Barrels o Fun (MAP23) Main"}, + 361320: {'name': 'Barrels o Fun (MAP23) - Computer area map', + 'episode': 3, + 'map': 3, + 'index': 224, + 'doom_type': 2026, + 'region': "Barrels o Fun (MAP23) Main"}, + 361321: {'name': 'Barrels o Fun (MAP23) - Armor', + 'episode': 3, + 'map': 3, + 'index': 249, + 'doom_type': 2018, + 'region': "Barrels o Fun (MAP23) Main"}, + 361322: {'name': 'Barrels o Fun (MAP23) - Rocket launcher', + 'episode': 3, + 'map': 3, + 'index': 264, + 'doom_type': 2003, + 'region': "Barrels o Fun (MAP23) Main"}, + 361323: {'name': 'Barrels o Fun (MAP23) - Megasphere', + 'episode': 3, + 'map': 3, + 'index': 266, + 'doom_type': 83, + 'region': "Barrels o Fun (MAP23) Main"}, + 361324: {'name': 'Barrels o Fun (MAP23) - Supercharge', + 'episode': 3, + 'map': 3, + 'index': 277, + 'doom_type': 2013, + 'region': "Barrels o Fun (MAP23) Main"}, + 361325: {'name': 'Barrels o Fun (MAP23) - Backpack 2', + 'episode': 3, + 'map': 3, + 'index': 301, + 'doom_type': 8, + 'region': "Barrels o Fun (MAP23) Main"}, + 361326: {'name': 'Barrels o Fun (MAP23) - Yellow skull key', + 'episode': 3, + 'map': 3, + 'index': 307, + 'doom_type': 39, + 'region': "Barrels o Fun (MAP23) Main"}, + 361327: {'name': 'Barrels o Fun (MAP23) - BFG9000', + 'episode': 3, + 'map': 3, + 'index': 342, + 'doom_type': 2006, + 'region': "Barrels o Fun (MAP23) Main"}, + 361328: {'name': 'Barrels o Fun (MAP23) - Exit', + 'episode': 3, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Barrels o Fun (MAP23) Yellow"}, + 361329: {'name': 'The Chasm (MAP24) - Plasma gun', + 'episode': 3, + 'map': 4, + 'index': 5, + 'doom_type': 2004, + 'region': "The Chasm (MAP24) Main"}, + 361330: {'name': 'The Chasm (MAP24) - Shotgun', + 'episode': 3, + 'map': 4, + 'index': 6, + 'doom_type': 2001, + 'region': "The Chasm (MAP24) Main"}, + 361331: {'name': 'The Chasm (MAP24) - Invulnerability', + 'episode': 3, + 'map': 4, + 'index': 12, + 'doom_type': 2022, + 'region': "The Chasm (MAP24) Main"}, + 361332: {'name': 'The Chasm (MAP24) - Rocket launcher', + 'episode': 3, + 'map': 4, + 'index': 22, + 'doom_type': 2003, + 'region': "The Chasm (MAP24) Main"}, + 361333: {'name': 'The Chasm (MAP24) - Blue keycard', + 'episode': 3, + 'map': 4, + 'index': 23, + 'doom_type': 5, + 'region': "The Chasm (MAP24) Main"}, + 361334: {'name': 'The Chasm (MAP24) - Backpack', + 'episode': 3, + 'map': 4, + 'index': 31, + 'doom_type': 8, + 'region': "The Chasm (MAP24) Main"}, + 361335: {'name': 'The Chasm (MAP24) - Berserk', + 'episode': 3, + 'map': 4, + 'index': 79, + 'doom_type': 2023, + 'region': "The Chasm (MAP24) Main"}, + 361336: {'name': 'The Chasm (MAP24) - Berserk 2', + 'episode': 3, + 'map': 4, + 'index': 155, + 'doom_type': 2023, + 'region': "The Chasm (MAP24) Main"}, + 361337: {'name': 'The Chasm (MAP24) - Armor', + 'episode': 3, + 'map': 4, + 'index': 169, + 'doom_type': 2018, + 'region': "The Chasm (MAP24) Main"}, + 361338: {'name': 'The Chasm (MAP24) - Red keycard', + 'episode': 3, + 'map': 4, + 'index': 261, + 'doom_type': 13, + 'region': "The Chasm (MAP24) Main"}, + 361339: {'name': 'The Chasm (MAP24) - BFG9000', + 'episode': 3, + 'map': 4, + 'index': 295, + 'doom_type': 2006, + 'region': "The Chasm (MAP24) Main"}, + 361340: {'name': 'The Chasm (MAP24) - Super Shotgun', + 'episode': 3, + 'map': 4, + 'index': 353, + 'doom_type': 82, + 'region': "The Chasm (MAP24) Main"}, + 361341: {'name': 'The Chasm (MAP24) - Megasphere', + 'episode': 3, + 'map': 4, + 'index': 355, + 'doom_type': 83, + 'region': "The Chasm (MAP24) Main"}, + 361342: {'name': 'The Chasm (MAP24) - Megasphere 2', + 'episode': 3, + 'map': 4, + 'index': 362, + 'doom_type': 83, + 'region': "The Chasm (MAP24) Main"}, + 361343: {'name': 'The Chasm (MAP24) - Exit', + 'episode': 3, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Chasm (MAP24) Red"}, + 361344: {'name': 'Bloodfalls (MAP25) - Super Shotgun', + 'episode': 3, + 'map': 5, + 'index': 6, + 'doom_type': 82, + 'region': "Bloodfalls (MAP25) Main"}, + 361345: {'name': 'Bloodfalls (MAP25) - Partial invisibility', + 'episode': 3, + 'map': 5, + 'index': 7, + 'doom_type': 2024, + 'region': "Bloodfalls (MAP25) Blue"}, + 361346: {'name': 'Bloodfalls (MAP25) - Megasphere', + 'episode': 3, + 'map': 5, + 'index': 23, + 'doom_type': 83, + 'region': "Bloodfalls (MAP25) Main"}, + 361347: {'name': 'Bloodfalls (MAP25) - BFG9000', + 'episode': 3, + 'map': 5, + 'index': 34, + 'doom_type': 2006, + 'region': "Bloodfalls (MAP25) Blue"}, + 361348: {'name': 'Bloodfalls (MAP25) - Mega Armor', + 'episode': 3, + 'map': 5, + 'index': 103, + 'doom_type': 2019, + 'region': "Bloodfalls (MAP25) Main"}, + 361349: {'name': 'Bloodfalls (MAP25) - Armor', + 'episode': 3, + 'map': 5, + 'index': 104, + 'doom_type': 2018, + 'region': "Bloodfalls (MAP25) Main"}, + 361350: {'name': 'Bloodfalls (MAP25) - Blue skull key', + 'episode': 3, + 'map': 5, + 'index': 106, + 'doom_type': 40, + 'region': "Bloodfalls (MAP25) Main"}, + 361351: {'name': 'Bloodfalls (MAP25) - Chaingun', + 'episode': 3, + 'map': 5, + 'index': 150, + 'doom_type': 2002, + 'region': "Bloodfalls (MAP25) Main"}, + 361352: {'name': 'Bloodfalls (MAP25) - Plasma gun', + 'episode': 3, + 'map': 5, + 'index': 169, + 'doom_type': 2004, + 'region': "Bloodfalls (MAP25) Main"}, + 361353: {'name': 'Bloodfalls (MAP25) - BFG9000 2', + 'episode': 3, + 'map': 5, + 'index': 186, + 'doom_type': 2006, + 'region': "Bloodfalls (MAP25) Main"}, + 361354: {'name': 'Bloodfalls (MAP25) - Rocket launcher', + 'episode': 3, + 'map': 5, + 'index': 236, + 'doom_type': 2003, + 'region': "Bloodfalls (MAP25) Main"}, + 361355: {'name': 'Bloodfalls (MAP25) - Exit', + 'episode': 3, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Bloodfalls (MAP25) Blue"}, + 361356: {'name': 'The Abandoned Mines (MAP26) - Blue keycard', + 'episode': 3, + 'map': 6, + 'index': 20, + 'doom_type': 5, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361357: {'name': 'The Abandoned Mines (MAP26) - Super Shotgun', + 'episode': 3, + 'map': 6, + 'index': 21, + 'doom_type': 82, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361358: {'name': 'The Abandoned Mines (MAP26) - Rocket launcher', + 'episode': 3, + 'map': 6, + 'index': 49, + 'doom_type': 2003, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361359: {'name': 'The Abandoned Mines (MAP26) - Mega Armor', + 'episode': 3, + 'map': 6, + 'index': 95, + 'doom_type': 2019, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361360: {'name': 'The Abandoned Mines (MAP26) - Plasma gun', + 'episode': 3, + 'map': 6, + 'index': 107, + 'doom_type': 2004, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361361: {'name': 'The Abandoned Mines (MAP26) - Supercharge', + 'episode': 3, + 'map': 6, + 'index': 154, + 'doom_type': 2013, + 'region': "The Abandoned Mines (MAP26) Red"}, + 361362: {'name': 'The Abandoned Mines (MAP26) - Chaingun', + 'episode': 3, + 'map': 6, + 'index': 155, + 'doom_type': 2002, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361363: {'name': 'The Abandoned Mines (MAP26) - Partial invisibility', + 'episode': 3, + 'map': 6, + 'index': 159, + 'doom_type': 2024, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361364: {'name': 'The Abandoned Mines (MAP26) - Armor', + 'episode': 3, + 'map': 6, + 'index': 170, + 'doom_type': 2018, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361365: {'name': 'The Abandoned Mines (MAP26) - Red keycard', + 'episode': 3, + 'map': 6, + 'index': 182, + 'doom_type': 13, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361366: {'name': 'The Abandoned Mines (MAP26) - Yellow keycard', + 'episode': 3, + 'map': 6, + 'index': 229, + 'doom_type': 6, + 'region': "The Abandoned Mines (MAP26) Blue"}, + 361367: {'name': 'The Abandoned Mines (MAP26) - Backpack', + 'episode': 3, + 'map': 6, + 'index': 254, + 'doom_type': 8, + 'region': "The Abandoned Mines (MAP26) Main"}, + 361368: {'name': 'The Abandoned Mines (MAP26) - Exit', + 'episode': 3, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Abandoned Mines (MAP26) Yellow"}, + 361369: {'name': 'Monster Condo (MAP27) - Rocket launcher', + 'episode': 3, + 'map': 7, + 'index': 4, + 'doom_type': 2003, + 'region': "Monster Condo (MAP27) Main"}, + 361370: {'name': 'Monster Condo (MAP27) - Partial invisibility', + 'episode': 3, + 'map': 7, + 'index': 51, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Main"}, + 361371: {'name': 'Monster Condo (MAP27) - Plasma gun', + 'episode': 3, + 'map': 7, + 'index': 58, + 'doom_type': 2004, + 'region': "Monster Condo (MAP27) Main"}, + 361372: {'name': 'Monster Condo (MAP27) - Invulnerability', + 'episode': 3, + 'map': 7, + 'index': 60, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Main"}, + 361373: {'name': 'Monster Condo (MAP27) - Armor', + 'episode': 3, + 'map': 7, + 'index': 86, + 'doom_type': 2018, + 'region': "Monster Condo (MAP27) Main"}, + 361374: {'name': 'Monster Condo (MAP27) - Backpack', + 'episode': 3, + 'map': 7, + 'index': 105, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361375: {'name': 'Monster Condo (MAP27) - Invulnerability 2', + 'episode': 3, + 'map': 7, + 'index': 107, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Main"}, + 361376: {'name': 'Monster Condo (MAP27) - Partial invisibility 2', + 'episode': 3, + 'map': 7, + 'index': 122, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Main"}, + 361377: {'name': 'Monster Condo (MAP27) - Supercharge', + 'episode': 3, + 'map': 7, + 'index': 236, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Main"}, + 361378: {'name': 'Monster Condo (MAP27) - Armor 2', + 'episode': 3, + 'map': 7, + 'index': 239, + 'doom_type': 2018, + 'region': "Monster Condo (MAP27) Main"}, + 361379: {'name': 'Monster Condo (MAP27) - Chaingun', + 'episode': 3, + 'map': 7, + 'index': 251, + 'doom_type': 2002, + 'region': "Monster Condo (MAP27) Main"}, + 361380: {'name': 'Monster Condo (MAP27) - BFG9000', + 'episode': 3, + 'map': 7, + 'index': 279, + 'doom_type': 2006, + 'region': "Monster Condo (MAP27) Main"}, + 361381: {'name': 'Monster Condo (MAP27) - Backpack 2', + 'episode': 3, + 'map': 7, + 'index': 285, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361382: {'name': 'Monster Condo (MAP27) - Backpack 3', + 'episode': 3, + 'map': 7, + 'index': 286, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361383: {'name': 'Monster Condo (MAP27) - Backpack 4', + 'episode': 3, + 'map': 7, + 'index': 287, + 'doom_type': 8, + 'region': "Monster Condo (MAP27) Main"}, + 361384: {'name': 'Monster Condo (MAP27) - Yellow skull key', + 'episode': 3, + 'map': 7, + 'index': 310, + 'doom_type': 39, + 'region': "Monster Condo (MAP27) Main"}, + 361385: {'name': 'Monster Condo (MAP27) - Red skull key', + 'episode': 3, + 'map': 7, + 'index': 364, + 'doom_type': 38, + 'region': "Monster Condo (MAP27) Blue"}, + 361386: {'name': 'Monster Condo (MAP27) - Supercharge 2', + 'episode': 3, + 'map': 7, + 'index': 365, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Blue"}, + 361387: {'name': 'Monster Condo (MAP27) - Blue skull key', + 'episode': 3, + 'map': 7, + 'index': 382, + 'doom_type': 40, + 'region': "Monster Condo (MAP27) Yellow"}, + 361388: {'name': 'Monster Condo (MAP27) - Supercharge 3', + 'episode': 3, + 'map': 7, + 'index': 392, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361389: {'name': 'Monster Condo (MAP27) - Computer area map', + 'episode': 3, + 'map': 7, + 'index': 393, + 'doom_type': 2026, + 'region': "Monster Condo (MAP27) Yellow"}, + 361390: {'name': 'Monster Condo (MAP27) - Berserk', + 'episode': 3, + 'map': 7, + 'index': 394, + 'doom_type': 2023, + 'region': "Monster Condo (MAP27) Yellow"}, + 361391: {'name': 'Monster Condo (MAP27) - Supercharge 4', + 'episode': 3, + 'map': 7, + 'index': 414, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361392: {'name': 'Monster Condo (MAP27) - Supercharge 5', + 'episode': 3, + 'map': 7, + 'index': 424, + 'doom_type': 2013, + 'region': "Monster Condo (MAP27) Yellow"}, + 361393: {'name': 'Monster Condo (MAP27) - Computer area map 2', + 'episode': 3, + 'map': 7, + 'index': 425, + 'doom_type': 2026, + 'region': "Monster Condo (MAP27) Yellow"}, + 361394: {'name': 'Monster Condo (MAP27) - Berserk 2', + 'episode': 3, + 'map': 7, + 'index': 426, + 'doom_type': 2023, + 'region': "Monster Condo (MAP27) Yellow"}, + 361395: {'name': 'Monster Condo (MAP27) - Partial invisibility 3', + 'episode': 3, + 'map': 7, + 'index': 454, + 'doom_type': 2024, + 'region': "Monster Condo (MAP27) Yellow"}, + 361396: {'name': 'Monster Condo (MAP27) - Invulnerability 3', + 'episode': 3, + 'map': 7, + 'index': 455, + 'doom_type': 2022, + 'region': "Monster Condo (MAP27) Yellow"}, + 361397: {'name': 'Monster Condo (MAP27) - Chainsaw', + 'episode': 3, + 'map': 7, + 'index': 460, + 'doom_type': 2005, + 'region': "Monster Condo (MAP27) Main"}, + 361398: {'name': 'Monster Condo (MAP27) - Super Shotgun', + 'episode': 3, + 'map': 7, + 'index': 470, + 'doom_type': 82, + 'region': "Monster Condo (MAP27) Main"}, + 361399: {'name': 'Monster Condo (MAP27) - Exit', + 'episode': 3, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Monster Condo (MAP27) Red"}, + 361400: {'name': 'The Spirit World (MAP28) - Armor', + 'episode': 3, + 'map': 8, + 'index': 19, + 'doom_type': 2018, + 'region': "The Spirit World (MAP28) Main"}, + 361401: {'name': 'The Spirit World (MAP28) - Chainsaw', + 'episode': 3, + 'map': 8, + 'index': 66, + 'doom_type': 2005, + 'region': "The Spirit World (MAP28) Main"}, + 361402: {'name': 'The Spirit World (MAP28) - Invulnerability', + 'episode': 3, + 'map': 8, + 'index': 76, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361403: {'name': 'The Spirit World (MAP28) - Yellow skull key', + 'episode': 3, + 'map': 8, + 'index': 87, + 'doom_type': 39, + 'region': "The Spirit World (MAP28) Main"}, + 361404: {'name': 'The Spirit World (MAP28) - Supercharge', + 'episode': 3, + 'map': 8, + 'index': 95, + 'doom_type': 2013, + 'region': "The Spirit World (MAP28) Main"}, + 361405: {'name': 'The Spirit World (MAP28) - Chaingun', + 'episode': 3, + 'map': 8, + 'index': 96, + 'doom_type': 2002, + 'region': "The Spirit World (MAP28) Main"}, + 361406: {'name': 'The Spirit World (MAP28) - Rocket launcher', + 'episode': 3, + 'map': 8, + 'index': 124, + 'doom_type': 2003, + 'region': "The Spirit World (MAP28) Main"}, + 361407: {'name': 'The Spirit World (MAP28) - Backpack', + 'episode': 3, + 'map': 8, + 'index': 155, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361408: {'name': 'The Spirit World (MAP28) - Backpack 2', + 'episode': 3, + 'map': 8, + 'index': 156, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361409: {'name': 'The Spirit World (MAP28) - Backpack 3', + 'episode': 3, + 'map': 8, + 'index': 157, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361410: {'name': 'The Spirit World (MAP28) - Backpack 4', + 'episode': 3, + 'map': 8, + 'index': 158, + 'doom_type': 8, + 'region': "The Spirit World (MAP28) Main"}, + 361411: {'name': 'The Spirit World (MAP28) - Berserk', + 'episode': 3, + 'map': 8, + 'index': 159, + 'doom_type': 2023, + 'region': "The Spirit World (MAP28) Main"}, + 361412: {'name': 'The Spirit World (MAP28) - Plasma gun', + 'episode': 3, + 'map': 8, + 'index': 163, + 'doom_type': 2004, + 'region': "The Spirit World (MAP28) Main"}, + 361413: {'name': 'The Spirit World (MAP28) - Invulnerability 2', + 'episode': 3, + 'map': 8, + 'index': 179, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361414: {'name': 'The Spirit World (MAP28) - Invulnerability 3', + 'episode': 3, + 'map': 8, + 'index': 180, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361415: {'name': 'The Spirit World (MAP28) - BFG9000', + 'episode': 3, + 'map': 8, + 'index': 181, + 'doom_type': 2006, + 'region': "The Spirit World (MAP28) Main"}, + 361416: {'name': 'The Spirit World (MAP28) - Megasphere', + 'episode': 3, + 'map': 8, + 'index': 183, + 'doom_type': 83, + 'region': "The Spirit World (MAP28) Main"}, + 361417: {'name': 'The Spirit World (MAP28) - Megasphere 2', + 'episode': 3, + 'map': 8, + 'index': 185, + 'doom_type': 83, + 'region': "The Spirit World (MAP28) Main"}, + 361418: {'name': 'The Spirit World (MAP28) - Invulnerability 4', + 'episode': 3, + 'map': 8, + 'index': 186, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361419: {'name': 'The Spirit World (MAP28) - Invulnerability 5', + 'episode': 3, + 'map': 8, + 'index': 195, + 'doom_type': 2022, + 'region': "The Spirit World (MAP28) Main"}, + 361420: {'name': 'The Spirit World (MAP28) - Super Shotgun', + 'episode': 3, + 'map': 8, + 'index': 214, + 'doom_type': 82, + 'region': "The Spirit World (MAP28) Main"}, + 361421: {'name': 'The Spirit World (MAP28) - Red skull key', + 'episode': 3, + 'map': 8, + 'index': 216, + 'doom_type': 38, + 'region': "The Spirit World (MAP28) Yellow"}, + 361422: {'name': 'The Spirit World (MAP28) - Exit', + 'episode': 3, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Spirit World (MAP28) Red"}, + 361423: {'name': 'The Living End (MAP29) - Chaingun', + 'episode': 3, + 'map': 9, + 'index': 85, + 'doom_type': 2002, + 'region': "The Living End (MAP29) Main"}, + 361424: {'name': 'The Living End (MAP29) - Plasma gun', + 'episode': 3, + 'map': 9, + 'index': 124, + 'doom_type': 2004, + 'region': "The Living End (MAP29) Main"}, + 361425: {'name': 'The Living End (MAP29) - Backpack', + 'episode': 3, + 'map': 9, + 'index': 179, + 'doom_type': 8, + 'region': "The Living End (MAP29) Main"}, + 361426: {'name': 'The Living End (MAP29) - Super Shotgun', + 'episode': 3, + 'map': 9, + 'index': 195, + 'doom_type': 82, + 'region': "The Living End (MAP29) Main"}, + 361427: {'name': 'The Living End (MAP29) - Mega Armor', + 'episode': 3, + 'map': 9, + 'index': 216, + 'doom_type': 2019, + 'region': "The Living End (MAP29) Main"}, + 361428: {'name': 'The Living End (MAP29) - Armor', + 'episode': 3, + 'map': 9, + 'index': 224, + 'doom_type': 2018, + 'region': "The Living End (MAP29) Main"}, + 361429: {'name': 'The Living End (MAP29) - Backpack 2', + 'episode': 3, + 'map': 9, + 'index': 235, + 'doom_type': 8, + 'region': "The Living End (MAP29) Main"}, + 361430: {'name': 'The Living End (MAP29) - Supercharge', + 'episode': 3, + 'map': 9, + 'index': 237, + 'doom_type': 2013, + 'region': "The Living End (MAP29) Main"}, + 361431: {'name': 'The Living End (MAP29) - Berserk', + 'episode': 3, + 'map': 9, + 'index': 241, + 'doom_type': 2023, + 'region': "The Living End (MAP29) Main"}, + 361432: {'name': 'The Living End (MAP29) - Berserk 2', + 'episode': 3, + 'map': 9, + 'index': 263, + 'doom_type': 2023, + 'region': "The Living End (MAP29) Main"}, + 361433: {'name': 'The Living End (MAP29) - Exit', + 'episode': 3, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Living End (MAP29) Main"}, + 361434: {'name': 'Icon of Sin (MAP30) - Supercharge', + 'episode': 3, + 'map': 10, + 'index': 25, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361435: {'name': 'Icon of Sin (MAP30) - Supercharge 2', + 'episode': 3, + 'map': 10, + 'index': 26, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361436: {'name': 'Icon of Sin (MAP30) - Supercharge 3', + 'episode': 3, + 'map': 10, + 'index': 28, + 'doom_type': 2013, + 'region': "Icon of Sin (MAP30) Main"}, + 361437: {'name': 'Icon of Sin (MAP30) - Invulnerability', + 'episode': 3, + 'map': 10, + 'index': 29, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361438: {'name': 'Icon of Sin (MAP30) - Invulnerability 2', + 'episode': 3, + 'map': 10, + 'index': 30, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361439: {'name': 'Icon of Sin (MAP30) - Invulnerability 3', + 'episode': 3, + 'map': 10, + 'index': 31, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361440: {'name': 'Icon of Sin (MAP30) - Invulnerability 4', + 'episode': 3, + 'map': 10, + 'index': 32, + 'doom_type': 2022, + 'region': "Icon of Sin (MAP30) Main"}, + 361441: {'name': 'Icon of Sin (MAP30) - BFG9000', + 'episode': 3, + 'map': 10, + 'index': 40, + 'doom_type': 2006, + 'region': "Icon of Sin (MAP30) Main"}, + 361442: {'name': 'Icon of Sin (MAP30) - Chaingun', + 'episode': 3, + 'map': 10, + 'index': 41, + 'doom_type': 2002, + 'region': "Icon of Sin (MAP30) Main"}, + 361443: {'name': 'Icon of Sin (MAP30) - Chainsaw', + 'episode': 3, + 'map': 10, + 'index': 42, + 'doom_type': 2005, + 'region': "Icon of Sin (MAP30) Main"}, + 361444: {'name': 'Icon of Sin (MAP30) - Plasma gun', + 'episode': 3, + 'map': 10, + 'index': 43, + 'doom_type': 2004, + 'region': "Icon of Sin (MAP30) Main"}, + 361445: {'name': 'Icon of Sin (MAP30) - Rocket launcher', + 'episode': 3, + 'map': 10, + 'index': 44, + 'doom_type': 2003, + 'region': "Icon of Sin (MAP30) Main"}, + 361446: {'name': 'Icon of Sin (MAP30) - Shotgun', + 'episode': 3, + 'map': 10, + 'index': 45, + 'doom_type': 2001, + 'region': "Icon of Sin (MAP30) Main"}, + 361447: {'name': 'Icon of Sin (MAP30) - Super Shotgun', + 'episode': 3, + 'map': 10, + 'index': 46, + 'doom_type': 82, + 'region': "Icon of Sin (MAP30) Main"}, + 361448: {'name': 'Icon of Sin (MAP30) - Backpack', + 'episode': 3, + 'map': 10, + 'index': 47, + 'doom_type': 8, + 'region': "Icon of Sin (MAP30) Main"}, + 361449: {'name': 'Icon of Sin (MAP30) - Megasphere', + 'episode': 3, + 'map': 10, + 'index': 64, + 'doom_type': 83, + 'region': "Icon of Sin (MAP30) Main"}, + 361450: {'name': 'Icon of Sin (MAP30) - Megasphere 2', + 'episode': 3, + 'map': 10, + 'index': 85, + 'doom_type': 83, + 'region': "Icon of Sin (MAP30) Main"}, + 361451: {'name': 'Icon of Sin (MAP30) - Berserk', + 'episode': 3, + 'map': 10, + 'index': 94, + 'doom_type': 2023, + 'region': "Icon of Sin (MAP30) Main"}, + 361452: {'name': 'Icon of Sin (MAP30) - Exit', + 'episode': 3, + 'map': 10, + 'index': -1, + 'doom_type': -1, + 'region': "Icon of Sin (MAP30) Main"}, + 361453: {'name': 'Wolfenstein2 (MAP31) - Rocket launcher', + 'episode': 4, + 'map': 1, + 'index': 110, + 'doom_type': 2003, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361454: {'name': 'Wolfenstein2 (MAP31) - Shotgun', + 'episode': 4, + 'map': 1, + 'index': 139, + 'doom_type': 2001, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361455: {'name': 'Wolfenstein2 (MAP31) - Berserk', + 'episode': 4, + 'map': 1, + 'index': 263, + 'doom_type': 2023, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361456: {'name': 'Wolfenstein2 (MAP31) - Supercharge', + 'episode': 4, + 'map': 1, + 'index': 278, + 'doom_type': 2013, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361457: {'name': 'Wolfenstein2 (MAP31) - Chaingun', + 'episode': 4, + 'map': 1, + 'index': 305, + 'doom_type': 2002, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361458: {'name': 'Wolfenstein2 (MAP31) - Super Shotgun', + 'episode': 4, + 'map': 1, + 'index': 308, + 'doom_type': 82, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361459: {'name': 'Wolfenstein2 (MAP31) - Partial invisibility', + 'episode': 4, + 'map': 1, + 'index': 309, + 'doom_type': 2024, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361460: {'name': 'Wolfenstein2 (MAP31) - Megasphere', + 'episode': 4, + 'map': 1, + 'index': 310, + 'doom_type': 83, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361461: {'name': 'Wolfenstein2 (MAP31) - Backpack', + 'episode': 4, + 'map': 1, + 'index': 311, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361462: {'name': 'Wolfenstein2 (MAP31) - Backpack 2', + 'episode': 4, + 'map': 1, + 'index': 312, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361463: {'name': 'Wolfenstein2 (MAP31) - Backpack 3', + 'episode': 4, + 'map': 1, + 'index': 313, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361464: {'name': 'Wolfenstein2 (MAP31) - Backpack 4', + 'episode': 4, + 'map': 1, + 'index': 314, + 'doom_type': 8, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361465: {'name': 'Wolfenstein2 (MAP31) - BFG9000', + 'episode': 4, + 'map': 1, + 'index': 315, + 'doom_type': 2006, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361466: {'name': 'Wolfenstein2 (MAP31) - Plasma gun', + 'episode': 4, + 'map': 1, + 'index': 316, + 'doom_type': 2004, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361467: {'name': 'Wolfenstein2 (MAP31) - Exit', + 'episode': 4, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Wolfenstein2 (MAP31) Main"}, + 361468: {'name': 'Grosse2 (MAP32) - Plasma gun', + 'episode': 4, + 'map': 2, + 'index': 33, + 'doom_type': 2004, + 'region': "Grosse2 (MAP32) Main"}, + 361469: {'name': 'Grosse2 (MAP32) - Rocket launcher', + 'episode': 4, + 'map': 2, + 'index': 57, + 'doom_type': 2003, + 'region': "Grosse2 (MAP32) Main"}, + 361470: {'name': 'Grosse2 (MAP32) - Invulnerability', + 'episode': 4, + 'map': 2, + 'index': 70, + 'doom_type': 2022, + 'region': "Grosse2 (MAP32) Main"}, + 361471: {'name': 'Grosse2 (MAP32) - Super Shotgun', + 'episode': 4, + 'map': 2, + 'index': 74, + 'doom_type': 82, + 'region': "Grosse2 (MAP32) Main"}, + 361472: {'name': 'Grosse2 (MAP32) - BFG9000', + 'episode': 4, + 'map': 2, + 'index': 75, + 'doom_type': 2006, + 'region': "Grosse2 (MAP32) Main"}, + 361473: {'name': 'Grosse2 (MAP32) - Megasphere', + 'episode': 4, + 'map': 2, + 'index': 78, + 'doom_type': 83, + 'region': "Grosse2 (MAP32) Main"}, + 361474: {'name': 'Grosse2 (MAP32) - Chaingun', + 'episode': 4, + 'map': 2, + 'index': 79, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361475: {'name': 'Grosse2 (MAP32) - Chaingun 2', + 'episode': 4, + 'map': 2, + 'index': 80, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361476: {'name': 'Grosse2 (MAP32) - Chaingun 3', + 'episode': 4, + 'map': 2, + 'index': 81, + 'doom_type': 2002, + 'region': "Grosse2 (MAP32) Main"}, + 361477: {'name': 'Grosse2 (MAP32) - Berserk', + 'episode': 4, + 'map': 2, + 'index': 82, + 'doom_type': 2023, + 'region': "Grosse2 (MAP32) Main"}, + 361478: {'name': 'Grosse2 (MAP32) - Exit', + 'episode': 4, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Grosse2 (MAP32) Main"}, +} + + +location_name_groups: Dict[str, Set[str]] = { + 'Barrels o Fun (MAP23)': { + 'Barrels o Fun (MAP23) - Armor', + 'Barrels o Fun (MAP23) - BFG9000', + 'Barrels o Fun (MAP23) - Backpack', + 'Barrels o Fun (MAP23) - Backpack 2', + 'Barrels o Fun (MAP23) - Berserk', + 'Barrels o Fun (MAP23) - Computer area map', + 'Barrels o Fun (MAP23) - Exit', + 'Barrels o Fun (MAP23) - Megasphere', + 'Barrels o Fun (MAP23) - Rocket launcher', + 'Barrels o Fun (MAP23) - Shotgun', + 'Barrels o Fun (MAP23) - Supercharge', + 'Barrels o Fun (MAP23) - Yellow skull key', + }, + 'Bloodfalls (MAP25)': { + 'Bloodfalls (MAP25) - Armor', + 'Bloodfalls (MAP25) - BFG9000', + 'Bloodfalls (MAP25) - BFG9000 2', + 'Bloodfalls (MAP25) - Blue skull key', + 'Bloodfalls (MAP25) - Chaingun', + 'Bloodfalls (MAP25) - Exit', + 'Bloodfalls (MAP25) - Mega Armor', + 'Bloodfalls (MAP25) - Megasphere', + 'Bloodfalls (MAP25) - Partial invisibility', + 'Bloodfalls (MAP25) - Plasma gun', + 'Bloodfalls (MAP25) - Rocket launcher', + 'Bloodfalls (MAP25) - Super Shotgun', + }, + 'Circle of Death (MAP11)': { + 'Circle of Death (MAP11) - Armor', + 'Circle of Death (MAP11) - BFG9000', + 'Circle of Death (MAP11) - Backpack', + 'Circle of Death (MAP11) - Blue keycard', + 'Circle of Death (MAP11) - Chaingun', + 'Circle of Death (MAP11) - Exit', + 'Circle of Death (MAP11) - Invulnerability', + 'Circle of Death (MAP11) - Mega Armor', + 'Circle of Death (MAP11) - Partial invisibility', + 'Circle of Death (MAP11) - Plasma gun', + 'Circle of Death (MAP11) - Red keycard', + 'Circle of Death (MAP11) - Rocket launcher', + 'Circle of Death (MAP11) - Shotgun', + 'Circle of Death (MAP11) - Supercharge', + 'Circle of Death (MAP11) - Supercharge 2', + }, + 'Dead Simple (MAP07)': { + 'Dead Simple (MAP07) - Backpack', + 'Dead Simple (MAP07) - Berserk', + 'Dead Simple (MAP07) - Chaingun', + 'Dead Simple (MAP07) - Exit', + 'Dead Simple (MAP07) - Megasphere', + 'Dead Simple (MAP07) - Partial invisibility', + 'Dead Simple (MAP07) - Partial invisibility 2', + 'Dead Simple (MAP07) - Partial invisibility 3', + 'Dead Simple (MAP07) - Partial invisibility 4', + 'Dead Simple (MAP07) - Plasma gun', + 'Dead Simple (MAP07) - Rocket launcher', + 'Dead Simple (MAP07) - Super Shotgun', + }, + 'Downtown (MAP13)': { + 'Downtown (MAP13) - BFG9000', + 'Downtown (MAP13) - Backpack', + 'Downtown (MAP13) - Berserk', + 'Downtown (MAP13) - Berserk 2', + 'Downtown (MAP13) - Berserk 3', + 'Downtown (MAP13) - Blue keycard', + 'Downtown (MAP13) - Chaingun', + 'Downtown (MAP13) - Chainsaw', + 'Downtown (MAP13) - Computer area map', + 'Downtown (MAP13) - Exit', + 'Downtown (MAP13) - Invulnerability', + 'Downtown (MAP13) - Invulnerability 2', + 'Downtown (MAP13) - Mega Armor', + 'Downtown (MAP13) - Partial invisibility', + 'Downtown (MAP13) - Partial invisibility 2', + 'Downtown (MAP13) - Partial invisibility 3', + 'Downtown (MAP13) - Plasma gun', + 'Downtown (MAP13) - Red keycard', + 'Downtown (MAP13) - Rocket launcher', + 'Downtown (MAP13) - Shotgun', + 'Downtown (MAP13) - Supercharge', + 'Downtown (MAP13) - Yellow keycard', + }, + 'Entryway (MAP01)': { + 'Entryway (MAP01) - Armor', + 'Entryway (MAP01) - Chainsaw', + 'Entryway (MAP01) - Exit', + 'Entryway (MAP01) - Rocket launcher', + 'Entryway (MAP01) - Shotgun', + }, + 'Gotcha! (MAP20)': { + 'Gotcha! (MAP20) - Armor', + 'Gotcha! (MAP20) - Armor 2', + 'Gotcha! (MAP20) - BFG9000', + 'Gotcha! (MAP20) - Berserk', + 'Gotcha! (MAP20) - Exit', + 'Gotcha! (MAP20) - Mega Armor', + 'Gotcha! (MAP20) - Mega Armor 2', + 'Gotcha! (MAP20) - Megasphere', + 'Gotcha! (MAP20) - Plasma gun', + 'Gotcha! (MAP20) - Rocket launcher', + 'Gotcha! (MAP20) - Super Shotgun', + 'Gotcha! (MAP20) - Supercharge', + 'Gotcha! (MAP20) - Supercharge 2', + 'Gotcha! (MAP20) - Supercharge 3', + 'Gotcha! (MAP20) - Supercharge 4', + }, + 'Grosse2 (MAP32)': { + 'Grosse2 (MAP32) - BFG9000', + 'Grosse2 (MAP32) - Berserk', + 'Grosse2 (MAP32) - Chaingun', + 'Grosse2 (MAP32) - Chaingun 2', + 'Grosse2 (MAP32) - Chaingun 3', + 'Grosse2 (MAP32) - Exit', + 'Grosse2 (MAP32) - Invulnerability', + 'Grosse2 (MAP32) - Megasphere', + 'Grosse2 (MAP32) - Plasma gun', + 'Grosse2 (MAP32) - Rocket launcher', + 'Grosse2 (MAP32) - Super Shotgun', + }, + 'Icon of Sin (MAP30)': { + 'Icon of Sin (MAP30) - BFG9000', + 'Icon of Sin (MAP30) - Backpack', + 'Icon of Sin (MAP30) - Berserk', + 'Icon of Sin (MAP30) - Chaingun', + 'Icon of Sin (MAP30) - Chainsaw', + 'Icon of Sin (MAP30) - Exit', + 'Icon of Sin (MAP30) - Invulnerability', + 'Icon of Sin (MAP30) - Invulnerability 2', + 'Icon of Sin (MAP30) - Invulnerability 3', + 'Icon of Sin (MAP30) - Invulnerability 4', + 'Icon of Sin (MAP30) - Megasphere', + 'Icon of Sin (MAP30) - Megasphere 2', + 'Icon of Sin (MAP30) - Plasma gun', + 'Icon of Sin (MAP30) - Rocket launcher', + 'Icon of Sin (MAP30) - Shotgun', + 'Icon of Sin (MAP30) - Super Shotgun', + 'Icon of Sin (MAP30) - Supercharge', + 'Icon of Sin (MAP30) - Supercharge 2', + 'Icon of Sin (MAP30) - Supercharge 3', + }, + 'Industrial Zone (MAP15)': { + 'Industrial Zone (MAP15) - Armor', + 'Industrial Zone (MAP15) - BFG9000', + 'Industrial Zone (MAP15) - Backpack', + 'Industrial Zone (MAP15) - Backpack 2', + 'Industrial Zone (MAP15) - Berserk', + 'Industrial Zone (MAP15) - Berserk 2', + 'Industrial Zone (MAP15) - Blue keycard', + 'Industrial Zone (MAP15) - Chaingun', + 'Industrial Zone (MAP15) - Chainsaw', + 'Industrial Zone (MAP15) - Computer area map', + 'Industrial Zone (MAP15) - Exit', + 'Industrial Zone (MAP15) - Invulnerability', + 'Industrial Zone (MAP15) - Mega Armor', + 'Industrial Zone (MAP15) - Mega Armor 2', + 'Industrial Zone (MAP15) - Megasphere', + 'Industrial Zone (MAP15) - Partial invisibility', + 'Industrial Zone (MAP15) - Partial invisibility 2', + 'Industrial Zone (MAP15) - Plasma gun', + 'Industrial Zone (MAP15) - Red keycard', + 'Industrial Zone (MAP15) - Rocket launcher', + 'Industrial Zone (MAP15) - Shotgun', + 'Industrial Zone (MAP15) - Supercharge', + 'Industrial Zone (MAP15) - Yellow keycard', + }, + 'Monster Condo (MAP27)': { + 'Monster Condo (MAP27) - Armor', + 'Monster Condo (MAP27) - Armor 2', + 'Monster Condo (MAP27) - BFG9000', + 'Monster Condo (MAP27) - Backpack', + 'Monster Condo (MAP27) - Backpack 2', + 'Monster Condo (MAP27) - Backpack 3', + 'Monster Condo (MAP27) - Backpack 4', + 'Monster Condo (MAP27) - Berserk', + 'Monster Condo (MAP27) - Berserk 2', + 'Monster Condo (MAP27) - Blue skull key', + 'Monster Condo (MAP27) - Chaingun', + 'Monster Condo (MAP27) - Chainsaw', + 'Monster Condo (MAP27) - Computer area map', + 'Monster Condo (MAP27) - Computer area map 2', + 'Monster Condo (MAP27) - Exit', + 'Monster Condo (MAP27) - Invulnerability', + 'Monster Condo (MAP27) - Invulnerability 2', + 'Monster Condo (MAP27) - Invulnerability 3', + 'Monster Condo (MAP27) - Partial invisibility', + 'Monster Condo (MAP27) - Partial invisibility 2', + 'Monster Condo (MAP27) - Partial invisibility 3', + 'Monster Condo (MAP27) - Plasma gun', + 'Monster Condo (MAP27) - Red skull key', + 'Monster Condo (MAP27) - Rocket launcher', + 'Monster Condo (MAP27) - Super Shotgun', + 'Monster Condo (MAP27) - Supercharge', + 'Monster Condo (MAP27) - Supercharge 2', + 'Monster Condo (MAP27) - Supercharge 3', + 'Monster Condo (MAP27) - Supercharge 4', + 'Monster Condo (MAP27) - Supercharge 5', + 'Monster Condo (MAP27) - Yellow skull key', + }, + 'Nirvana (MAP21)': { + 'Nirvana (MAP21) - Backpack', + 'Nirvana (MAP21) - Blue skull key', + 'Nirvana (MAP21) - Exit', + 'Nirvana (MAP21) - Invulnerability', + 'Nirvana (MAP21) - Megasphere', + 'Nirvana (MAP21) - Red skull key', + 'Nirvana (MAP21) - Rocket launcher', + 'Nirvana (MAP21) - Super Shotgun', + 'Nirvana (MAP21) - Yellow skull key', + }, + 'Refueling Base (MAP10)': { + 'Refueling Base (MAP10) - Armor', + 'Refueling Base (MAP10) - Armor 2', + 'Refueling Base (MAP10) - BFG9000', + 'Refueling Base (MAP10) - Backpack', + 'Refueling Base (MAP10) - Berserk', + 'Refueling Base (MAP10) - Berserk 2', + 'Refueling Base (MAP10) - Blue keycard', + 'Refueling Base (MAP10) - Chaingun', + 'Refueling Base (MAP10) - Chainsaw', + 'Refueling Base (MAP10) - Exit', + 'Refueling Base (MAP10) - Invulnerability', + 'Refueling Base (MAP10) - Invulnerability 2', + 'Refueling Base (MAP10) - Mega Armor', + 'Refueling Base (MAP10) - Megasphere', + 'Refueling Base (MAP10) - Partial invisibility', + 'Refueling Base (MAP10) - Plasma gun', + 'Refueling Base (MAP10) - Rocket launcher', + 'Refueling Base (MAP10) - Shotgun', + 'Refueling Base (MAP10) - Supercharge', + 'Refueling Base (MAP10) - Supercharge 2', + 'Refueling Base (MAP10) - Yellow keycard', + }, + 'Suburbs (MAP16)': { + 'Suburbs (MAP16) - BFG9000', + 'Suburbs (MAP16) - Backpack', + 'Suburbs (MAP16) - Berserk', + 'Suburbs (MAP16) - Blue skull key', + 'Suburbs (MAP16) - Chaingun', + 'Suburbs (MAP16) - Exit', + 'Suburbs (MAP16) - Invulnerability', + 'Suburbs (MAP16) - Megasphere', + 'Suburbs (MAP16) - Partial invisibility', + 'Suburbs (MAP16) - Plasma gun', + 'Suburbs (MAP16) - Plasma gun 2', + 'Suburbs (MAP16) - Plasma gun 3', + 'Suburbs (MAP16) - Plasma gun 4', + 'Suburbs (MAP16) - Red skull key', + 'Suburbs (MAP16) - Rocket launcher', + 'Suburbs (MAP16) - Shotgun', + 'Suburbs (MAP16) - Super Shotgun', + 'Suburbs (MAP16) - Supercharge', + }, + 'Tenements (MAP17)': { + 'Tenements (MAP17) - Armor', + 'Tenements (MAP17) - Armor 2', + 'Tenements (MAP17) - BFG9000', + 'Tenements (MAP17) - Backpack', + 'Tenements (MAP17) - Berserk', + 'Tenements (MAP17) - Blue keycard', + 'Tenements (MAP17) - Chaingun', + 'Tenements (MAP17) - Exit', + 'Tenements (MAP17) - Mega Armor', + 'Tenements (MAP17) - Megasphere', + 'Tenements (MAP17) - Partial invisibility', + 'Tenements (MAP17) - Plasma gun', + 'Tenements (MAP17) - Red keycard', + 'Tenements (MAP17) - Rocket launcher', + 'Tenements (MAP17) - Shotgun', + 'Tenements (MAP17) - Supercharge', + 'Tenements (MAP17) - Supercharge 2', + 'Tenements (MAP17) - Yellow skull key', + }, + 'The Abandoned Mines (MAP26)': { + 'The Abandoned Mines (MAP26) - Armor', + 'The Abandoned Mines (MAP26) - Backpack', + 'The Abandoned Mines (MAP26) - Blue keycard', + 'The Abandoned Mines (MAP26) - Chaingun', + 'The Abandoned Mines (MAP26) - Exit', + 'The Abandoned Mines (MAP26) - Mega Armor', + 'The Abandoned Mines (MAP26) - Partial invisibility', + 'The Abandoned Mines (MAP26) - Plasma gun', + 'The Abandoned Mines (MAP26) - Red keycard', + 'The Abandoned Mines (MAP26) - Rocket launcher', + 'The Abandoned Mines (MAP26) - Super Shotgun', + 'The Abandoned Mines (MAP26) - Supercharge', + 'The Abandoned Mines (MAP26) - Yellow keycard', + }, + 'The Catacombs (MAP22)': { + 'The Catacombs (MAP22) - Armor', + 'The Catacombs (MAP22) - Berserk', + 'The Catacombs (MAP22) - Blue skull key', + 'The Catacombs (MAP22) - Exit', + 'The Catacombs (MAP22) - Plasma gun', + 'The Catacombs (MAP22) - Red skull key', + 'The Catacombs (MAP22) - Rocket launcher', + 'The Catacombs (MAP22) - Shotgun', + 'The Catacombs (MAP22) - Supercharge', + }, + 'The Chasm (MAP24)': { + 'The Chasm (MAP24) - Armor', + 'The Chasm (MAP24) - BFG9000', + 'The Chasm (MAP24) - Backpack', + 'The Chasm (MAP24) - Berserk', + 'The Chasm (MAP24) - Berserk 2', + 'The Chasm (MAP24) - Blue keycard', + 'The Chasm (MAP24) - Exit', + 'The Chasm (MAP24) - Invulnerability', + 'The Chasm (MAP24) - Megasphere', + 'The Chasm (MAP24) - Megasphere 2', + 'The Chasm (MAP24) - Plasma gun', + 'The Chasm (MAP24) - Red keycard', + 'The Chasm (MAP24) - Rocket launcher', + 'The Chasm (MAP24) - Shotgun', + 'The Chasm (MAP24) - Super Shotgun', + }, + 'The Citadel (MAP19)': { + 'The Citadel (MAP19) - Armor', + 'The Citadel (MAP19) - Armor 2', + 'The Citadel (MAP19) - Backpack', + 'The Citadel (MAP19) - Berserk', + 'The Citadel (MAP19) - Blue skull key', + 'The Citadel (MAP19) - Chaingun', + 'The Citadel (MAP19) - Computer area map', + 'The Citadel (MAP19) - Exit', + 'The Citadel (MAP19) - Invulnerability', + 'The Citadel (MAP19) - Mega Armor', + 'The Citadel (MAP19) - Partial invisibility', + 'The Citadel (MAP19) - Red skull key', + 'The Citadel (MAP19) - Rocket launcher', + 'The Citadel (MAP19) - Super Shotgun', + 'The Citadel (MAP19) - Supercharge', + 'The Citadel (MAP19) - Yellow skull key', + }, + 'The Courtyard (MAP18)': { + 'The Courtyard (MAP18) - Armor', + 'The Courtyard (MAP18) - BFG9000', + 'The Courtyard (MAP18) - Backpack', + 'The Courtyard (MAP18) - Berserk', + 'The Courtyard (MAP18) - Blue skull key', + 'The Courtyard (MAP18) - Chaingun', + 'The Courtyard (MAP18) - Computer area map', + 'The Courtyard (MAP18) - Exit', + 'The Courtyard (MAP18) - Invulnerability', + 'The Courtyard (MAP18) - Invulnerability 2', + 'The Courtyard (MAP18) - Partial invisibility', + 'The Courtyard (MAP18) - Partial invisibility 2', + 'The Courtyard (MAP18) - Plasma gun', + 'The Courtyard (MAP18) - Rocket launcher', + 'The Courtyard (MAP18) - Shotgun', + 'The Courtyard (MAP18) - Super Shotgun', + 'The Courtyard (MAP18) - Supercharge', + 'The Courtyard (MAP18) - Yellow skull key', + }, + 'The Crusher (MAP06)': { + 'The Crusher (MAP06) - Armor', + 'The Crusher (MAP06) - Backpack', + 'The Crusher (MAP06) - Blue keycard', + 'The Crusher (MAP06) - Blue keycard 2', + 'The Crusher (MAP06) - Blue keycard 3', + 'The Crusher (MAP06) - Exit', + 'The Crusher (MAP06) - Mega Armor', + 'The Crusher (MAP06) - Megasphere', + 'The Crusher (MAP06) - Megasphere 2', + 'The Crusher (MAP06) - Plasma gun', + 'The Crusher (MAP06) - Red keycard', + 'The Crusher (MAP06) - Rocket launcher', + 'The Crusher (MAP06) - Super Shotgun', + 'The Crusher (MAP06) - Supercharge', + 'The Crusher (MAP06) - Yellow keycard', + }, + 'The Factory (MAP12)': { + 'The Factory (MAP12) - Armor', + 'The Factory (MAP12) - Armor 2', + 'The Factory (MAP12) - BFG9000', + 'The Factory (MAP12) - Backpack', + 'The Factory (MAP12) - Berserk', + 'The Factory (MAP12) - Berserk 2', + 'The Factory (MAP12) - Berserk 3', + 'The Factory (MAP12) - Blue keycard', + 'The Factory (MAP12) - Chaingun', + 'The Factory (MAP12) - Exit', + 'The Factory (MAP12) - Partial invisibility', + 'The Factory (MAP12) - Shotgun', + 'The Factory (MAP12) - Super Shotgun', + 'The Factory (MAP12) - Supercharge', + 'The Factory (MAP12) - Supercharge 2', + 'The Factory (MAP12) - Yellow keycard', + }, + 'The Focus (MAP04)': { + 'The Focus (MAP04) - Blue keycard', + 'The Focus (MAP04) - Exit', + 'The Focus (MAP04) - Red keycard', + 'The Focus (MAP04) - Super Shotgun', + 'The Focus (MAP04) - Yellow keycard', + }, + 'The Gantlet (MAP03)': { + 'The Gantlet (MAP03) - Backpack', + 'The Gantlet (MAP03) - Blue keycard', + 'The Gantlet (MAP03) - Chaingun', + 'The Gantlet (MAP03) - Exit', + 'The Gantlet (MAP03) - Mega Armor', + 'The Gantlet (MAP03) - Mega Armor 2', + 'The Gantlet (MAP03) - Partial invisibility', + 'The Gantlet (MAP03) - Red keycard', + 'The Gantlet (MAP03) - Rocket launcher', + 'The Gantlet (MAP03) - Shotgun', + 'The Gantlet (MAP03) - Supercharge', + }, + 'The Inmost Dens (MAP14)': { + 'The Inmost Dens (MAP14) - Berserk', + 'The Inmost Dens (MAP14) - Blue skull key', + 'The Inmost Dens (MAP14) - Chaingun', + 'The Inmost Dens (MAP14) - Exit', + 'The Inmost Dens (MAP14) - Mega Armor', + 'The Inmost Dens (MAP14) - Partial invisibility', + 'The Inmost Dens (MAP14) - Plasma gun', + 'The Inmost Dens (MAP14) - Red skull key', + 'The Inmost Dens (MAP14) - Rocket launcher', + 'The Inmost Dens (MAP14) - Shotgun', + 'The Inmost Dens (MAP14) - Supercharge', + }, + 'The Living End (MAP29)': { + 'The Living End (MAP29) - Armor', + 'The Living End (MAP29) - Backpack', + 'The Living End (MAP29) - Backpack 2', + 'The Living End (MAP29) - Berserk', + 'The Living End (MAP29) - Berserk 2', + 'The Living End (MAP29) - Chaingun', + 'The Living End (MAP29) - Exit', + 'The Living End (MAP29) - Mega Armor', + 'The Living End (MAP29) - Plasma gun', + 'The Living End (MAP29) - Super Shotgun', + 'The Living End (MAP29) - Supercharge', + }, + 'The Pit (MAP09)': { + 'The Pit (MAP09) - Armor', + 'The Pit (MAP09) - BFG9000', + 'The Pit (MAP09) - Backpack', + 'The Pit (MAP09) - Berserk', + 'The Pit (MAP09) - Berserk 2', + 'The Pit (MAP09) - Berserk 3', + 'The Pit (MAP09) - Blue keycard', + 'The Pit (MAP09) - Computer area map', + 'The Pit (MAP09) - Exit', + 'The Pit (MAP09) - Mega Armor', + 'The Pit (MAP09) - Mega Armor 2', + 'The Pit (MAP09) - Rocket launcher', + 'The Pit (MAP09) - Shotgun', + 'The Pit (MAP09) - Supercharge', + 'The Pit (MAP09) - Supercharge 2', + 'The Pit (MAP09) - Yellow keycard', + }, + 'The Spirit World (MAP28)': { + 'The Spirit World (MAP28) - Armor', + 'The Spirit World (MAP28) - BFG9000', + 'The Spirit World (MAP28) - Backpack', + 'The Spirit World (MAP28) - Backpack 2', + 'The Spirit World (MAP28) - Backpack 3', + 'The Spirit World (MAP28) - Backpack 4', + 'The Spirit World (MAP28) - Berserk', + 'The Spirit World (MAP28) - Chaingun', + 'The Spirit World (MAP28) - Chainsaw', + 'The Spirit World (MAP28) - Exit', + 'The Spirit World (MAP28) - Invulnerability', + 'The Spirit World (MAP28) - Invulnerability 2', + 'The Spirit World (MAP28) - Invulnerability 3', + 'The Spirit World (MAP28) - Invulnerability 4', + 'The Spirit World (MAP28) - Invulnerability 5', + 'The Spirit World (MAP28) - Megasphere', + 'The Spirit World (MAP28) - Megasphere 2', + 'The Spirit World (MAP28) - Plasma gun', + 'The Spirit World (MAP28) - Red skull key', + 'The Spirit World (MAP28) - Rocket launcher', + 'The Spirit World (MAP28) - Super Shotgun', + 'The Spirit World (MAP28) - Supercharge', + 'The Spirit World (MAP28) - Yellow skull key', + }, + 'The Waste Tunnels (MAP05)': { + 'The Waste Tunnels (MAP05) - Armor', + 'The Waste Tunnels (MAP05) - Berserk', + 'The Waste Tunnels (MAP05) - Blue keycard', + 'The Waste Tunnels (MAP05) - Exit', + 'The Waste Tunnels (MAP05) - Mega Armor', + 'The Waste Tunnels (MAP05) - Plasma gun', + 'The Waste Tunnels (MAP05) - Red keycard', + 'The Waste Tunnels (MAP05) - Rocket launcher', + 'The Waste Tunnels (MAP05) - Shotgun', + 'The Waste Tunnels (MAP05) - Super Shotgun', + 'The Waste Tunnels (MAP05) - Supercharge', + 'The Waste Tunnels (MAP05) - Supercharge 2', + 'The Waste Tunnels (MAP05) - Yellow keycard', + }, + 'Tricks and Traps (MAP08)': { + 'Tricks and Traps (MAP08) - Armor', + 'Tricks and Traps (MAP08) - Armor 2', + 'Tricks and Traps (MAP08) - BFG9000', + 'Tricks and Traps (MAP08) - Backpack', + 'Tricks and Traps (MAP08) - Backpack 2', + 'Tricks and Traps (MAP08) - Backpack 3', + 'Tricks and Traps (MAP08) - Backpack 4', + 'Tricks and Traps (MAP08) - Backpack 5', + 'Tricks and Traps (MAP08) - Chaingun', + 'Tricks and Traps (MAP08) - Chainsaw', + 'Tricks and Traps (MAP08) - Exit', + 'Tricks and Traps (MAP08) - Invulnerability', + 'Tricks and Traps (MAP08) - Invulnerability 2', + 'Tricks and Traps (MAP08) - Invulnerability 3', + 'Tricks and Traps (MAP08) - Invulnerability 4', + 'Tricks and Traps (MAP08) - Invulnerability 5', + 'Tricks and Traps (MAP08) - Partial invisibility', + 'Tricks and Traps (MAP08) - Plasma gun', + 'Tricks and Traps (MAP08) - Red skull key', + 'Tricks and Traps (MAP08) - Rocket launcher', + 'Tricks and Traps (MAP08) - Shotgun', + 'Tricks and Traps (MAP08) - Supercharge', + 'Tricks and Traps (MAP08) - Supercharge 2', + 'Tricks and Traps (MAP08) - Yellow skull key', + }, + 'Underhalls (MAP02)': { + 'Underhalls (MAP02) - Blue keycard', + 'Underhalls (MAP02) - Exit', + 'Underhalls (MAP02) - Mega Armor', + 'Underhalls (MAP02) - Red keycard', + 'Underhalls (MAP02) - Super Shotgun', + }, + 'Wolfenstein2 (MAP31)': { + 'Wolfenstein2 (MAP31) - BFG9000', + 'Wolfenstein2 (MAP31) - Backpack', + 'Wolfenstein2 (MAP31) - Backpack 2', + 'Wolfenstein2 (MAP31) - Backpack 3', + 'Wolfenstein2 (MAP31) - Backpack 4', + 'Wolfenstein2 (MAP31) - Berserk', + 'Wolfenstein2 (MAP31) - Chaingun', + 'Wolfenstein2 (MAP31) - Exit', + 'Wolfenstein2 (MAP31) - Megasphere', + 'Wolfenstein2 (MAP31) - Partial invisibility', + 'Wolfenstein2 (MAP31) - Plasma gun', + 'Wolfenstein2 (MAP31) - Rocket launcher', + 'Wolfenstein2 (MAP31) - Shotgun', + 'Wolfenstein2 (MAP31) - Super Shotgun', + 'Wolfenstein2 (MAP31) - Supercharge', + }, +} + + +death_logic_locations = [ + "Entryway (MAP01) - Armor", +] diff --git a/worlds/doom_ii/Maps.py b/worlds/doom_ii/Maps.py new file mode 100644 index 0000000000..cf41939fa5 --- /dev/null +++ b/worlds/doom_ii/Maps.py @@ -0,0 +1,39 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List + + +map_names: List[str] = [ + 'Entryway (MAP01)', + 'Underhalls (MAP02)', + 'The Gantlet (MAP03)', + 'The Focus (MAP04)', + 'The Waste Tunnels (MAP05)', + 'The Crusher (MAP06)', + 'Dead Simple (MAP07)', + 'Tricks and Traps (MAP08)', + 'The Pit (MAP09)', + 'Refueling Base (MAP10)', + 'Circle of Death (MAP11)', + 'The Factory (MAP12)', + 'Downtown (MAP13)', + 'The Inmost Dens (MAP14)', + 'Industrial Zone (MAP15)', + 'Suburbs (MAP16)', + 'Tenements (MAP17)', + 'The Courtyard (MAP18)', + 'The Citadel (MAP19)', + 'Gotcha! (MAP20)', + 'Nirvana (MAP21)', + 'The Catacombs (MAP22)', + 'Barrels o Fun (MAP23)', + 'The Chasm (MAP24)', + 'Bloodfalls (MAP25)', + 'The Abandoned Mines (MAP26)', + 'Monster Condo (MAP27)', + 'The Spirit World (MAP28)', + 'The Living End (MAP29)', + 'Icon of Sin (MAP30)', + 'Wolfenstein2 (MAP31)', + 'Grosse2 (MAP32)', +] diff --git a/worlds/doom_ii/Options.py b/worlds/doom_ii/Options.py new file mode 100644 index 0000000000..cc39512a17 --- /dev/null +++ b/worlds/doom_ii/Options.py @@ -0,0 +1,150 @@ +import typing + +from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool +from dataclasses import dataclass + + +class Difficulty(Choice): + """ + Choose the difficulty option. Those match DOOM's difficulty options. + baby (I'm too young to die.) double ammos, half damage, less monsters or strength. + easy (Hey, not too rough.) less monsters or strength. + medium (Hurt me plenty.) Default. + hard (Ultra-Violence.) More monsters or strength. + nightmare (Nightmare!) Monsters attack more rapidly and respawn. + """ + display_name = "Difficulty" + option_baby = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_nightmare = 4 + default = 2 + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + vanilla: No randomization + shuffle: Monsters are shuffled within the level + random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + option_random_chaotic = 3 + default = 2 + + +class RandomPickups(Choice): + """ + Choose how pickups are randomized. + vanilla: No randomization + shuffle: Pickups are shuffled within the level + random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big) + """ + display_name = "Random Pickups" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + default = 1 + + +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class FlipLevels(Choice): + """ + Flip levels on one axis. + vanilla: No flipping + flipped: All levels are flipped + random: Random levels are flipped + """ + display_name = "Flip Levels" + option_vanilla = 0 + option_flipped = 1 + option_randomly_flipped = 2 + default = 0 + + +class AllowDeathLogic(Toggle): + """Some locations require a timed puzzle that can only be tried once. + After which, if the player failed to get it, the location cannot be checked anymore. + By default, no progression items are placed here. There is a way, hovewer, to still get them: + Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" + display_name = "Allow Death Logic" + + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Doom" + + +class StartWithComputerAreaMaps(Toggle): + """Give the player all Computer Area Map items from the start.""" + display_name = "Start With Computer Area Maps" + + +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_message="Reset level on death" + + +class Episode1(DefaultOnToggle): + """Subterranean and Outpost. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 1" + + +class Episode2(DefaultOnToggle): + """City. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 2" + + +class Episode3(DefaultOnToggle): + """Hell. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 3" + + +class SecretLevels(Toggle): + """Secret levels. + This is too short to be an episode. It's additive. + Another episode will have to be selected along with this one. + Otherwise episode 1 will be added.""" + display_name = "Secret Levels" + + +@dataclass +class DOOM2Options(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + difficulty: Difficulty + random_monsters: RandomMonsters + random_pickups: RandomPickups + random_music: RandomMusic + flip_levels: FlipLevels + allow_death_logic: AllowDeathLogic + pro: Pro + start_with_computer_area_maps: StartWithComputerAreaMaps + death_link: DeathLink + reset_level_on_death: ResetLevelOnDeath + episode1: Episode1 + episode2: Episode2 + episode3: Episode3 + episode4: SecretLevels diff --git a/worlds/doom_ii/Regions.py b/worlds/doom_ii/Regions.py new file mode 100644 index 0000000000..3d81d7abb8 --- /dev/null +++ b/worlds/doom_ii/Regions.py @@ -0,0 +1,502 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List +from BaseClasses import TypedDict + +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): + name: str + connects_to_hub: bool + episode: int + connections: List[ConnectionDict] + + +regions:List[RegionDict] = [ + # Entryway (MAP01) + {"name":"Entryway (MAP01) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # Underhalls (MAP02) + {"name":"Underhalls (MAP02) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]}, + {"name":"Underhalls (MAP02) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Underhalls (MAP02) Red","pro":False}]}, + {"name":"Underhalls (MAP02) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Underhalls (MAP02) Blue","pro":False}, + {"target":"Underhalls (MAP02) Main","pro":False}]}, + + # The Gantlet (MAP03) + {"name":"The Gantlet (MAP03) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Gantlet (MAP03) Blue","pro":False}, + {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":True}]}, + {"name":"The Gantlet (MAP03) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Gantlet (MAP03) Main","pro":False}, + {"target":"The Gantlet (MAP03) Red","pro":False}, + {"target":"The Gantlet (MAP03) Blue Pro Jump","pro":False}]}, + {"name":"The Gantlet (MAP03) Red", + "connects_to_hub":False, + "episode":1, + "connections":[]}, + {"name":"The Gantlet (MAP03) Blue Pro Jump", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gantlet (MAP03) Blue","pro":False}]}, + + # The Focus (MAP04) + {"name":"The Focus (MAP04) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Focus (MAP04) Red","pro":False}, + {"target":"The Focus (MAP04) Blue","pro":False}]}, + {"name":"The Focus (MAP04) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Focus (MAP04) Main","pro":False}]}, + {"name":"The Focus (MAP04) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Focus (MAP04) Red","pro":False}]}, + {"name":"The Focus (MAP04) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Focus (MAP04) Yellow","pro":False}, + {"target":"The Focus (MAP04) Main","pro":False}]}, + + # The Waste Tunnels (MAP05) + {"name":"The Waste Tunnels (MAP05) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Waste Tunnels (MAP05) Red","pro":False}, + {"target":"The Waste Tunnels (MAP05) Blue","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Waste Tunnels (MAP05) Yellow","pro":False}, + {"target":"The Waste Tunnels (MAP05) Main","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Waste Tunnels (MAP05) Blue","pro":False}]}, + {"name":"The Waste Tunnels (MAP05) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Waste Tunnels (MAP05) Main","pro":False}]}, + + # The Crusher (MAP06) + {"name":"The Crusher (MAP06) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Crusher (MAP06) Blue","pro":False}]}, + {"name":"The Crusher (MAP06) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crusher (MAP06) Red","pro":False}, + {"target":"The Crusher (MAP06) Main","pro":False}]}, + {"name":"The Crusher (MAP06) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Crusher (MAP06) Red","pro":False}]}, + {"name":"The Crusher (MAP06) Red", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crusher (MAP06) Yellow","pro":False}, + {"target":"The Crusher (MAP06) Blue","pro":False}, + {"target":"The Crusher (MAP06) Main","pro":False}]}, + + # Dead Simple (MAP07) + {"name":"Dead Simple (MAP07) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # Tricks and Traps (MAP08) + {"name":"Tricks and Traps (MAP08) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"Tricks and Traps (MAP08) Red","pro":False}, + {"target":"Tricks and Traps (MAP08) Yellow","pro":False}]}, + {"name":"Tricks and Traps (MAP08) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]}, + {"name":"Tricks and Traps (MAP08) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Tricks and Traps (MAP08) Main","pro":False}]}, + + # The Pit (MAP09) + {"name":"The Pit (MAP09) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Pit (MAP09) Yellow","pro":False}, + {"target":"The Pit (MAP09) Blue","pro":False}]}, + {"name":"The Pit (MAP09) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[]}, + {"name":"The Pit (MAP09) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Pit (MAP09) Main","pro":False}]}, + + # Refueling Base (MAP10) + {"name":"Refueling Base (MAP10) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]}, + {"name":"Refueling Base (MAP10) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Refueling Base (MAP10) Main","pro":False}, + {"target":"Refueling Base (MAP10) Yellow Blue","pro":False}]}, + {"name":"Refueling Base (MAP10) Yellow Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Refueling Base (MAP10) Yellow","pro":False}]}, + + # Circle of Death (MAP11) + {"name":"Circle of Death (MAP11) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"Circle of Death (MAP11) Blue","pro":False}, + {"target":"Circle of Death (MAP11) Red","pro":False}]}, + {"name":"Circle of Death (MAP11) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]}, + {"name":"Circle of Death (MAP11) Red", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"Circle of Death (MAP11) Main","pro":False}]}, + + # The Factory (MAP12) + {"name":"The Factory (MAP12) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Factory (MAP12) Yellow","pro":False}, + {"target":"The Factory (MAP12) Blue","pro":False}]}, + {"name":"The Factory (MAP12) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Factory (MAP12) Main","pro":False}]}, + {"name":"The Factory (MAP12) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[]}, + + # Downtown (MAP13) + {"name":"Downtown (MAP13) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Downtown (MAP13) Yellow","pro":False}, + {"target":"Downtown (MAP13) Red","pro":False}, + {"target":"Downtown (MAP13) Blue","pro":False}]}, + {"name":"Downtown (MAP13) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + {"name":"Downtown (MAP13) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + {"name":"Downtown (MAP13) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Downtown (MAP13) Main","pro":False}]}, + + # The Inmost Dens (MAP14) + {"name":"The Inmost Dens (MAP14) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Inmost Dens (MAP14) Red","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Main","pro":False}, + {"target":"The Inmost Dens (MAP14) Red East","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Main","pro":False}, + {"target":"The Inmost Dens (MAP14) Red South","pro":False}, + {"target":"The Inmost Dens (MAP14) Red East","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red East", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Inmost Dens (MAP14) Blue","pro":False}, + {"target":"The Inmost Dens (MAP14) Main","pro":False}]}, + {"name":"The Inmost Dens (MAP14) Red South", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Inmost Dens (MAP14) Main","pro":False}]}, + + # Industrial Zone (MAP15) + {"name":"Industrial Zone (MAP15) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Industrial Zone (MAP15) Yellow East","pro":False}, + {"target":"Industrial Zone (MAP15) Yellow West","pro":False}]}, + {"name":"Industrial Zone (MAP15) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Industrial Zone (MAP15) Yellow East","pro":False}]}, + {"name":"Industrial Zone (MAP15) Yellow East", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Industrial Zone (MAP15) Blue","pro":False}, + {"target":"Industrial Zone (MAP15) Main","pro":False}]}, + {"name":"Industrial Zone (MAP15) Yellow West", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Industrial Zone (MAP15) Main","pro":False}]}, + + # Suburbs (MAP16) + {"name":"Suburbs (MAP16) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"Suburbs (MAP16) Red","pro":False}, + {"target":"Suburbs (MAP16) Blue","pro":False}]}, + {"name":"Suburbs (MAP16) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]}, + {"name":"Suburbs (MAP16) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Suburbs (MAP16) Main","pro":False}]}, + + # Tenements (MAP17) + {"name":"Tenements (MAP17) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"Tenements (MAP17) Red","pro":False}]}, + {"name":"Tenements (MAP17) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"Tenements (MAP17) Red","pro":False}]}, + {"name":"Tenements (MAP17) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Tenements (MAP17) Red","pro":False}, + {"target":"Tenements (MAP17) Blue","pro":False}]}, + {"name":"Tenements (MAP17) Red", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"Tenements (MAP17) Yellow","pro":False}, + {"target":"Tenements (MAP17) Blue","pro":False}, + {"target":"Tenements (MAP17) Main","pro":False}]}, + + # The Courtyard (MAP18) + {"name":"The Courtyard (MAP18) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Courtyard (MAP18) Yellow","pro":False}, + {"target":"The Courtyard (MAP18) Blue","pro":False}]}, + {"name":"The Courtyard (MAP18) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]}, + {"name":"The Courtyard (MAP18) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Courtyard (MAP18) Main","pro":False}]}, + + # The Citadel (MAP19) + {"name":"The Citadel (MAP19) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Citadel (MAP19) Red","pro":False}]}, + {"name":"The Citadel (MAP19) Red", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Citadel (MAP19) Main","pro":False}]}, + + # Gotcha! (MAP20) + {"name":"Gotcha! (MAP20) Main", + "connects_to_hub":True, + "episode":2, + "connections":[]}, + + # Nirvana (MAP21) + {"name":"Nirvana (MAP21) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]}, + {"name":"Nirvana (MAP21) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"Nirvana (MAP21) Main","pro":False}, + {"target":"Nirvana (MAP21) Magenta","pro":False}]}, + {"name":"Nirvana (MAP21) Magenta", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Nirvana (MAP21) Yellow","pro":False}]}, + + # The Catacombs (MAP22) + {"name":"The Catacombs (MAP22) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Catacombs (MAP22) Blue","pro":False}, + {"target":"The Catacombs (MAP22) Red","pro":False}]}, + {"name":"The Catacombs (MAP22) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]}, + {"name":"The Catacombs (MAP22) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Catacombs (MAP22) Main","pro":False}]}, + + # Barrels o Fun (MAP23) + {"name":"Barrels o Fun (MAP23) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Barrels o Fun (MAP23) Yellow","pro":False}]}, + {"name":"Barrels o Fun (MAP23) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Barrels o Fun (MAP23) Main","pro":False}]}, + + # The Chasm (MAP24) + {"name":"The Chasm (MAP24) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Chasm (MAP24) Red","pro":False}]}, + {"name":"The Chasm (MAP24) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Chasm (MAP24) Main","pro":False}]}, + + # Bloodfalls (MAP25) + {"name":"Bloodfalls (MAP25) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"Bloodfalls (MAP25) Blue","pro":False}]}, + {"name":"Bloodfalls (MAP25) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Bloodfalls (MAP25) Main","pro":False}]}, + + # The Abandoned Mines (MAP26) + {"name":"The Abandoned Mines (MAP26) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Abandoned Mines (MAP26) Yellow","pro":False}, + {"target":"The Abandoned Mines (MAP26) Red","pro":False}, + {"target":"The Abandoned Mines (MAP26) Blue","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + {"name":"The Abandoned Mines (MAP26) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Abandoned Mines (MAP26) Main","pro":False}]}, + + # Monster Condo (MAP27) + {"name":"Monster Condo (MAP27) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"Monster Condo (MAP27) Yellow","pro":False}, + {"target":"Monster Condo (MAP27) Red","pro":False}, + {"target":"Monster Condo (MAP27) Blue","pro":False}]}, + {"name":"Monster Condo (MAP27) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + {"name":"Monster Condo (MAP27) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + {"name":"Monster Condo (MAP27) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"Monster Condo (MAP27) Main","pro":False}]}, + + # The Spirit World (MAP28) + {"name":"The Spirit World (MAP28) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Spirit World (MAP28) Yellow","pro":False}, + {"target":"The Spirit World (MAP28) Red","pro":False}]}, + {"name":"The Spirit World (MAP28) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]}, + {"name":"The Spirit World (MAP28) Red", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Spirit World (MAP28) Main","pro":False}]}, + + # The Living End (MAP29) + {"name":"The Living End (MAP29) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # Icon of Sin (MAP30) + {"name":"Icon of Sin (MAP30) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # Wolfenstein2 (MAP31) + {"name":"Wolfenstein2 (MAP31) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, + + # Grosse2 (MAP32) + {"name":"Grosse2 (MAP32) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, +] diff --git a/worlds/doom_ii/Rules.py b/worlds/doom_ii/Rules.py new file mode 100644 index 0000000000..89f3a10f9f --- /dev/null +++ b/worlds/doom_ii/Rules.py @@ -0,0 +1,501 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import DOOM2World + + +def set_episode1_rules(player, world, pro): + # Entryway (MAP01) + set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state: + state.has("Entryway (MAP01)", player, 1)) + set_rule(world.get_entrance("Hub -> Entryway (MAP01) Main", player), lambda state: + state.has("Entryway (MAP01)", player, 1)) + + # Underhalls (MAP02) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Hub -> Underhalls (MAP02) Main", player), lambda state: + state.has("Underhalls (MAP02)", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Main -> Underhalls (MAP02) Red", player), lambda state: + state.has("Underhalls (MAP02) - Red keycard", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Blue -> Underhalls (MAP02) Red", player), lambda state: + state.has("Underhalls (MAP02) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Underhalls (MAP02) Red -> Underhalls (MAP02) Blue", player), lambda state: + state.has("Underhalls (MAP02) - Blue keycard", player, 1)) + + # The Gantlet (MAP03) + set_rule(world.get_entrance("Hub -> The Gantlet (MAP03) Main", player), lambda state: + (state.has("The Gantlet (MAP03)", player, 1)) and + (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1) or + state.has("Super Shotgun", player, 1))) + set_rule(world.get_entrance("The Gantlet (MAP03) Main -> The Gantlet (MAP03) Blue", player), lambda state: + state.has("The Gantlet (MAP03) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Gantlet (MAP03) Blue -> The Gantlet (MAP03) Red", player), lambda state: + state.has("The Gantlet (MAP03) - Red keycard", player, 1)) + + # The Focus (MAP04) + set_rule(world.get_entrance("Hub -> The Focus (MAP04) Main", player), lambda state: + (state.has("The Focus (MAP04)", player, 1)) and + (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1) or + state.has("Super Shotgun", player, 1))) + set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Red", player), lambda state: + state.has("The Focus (MAP04) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Main -> The Focus (MAP04) Blue", player), lambda state: + state.has("The Focus (MAP04) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Yellow -> The Focus (MAP04) Red", player), lambda state: + state.has("The Focus (MAP04) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Yellow", player), lambda state: + state.has("The Focus (MAP04) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Focus (MAP04) Red -> The Focus (MAP04) Main", player), lambda state: + state.has("The Focus (MAP04) - Red keycard", player, 1)) + + # The Waste Tunnels (MAP05) + set_rule(world.get_entrance("Hub -> The Waste Tunnels (MAP05) Main", player), lambda state: + (state.has("The Waste Tunnels (MAP05)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Red", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Main -> The Waste Tunnels (MAP05) Blue", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Yellow", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Blue -> The Waste Tunnels (MAP05) Main", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Waste Tunnels (MAP05) Yellow -> The Waste Tunnels (MAP05) Blue", player), lambda state: + state.has("The Waste Tunnels (MAP05) - Yellow keycard", player, 1)) + + # The Crusher (MAP06) + set_rule(world.get_entrance("Hub -> The Crusher (MAP06) Main", player), lambda state: + (state.has("The Crusher (MAP06)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Crusher (MAP06) Main -> The Crusher (MAP06) Blue", player), lambda state: + state.has("The Crusher (MAP06) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Red", player), lambda state: + state.has("The Crusher (MAP06) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Blue -> The Crusher (MAP06) Main", player), lambda state: + state.has("The Crusher (MAP06) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Yellow -> The Crusher (MAP06) Red", player), lambda state: + state.has("The Crusher (MAP06) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Yellow", player), lambda state: + state.has("The Crusher (MAP06) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Crusher (MAP06) Red -> The Crusher (MAP06) Blue", player), lambda state: + state.has("The Crusher (MAP06) - Red keycard", player, 1)) + + # Dead Simple (MAP07) + set_rule(world.get_entrance("Hub -> Dead Simple (MAP07) Main", player), lambda state: + (state.has("Dead Simple (MAP07)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + # Tricks and Traps (MAP08) + set_rule(world.get_entrance("Hub -> Tricks and Traps (MAP08) Main", player), lambda state: + (state.has("Tricks and Traps (MAP08)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Red", player), lambda state: + state.has("Tricks and Traps (MAP08) - Red skull key", player, 1)) + set_rule(world.get_entrance("Tricks and Traps (MAP08) Main -> Tricks and Traps (MAP08) Yellow", player), lambda state: + state.has("Tricks and Traps (MAP08) - Yellow skull key", player, 1)) + + # The Pit (MAP09) + set_rule(world.get_entrance("Hub -> The Pit (MAP09) Main", player), lambda state: + (state.has("The Pit (MAP09)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Yellow", player), lambda state: + state.has("The Pit (MAP09) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Pit (MAP09) Main -> The Pit (MAP09) Blue", player), lambda state: + state.has("The Pit (MAP09) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Pit (MAP09) Yellow -> The Pit (MAP09) Main", player), lambda state: + state.has("The Pit (MAP09) - Yellow keycard", player, 1)) + + # Refueling Base (MAP10) + set_rule(world.get_entrance("Hub -> Refueling Base (MAP10) Main", player), lambda state: + (state.has("Refueling Base (MAP10)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Refueling Base (MAP10) Main -> Refueling Base (MAP10) Yellow", player), lambda state: + state.has("Refueling Base (MAP10) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Refueling Base (MAP10) Yellow -> Refueling Base (MAP10) Yellow Blue", player), lambda state: + state.has("Refueling Base (MAP10) - Blue keycard", player, 1)) + + # Circle of Death (MAP11) + set_rule(world.get_entrance("Hub -> Circle of Death (MAP11) Main", player), lambda state: + (state.has("Circle of Death (MAP11)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Blue", player), lambda state: + state.has("Circle of Death (MAP11) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Circle of Death (MAP11) Main -> Circle of Death (MAP11) Red", player), lambda state: + state.has("Circle of Death (MAP11) - Red keycard", player, 1)) + + +def set_episode2_rules(player, world, pro): + # The Factory (MAP12) + set_rule(world.get_entrance("Hub -> The Factory (MAP12) Main", player), lambda state: + (state.has("The Factory (MAP12)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Yellow", player), lambda state: + state.has("The Factory (MAP12) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Factory (MAP12) Main -> The Factory (MAP12) Blue", player), lambda state: + state.has("The Factory (MAP12) - Blue keycard", player, 1)) + + # Downtown (MAP13) + set_rule(world.get_entrance("Hub -> Downtown (MAP13) Main", player), lambda state: + (state.has("Downtown (MAP13)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Yellow", player), lambda state: + state.has("Downtown (MAP13) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Red", player), lambda state: + state.has("Downtown (MAP13) - Red keycard", player, 1)) + set_rule(world.get_entrance("Downtown (MAP13) Main -> Downtown (MAP13) Blue", player), lambda state: + state.has("Downtown (MAP13) - Blue keycard", player, 1)) + + # The Inmost Dens (MAP14) + set_rule(world.get_entrance("Hub -> The Inmost Dens (MAP14) Main", player), lambda state: + (state.has("The Inmost Dens (MAP14)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Main -> The Inmost Dens (MAP14) Red", player), lambda state: + state.has("The Inmost Dens (MAP14) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Blue -> The Inmost Dens (MAP14) Red East", player), lambda state: + state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Red -> The Inmost Dens (MAP14) Main", player), lambda state: + state.has("The Inmost Dens (MAP14) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Inmost Dens (MAP14) Red East -> The Inmost Dens (MAP14) Blue", player), lambda state: + state.has("The Inmost Dens (MAP14) - Blue skull key", player, 1)) + + # Industrial Zone (MAP15) + set_rule(world.get_entrance("Hub -> Industrial Zone (MAP15) Main", player), lambda state: + (state.has("Industrial Zone (MAP15)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow East", player), lambda state: + state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Main -> Industrial Zone (MAP15) Yellow West", player), lambda state: + state.has("Industrial Zone (MAP15) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Blue -> Industrial Zone (MAP15) Yellow East", player), lambda state: + state.has("Industrial Zone (MAP15) - Blue keycard", player, 1)) + set_rule(world.get_entrance("Industrial Zone (MAP15) Yellow East -> Industrial Zone (MAP15) Blue", player), lambda state: + state.has("Industrial Zone (MAP15) - Blue keycard", player, 1)) + + # Suburbs (MAP16) + set_rule(world.get_entrance("Hub -> Suburbs (MAP16) Main", player), lambda state: + (state.has("Suburbs (MAP16)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Red", player), lambda state: + state.has("Suburbs (MAP16) - Red skull key", player, 1)) + set_rule(world.get_entrance("Suburbs (MAP16) Main -> Suburbs (MAP16) Blue", player), lambda state: + state.has("Suburbs (MAP16) - Blue skull key", player, 1)) + + # Tenements (MAP17) + set_rule(world.get_entrance("Hub -> Tenements (MAP17) Main", player), lambda state: + (state.has("Tenements (MAP17)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Tenements (MAP17) Main -> Tenements (MAP17) Red", player), lambda state: + state.has("Tenements (MAP17) - Red keycard", player, 1)) + set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Yellow", player), lambda state: + state.has("Tenements (MAP17) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Tenements (MAP17) Red -> Tenements (MAP17) Blue", player), lambda state: + state.has("Tenements (MAP17) - Blue keycard", player, 1)) + + # The Courtyard (MAP18) + set_rule(world.get_entrance("Hub -> The Courtyard (MAP18) Main", player), lambda state: + (state.has("The Courtyard (MAP18)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Yellow", player), lambda state: + state.has("The Courtyard (MAP18) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Main -> The Courtyard (MAP18) Blue", player), lambda state: + state.has("The Courtyard (MAP18) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Blue -> The Courtyard (MAP18) Main", player), lambda state: + state.has("The Courtyard (MAP18) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Courtyard (MAP18) Yellow -> The Courtyard (MAP18) Main", player), lambda state: + state.has("The Courtyard (MAP18) - Yellow skull key", player, 1)) + + # The Citadel (MAP19) + set_rule(world.get_entrance("Hub -> The Citadel (MAP19) Main", player), lambda state: + (state.has("The Citadel (MAP19)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("The Citadel (MAP19) Main -> The Citadel (MAP19) Red", player), lambda state: + (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Blue skull key", player, 1) or + state.has("The Citadel (MAP19) - Yellow skull key", player, 1))) + set_rule(world.get_entrance("The Citadel (MAP19) Red -> The Citadel (MAP19) Main", player), lambda state: + (state.has("The Citadel (MAP19) - Red skull key", player, 1)) and (state.has("The Citadel (MAP19) - Yellow skull key", player, 1) or + state.has("The Citadel (MAP19) - Blue skull key", player, 1))) + + # Gotcha! (MAP20) + set_rule(world.get_entrance("Hub -> Gotcha! (MAP20) Main", player), lambda state: + (state.has("Gotcha! (MAP20)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + +def set_episode3_rules(player, world, pro): + # Nirvana (MAP21) + set_rule(world.get_entrance("Hub -> Nirvana (MAP21) Main", player), lambda state: + (state.has("Nirvana (MAP21)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Nirvana (MAP21) Main -> Nirvana (MAP21) Yellow", player), lambda state: + state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Main", player), lambda state: + state.has("Nirvana (MAP21) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Yellow -> Nirvana (MAP21) Magenta", player), lambda state: + state.has("Nirvana (MAP21) - Red skull key", player, 1) and + state.has("Nirvana (MAP21) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Nirvana (MAP21) Magenta -> Nirvana (MAP21) Yellow", player), lambda state: + state.has("Nirvana (MAP21) - Red skull key", player, 1) and + state.has("Nirvana (MAP21) - Blue skull key", player, 1)) + + # The Catacombs (MAP22) + set_rule(world.get_entrance("Hub -> The Catacombs (MAP22) Main", player), lambda state: + (state.has("The Catacombs (MAP22)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("BFG9000", player, 1) or + state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1))) + set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Blue", player), lambda state: + state.has("The Catacombs (MAP22) - Blue skull key", player, 1)) + set_rule(world.get_entrance("The Catacombs (MAP22) Main -> The Catacombs (MAP22) Red", player), lambda state: + state.has("The Catacombs (MAP22) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Catacombs (MAP22) Red -> The Catacombs (MAP22) Main", player), lambda state: + state.has("The Catacombs (MAP22) - Red skull key", player, 1)) + + # Barrels o Fun (MAP23) + set_rule(world.get_entrance("Hub -> Barrels o Fun (MAP23) Main", player), lambda state: + (state.has("Barrels o Fun (MAP23)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + set_rule(world.get_entrance("Barrels o Fun (MAP23) Main -> Barrels o Fun (MAP23) Yellow", player), lambda state: + state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Barrels o Fun (MAP23) Yellow -> Barrels o Fun (MAP23) Main", player), lambda state: + state.has("Barrels o Fun (MAP23) - Yellow skull key", player, 1)) + + # The Chasm (MAP24) + set_rule(world.get_entrance("Hub -> The Chasm (MAP24) Main", player), lambda state: + state.has("The Chasm (MAP24)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Chasm (MAP24) Main -> The Chasm (MAP24) Red", player), lambda state: + state.has("The Chasm (MAP24) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Chasm (MAP24) Red -> The Chasm (MAP24) Main", player), lambda state: + state.has("The Chasm (MAP24) - Red keycard", player, 1)) + + # Bloodfalls (MAP25) + set_rule(world.get_entrance("Hub -> Bloodfalls (MAP25) Main", player), lambda state: + state.has("Bloodfalls (MAP25)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("Bloodfalls (MAP25) Main -> Bloodfalls (MAP25) Blue", player), lambda state: + state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Bloodfalls (MAP25) Blue -> Bloodfalls (MAP25) Main", player), lambda state: + state.has("Bloodfalls (MAP25) - Blue skull key", player, 1)) + + # The Abandoned Mines (MAP26) + set_rule(world.get_entrance("Hub -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Yellow", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Red", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Red keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Main -> The Abandoned Mines (MAP26) Blue", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Blue -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Blue keycard", player, 1)) + set_rule(world.get_entrance("The Abandoned Mines (MAP26) Yellow -> The Abandoned Mines (MAP26) Main", player), lambda state: + state.has("The Abandoned Mines (MAP26) - Yellow keycard", player, 1)) + + # Monster Condo (MAP27) + set_rule(world.get_entrance("Hub -> Monster Condo (MAP27) Main", player), lambda state: + state.has("Monster Condo (MAP27)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Yellow", player), lambda state: + state.has("Monster Condo (MAP27) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Red", player), lambda state: + state.has("Monster Condo (MAP27) - Red skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Main -> Monster Condo (MAP27) Blue", player), lambda state: + state.has("Monster Condo (MAP27) - Blue skull key", player, 1)) + set_rule(world.get_entrance("Monster Condo (MAP27) Red -> Monster Condo (MAP27) Main", player), lambda state: + state.has("Monster Condo (MAP27) - Red skull key", player, 1)) + + # The Spirit World (MAP28) + set_rule(world.get_entrance("Hub -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Yellow", player), lambda state: + state.has("The Spirit World (MAP28) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Main -> The Spirit World (MAP28) Red", player), lambda state: + state.has("The Spirit World (MAP28) - Red skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Yellow -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28) - Yellow skull key", player, 1)) + set_rule(world.get_entrance("The Spirit World (MAP28) Red -> The Spirit World (MAP28) Main", player), lambda state: + state.has("The Spirit World (MAP28) - Red skull key", player, 1)) + + # The Living End (MAP29) + set_rule(world.get_entrance("Hub -> The Living End (MAP29) Main", player), lambda state: + state.has("The Living End (MAP29)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + + # Icon of Sin (MAP30) + set_rule(world.get_entrance("Hub -> Icon of Sin (MAP30) Main", player), lambda state: + state.has("Icon of Sin (MAP30)", player, 1) and + state.has("Rocket launcher", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Plasma gun", player, 1) and + state.has("BFG9000", player, 1) and + state.has("Super Shotgun", player, 1)) + + +def set_episode4_rules(player, world, pro): + # Wolfenstein2 (MAP31) + set_rule(world.get_entrance("Hub -> Wolfenstein2 (MAP31) Main", player), lambda state: + (state.has("Wolfenstein2 (MAP31)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + # Grosse2 (MAP32) + set_rule(world.get_entrance("Hub -> Grosse2 (MAP32) Main", player), lambda state: + (state.has("Grosse2 (MAP32)", player, 1) and + state.has("Shotgun", player, 1) and + state.has("Chaingun", player, 1) and + state.has("Super Shotgun", player, 1)) and + (state.has("Rocket launcher", player, 1) or + state.has("Plasma gun", player, 1) or + state.has("BFG9000", player, 1))) + + +def set_rules(doom_ii_world: "DOOM2World", included_episodes, pro): + player = doom_ii_world.player + world = doom_ii_world.multiworld + + if included_episodes[0]: + set_episode1_rules(player, world, pro) + if included_episodes[1]: + set_episode2_rules(player, world, pro) + if included_episodes[2]: + set_episode3_rules(player, world, pro) + if included_episodes[3]: + set_episode4_rules(player, world, pro) diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py new file mode 100644 index 0000000000..22dee2ab74 --- /dev/null +++ b/worlds/doom_ii/__init__.py @@ -0,0 +1,267 @@ +import functools +import logging +from typing import Any, Dict, List + +from BaseClasses import Entrance, CollectionState, Item, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Maps, Regions, Rules +from .Options import DOOM2Options + +logger = logging.getLogger("DOOM II") + +DOOM_TYPE_LEVEL_COMPLETE = -2 +DOOM_TYPE_COMPUTER_AREA_MAP = 2026 + + +class DOOM2Location(Location): + game: str = "DOOM II" + + +class DOOM2Item(Item): + game: str = "DOOM II" + + +class DOOM2Web(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the DOOM II randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class DOOM2World(World): + """ + Doom II, also known as Doom II: Hell on Earth, is a first-person shooter game by id Software. + It was released for MS-DOS in 1994. + Compared to its predecessor, Doom II features larger levels, new enemies, a new "super shotgun" weapon + """ + options_dataclass = DOOM2Options + options: DOOM2Options + game = "DOOM II" + web = DOOM2Web() + data_version = 3 + required_client_version = (0, 3, 9) + + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + item_name_groups = Items.item_name_groups + + location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_groups = Locations.location_name_groups + + starting_level_for_episode: List[str] = [ + "Entryway (MAP01)", + "The Factory (MAP12)", + "Nirvana (MAP21)" + ] + + # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1. + # The ratio have been tweaked seem, and feel good. + items_ratio: Dict[str, float] = { + "Armor": 41, + "Mega Armor": 25, + "Berserk": 12, + "Invulnerability": 10, + "Partial invisibility": 18, + "Supercharge": 28, + "Medikit": 15, + "Box of bullets": 13, + "Box of rockets": 13, + "Box of shotgun shells": 13, + "Energy cell pack": 10 + } + + def __init__(self, world: MultiWorld, player: int): + self.included_episodes = [1, 1, 1, 0] + self.location_count = 0 + + super().__init__(world, player) + + def get_episode_count(self): + # Don't include 4th, those are secret levels they are additive + return sum(self.included_episodes[:3]) + + def generate_early(self): + # Cache which episodes are included + self.included_episodes[0] = self.options.episode1.value + self.included_episodes[1] = self.options.episode2.value + self.included_episodes[2] = self.options.episode3.value + self.included_episodes[3] = self.options.episode4.value # 4th episode are secret levels + + # If no episodes selected, select Episode 1 + if self.get_episode_count() == 0: + self.included_episodes[0] = 1 + + def create_regions(self): + pro = self.options.pro.value + + # Main regions + menu_region = Region("Menu", self.player, self.multiworld) + hub_region = Region("Hub", self.player, self.multiworld) + self.multiworld.regions += [menu_region, hub_region] + menu_region.add_exits(["Hub"]) + + # Create regions and locations + main_regions = [] + connections = [] + for region_dict in Regions.regions: + if not self.included_episodes[region_dict["episode"] - 1]: + continue + + region_name = region_dict["name"] + if region_dict["connects_to_hub"]: + main_regions.append(region_name) + + region = Region(region_name, self.player, self.multiworld) + region.add_locations({ + loc["name"]: loc_id + for loc_id, loc in Locations.location_table.items() + if loc["region"] == region_name and self.included_episodes[loc["episode"] - 1] + }, DOOM2Location) + + self.multiworld.regions.append(region) + + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) + + # Connect main regions to Hub + hub_region.add_exits(main_regions) + + # Do the other connections between regions (They are not all both ways) + for connection in connections: + source = connection[0] + target = self.multiworld.get_region(connection[1], self.player) + + entrance = Entrance(self.player, f"{source.name} -> {target.name}", source) + source.exits.append(entrance) + entrance.connect(target) + + # Sum locations for items creation + self.location_count = len(self.multiworld.get_locations(self.player)) + + def completion_rule(self, state: CollectionState): + for map_name in Maps.map_names: + if map_name + " - Exit" not in self.location_name_to_id: + continue + + # Exit location names are in form: Entryway (MAP01) - Exit + loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + # Map complete item names are in form: Entryway (MAP01) - Complete + if not state.has(map_name + " - Complete", self.player, 1): + return False + + return True + + def set_rules(self): + pro = self.options.pro.value + allow_death_logic = self.options.allow_death_logic.value + + Rules.set_rules(self, self.included_episodes, pro) + self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) + + # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed + # platform) Unless the user allows for it. + if not allow_death_logic: + for death_logic_location in Locations.death_logic_locations: + self.multiworld.exclude_locations[self.player].value.add(death_logic_location) + + def create_item(self, name: str) -> DOOM2Item: + item_id: int = self.item_name_to_id[name] + return DOOM2Item(name, Items.item_table[item_id]["classification"], item_id, self.player) + + def create_items(self): + itempool: List[DOOM2Item] = [] + start_with_computer_area_maps: bool = self.options.start_with_computer_area_maps.value + + # Items + for item_id, item in Items.item_table.items(): + if item["doom_type"] == DOOM_TYPE_LEVEL_COMPLETE: + continue # We'll fill it manually later + + if item["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and start_with_computer_area_maps: + continue # We'll fill it manually, and we will put fillers in place + + if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: + continue + + count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 + itempool += [self.create_item(item["name"]) for _ in range(count)] + + # Place end level items in locked locations + for map_name in Maps.map_names: + loc_name = map_name + " - Exit" + item_name = map_name + " - Complete" + + if loc_name not in self.location_name_to_id: + continue + + if item_name not in self.item_name_to_id: + continue + + loc = Locations.location_table[self.location_name_to_id[loc_name]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name)) + self.location_count -= 1 + + # Give starting levels right away + for i in range(len(self.starting_level_for_episode)): + if self.included_episodes[i]: + self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i])) + + # Give Computer area maps if option selected + if start_with_computer_area_maps: + for item_id, item_dict in Items.item_table.items(): + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) + + # Fill the rest starting with powerups, then fillers + self.create_ratioed_items("Armor", itempool) + self.create_ratioed_items("Mega Armor", itempool) + self.create_ratioed_items("Berserk", itempool) + self.create_ratioed_items("Invulnerability", itempool) + self.create_ratioed_items("Partial invisibility", itempool) + self.create_ratioed_items("Supercharge", itempool) + + while len(itempool) < self.location_count: + itempool.append(self.create_item(self.get_filler_item_name())) + + # add itempool to multiworld + self.multiworld.itempool += itempool + + def get_filler_item_name(self): + return self.multiworld.random.choice([ + "Medikit", + "Box of bullets", + "Box of rockets", + "Box of shotgun shells", + "Energy cell pack" + ]) + + def create_ratioed_items(self, item_name: str, itempool: List[DOOM2Item]): + remaining_loc = self.location_count - len(itempool) + ep_count = self.get_episode_count() + + # Was balanced based on DOOM 1993's first 3 episodes + count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3)))) + if count == 0: + logger.warning("Warning, no ", item_name, " will be placed.") + return + + for i in range(count): + itempool.append(self.create_item(item_name)) + + def fill_slot_data(self) -> Dict[str, Any]: + return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4") diff --git a/worlds/doom_ii/docs/en_DOOM II.md b/worlds/doom_ii/docs/en_DOOM II.md new file mode 100644 index 0000000000..d561745b76 --- /dev/null +++ b/worlds/doom_ii/docs/en_DOOM II.md @@ -0,0 +1,23 @@ +# DOOM II + +## Where is the settings page? + +The [player settings page](../player-settings) contains the options needed to configure your game session. + +## What does randomization do to this game? + +Guns, keycards, and level unlocks have been randomized. Typically, you will end up playing different levels out of order to find your keycards and level unlocks and eventually complete your game. + +Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level. + +## What is the goal? + +The goal is to complete every level. + +## What is a "check" in DOOM II? + +Guns, keycards, and powerups have been replaced with Archipelago checks. The switch at the end of each level is also a check. + +## What "items" can you unlock in DOOM II? + +Keycards and level unlocks are your main progression items. Gun unlocks and some upgrades are your useful items. Temporary powerups, ammo, healing, and armor are filler items. diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md new file mode 100644 index 0000000000..321d440ea6 --- /dev/null +++ b/worlds/doom_ii/docs/setup_en.md @@ -0,0 +1,51 @@ +# DOOM II Randomizer Setup + +## Required Software + +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing AP Doom +1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. +2. Copy DOOM2.WAD from your steam install into the extracted folder. + You can find the folder in steam by finding the game in your library, + right clicking it and choosing *Manage→Browse Local Files*. + +## Joining a MultiWorld Game + +1. Launch apdoom-launcher.exe +2. Select `DOOM II` from the drop-down +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch DOOM" +5. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +APDOOM has in-game messages, +but they disappear quickly and there's no reasonable way to check your message history in-game. + +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM II can be difficult because names are rather long and contain special characters. For example: +``` +!hint Underhalls (MAP02) - Red keycard +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint map02 red +``` +For this to work, include the map short name (`MAP01`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`. + +## Auto-Tracking + +APDOOM has a functional map tracker integrated into the level select screen. +It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, +and how many of the checks you have completed in each level. From a18fb0a14f5c7dda82575d4e92cbdfeb95e57ea9 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 24 Nov 2023 12:11:34 -0500 Subject: [PATCH 215/327] Lingo: Move datafiles into a subdirectory (#2459) --- worlds/lingo/{ => data}/LL1.yaml | 61 ++++++++++++++++++++++++++++++++ worlds/lingo/data/__init__.py | 0 worlds/lingo/{ => data}/ids.yaml | 0 worlds/lingo/static_logic.py | 12 ++++--- 4 files changed, 68 insertions(+), 5 deletions(-) rename worlds/lingo/{ => data}/LL1.yaml (99%) create mode 100644 worlds/lingo/data/__init__.py rename worlds/lingo/{ => data}/ids.yaml (100%) diff --git a/worlds/lingo/LL1.yaml b/worlds/lingo/data/LL1.yaml similarity index 99% rename from worlds/lingo/LL1.yaml rename to worlds/lingo/data/LL1.yaml index f8b07b8651..d46403e8da 100644 --- a/worlds/lingo/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -42,6 +42,8 @@ # this panel. # - non_counting: If True, this panel does not contribute to the total needed # to unlock Level 2. + # - hunt: If True, the tracker will show this panel even when it is + # not a check. Used for hunts like the Number Hunt. # # doors is an array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that @@ -449,6 +451,7 @@ FOUR: id: Backside Room/Panel_four_four_3 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours @@ -521,12 +524,14 @@ FOUR: id: Backside Room/Panel_four_four_2 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours EIGHT: id: Backside Room/Panel_eight_eight_8 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -551,6 +556,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery14 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -662,6 +668,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -843,6 +850,7 @@ NINE: id: Backside Room/Panel_nine_nine_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -1055,17 +1063,20 @@ PURPLE: id: Color Arrow Room/Panel_purple_afar tag: midwhite + hunt: True required_door: door: Purple Barrier FIVE (1): id: Backside Room/Panel_five_five_5 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fives FIVE (2): id: Backside Room/Panel_five_five_4 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fives @@ -1296,12 +1307,14 @@ MASTERY (1): id: Master Room/Panel_mastery_mastery5 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (2): id: Master Room/Panel_mastery_mastery9 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -1545,6 +1558,7 @@ BACKSIDE: id: Backside Room/Panel_backside_2 tag: midwhite + hunt: True required_door: door: Backside Door STAIRS: @@ -1912,6 +1926,7 @@ RED: id: Color Arrow Room/Panel_red_afar tag: midwhite + hunt: True required_door: door: Red Barrier DEER + WREN: @@ -2013,6 +2028,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -2058,6 +2074,7 @@ NINE: id: Backside Room/Panel_nine_nine_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -2163,6 +2180,7 @@ # accessed by jumping from the top of the tower. id: Master Room/Panel_mastery_mastery8 tag: midwhite + hunt: True required_door: door: Mastery doors: @@ -2234,36 +2252,42 @@ MASTERY (1): id: Master Room/Panel_mastery_mastery6 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (2): id: Master Room/Panel_mastery_mastery7 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (3): id: Master Room/Panel_mastery_mastery10 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (4): id: Master Room/Panel_mastery_mastery11 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (5): id: Master Room/Panel_mastery_mastery12 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery MASTERY (6): id: Master Room/Panel_mastery_mastery15 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -2279,6 +2303,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery3 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -2309,9 +2334,11 @@ id: Strand Room/Panel_i_staring colors: blue tag: forbid + hunt: True GREEN: id: Color Arrow Room/Panel_green_afar tag: midwhite + hunt: True required_door: door: Green Barrier PINECONE: @@ -2356,9 +2383,11 @@ BACKSIDE: id: Backside Room/Panel_backside_3 tag: midwhite + hunt: True NINE: id: Backside Room/Panel_nine_nine_8 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -2725,35 +2754,41 @@ SEVEN (1): id: Backside Room/Panel_seven_seven_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens SEVEN (2): id: Backside Room/Panel_seven_seven_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens EIGHT: id: Backside Room/Panel_eight_eight_7 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights NINE: id: Backside Room/Panel_nine_nine_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines BLUE: id: Color Arrow Room/Panel_blue_afar tag: midwhite + hunt: True required_door: door: Blue Barrier ORANGE: id: Color Arrow Room/Panel_orange_afar tag: midwhite + hunt: True required_door: door: Orange Barrier UNCOVER: @@ -3077,6 +3112,7 @@ FOUR: id: Backside Room/Panel_four_four_4 tag: midwhite + hunt: True required_door: room: Outside The Undeterred door: Fours @@ -3135,12 +3171,14 @@ SIX: id: Backside Room/Panel_six_six_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes NINE: id: Backside Room/Panel_nine_nine_5 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -3804,46 +3842,54 @@ FIVE (1): id: Backside Room/Panel_five_five_3 tag: midwhite + hunt: True required_panel: panel: LIGHT FIVE (2): id: Backside Room/Panel_five_five_2 tag: midwhite + hunt: True required_panel: panel: WARD SIX (1): id: Backside Room/Panel_six_six_3 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes SIX (2): id: Backside Room/Panel_six_six_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sixes SEVEN: id: Backside Room/Panel_seven_seven_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Sevens EIGHT: id: Backside Room/Panel_eight_eight_2 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights NINE: id: Backside Room/Panel_nine_nine_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines BACKSIDE: id: Backside Room/Panel_backside_4 tag: midwhite + hunt: True "834283054": id: Tower Room/Panel_834283054_undaunted colors: orange @@ -3864,6 +3910,7 @@ YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite + hunt: True required_door: door: Yellow Barrier WADED + WEE: @@ -4100,6 +4147,7 @@ BACKSIDE: id: Backside Room/Panel_backside_5 tag: midwhite + hunt: True required_door: door: Backside Door PART: @@ -4144,6 +4192,7 @@ colors: - red - yellow + hunt: True required_door: room: Number Hunt door: Sixes @@ -4221,6 +4270,7 @@ colors: - red - yellow + hunt: True required_door: room: Number Hunt door: Sixes @@ -4547,6 +4597,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery2 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -4988,18 +5039,21 @@ SEVEN (1): id: Backside Room/Panel_seven_seven_7 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens SEVEN (2): id: Backside Room/Panel_seven_seven_3 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens SEVEN (3): id: Backside Room/Panel_seven_seven_4 tag: midwhite + hunt: True required_door: - room: Number Hunt door: Sevens @@ -5598,6 +5652,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_4 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -5797,6 +5852,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery4 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -5915,9 +5971,11 @@ id: Strand Room/Panel_a_strands colors: blue tag: forbid + hunt: True NINE: id: Backside Room/Panel_nine_nine_7 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines @@ -5929,6 +5987,7 @@ MASTERY: id: Master Room/Panel_mastery_mastery13 tag: midwhite + hunt: True required_door: room: Orange Tower Seventh Floor door: Mastery @@ -6025,6 +6084,7 @@ EIGHT: id: Backside Room/Panel_eight_eight_6 tag: midwhite + hunt: True required_door: room: Number Hunt door: Eights @@ -6322,6 +6382,7 @@ NINE: id: Backside Room/Panel_nine_nine_9 tag: midwhite + hunt: True required_door: room: Number Hunt door: Nines diff --git a/worlds/lingo/data/__init__.py b/worlds/lingo/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/lingo/ids.yaml b/worlds/lingo/data/ids.yaml similarity index 100% rename from worlds/lingo/ids.yaml rename to worlds/lingo/data/ids.yaml diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index f6690f93a4..e9f82fb751 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -1,6 +1,6 @@ from typing import Dict, List, NamedTuple, Optional, Set -import yaml +import Utils class RoomAndDoor(NamedTuple): @@ -108,9 +108,11 @@ def load_static_data(): except ImportError: from importlib_resources import files + from . import data + # Load in all item and location IDs. These are broken up into groups based on the type of item/location. - with files("worlds.lingo").joinpath("ids.yaml").open() as file: - config = yaml.load(file, Loader=yaml.Loader) + with files(data).joinpath("ids.yaml").open() as file: + config = Utils.parse_yaml(file) if "special_items" in config: for item_name, item_id in config["special_items"].items(): @@ -144,8 +146,8 @@ def load_static_data(): PROGRESSIVE_ITEM_IDS[item_name] = item_id # Process the main world file. - with files("worlds.lingo").joinpath("LL1.yaml").open() as file: - config = yaml.load(file, Loader=yaml.Loader) + with files(data).joinpath("LL1.yaml").open() as file: + config = Utils.parse_yaml(file) for room_name, room_data in config.items(): process_room(room_name, room_data) From 4641456ba26a06c56286bd1f5b8a65505f29dfe0 Mon Sep 17 00:00:00 2001 From: digiholic Date: Fri, 24 Nov 2023 10:14:05 -0700 Subject: [PATCH 216/327] MMBN3: Small Bug Fixes (#2282) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: Zach Parks --- MMBN3Client.py | 2 +- worlds/mmbn3/Locations.py | 2 +- worlds/mmbn3/__init__.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/MMBN3Client.py b/MMBN3Client.py index 3f7474a6fd..140a98745c 100644 --- a/MMBN3Client.py +++ b/MMBN3Client.py @@ -58,7 +58,7 @@ class MMBN3CommandProcessor(ClientCommandProcessor): class MMBN3Context(CommonContext): command_processor = MMBN3CommandProcessor game = "MegaMan Battle Network 3" - items_handling = 0b001 # full local + items_handling = 0b101 # full local except starting items def __init__(self, server_address, password): super().__init__(server_address, password) diff --git a/worlds/mmbn3/Locations.py b/worlds/mmbn3/Locations.py index fc59103340..0e2a1c51d1 100644 --- a/worlds/mmbn3/Locations.py +++ b/worlds/mmbn3/Locations.py @@ -208,7 +208,7 @@ overworlds = [ LocationData(LocationName.ACDC_Class_5B_Bookshelf, 0xb3109e, 0x200024c, 0x40, 0x737634, 235, [5, 6]), LocationData(LocationName.SciLab_Garbage_Can, 0xb3109f, 0x200024c, 0x8, 0x73AC20, 222, [4, 5]), LocationData(LocationName.Yoka_Inn_Jars, 0xb310a0, 0x200024c, 0x80, 0x747B1C, 237, [4, 5]), - LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [4]), + LocationData(LocationName.Yoka_Zoo_Garbage, 0xb310a1, 0x200024d, 0x8, 0x749444, 226, [5]), LocationData(LocationName.Beach_Department_Store, 0xb310a2, 0x2000161, 0x40, 0x74C27C, 196, [0, 1]), LocationData(LocationName.Beach_Hospital_Plaque, 0xb310a3, 0x200024c, 0x4, 0x754394, 220, [3, 4]), LocationData(LocationName.Beach_Hospital_Pink_Door, 0xb310a4, 0x200024d, 0x4, 0x754D00, 220, [4]), diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index ec68825c2d..acf258a730 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -15,6 +15,7 @@ from .Options import MMBN3Options from .Regions import regions, RegionName from .Names.ItemName import ItemName from .Names.LocationName import LocationName +from worlds.generic.Rules import add_item_rule class MMBN3Settings(settings.Group): @@ -91,6 +92,9 @@ class MMBN3World(World): loc = MMBN3Location(self.player, location, self.location_name_to_id.get(location, None), region) if location in self.excluded_locations: loc.progress_type = LocationProgressType.EXCLUDED + # Do not place any progression items on WWW Island + if region_info.name == RegionName.WWW_Island: + add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) for region_info in regions: From 15797175c7a4d47aee9f031aa59e14a265ee0996 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:38:46 +0100 Subject: [PATCH 217/327] The Witness: New junk hints (#2495) --- worlds/witness/hints.py | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 24302f0c67..1e54ec352c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -69,6 +69,12 @@ joke_hints = [ "Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.", "Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.", "Have you tried Blasphemous?\nYou haven't? Blasphemy!\n...Sorry. You should try it, though!", + "Have you tried Doom II?\nGot a good game on your hands? Just make it bigger and better.", + "Have you tried Lingo?\nIt's an open world puzzle game. It features panels with non-verbally explained mechanics.\nIf you like this game, you'll like Lingo too.", + "(Middle Yellow)\nYOU AILED OVERNIGHT\nH--- --- ----- -----?", + "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.", + "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.", + "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.", "One day I was fascinated by the subject of generation of waves by wind.", "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?", @@ -112,8 +118,46 @@ joke_hints = [ "Have you found a red page yet? No? Then have you found a blue page?", "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?", - "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi," - "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch.", + "Be quiet. I can't hear the elevator.", + "Witness me.\n- The famous last words of John Witness.", + "It's okay, I always have to skip the Rotated Shaper puzzles too.", + "Alan please add hint.", + "Rumor has it there's an audio log with a hint nearby.", + "In the future, war will break out between obelisk_sides and individual EP players.\nWhich side are you on?", + "Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.", + "Name a better game involving lines. I'll wait.", + "\"You have to draw a line in the sand.\"\n- Arin \"Egoraptor\" Hanson", + "Have you tried?\nThe puzzles tend to get easier if you do.", + "Sorry, I accidentally left my phone in the Jungle.\nAnd also all my fragile dishes.", + "Winner of the \"Most Irrelevant PR in AP History\" award!", + "I bet you wish this was a real hint :)", + "\"This hint is an impostor.\"- Junk hint submitted by T1mshady.\n...wait, I'm not supposed to say that part?", + "Wouldn't you like to know, weather buoy?", + "Give me a few minutes, I should have better material by then.", + "Just pet the doggy! You know you want to!!!", + "ceci n'est pas une metroidvania", + "HINT is MELT\nYOU is HOT", + "Who's that behind you?", + ":3", + "^v ^^v> >>^>v\n^^v>v ^v>> v>^> v>v^", + "Statement #0162601, regarding a strange island that--\nOh, wait, sorry. I'm not supposed to be here.", + "Hollow Bastion has 6 progression items.\nOr maybe it doesn't.\nI wouldn't know.", + "Set your hint count lower so I can tell you more jokes next time.", + "A non-edge start point is similar to a cat.\nIt must be either inside or outside, it can't be both.", + "What if we kissed on the Bunker Laser Platform?\nJk... unless?", + "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!", + "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.", + "How many of you have personally witnessed a total solar eclipse?", + "In the Treehouse area, you will find \n[Error: Data not found] progression items.", + "Lingo\nLingoing\nLingone", + "The name of the captain was Albert Einstein.", + "Panel impossible Sigma plz fix", + "Welcome Back! (:", + "R R R U L L U L U R U R D R D R U U", + "Have you tried checking your tracker?", + + "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi, Jasper Bird, T1mshady," + "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch, Rever, Ishigh, snolid.", ] From e64c7b1cbb7ece0c139462c47175cd7ed4f19294 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 24 Nov 2023 16:50:32 -0500 Subject: [PATCH 218/327] Fix player-options and weighted-options failing to validate settings if a payer's name is entirely numeric (#2496) --- WebHostLib/static/assets/player-options.js | 2 +- WebHostLib/static/assets/weighted-options.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 54fae2909a..2bb4a3ba13 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -463,7 +463,7 @@ const exportOptions = () => { options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`; } - if (!options.name || options.name.trim().length === 0) { + if (!options.name || options.name.toString().trim().length === 0) { return showUserMessage('You must enter a player name!'); } const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 34dfbae4bb..19928327bb 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -210,7 +210,11 @@ class WeightedSettings { let errorMessage = null; // User must choose a name for their file - if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { + if ( + !settings.name || + settings.name.toString().trim().length === 0 || + settings.name.toString().toLowerCase().trim() === 'player' + ) { userMessage.innerText = 'You forgot to set your player name at the top of the page!'; userMessage.classList.add('visible'); userMessage.scrollIntoView({ @@ -256,7 +260,7 @@ class WeightedSettings { // Remove empty arrays else if ( - ['exclude_locations', 'priority_locations', 'local_items', + ['exclude_locations', 'priority_locations', 'local_items', 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) && settings[game][setting].length === 0 ) { From c944ecf628c393ff3d296ce95355298cbce31086 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:10:52 +0100 Subject: [PATCH 219/327] Core: Introduce new Option class NamedRange (#2330) Co-authored-by: Chris Wilson Co-authored-by: Zach Parks --- Options.py | 29 +- WebHostLib/options.py | 6 +- WebHostLib/static/assets/player-options.js | 62 ++-- WebHostLib/static/assets/weighted-options.js | 337 +++++++++--------- WebHostLib/static/styles/player-options.css | 6 +- docs/options api.md | 9 +- docs/world api.md | 4 +- test/webhost/test_option_presets.py | 8 +- worlds/dlcquest/Options.py | 4 +- worlds/dlcquest/test/TestOptionsLong.py | 4 +- worlds/hk/Options.py | 6 +- worlds/lufia2ac/Options.py | 17 +- worlds/pokemon_rb/options.py | 14 +- worlds/stardew_valley/options.py | 16 +- worlds/stardew_valley/test/TestOptions.py | 8 +- .../test/long/TestOptionsLong.py | 4 +- .../test/long/TestRandomWorlds.py | 4 +- worlds/zillion/options.py | 6 +- 18 files changed, 290 insertions(+), 254 deletions(-) diff --git a/Options.py b/Options.py index 9b4f9d9908..2e3927aae3 100644 --- a/Options.py +++ b/Options.py @@ -696,11 +696,19 @@ class Range(NumericOption): return int(round(random.triangular(lower, end, tri), 0)) -class SpecialRange(Range): - special_range_cutoff = 0 +class NamedRange(Range): special_range_names: typing.Dict[str, int] = {} """Special Range names have to be all lowercase as matching is done with text.lower()""" + def __init__(self, value: int) -> None: + if value < self.range_start and value not in self.special_range_names.values(): + raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " + + f"and is also not one of the supported named special values: {self.special_range_names}") + 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}") + self.value = value + @classmethod def from_text(cls, text: str) -> Range: text = text.lower() @@ -708,6 +716,19 @@ class SpecialRange(Range): return cls(cls.special_range_names[text]) return super().from_text(text) + +class SpecialRange(NamedRange): + special_range_cutoff = 0 + + # TODO: remove class SpecialRange, earliest 3 releases after 0.4.3 + def __new__(cls, value: int) -> SpecialRange: + from Utils import deprecate + deprecate(f"Option type {cls.__name__} is a subclass of SpecialRange, which is deprecated and pending removal. " + "Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In " + "NamedRange, range_start specifies the lower end of the regular range, while special values can be " + "placed anywhere (below, inside, or above the regular range).") + return super().__new__(cls, value) + @classmethod def weighted_range(cls, text) -> Range: if text == "random-low": @@ -891,7 +912,7 @@ class Accessibility(Choice): default = 1 -class ProgressionBalancing(SpecialRange): +class ProgressionBalancing(NamedRange): """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. A lower setting means more getting stuck. A higher setting means less getting stuck.""" default = 50 @@ -1108,7 +1129,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge if os.path.isfile(full_path) and full_path.endswith(".yaml"): os.unlink(full_path) - def dictify_range(option: typing.Union[Range, SpecialRange]): + def dictify_range(option: Range): data = {option.default: 50} for sub_option in ["random", "random-low", "random-high"]: if sub_option != option.default: diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 4d17c7fdde..0158de7e24 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -81,8 +81,8 @@ def create(): "max": option.range_end, } - if issubclass(option, Options.SpecialRange): - game_options[option_name]["type"] = 'special_range' + if issubclass(option, Options.NamedRange): + game_options[option_name]["type"] = 'named_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): game_options[option_name]["value_names"][key] = val @@ -133,7 +133,7 @@ def create(): continue option = world.options_dataclass.type_hints[option_name].from_any(option_value) - if isinstance(option, Options.SpecialRange) and isinstance(option_value, str): + if isinstance(option, Options.NamedRange) and isinstance(option_value, str): assert option_value in option.special_range_names, \ f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \ f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 2bb4a3ba13..37ba7f98ff 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -216,13 +216,13 @@ const buildOptionsTable = (options, romOpts = false) => { element.appendChild(randomButton); break; - case 'special_range': + case 'named_range': element = document.createElement('div'); - element.classList.add('special-range-container'); + element.classList.add('named-range-container'); // Build the select element - let specialRangeSelect = document.createElement('select'); - specialRangeSelect.setAttribute('data-key', option); + let namedRangeSelect = document.createElement('select'); + namedRangeSelect.setAttribute('data-key', option); Object.keys(options[option].value_names).forEach((presetName) => { let presetOption = document.createElement('option'); presetOption.innerText = presetName; @@ -232,58 +232,58 @@ const buildOptionsTable = (options, romOpts = false) => { words[i] = words[i][0].toUpperCase() + words[i].substring(1); } presetOption.innerText = words.join(' '); - specialRangeSelect.appendChild(presetOption); + namedRangeSelect.appendChild(presetOption); }); let customOption = document.createElement('option'); customOption.innerText = 'Custom'; customOption.value = 'custom'; customOption.selected = true; - specialRangeSelect.appendChild(customOption); + namedRangeSelect.appendChild(customOption); if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) { - specialRangeSelect.value = Number(currentOptions[gameName][option]); + namedRangeSelect.value = Number(currentOptions[gameName][option]); } // Build range element - let specialRangeWrapper = document.createElement('div'); - specialRangeWrapper.classList.add('special-range-wrapper'); - let specialRange = document.createElement('input'); - specialRange.setAttribute('type', 'range'); - specialRange.setAttribute('data-key', option); - specialRange.setAttribute('min', options[option].min); - specialRange.setAttribute('max', options[option].max); - specialRange.value = currentOptions[gameName][option]; + let namedRangeWrapper = document.createElement('div'); + namedRangeWrapper.classList.add('named-range-wrapper'); + let namedRange = document.createElement('input'); + namedRange.setAttribute('type', 'range'); + namedRange.setAttribute('data-key', option); + namedRange.setAttribute('min', options[option].min); + namedRange.setAttribute('max', options[option].max); + namedRange.value = currentOptions[gameName][option]; // Build rage value element - let specialRangeVal = document.createElement('span'); - specialRangeVal.classList.add('range-value'); - specialRangeVal.setAttribute('id', `${option}-value`); - specialRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? + let namedRangeVal = document.createElement('span'); + namedRangeVal.classList.add('range-value'); + namedRangeVal.setAttribute('id', `${option}-value`); + namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ? currentOptions[gameName][option] : options[option].defaultValue; // Configure select event listener - specialRangeSelect.addEventListener('change', (event) => { + namedRangeSelect.addEventListener('change', (event) => { if (event.target.value === 'custom') { return; } // Update range slider - specialRange.value = event.target.value; + namedRange.value = event.target.value; document.getElementById(`${option}-value`).innerText = event.target.value; updateGameOption(event.target); }); // Configure range event handler - specialRange.addEventListener('change', (event) => { + namedRange.addEventListener('change', (event) => { // Update select element - specialRangeSelect.value = + namedRangeSelect.value = (Object.values(options[option].value_names).includes(parseInt(event.target.value))) ? parseInt(event.target.value) : 'custom'; document.getElementById(`${option}-value`).innerText = event.target.value; updateGameOption(event.target); }); - element.appendChild(specialRangeSelect); - specialRangeWrapper.appendChild(specialRange); - specialRangeWrapper.appendChild(specialRangeVal); - element.appendChild(specialRangeWrapper); + element.appendChild(namedRangeSelect); + namedRangeWrapper.appendChild(namedRange); + namedRangeWrapper.appendChild(namedRangeVal); + element.appendChild(namedRangeWrapper); // Randomize button randomButton.innerText = '🎲'; @@ -291,15 +291,15 @@ const buildOptionsTable = (options, romOpts = false) => { randomButton.setAttribute('data-key', option); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize( - event, specialRange, specialRangeSelect) + event, namedRange, namedRangeSelect) ); if (currentOptions[gameName][option] === 'random') { randomButton.classList.add('active'); - specialRange.disabled = true; - specialRangeSelect.disabled = true; + namedRange.disabled = true; + namedRangeSelect.disabled = true; } - specialRangeWrapper.appendChild(randomButton); + namedRangeWrapper.appendChild(randomButton); break; default: diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 19928327bb..a2fedb5383 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -93,9 +93,10 @@ class WeightedSettings { }); break; case 'range': - case 'special_range': + case 'named_range': this.current[game][gameSetting]['random'] = 0; this.current[game][gameSetting]['random-low'] = 0; + this.current[game][gameSetting]['random-middle'] = 0; this.current[game][gameSetting]['random-high'] = 0; if (setting.hasOwnProperty('defaultValue')) { this.current[game][gameSetting][setting.defaultValue] = 25; @@ -522,178 +523,185 @@ class GameSettings { break; case 'range': - case 'special_range': + case 'named_range': const rangeTable = document.createElement('table'); const rangeTbody = document.createElement('tbody'); - if (((setting.max - setting.min) + 1) < 11) { - for (let i=setting.min; i <= setting.max; ++i) { - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = i; - tr.appendChild(tdLeft); + const hintText = document.createElement('p'); + hintText.classList.add('hint-text'); + hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + + `below, then press the "Add" button to add a weight for it.

    Accepted values:
    ` + + `Normal range: ${setting.min} - ${setting.max}`; - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${i}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', i); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][i] || 0; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - rangeTbody.appendChild(tr); - } - } else { - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + - `below, then press the "Add" button to add a weight for it.
    Minimum value: ${setting.min}
    ` + - `Maximum value: ${setting.max}`; - - if (setting.hasOwnProperty('value_names')) { - hintText.innerHTML += '

    Certain values have special meaning:'; - Object.keys(setting.value_names).forEach((specialName) => { + const acceptedValuesOutsideRange = []; + if (setting.hasOwnProperty('value_names')) { + Object.keys(setting.value_names).forEach((specialName) => { + if ( + (setting.value_names[specialName] < setting.min) || + (setting.value_names[specialName] > setting.max) + ) { hintText.innerHTML += `
    ${specialName}: ${setting.value_names[specialName]}`; - }); - } - - settingWrapper.appendChild(hintText); - - const addOptionDiv = document.createElement('div'); - addOptionDiv.classList.add('add-option-div'); - const optionInput = document.createElement('input'); - optionInput.setAttribute('id', `${this.name}-${settingName}-option`); - optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); - addOptionDiv.appendChild(optionInput); - const addOptionButton = document.createElement('button'); - addOptionButton.innerText = 'Add'; - addOptionDiv.appendChild(addOptionButton); - settingWrapper.appendChild(addOptionDiv); - optionInput.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + acceptedValuesOutsideRange.push(setting.value_names[specialName]); + } }); - addOptionButton.addEventListener('click', () => { - const optionInput = document.getElementById(`${this.name}-${settingName}-option`); - let option = optionInput.value; - if (!option || !option.trim()) { return; } - option = parseInt(option, 10); - if ((option < setting.min) || (option > setting.max)) { return; } - optionInput.value = ''; - if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); - - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - range.dispatchEvent(new Event('change')); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - - rangeTbody.appendChild(tr); - - // Save new option to settings - range.dispatchEvent(new Event('change')); - }); - - Object.keys(this.current[settingName]).forEach((option) => { - // These options are statically generated below, and should always appear even if they are deleted - // from localStorage - if (['random-low', 'random', 'random-high'].includes(option)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); - - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); - range.setAttribute('data-game', this.name); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); - range.value = this.current[settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - const changeEvent = new Event('change'); - changeEvent.action = 'rangeDelete'; - range.dispatchEvent(changeEvent); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - - rangeTbody.appendChild(tr); + hintText.innerHTML += '

    Certain values have special meaning:'; + Object.keys(setting.value_names).forEach((specialName) => { + hintText.innerHTML += `
    ${specialName}: ${setting.value_names[specialName]}`; }); } - ['random', 'random-low', 'random-high'].forEach((option) => { + settingWrapper.appendChild(hintText); + + const addOptionDiv = document.createElement('div'); + addOptionDiv.classList.add('add-option-div'); + const optionInput = document.createElement('input'); + optionInput.setAttribute('id', `${this.name}-${settingName}-option`); + let placeholderText = `${setting.min} - ${setting.max}`; + acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`); + optionInput.setAttribute('placeholder', placeholderText); + addOptionDiv.appendChild(optionInput); + const addOptionButton = document.createElement('button'); + addOptionButton.innerText = 'Add'; + addOptionDiv.appendChild(addOptionButton); + settingWrapper.appendChild(addOptionDiv); + optionInput.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + }); + + addOptionButton.addEventListener('click', () => { + const optionInput = document.getElementById(`${this.name}-${settingName}-option`); + let option = optionInput.value; + if (!option || !option.trim()) { return; } + option = parseInt(option, 10); + + let optionAcceptable = false; + if ((option > setting.min) && (option < setting.max)) { + optionAcceptable = true; + } + if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){ + optionAcceptable = true; + } + if (!optionAcceptable) { return; } + + optionInput.value = ''; + if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; } + + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + if ( + setting.hasOwnProperty('value_names') && + Object.values(setting.value_names).includes(parseInt(option, 10)) + ) { + const optionName = Object.keys(setting.value_names).find( + (key) => setting.value_names[key] === parseInt(option, 10) + ); + tdLeft.innerText += ` [${optionName}]`; + } + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + range.dispatchEvent(new Event('change')); + rangeTbody.removeChild(tr); + }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); + + rangeTbody.appendChild(tr); + + // Save new option to settings + range.dispatchEvent(new Event('change')); + }); + + Object.keys(this.current[settingName]).forEach((option) => { + // These options are statically generated below, and should always appear even if they are deleted + // from localStorage + if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; } + + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + if ( + setting.hasOwnProperty('value_names') && + Object.values(setting.value_names).includes(parseInt(option, 10)) + ) { + const optionName = Object.keys(setting.value_names).find( + (key) => setting.value_names[key] === parseInt(option, 10) + ); + tdLeft.innerText += ` [${optionName}]`; + } + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${this.name}-${settingName}-${option}-range`); + range.setAttribute('data-game', this.name); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', (evt) => this.#updateRangeSetting(evt)); + range.value = this.current[settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + const changeEvent = new Event('change'); + changeEvent.action = 'rangeDelete'; + range.dispatchEvent(changeEvent); + rangeTbody.removeChild(tr); + }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); + + rangeTbody.appendChild(tr); + }); + + ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); @@ -704,6 +712,9 @@ class GameSettings { case 'random-low': tdLeft.innerText = "Random (Low)"; break; + case 'random-middle': + tdLeft.innerText = 'Random (Middle)'; + break; case 'random-high': tdLeft.innerText = "Random (High)"; break; diff --git a/WebHostLib/static/styles/player-options.css b/WebHostLib/static/styles/player-options.css index 7d6a19709a..cc2d5e2de5 100644 --- a/WebHostLib/static/styles/player-options.css +++ b/WebHostLib/static/styles/player-options.css @@ -160,18 +160,18 @@ html{ margin-left: 0.25rem; } -#player-options table .special-range-container{ +#player-options table .named-range-container{ display: flex; flex-direction: column; } -#player-options table .special-range-wrapper{ +#player-options table .named-range-wrapper{ display: flex; flex-direction: row; margin-top: 0.25rem; } -#player-options table .special-range-wrapper input[type=range]{ +#player-options table .named-range-wrapper input[type=range]{ flex-grow: 1; } diff --git a/docs/options api.md b/docs/options api.md index 622d0a7ec7..80d0737e3a 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -144,13 +144,20 @@ A numeric option allowing a variety of integers including the endpoints. Has a d `range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string comparisons. -### SpecialRange +### NamedRange Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value. +`special_range_names` can be used to +- give descriptive names to certain values from within the range +- add option values above or below the regular range, to be associated with a special meaning + For example: ```python +range_start = 1 +range_end = 99 special_range_names: { "normal": 20, "extreme": 99, + "unlimited": -1, } ``` diff --git a/docs/world api.md b/docs/world api.md index 71710ac293..6393f245ba 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -79,9 +79,9 @@ the options and the values are the values to be set for that option. These prese Note: The values must be a non-aliased value for the option type and can only include the following option types: - - If you have a `Range`/`SpecialRange` option, the value should be an `int` between the `range_start` and `range_end` + - If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` values. - - If you have a `SpecialRange` option, the value can alternatively be a `str` that is one of the + - If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the `special_range_names` keys. - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index 8c6ebea208..0c88b6c2ee 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,7 +1,7 @@ import unittest from worlds import AutoWorldRegister -from Options import Choice, SpecialRange, Toggle, Range +from Options import Choice, NamedRange, Toggle, Range class TestOptionPresets(unittest.TestCase): @@ -14,7 +14,7 @@ class TestOptionPresets(unittest.TestCase): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) - supported_types = [Choice, Toggle, Range, SpecialRange] + supported_types = [Choice, Toggle, Range, NamedRange] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " f"is not a supported type for webhost. " @@ -46,8 +46,8 @@ class TestOptionPresets(unittest.TestCase): # Check for from_text resolving to a different value. ("random" is allowed though.) if option_value != "random" and isinstance(option_value, str): - # Allow special named values for SpecialRange option presets. - if isinstance(option, SpecialRange): + # Allow special named values for NamedRange option presets. + if isinstance(option, NamedRange): self.assertTrue( option_value in option.special_range_names, f"Invalid preset '{option_name}': '{option_value}' in preset '{preset_name}' " diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py index ce728b4e92..769acbec15 100644 --- a/worlds/dlcquest/Options.py +++ b/worlds/dlcquest/Options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange +from Options import Choice, DeathLink, NamedRange, PerGameCommonOptions class DoubleJumpGlitch(Choice): @@ -33,7 +33,7 @@ class CoinSanity(Choice): default = 0 -class CoinSanityRange(SpecialRange): +class CoinSanityRange(NamedRange): """This is the amount of coins in a coin bundle You need to collect that number of coins to get a location check, and when receiving coin items, you will get bundles of this size It is highly recommended to not set this value below 10, as it generates a very large number of boring locations and items. diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index d0a5c0ed7d..3e9acac7e7 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -1,7 +1,7 @@ from typing import Dict from BaseClasses import MultiWorld -from Options import SpecialRange +from Options import NamedRange from .option_names import options_to_include from .checks.world_checks import assert_can_win, assert_same_number_items_locations from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld @@ -14,7 +14,7 @@ def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 2a19ffd3e7..fcc938474d 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -2,7 +2,7 @@ import typing from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -242,7 +242,7 @@ class MaximumGeoPrice(Range): default = 400 -class RandomCharmCosts(SpecialRange): +class RandomCharmCosts(NamedRange): """Total Notch Cost of all Charms together. Vanilla sums to 90. This value is distributed among all charms in a random fashion. Special Cases: @@ -250,7 +250,7 @@ class RandomCharmCosts(SpecialRange): Set to -2 or shuffle to shuffle around the vanilla costs to different charms.""" display_name = "Randomize Charm Notch Costs" - range_start = -2 + range_start = 0 range_end = 240 default = -1 vanilla_costs: typing.List[int] = vanilla_costs diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py index 419532cded..5f33d0bd5d 100644 --- a/worlds/lufia2ac/Options.py +++ b/worlds/lufia2ac/Options.py @@ -7,8 +7,8 @@ from dataclasses import dataclass from itertools import accumulate, chain, combinations from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union -from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \ - SpecialRange, TextChoice, Toggle +from Options import AssembleOptions, Choice, DeathLink, ItemDict, NamedRange, OptionDict, PerGameCommonOptions, Range, \ + TextChoice, Toggle from .Enemies import enemy_name_to_sprite from .Items import ItemType, l2ac_item_table @@ -255,7 +255,7 @@ class CapsuleCravingsJPStyle(Toggle): display_name = "Capsule cravings JP style" -class CapsuleStartingForm(SpecialRange): +class CapsuleStartingForm(NamedRange): """The starting form of your capsule monsters. Supported values: 1 – 4, m @@ -266,7 +266,6 @@ class CapsuleStartingForm(SpecialRange): range_start = 1 range_end = 5 default = 1 - special_range_cutoff = 1 special_range_names = { "default": 1, "m": 5, @@ -280,7 +279,7 @@ class CapsuleStartingForm(SpecialRange): return self.value - 1 -class CapsuleStartingLevel(LevelMixin, SpecialRange): +class CapsuleStartingLevel(LevelMixin, NamedRange): """The starting level of your capsule monsters. Can be set to the special value party_starting_level to make it the same value as the party_starting_level option. @@ -289,10 +288,9 @@ class CapsuleStartingLevel(LevelMixin, SpecialRange): """ display_name = "Capsule monster starting level" - range_start = 0 + range_start = 1 range_end = 99 default = 1 - special_range_cutoff = 1 special_range_names = { "default": 1, "party_starting_level": 0, @@ -685,7 +683,7 @@ class RunSpeed(Choice): default = option_disabled -class ShopInterval(SpecialRange): +class ShopInterval(NamedRange): """Place shops after a certain number of floors. E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc., @@ -698,10 +696,9 @@ class ShopInterval(SpecialRange): """ display_name = "Shop interval" - range_start = 0 + range_start = 1 range_end = 10 default = 0 - special_range_cutoff = 1 special_range_names = { "disabled": 0, } diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 794977d32d..8afe91b867 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,4 @@ -from Options import Toggle, Choice, Range, SpecialRange, TextChoice, DeathLink +from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink class GameVersion(Choice): @@ -285,7 +285,7 @@ class AllPokemonSeen(Toggle): display_name = "All Pokemon Seen" -class DexSanity(SpecialRange): +class DexSanity(NamedRange): """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage @@ -412,7 +412,7 @@ class LevelScaling(Choice): default = 1 -class ExpModifier(SpecialRange): +class ExpModifier(NamedRange): """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" display_name = "Exp Modifier" default = 16 @@ -607,8 +607,8 @@ class RandomizeTMMoves(Toggle): display_name = "Randomize TM Moves" -class TMHMCompatibility(SpecialRange): - range_start = -1 +class TMHMCompatibility(NamedRange): + range_start = 0 range_end = 100 special_range_names = { "vanilla": -1, @@ -675,12 +675,12 @@ class RandomizeMoveTypes(Toggle): default = 0 -class SecondaryTypeChance(SpecialRange): +class SecondaryTypeChance(NamedRange): """If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types to Pokemon that normally have a secondary type.""" display_name = "Secondary Type Chance" - range_start = -1 + range_start = 0 range_end = 100 default = -1 special_range_names = { diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index d85bbf06f6..267ebd7a63 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict -from Options import Range, SpecialRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option +from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, Option from .mods.mod_data import ModNames @@ -48,12 +48,12 @@ class Goal(Choice): return super().get_option_name(value) -class StartingMoney(SpecialRange): +class StartingMoney(NamedRange): """Amount of gold when arriving at the farm. Set to -1 or unlimited for infinite money""" internal_name = "starting_money" display_name = "Starting Gold" - range_start = -1 + range_start = 0 range_end = 50000 default = 5000 @@ -67,7 +67,7 @@ class StartingMoney(SpecialRange): } -class ProfitMargin(SpecialRange): +class ProfitMargin(NamedRange): """Multiplier over all gold earned in-game by the player.""" internal_name = "profit_margin" display_name = "Profit Margin" @@ -283,7 +283,7 @@ class SpecialOrderLocations(Choice): option_board_qi = 2 -class HelpWantedLocations(SpecialRange): +class HelpWantedLocations(NamedRange): """Include location checks for Help Wanted quests Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" @@ -429,7 +429,7 @@ class MultipleDaySleepEnabled(Toggle): default = 1 -class MultipleDaySleepCost(SpecialRange): +class MultipleDaySleepCost(NamedRange): """How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped.""" internal_name = "multiple_day_sleep_cost" display_name = "Multiple Day Sleep Cost" @@ -446,7 +446,7 @@ class MultipleDaySleepCost(SpecialRange): } -class ExperienceMultiplier(SpecialRange): +class ExperienceMultiplier(NamedRange): """How fast you want to earn skill experience. A lower setting mean less experience. A higher setting means more experience.""" @@ -466,7 +466,7 @@ class ExperienceMultiplier(SpecialRange): } -class FriendshipMultiplier(SpecialRange): +class FriendshipMultiplier(NamedRange): """How fast you want to earn friendship points with villagers. A lower setting mean less friendship per action. A higher setting means more friendship per action.""" diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 02b1ebf643..ccffc2848a 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -4,7 +4,7 @@ from random import random from typing import Dict from BaseClasses import ItemClassification, MultiWorld -from Options import SpecialRange +from Options import NamedRange from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table @@ -42,7 +42,7 @@ def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options @@ -53,7 +53,7 @@ class TestGenerateDynamicOptions(SVTestCase): def test_given_special_range_when_generate_then_basic_checks(self): options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): - if not isinstance(option, SpecialRange): + if not isinstance(option, NamedRange): continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): @@ -152,7 +152,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_special_range_when_generate_exclude_ginger_island(self): options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): - if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: + if not isinstance(option, NamedRange) or option_name == ExcludeGingerIsland.internal_name: continue for value in option.special_range_names: with self.subTest(f"{option_name}: {value}"): diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index 3634dc5fd1..e3da6968ed 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -2,7 +2,7 @@ import unittest from typing import Dict from BaseClasses import MultiWorld -from Options import SpecialRange +from Options import NamedRange from .option_names import options_to_include from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations from .. import setup_solo_multiworld, SVTestCase @@ -14,7 +14,7 @@ def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names elif option.options: return option.options diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index e22c6c3564..1f1d59652c 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -2,7 +2,7 @@ from typing import Dict import random from BaseClasses import MultiWorld -from Options import SpecialRange, Range +from Options import NamedRange, Range from .option_names import options_to_include from .. import setup_solo_multiworld, SVTestCase from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid @@ -12,7 +12,7 @@ from ..checks.world_checks import assert_same_number_items_locations, assert_vic def get_option_choices(option) -> Dict[str, int]: - if issubclass(option, SpecialRange): + if issubclass(option, NamedRange): return option.special_range_names if issubclass(option, Range): return {f"{val}": val for val in range(option.range_start, option.range_end + 1)} diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 80f9469ec8..cb861e9621 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 -from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice +from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice from zilliandomizer.options import \ Options as ZzOptions, char_to_gun, char_to_jump, ID, \ @@ -11,7 +11,7 @@ from zilliandomizer.options import \ from zilliandomizer.options.parsing import validate as zz_validate -class ZillionContinues(SpecialRange): +class ZillionContinues(NamedRange): """ number of continues before game over @@ -218,7 +218,7 @@ class ZillionSkill(Range): default = 2 -class ZillionStartingCards(SpecialRange): +class ZillionStartingCards(NamedRange): """ how many ID Cards to start the game with From e46420f4a9f0676a5f93db573515f73b0e4c3778 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 24 Nov 2023 17:14:07 -0600 Subject: [PATCH 220/327] MultiServer: Create read-only data storage key for client statuses. (#2412) --- MultiServer.py | 11 ++++++++++- docs/network protocol.md | 13 +++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index bd9d2446af..9d2e9b564e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2,8 +2,8 @@ from __future__ import annotations import argparse import asyncio -import copy import collections +import copy import datetime import functools import hashlib @@ -417,6 +417,8 @@ class Context: self.player_name_lookup[slot_info.name] = 0, slot_id self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ list(self.get_rechecked_hints(local_team, local_player)) + self.read_data[f"client_status_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ + self.client_game_state[local_team, local_player] self.seed_name = decoded_obj["seed_name"] self.random.seed(self.seed_name) @@ -712,6 +714,12 @@ class Context: "hint_points": get_slot_points(self, team, slot) }]) + def on_client_status_change(self, team: int, slot: int): + key: str = f"_read_client_status_{team}_{slot}" + targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) + if targets: + self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.client_game_state[team, slot]}]) + def update_aliases(ctx: Context, team: int): cmd = ctx.dumper([{"cmd": "RoomUpdate", @@ -1819,6 +1827,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) ctx.on_goal_achieved(client) ctx.client_game_state[client.team, client.slot] = new_status + ctx.on_client_status_change(client.team, client.slot) ctx.save() diff --git a/docs/network protocol.md b/docs/network protocol.md index c17cc74a8a..199f96f481 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -380,11 +380,12 @@ Additional arguments sent in this package will also be added to the [Retrieved]( Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. -| Name | Type | Notes | -|-------------------------------|--------------------------|---------------------------------------------------| -| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | -| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | -| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| Name | Type | Notes | +|------------------------------|-------------------------------|---------------------------------------------------| +| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | +| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | +| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. @@ -558,7 +559,7 @@ Color options: `player` marks owning player id for location/item, `flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item -### Client States +### ClientStatus An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate). The MultiServer automatically sets the client state to `ClientStatus.CLIENT_CONNECTED` on the first active connection From 8173fd54e7ceead44a20ac7ddf32b3944733bf52 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 24 Nov 2023 17:16:19 -0600 Subject: [PATCH 221/327] DOOM II: Add to `CODEOWNERS` (#2492) --- docs/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 83f4723532..b40078dc34 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,6 +46,9 @@ # DOOM 1993 /worlds/doom_1993/ @Daivuk +# DOOM II +/worlds/doom_ii/ @Daivuk + # Factorio /worlds/factorio/ @Berserker66 From 8d41430cc8d3509cd86805af8b4924fe3484ae91 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:23:45 -0700 Subject: [PATCH 222/327] Shivers: Implement New Game (#1836) Co-authored-by: Mathx2 Co-authored-by: Zach Parks --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/shivers/Constants.py | 17 + worlds/shivers/Items.py | 112 +++++++ worlds/shivers/Options.py | 50 +++ worlds/shivers/Rules.py | 228 ++++++++++++++ worlds/shivers/__init__.py | 178 +++++++++++ worlds/shivers/data/excluded_locations.json | 52 ++++ worlds/shivers/data/locations.json | 325 ++++++++++++++++++++ worlds/shivers/data/regions.json | 145 +++++++++ worlds/shivers/docs/en_Shivers.md | 31 ++ worlds/shivers/docs/setup_en.md | 60 ++++ 12 files changed, 1202 insertions(+) create mode 100644 worlds/shivers/Constants.py create mode 100644 worlds/shivers/Items.py create mode 100644 worlds/shivers/Options.py create mode 100644 worlds/shivers/Rules.py create mode 100644 worlds/shivers/__init__.py create mode 100644 worlds/shivers/data/excluded_locations.json create mode 100644 worlds/shivers/data/locations.json create mode 100644 worlds/shivers/data/regions.json create mode 100644 worlds/shivers/docs/en_Shivers.md create mode 100644 worlds/shivers/docs/setup_en.md diff --git a/README.md b/README.md index a57f0f9802..2bca422fea 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Currently, the following games are supported: * Lingo * Pokémon Emerald * DOOM II +* Shivers For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index b40078dc34..6231da8232 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -113,6 +113,9 @@ # Risk of Rain 2 /worlds/ror2/ @kindasneaki +# Shivers +/worlds/shivers/ @GodlFire + # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace diff --git a/worlds/shivers/Constants.py b/worlds/shivers/Constants.py new file mode 100644 index 0000000000..0b00cecec3 --- /dev/null +++ b/worlds/shivers/Constants.py @@ -0,0 +1,17 @@ +import os +import json +import pkgutil + +def load_data_file(*args) -> dict: + fname = os.path.join("data", *args) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + +location_id_offset: int = 27000 + +location_info = load_data_file("locations.json") +location_name_to_id = {name: location_id_offset + index \ + for index, name in enumerate(location_info["all_locations"])} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py new file mode 100644 index 0000000000..caf24ded29 --- /dev/null +++ b/worlds/shivers/Items.py @@ -0,0 +1,112 @@ +from BaseClasses import Item, ItemClassification +import typing + +class ShiversItem(Item): + game: str = "Shivers" + +class ItemData(typing.NamedTuple): + code: int + type: str + classification: ItemClassification = ItemClassification.progression + +SHIVERS_ITEM_ID_OFFSET = 27000 + +item_table = { + #Pot Pieces + "Water Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 0, "pot"), + "Wax Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 1, "pot"), + "Ash Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 2, "pot"), + "Oil Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 3, "pot"), + "Cloth Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 4, "pot"), + "Wood Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 5, "pot"), + "Crystal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 6, "pot"), + "Lightning Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 7, "pot"), + "Sand Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 8, "pot"), + "Metal Pot Bottom": ItemData(SHIVERS_ITEM_ID_OFFSET + 9, "pot"), + "Water Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 10, "pot"), + "Wax Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 11, "pot"), + "Ash Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 12, "pot"), + "Oil Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 13, "pot"), + "Cloth Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 14, "pot"), + "Wood Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 15, "pot"), + "Crystal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 16, "pot"), + "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"), + "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"), + "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"), + + #Keys + "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "key"), + "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "key"), + "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "key"), + "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "key"), + "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "key"), + "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "key"), + "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "key"), + "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "key"), + "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "key"), + "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "key"), + "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), + "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), + "Key for Tiki Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), + "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), + "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), + "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), + "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), + "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), + "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), + "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key-optional"), + + #Abilities + "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"), + + #Event Items + "Victory": ItemData(SHIVERS_ITEM_ID_OFFSET + 60, "victory"), + + #Duplicate pot pieces for fill_Restrictive + "Water Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 70, "potduplicate"), + "Wax Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 71, "potduplicate"), + "Ash Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 72, "potduplicate"), + "Oil Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 73, "potduplicate"), + "Cloth Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 74, "potduplicate"), + "Wood Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 75, "potduplicate"), + "Crystal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 76, "potduplicate"), + "Lightning Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 77, "potduplicate"), + "Sand Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 78, "potduplicate"), + "Metal Pot Bottom DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 79, "potduplicate"), + "Water Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 80, "potduplicate"), + "Wax Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 81, "potduplicate"), + "Ash Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 82, "potduplicate"), + "Oil Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 83, "potduplicate"), + "Cloth Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 84, "potduplicate"), + "Wood Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 85, "potduplicate"), + "Crystal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 86, "potduplicate"), + "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"), + "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"), + "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"), + + #Filler + "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"), + "Easier Lyre": ItemData(SHIVERS_ITEM_ID_OFFSET + 91, "filler", ItemClassification.filler), + "Water Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 92, "filler2", ItemClassification.filler), + "Wax Always Available in Library": ItemData(SHIVERS_ITEM_ID_OFFSET + 93, "filler2", ItemClassification.filler), + "Wax Always Available in Anansi Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 94, "filler2", ItemClassification.filler), + "Wax Always Available in Tiki Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 95, "filler2", ItemClassification.filler), + "Ash Always Available in Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 96, "filler2", ItemClassification.filler), + "Ash Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 97, "filler2", ItemClassification.filler), + "Oil Always Available in Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 98, "filler2", ItemClassification.filler), + "Cloth Always Available in Egypt": ItemData(SHIVERS_ITEM_ID_OFFSET + 99, "filler2", ItemClassification.filler), + "Cloth Always Available in Burial Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 100, "filler2", ItemClassification.filler), + "Wood Always Available in Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 101, "filler2", ItemClassification.filler), + "Wood Always Available in Blue Maze": ItemData(SHIVERS_ITEM_ID_OFFSET + 102, "filler2", ItemClassification.filler), + "Wood Always Available in Pegasus Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 103, "filler2", ItemClassification.filler), + "Wood Always Available in Gods Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 104, "filler2", ItemClassification.filler), + "Crystal Always Available in Lobby": ItemData(SHIVERS_ITEM_ID_OFFSET + 105, "filler2", ItemClassification.filler), + "Crystal Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 106, "filler2", ItemClassification.filler), + "Sand Always Available in Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 107, "filler2", ItemClassification.filler), + "Sand Always Available in Ocean": ItemData(SHIVERS_ITEM_ID_OFFSET + 108, "filler2", ItemClassification.filler), + "Metal Always Available in Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 109, "filler2", ItemClassification.filler), + "Metal Always Available in Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 110, "filler2", ItemClassification.filler), + "Metal Always Available in Prehistoric": ItemData(SHIVERS_ITEM_ID_OFFSET + 111, "filler2", ItemClassification.filler), + "Heal": ItemData(SHIVERS_ITEM_ID_OFFSET + 112, "filler3", ItemClassification.filler) + +} diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py new file mode 100644 index 0000000000..6d18804069 --- /dev/null +++ b/worlds/shivers/Options.py @@ -0,0 +1,50 @@ +from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions +from dataclasses import dataclass + + +class LobbyAccess(Choice): + """Chooses how keys needed to reach the lobby are placed. + - Normal: Keys are placed anywhere + - Early: Keys are placed early + - Local: Keys are placed locally""" + display_name = "Lobby Access" + option_normal = 0 + option_early = 1 + option_local = 2 + +class PuzzleHintsRequired(DefaultOnToggle): + """If turned on puzzle hints will be available before the corresponding puzzle is required. For example: The Tiki + Drums puzzle will be placed after access to the security cameras which give you the solution. Turning this off + allows for greater randomization.""" + display_name = "Puzzle Hints Required" + +class InformationPlaques(Toggle): + """Adds Information Plaques as checks.""" + display_name = "Include Information Plaques" + +class FrontDoorUsable(Toggle): + """Adds a key to unlock the front door of the museum.""" + display_name = "Front Door Usable" + +class ElevatorsStaySolved(DefaultOnToggle): + """Adds elevators as checks and will remain open upon solving them.""" + display_name = "Elevators Stay Solved" + +class EarlyBeth(DefaultOnToggle): + """Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.""" + display_name = "Early Beth" + +class EarlyLightning(Toggle): + """Allows lightning to be captured at any point in the game. You will still need to capture all ten Ixupi for victory.""" + display_name = "Early Lightning" + + +@dataclass +class ShiversOptions(PerGameCommonOptions): + lobby_access: LobbyAccess + puzzle_hints_required: PuzzleHintsRequired + include_information_plaques: InformationPlaques + front_door_usable: FrontDoorUsable + elevators_stay_solved: ElevatorsStaySolved + early_beth: EarlyBeth + early_lightning: EarlyLightning diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py new file mode 100644 index 0000000000..fdd260ca91 --- /dev/null +++ b/worlds/shivers/Rules.py @@ -0,0 +1,228 @@ +from typing import Dict, List, TYPE_CHECKING +from collections.abc import Callable +from BaseClasses import CollectionState +from worlds.generic.Rules import forbid_item + +if TYPE_CHECKING: + from . import ShiversWorld + + +def water_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Lobby", "Region", player) or (state.can_reach("Janitor Closet", "Region", player) and cloth_capturable(state, player))) \ + and state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) + + +def wax_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Library", "Region", player) or state.can_reach("Anansi", "Region", player)) \ + and state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) + + +def ash_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Office", "Region", player) or state.can_reach("Burial", "Region", player)) \ + and state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) + + +def oil_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Prehistoric", "Region", player) or state.can_reach("Tar River", "Region", player)) \ + and state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) + + +def cloth_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Egypt", "Region", player) or state.can_reach("Burial", "Region", player) or state.can_reach("Janitor Closet", "Region", player)) \ + and state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) + + +def wood_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Workshop", "Region", player) or state.can_reach("Blue Maze", "Region", player) or state.can_reach("Gods Room", "Region", player) or state.can_reach("Anansi", "Region", player)) \ + and state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) + + +def crystal_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Lobby", "Region", player) or state.can_reach("Ocean", "Region", player)) \ + and state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) + + +def sand_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Greenhouse", "Region", player) or state.can_reach("Ocean", "Region", player)) \ + and state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) + + +def metal_capturable(state: CollectionState, player: int) -> bool: + return (state.can_reach("Projector Room", "Region", player) or state.can_reach("Prehistoric", "Region", player) or state.can_reach("Bedroom", "Region", player)) \ + and state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) + + +def lightning_capturable(state: CollectionState, player: int) -> bool: + return (first_nine_ixupi_capturable or state.multiworld.early_lightning[player].value) \ + and state.can_reach("Generator", "Region", player) \ + and state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) + + +def beths_body_available(state: CollectionState, player: int) -> bool: + return (first_nine_ixupi_capturable(state, player) or state.multiworld.early_beth[player].value) \ + and state.can_reach("Generator", "Region", player) + + +def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool: + return water_capturable(state, player) and wax_capturable(state, player) \ + and ash_capturable(state, player) and oil_capturable(state, player) \ + and cloth_capturable(state, player) and wood_capturable(state, player) \ + and crystal_capturable(state, player) and sand_capturable(state, player) \ + and metal_capturable(state, player) + + +def get_rules_lookup(player: int): + rules_lookup: Dict[str, List[Callable[[CollectionState], bool]]] = { + "entrances": { + "To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player), + "To Office Elevator From Office": lambda state: state.has("Key for Office Elevator", player), + "To Bedroom Elevator From Office": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player), + "To Office From Bedroom Elevator": lambda state: state.has_all({"Key for Bedroom Elevator", "Crawling"}, player), + "To Three Floor Elevator From Maintenance Tunnels": lambda state: state.has("Key for Three Floor Elevator", player), + "To Three Floor Elevator From Blue Maze Bottom": lambda state: state.has("Key for Three Floor Elevator", player), + "To Three Floor Elevator From Blue Maze Top": lambda state: state.has("Key for Three Floor Elevator", player), + "To Workshop": lambda state: state.has("Key for Workshop", player), + "To Lobby From Office": lambda state: state.has("Key for Office", player), + "To Office From Lobby": lambda state: state.has("Key for Office", player), + "To Library From Lobby": lambda state: state.has("Key for Library Room", player), + "To Lobby From Library": lambda state: state.has("Key for Library Room", player), + "To Prehistoric From Lobby": lambda state: state.has("Key for Prehistoric Room", player), + "To Lobby From Prehistoric": lambda state: state.has("Key for Prehistoric Room", player), + "To Greenhouse": lambda state: state.has("Key for Greenhouse Room", player), + "To Ocean From Prehistoric": lambda state: state.has("Key for Ocean Room", player), + "To Prehistoric From Ocean": lambda state: state.has("Key for Ocean Room", player), + "To Projector Room": lambda state: state.has("Key for Projector Room", player), + "To Generator": lambda state: state.has("Key for Generator Room", player), + "To Lobby From Egypt": lambda state: state.has("Key for Egypt Room", player), + "To Egypt From Lobby": lambda state: state.has("Key for Egypt Room", player), + "To Janitor Closet": lambda state: state.has("Key for Janitor Closet", player), + "To Tiki From Burial": lambda state: state.has("Key for Tiki Room", player), + "To Burial From Tiki": lambda state: state.has("Key for Tiki Room", player), + "To Inventions From UFO": lambda state: state.has("Key for UFO Room", player), + "To UFO From Inventions": lambda state: state.has("Key for UFO Room", player), + "To Torture From Inventions": lambda state: state.has("Key for Torture Room", player), + "To Inventions From Torture": lambda state: state.has("Key for Torture Room", player), + "To Torture": lambda state: state.has("Key for Puzzle Room", player), + "To Puzzle Room Mastermind From Torture": lambda state: state.has("Key for Puzzle Room", player), + "To Bedroom": lambda state: state.has("Key for Bedroom", player), + "To Underground Lake From Underground Tunnels": lambda state: state.has("Key for Underground Lake Room", player), + "To Underground Tunnels From Underground Lake": lambda state: state.has("Key for Underground Lake Room", player), + "To Outside From Lobby": lambda state: state.has("Key for Front Door", player), + "To Lobby From Outside": lambda state: state.has("Key for Front Door", player), + "To Maintenance Tunnels From Theater Back Hallways": lambda state: state.has("Crawling", player), + "To Blue Maze From Egypt": lambda state: state.has("Crawling", player), + "To Egypt From Blue Maze": lambda state: state.has("Crawling", player), + "To Lobby From Tar River": lambda state: (state.has("Crawling", player) and oil_capturable(state, player)), + "To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)), + "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), + "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), + "To Slide Room": lambda state: ( + state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region",player) and + state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) and + state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)), + "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) + }, + "locations_required": { + "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player), + "Accessible: Storage: Janitor Closet": lambda state: cloth_capturable(state, player), + "Accessible: Storage: Tar River": lambda state: oil_capturable(state, player), + "Accessible: Storage: Theater": lambda state: state.can_reach("Projector Room", "Region", player), + "Accessible: Storage: Slide": lambda state: beths_body_available(state, player) and state.can_reach("Slide Room", "Region", player), + "Ixupi Captured Water": lambda state: water_capturable(state, player), + "Ixupi Captured Wax": lambda state: wax_capturable(state, player), + "Ixupi Captured Ash": lambda state: ash_capturable(state, player), + "Ixupi Captured Oil": lambda state: oil_capturable(state, player), + "Ixupi Captured Cloth": lambda state: cloth_capturable(state, player), + "Ixupi Captured Wood": lambda state: wood_capturable(state, player), + "Ixupi Captured Crystal": lambda state: crystal_capturable(state, player), + "Ixupi Captured Sand": lambda state: sand_capturable(state, player), + "Ixupi Captured Metal": lambda state: metal_capturable(state, player), + "Final Riddle: Planets Aligned": lambda state: state.can_reach("Fortune Teller", "Region", player), + "Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)), + "Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player), + "Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player), + }, + "locations_puzzle_hints": { + "Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player), + "Puzzle Solved Clock Chains": lambda state: state.can_reach("Bedroom", "Region", player), + "Puzzle Solved Tiki Drums": lambda state: state.can_reach("Clock Tower", "Region", player), + "Puzzle Solved Red Door": lambda state: state.can_reach("Maintenance Tunnels", "Region", player), + "Puzzle Solved UFO Symbols": lambda state: state.can_reach("Library", "Region", player), + "Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player), + "Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player), + "Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player), + "Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player) + }, + "elevators": { + "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) + and state.has("Key for Office Elevator", player))), + "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), + "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + and state.has("Key for Three Floor Elevator", player))) + }, + "lightning": { + "Ixupi Captured Lightning": lambda state: lightning_capturable(state, player) + } + } + return rules_lookup + + +def set_rules(world: "ShiversWorld") -> None: + multiworld = world.multiworld + player = world.player + + rules_lookup = get_rules_lookup(player) + # Set required entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set required location rules + for location_name, rule in rules_lookup["locations_required"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set option location rules + if world.options.puzzle_hints_required.value: + for location_name, rule in rules_lookup["locations_puzzle_hints"].items(): + multiworld.get_location(location_name, player).access_rule = rule + if world.options.elevators_stay_solved.value: + for location_name, rule in rules_lookup["elevators"].items(): + multiworld.get_location(location_name, player).access_rule = rule + if world.options.early_lightning.value: + for location_name, rule in rules_lookup["lightning"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # forbid cloth in janitor closet and oil in tar river + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player) + + # Filler Item Forbids + forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player) + forbid_item(multiworld.get_location("Ixupi Captured Water", player), "Water Always Available in Lobby", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Library", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Anansi Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wax", player), "Wax Always Available in Tiki Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Office", player) + forbid_item(multiworld.get_location("Ixupi Captured Ash", player), "Ash Always Available in Burial Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Oil", player), "Oil Always Available in Prehistoric Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Egypt", player) + forbid_item(multiworld.get_location("Ixupi Captured Cloth", player), "Cloth Always Available in Burial Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Workshop", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Blue Maze", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Pegasus Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Wood", player), "Wood Always Available in Gods Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Lobby", player) + forbid_item(multiworld.get_location("Ixupi Captured Crystal", player), "Crystal Always Available in Ocean", player) + forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Plants Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Sand", player), "Sand Always Available in Ocean", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Projector Room", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Bedroom", player) + forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player) + + # Set completion condition + multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) + + + + diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py new file mode 100644 index 0000000000..e43e91fb5a --- /dev/null +++ b/worlds/shivers/__init__.py @@ -0,0 +1,178 @@ +from .Items import item_table, ShiversItem +from .Rules import set_rules +from BaseClasses import Item, Tutorial, Region, Location +from Fill import fill_restrictive +from worlds.AutoWorld import WebWorld, World +from . import Constants, Rules +from .Options import ShiversOptions + + +class ShiversWeb(WebWorld): + tutorials = [Tutorial( + "Shivers Setup Guide", + "A guide to setting up Shivers for Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["GodlFire", "Mathx2"] + )] + +class ShiversWorld(World): + """ + Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual. + """ + + game: str = "Shivers" + topology_present = False + web = ShiversWeb() + options_dataclass = ShiversOptions + options: ShiversOptions + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = Constants.location_name_to_id + + def create_item(self, name: str) -> Item: + data = item_table[name] + return ShiversItem(name, data.classification, data.code, self.player) + + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = ShiversLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) + + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + self.multiworld.regions.append(r) + for exit_name in exits: + r.create_exit(exit_name) + + + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) + + # Locations + # Build exclusion list + self.removed_locations = set() + if not self.options.include_information_plaques: + self.removed_locations.update(Constants.exclusion_info["plaques"]) + if not self.options.elevators_stay_solved: + self.removed_locations.update(Constants.exclusion_info["elevators"]) + if not self.options.early_lightning: + self.removed_locations.update(Constants.exclusion_info["lightning"]) + + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + if loc_name not in self.removed_locations: + loc = ShiversLocation(self.player, loc_name, self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) + + def create_items(self) -> None: + #Add items to item pool + itempool = [] + for name, data in item_table.items(): + if data.type in {"pot", "key", "ability", "filler2"}: + itempool.append(self.create_item(name)) + + #Add Filler + itempool += [self.create_item("Easier Lyre") for i in range(9)] + + #Extra filler is random between Heals and Easier Lyre. Heals weighted 95%. + filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool) + itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)] + + + #Place library escape items. Choose a location to place the escape item + library_region = self.multiworld.get_region("Library", self.player) + librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")]) + + #Roll for which escape items will be placed in the Library + library_random = self.random.randint(1, 3) + if library_random == 1: + librarylocation.place_locked_item(self.create_item("Crawling")) + + itempool = [item for item in itempool if item.name != "Crawling"] + + elif library_random == 2: + librarylocation.place_locked_item(self.create_item("Key for Library Room")) + + itempool = [item for item in itempool if item.name != "Key for Library Room"] + elif library_random == 3: + librarylocation.place_locked_item(self.create_item("Key for Three Floor Elevator")) + + librarylocationkeytwo = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:") and loc != librarylocation]) + librarylocationkeytwo.place_locked_item(self.create_item("Key for Egypt Room")) + + itempool = [item for item in itempool if item.name not in ["Key for Three Floor Elevator", "Key for Egypt Room"]] + + #If front door option is on, determine which set of keys will be used for lobby access and add front door key to item pool + lobby_access_keys = 1 + if self.options.front_door_usable: + lobby_access_keys = self.random.randint(1, 2) + itempool += [self.create_item("Key for Front Door")] + else: + itempool += [self.create_item("Heal")] + + self.multiworld.itempool += itempool + + #Lobby acess: + if self.options.lobby_access == 1: + if lobby_access_keys == 1: + self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1 + self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1 + self.multiworld.early_items[self.player]["Key for Office"] = 1 + elif lobby_access_keys == 2: + self.multiworld.early_items[self.player]["Key for Front Door"] = 1 + if self.options.lobby_access == 2: + if lobby_access_keys == 1: + self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1 + self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1 + self.multiworld.local_early_items[self.player]["Key for Office"] = 1 + elif lobby_access_keys == 2: + self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1 + + def pre_fill(self) -> None: + # Prefills event storage locations with duplicate pots + storagelocs = [] + storageitems = [] + self.storage_placements = [] + + for locations in Constants.location_info["locations_by_region"].values(): + for loc_name in locations: + if loc_name.startswith("Accessible: "): + storagelocs.append(self.multiworld.get_location(loc_name, self.player)) + + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + storageitems += [self.create_item("Empty") for i in range(3)] + + state = self.multiworld.get_all_state(True) + + self.random.shuffle(storagelocs) + self.random.shuffle(storageitems) + + fill_restrictive(self.multiworld, state, storagelocs.copy(), storageitems, True, True) + + self.storage_placements = {location.name: location.item.name for location in storagelocs} + + set_rules = set_rules + + def fill_slot_data(self) -> dict: + + return { + "storageplacements": self.storage_placements, + "excludedlocations": {str(excluded_location).replace('ExcludeLocations(', '').replace(')', '') for excluded_location in self.multiworld.exclude_locations.values()}, + "elevatorsstaysolved": {self.options.elevators_stay_solved.value}, + "earlybeth": {self.options.early_beth.value}, + "earlylightning": {self.options.early_lightning.value}, + } + + +class ShiversLocation(Location): + game = "Shivers" diff --git a/worlds/shivers/data/excluded_locations.json b/worlds/shivers/data/excluded_locations.json new file mode 100644 index 0000000000..6ed625077a --- /dev/null +++ b/worlds/shivers/data/excluded_locations.json @@ -0,0 +1,52 @@ +{ + "plaques": [ + "Information Plaque: Transforming Masks (Lobby)", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Cremation (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Guillotine (Torture)", + "Information Plaque: Aliens (UFO)" + ], + "elevators": [ + "Puzzle Solved Underground Elevator", + "Puzzle Solved Bedroom Elevator", + "Puzzle Solved Three Floor Elevator" + ], + "lightning": [ + "Ixupi Captured Lightning" + ] +} \ No newline at end of file diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json new file mode 100644 index 0000000000..7d031b886b --- /dev/null +++ b/worlds/shivers/data/locations.json @@ -0,0 +1,325 @@ +{ + "all_locations": [ + "Puzzle Solved Gears", + "Puzzle Solved Stone Henge", + "Puzzle Solved Workshop Drawers", + "Puzzle Solved Library Statue", + "Puzzle Solved Theater Door", + "Puzzle Solved Clock Tower Door", + "Puzzle Solved Clock Chains", + "Puzzle Solved Atlantis", + "Puzzle Solved Organ", + "Puzzle Solved Maze Door", + "Puzzle Solved Columns of RA", + "Puzzle Solved Burial Door", + "Puzzle Solved Chinese Solitaire", + "Puzzle Solved Tiki Drums", + "Puzzle Solved Lyre", + "Puzzle Solved Red Door", + "Puzzle Solved Fortune Teller Door", + "Puzzle Solved Alchemy", + "Puzzle Solved UFO Symbols", + "Puzzle Solved Anansi Musicbox", + "Puzzle Solved Gallows", + "Puzzle Solved Mastermind", + "Puzzle Solved Marble Flipper", + "Puzzle Solved Skull Dial Door", + "Flashback Memory Obtained Beth's Ghost", + "Flashback Memory Obtained Merrick's Ghost", + "Flashback Memory Obtained Windlenot's Ghost", + "Flashback Memory Obtained Ancient Astrology", + "Flashback Memory Obtained Scrapbook", + "Flashback Memory Obtained Museum Brochure", + "Flashback Memory Obtained In Search of the Unexplained", + "Flashback Memory Obtained Egyptian Hieroglyphics Explained", + "Flashback Memory Obtained South American Pictographs", + "Flashback Memory Obtained Mythology of the Stars", + "Flashback Memory Obtained Black Book", + "Flashback Memory Obtained Theater Movie", + "Flashback Memory Obtained Museum Blueprints", + "Flashback Memory Obtained Beth's Address Book", + "Flashback Memory Obtained Merick's Notebook", + "Flashback Memory Obtained Professor Windlenot's Diary", + "Ixupi Captured Water", + "Ixupi Captured Wax", + "Ixupi Captured Ash", + "Ixupi Captured Oil", + "Ixupi Captured Cloth", + "Ixupi Captured Wood", + "Ixupi Captured Crystal", + "Ixupi Captured Sand", + "Ixupi Captured Metal", + "Final Riddle: Fortune Teller", + "Final Riddle: Planets Aligned", + "Final Riddle: Norse God Stone Message", + "Final Riddle: Beth's Body Page 17", + "Final Riddle: Guillotine Dropped", + "Puzzle Hint Found: Combo Lock in Mailbox", + "Puzzle Hint Found: Orange Symbol", + "Puzzle Hint Found: Silver Symbol", + "Puzzle Hint Found: Green Symbol", + "Puzzle Hint Found: White Symbol", + "Puzzle Hint Found: Brown Symbol", + "Puzzle Hint Found: Tan Symbol", + "Puzzle Hint Found: Basilisk Bone Fragments", + "Puzzle Hint Found: Atlantis Map", + "Puzzle Hint Found: Sirens Song Heard", + "Puzzle Hint Found: Egyptian Sphinx Heard", + "Puzzle Hint Found: Gallows Information Plaque", + "Puzzle Hint Found: Mastermind Information Plaque", + "Puzzle Hint Found: Elevator Writing", + "Puzzle Hint Found: Tiki Security Camera", + "Puzzle Hint Found: Tape Recorder Heard", + "Information Plaque: Transforming Masks (Lobby)", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Cremation (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Guillotine (Torture)", + "Information Plaque: Aliens (UFO)", + "Puzzle Solved Underground Elevator", + "Puzzle Solved Bedroom Elevator", + "Puzzle Solved Three Floor Elevator", + "Ixupi Captured Lightning" + ], + "locations_by_region": { + "Outside": [ + "Puzzle Solved Gears", + "Puzzle Solved Stone Henge", + "Ixupi Captured Water", + "Ixupi Captured Wax", + "Ixupi Captured Ash", + "Ixupi Captured Oil", + "Ixupi Captured Cloth", + "Ixupi Captured Wood", + "Ixupi Captured Crystal", + "Ixupi Captured Sand", + "Ixupi Captured Metal", + "Ixupi Captured Lightning", + "Puzzle Solved Underground Elevator", + "Puzzle Solved Three Floor Elevator", + "Puzzle Hint Found: Combo Lock in Mailbox", + "Puzzle Hint Found: Orange Symbol", + "Puzzle Hint Found: Silver Symbol", + "Puzzle Hint Found: Green Symbol", + "Puzzle Hint Found: White Symbol", + "Puzzle Hint Found: Brown Symbol", + "Puzzle Hint Found: Tan Symbol" + ], + "Underground Lake": [ + "Flashback Memory Obtained Windlenot's Ghost", + "Flashback Memory Obtained Egyptian Hieroglyphics Explained" + ], + "Office": [ + "Flashback Memory Obtained Scrapbook", + "Accessible: Storage: Desk Drawer", + "Puzzle Hint Found: Atlantis Map", + "Puzzle Hint Found: Tape Recorder Heard", + "Puzzle Solved Bedroom Elevator" + ], + "Workshop": [ + "Puzzle Solved Workshop Drawers", + "Accessible: Storage: Workshop Drawers", + "Puzzle Hint Found: Basilisk Bone Fragments" + ], + "Bedroom": [ + "Flashback Memory Obtained Professor Windlenot's Diary" + ], + "Library": [ + "Puzzle Solved Library Statue", + "Flashback Memory Obtained In Search of the Unexplained", + "Flashback Memory Obtained South American Pictographs", + "Flashback Memory Obtained Mythology of the Stars", + "Flashback Memory Obtained Black Book", + "Accessible: Storage: Library Cabinet", + "Accessible: Storage: Library Statue" + ], + "Maintenance Tunnels": [ + "Flashback Memory Obtained Beth's Address Book" + ], + "Three Floor Elevator": [ + "Puzzle Hint Found: Elevator Writing" + ], + "Lobby": [ + "Puzzle Solved Theater Door", + "Flashback Memory Obtained Museum Brochure", + "Information Plaque: Jade Skull (Lobby)", + "Information Plaque: Transforming Masks (Lobby)", + "Accessible: Storage: Slide", + "Accessible: Storage: Eagles Head" + ], + "Generator": [ + "Final Riddle: Beth's Body Page 17" + ], + "Theater Back Hallways": [ + "Puzzle Solved Clock Tower Door" + ], + "Clock Tower Staircase": [ + "Puzzle Solved Clock Chains" + ], + "Clock Tower": [ + "Flashback Memory Obtained Beth's Ghost", + "Accessible: Storage: Clock Tower", + "Puzzle Hint Found: Tiki Security Camera" + ], + "Projector Room": [ + "Flashback Memory Obtained Theater Movie" + ], + "Ocean": [ + "Puzzle Solved Atlantis", + "Puzzle Solved Organ", + "Flashback Memory Obtained Museum Blueprints", + "Accessible: Storage: Ocean", + "Puzzle Hint Found: Sirens Song Heard", + "Information Plaque: Quartz Crystal (Ocean)", + "Information Plaque: Poseidon (Ocean)", + "Information Plaque: Colossus of Rhodes (Ocean)", + "Information Plaque: Poseidon's Temple (Ocean)" + ], + "Maze Staircase": [ + "Puzzle Solved Maze Door" + ], + "Egypt": [ + "Puzzle Solved Columns of RA", + "Puzzle Solved Burial Door", + "Accessible: Storage: Egypt", + "Puzzle Hint Found: Egyptian Sphinx Heard", + "Information Plaque: Tomb of the Ixupi (Egypt)", + "Information Plaque: The Sphinx (Egypt)", + "Information Plaque: Curse of Anubis (Egypt)" + ], + "Burial": [ + "Puzzle Solved Chinese Solitaire", + "Flashback Memory Obtained Merick's Notebook", + "Accessible: Storage: Chinese Solitaire", + "Information Plaque: Norse Burial Ship (Burial)", + "Information Plaque: Paracas Burial Bundles (Burial)", + "Information Plaque: Spectacular Coffins of Ghana (Burial)", + "Information Plaque: Animal Crematorium (Burial)", + "Information Plaque: Cremation (Burial)" + ], + "Tiki": [ + "Puzzle Solved Tiki Drums", + "Accessible: Storage: Tiki Hut", + "Information Plaque: Witch Doctors of the Congo (Tiki)", + "Information Plaque: Sarombe doctor of Mozambique (Tiki)" + ], + "Gods Room": [ + "Puzzle Solved Lyre", + "Puzzle Solved Red Door", + "Accessible: Storage: Lyre", + "Final Riddle: Norse God Stone Message", + "Information Plaque: Fisherman's Canoe God (Gods)", + "Information Plaque: Mayan Gods (Gods)", + "Information Plaque: Thor (Gods)", + "Information Plaque: Celtic Janus Sculpture (Gods)", + "Information Plaque: Sumerian Bull God - An (Gods)", + "Information Plaque: Sumerian Lyre (Gods)", + "Information Plaque: Chuen (Gods)" + ], + "Blue Maze": [ + "Puzzle Solved Fortune Teller Door" + ], + "Fortune Teller": [ + "Flashback Memory Obtained Merrick's Ghost", + "Final Riddle: Fortune Teller" + ], + "Inventions": [ + "Puzzle Solved Alchemy", + "Accessible: Storage: Alchemy" + ], + "UFO": [ + "Puzzle Solved UFO Symbols", + "Accessible: Storage: UFO", + "Final Riddle: Planets Aligned", + "Information Plaque: Coincidence or Extraterrestrial Visits? (UFO)", + "Information Plaque: Planets (UFO)", + "Information Plaque: Astronomical Construction (UFO)", + "Information Plaque: Aliens (UFO)" + ], + "Anansi": [ + "Puzzle Solved Anansi Musicbox", + "Flashback Memory Obtained Ancient Astrology", + "Accessible: Storage: Skeleton", + "Accessible: Storage: Anansi", + "Information Plaque: African Creation Myth (Anansi)", + "Information Plaque: Apophis the Serpent (Anansi)", + "Information Plaque: Death (Anansi)", + "Information Plaque: Cyclops (Pegasus)", + "Information Plaque: Lycanthropy (Werewolf)" + ], + "Torture": [ + "Puzzle Solved Gallows", + "Accessible: Storage: Hanging", + "Final Riddle: Guillotine Dropped", + "Puzzle Hint Found: Gallows Information Plaque", + "Information Plaque: Guillotine (Torture)" + ], + "Puzzle Room Mastermind": [ + "Puzzle Solved Mastermind", + "Puzzle Hint Found: Mastermind Information Plaque" + ], + "Puzzle Room Marbles": [ + "Puzzle Solved Marble Flipper" + ], + "Prehistoric": [ + "Information Plaque: Bronze Unicorn (Prehistoric)", + "Information Plaque: Griffin (Prehistoric)", + "Information Plaque: Eagles Nest (Prehistoric)", + "Information Plaque: Large Spider (Prehistoric)", + "Information Plaque: Starfish (Prehistoric)", + "Accessible: Storage: Eagles Nest" + ], + "Tar River": [ + "Accessible: Storage: Tar River", + "Information Plaque: Subterranean World (Underground Maze)", + "Information Plaque: Dero (Underground Maze)" + ], + "Theater": [ + "Accessible: Storage: Theater" + ], + "Greenhouse": [ + "Accessible: Storage: Greenhouse" + ], + "Janitor Closet": [ + "Accessible: Storage: Janitor Closet" + ], + "Skull Dial Bridge": [ + "Accessible: Storage: Skull Bridge", + "Puzzle Solved Skull Dial Door" + ] + } +} diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json new file mode 100644 index 0000000000..3e81136c45 --- /dev/null +++ b/worlds/shivers/data/regions.json @@ -0,0 +1,145 @@ +{ + "regions": [ + ["Menu", ["To Registry"]], + ["Registry", ["To Outside From Registry"]], + ["Outside", ["To Underground Tunnels From Outside", "To Lobby From Outside"]], + ["Underground Tunnels", ["To Underground Lake From Underground Tunnels", "To Outside From Underground"]], + ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]], + ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]], + ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]], + ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office"]], + ["Workshop", ["To Office From Workshop"]], + ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]], + ["Bedroom", ["To Bedroom Elevator From Bedroom"]], + ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby"]], + ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library"]], + ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]], + ["Generator", ["To Maintenance Tunnels From Generator"]], + ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]], + ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]], + ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]], + ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]], + ["Projector Room", ["To Theater Back Hallways From Projector Room"]], + ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric"]], + ["Greenhouse", ["To Prehistoric From Greenhouse"]], + ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean"]], + ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]], + ["Maze", ["To Maze Staircase From Maze", "To Tar River"]], + ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River"]], + ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt"]], + ["Burial", ["To Egypt From Burial", "To Tiki From Burial"]], + ["Tiki", ["To Burial From Tiki", "To Gods Room"]], + ["Gods Room", ["To Tiki From Gods Room", "To Anansi From Gods Room"]], + ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi"]], + ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]], + ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]], + ["Janitor Closet", ["To Night Staircase From Janitor Closet"]], + ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]], + ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze"]], + ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]], + ["Fortune Teller", ["To Blue Maze From Fortune Teller"]], + ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]], + ["Torture", ["To Inventions From Torture", "To Puzzle Room Mastermind From Torture"]], + ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]], + ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]], + ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]], + ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]] + ], + "mandatory_connections": [ + ["To Registry", "Registry"], + ["To Outside From Registry", "Outside"], + ["To Outside From Underground", "Outside"], + ["To Outside From Lobby", "Outside"], + ["To Underground Tunnels From Outside", "Underground Tunnels"], + ["To Underground Tunnels From Underground Lake", "Underground Tunnels"], + ["To Underground Lake From Underground Tunnels", "Underground Lake"], + ["To Underground Lake From Underground Blue Tunnels", "Underground Lake"], + ["To Underground Blue Tunnels From Underground Lake", "Underground Blue Tunnels"], + ["To Underground Blue Tunnels From Office Elevator", "Underground Blue Tunnels"], + ["To Office Elevator From Underground Blue Tunnels", "Office Elevator"], + ["To Office Elevator From Office", "Office Elevator"], + ["To Office From Office Elevator", "Office"], + ["To Office From Workshop", "Office"], + ["To Office From Bedroom Elevator", "Office"], + ["To Office From Lobby", "Office"], + ["To Workshop", "Workshop"], + ["To Lobby From Office", "Lobby"], + ["To Lobby From Library", "Lobby"], + ["To Lobby From Tar River", "Lobby"], + ["To Lobby From Slide Room", "Lobby"], + ["To Lobby From Egypt", "Lobby"], + ["To Lobby From Theater", "Lobby"], + ["To Lobby From Prehistoric", "Lobby"], + ["To Lobby From Outside", "Lobby"], + ["To Bedroom Elevator From Office", "Bedroom Elevator"], + ["To Bedroom Elevator From Bedroom", "Bedroom Elevator"], + ["To Bedroom", "Bedroom"], + ["To Library From Lobby", "Library"], + ["To Library From Maintenance Tunnels", "Library"], + ["To Theater From Lobby", "Theater" ], + ["To Theater From Theater Back Hallways", "Theater"], + ["To Prehistoric From Lobby", "Prehistoric"], + ["To Prehistoric From Greenhouse", "Prehistoric"], + ["To Prehistoric From Ocean", "Prehistoric"], + ["To Egypt From Lobby", "Egypt"], + ["To Egypt From Burial", "Egypt"], + ["To Egypt From Blue Maze", "Egypt"], + ["To Maintenance Tunnels From Generator", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Three Floor Elevator", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Library", "Maintenance Tunnels"], + ["To Maintenance Tunnels From Theater Back Hallways", "Maintenance Tunnels"], + ["To Three Floor Elevator From Maintenance Tunnels", "Three Floor Elevator"], + ["To Three Floor Elevator From Blue Maze Bottom", "Three Floor Elevator"], + ["To Three Floor Elevator From Blue Maze Top", "Three Floor Elevator"], + ["To Generator", "Generator"], + ["To Theater Back Hallways From Theater", "Theater Back Hallways"], + ["To Theater Back Hallways From Clock Tower Staircase", "Theater Back Hallways"], + ["To Theater Back Hallways From Projector Room", "Theater Back Hallways"], + ["To Clock Tower Staircase From Theater Back Hallways", "Clock Tower Staircase"], + ["To Clock Tower Staircase From Clock Tower", "Clock Tower Staircase"], + ["To Projector Room", "Projector Room"], + ["To Clock Tower", "Clock Tower"], + ["To Greenhouse", "Greenhouse"], + ["To Ocean From Prehistoric", "Ocean"], + ["To Ocean From Maze Staircase", "Ocean"], + ["To Maze Staircase From Ocean", "Maze Staircase"], + ["To Maze Staircase From Maze", "Maze Staircase"], + ["To Maze From Maze Staircase", "Maze"], + ["To Maze From Tar River", "Maze"], + ["To Tar River", "Tar River"], + ["To Tar River From Lobby", "Tar River"], + ["To Burial From Egypt", "Burial"], + ["To Burial From Tiki", "Burial"], + ["To Blue Maze From Three Floor Elevator", "Blue Maze"], + ["To Blue Maze From Fortune Teller", "Blue Maze"], + ["To Blue Maze From Inventions", "Blue Maze"], + ["To Blue Maze From Egypt", "Blue Maze"], + ["To Tiki From Burial", "Tiki"], + ["To Tiki From Gods Room", "Tiki"], + ["To Gods Room", "Gods Room" ], + ["To Gods Room From Anansi", "Gods Room"], + ["To Anansi From Gods Room", "Anansi"], + ["To Anansi From Werewolf", "Anansi"], + ["To Werewolf From Anansi", "Werewolf"], + ["To Werewolf From Night Staircase", "Werewolf"], + ["To Night Staircase From Werewolf", "Night Staircase"], + ["To Night Staircase From Janitor Closet", "Night Staircase"], + ["To Night Staircase From UFO", "Night Staircase"], + ["To Janitor Closet", "Janitor Closet"], + ["To UFO", "UFO"], + ["To UFO From Inventions", "UFO"], + ["To Inventions From UFO", "Inventions"], + ["To Inventions From Blue Maze", "Inventions"], + ["To Inventions From Torture", "Inventions"], + ["To Fortune Teller", "Fortune Teller"], + ["To Torture", "Torture"], + ["To Torture From Inventions", "Torture"], + ["To Puzzle Room Mastermind From Torture", "Puzzle Room Mastermind"], + ["To Puzzle Room Mastermind From Puzzle Room Marbles", "Puzzle Room Mastermind"], + ["To Puzzle Room Marbles From Puzzle Room Mastermind", "Puzzle Room Marbles"], + ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"], + ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"], + ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"], + ["To Slide Room", "Slide Room"] + ] +} \ No newline at end of file diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md new file mode 100644 index 0000000000..51730057b0 --- /dev/null +++ b/worlds/shivers/docs/en_Shivers.md @@ -0,0 +1,31 @@ +# Shivers + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +configuration file. + +## What does randomization do to this game? + +All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum, +these are randomized. Crawling has been added and is required to use any crawl space. + +## What is considered a location check in Shivers? + +1. All puzzle solves are location checks excluding elevator puzzles. +2. All Ixupi captures are location checks excluding Lightning. +3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map. +4. Optionally information plaques are location checks. + +## When the player receives an item, what happens? + +If the player receives a key then the corresponding door will be unlocked. If the player receives a pot piece, it is placed into a pot piece storage location. + +## What is the victory condition? + +Victory is achieved when the player captures Lightning in the generator room. + +## Encountered a bug? + +Please contact GodlFire on Discord for bugs related to Shivers world generation.\ +Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer. diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md new file mode 100644 index 0000000000..ee33bb7040 --- /dev/null +++ b/worlds/shivers/docs/setup_en.md @@ -0,0 +1,60 @@ +# Shivers Randomizer Setup Guide + + +## Required Software + +- [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc +- [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later +- [Shivers Randomizer](https://www.speedrun.com/shivers/resources) + +## Setup ScummVM for Shivers + +### GOG version of Shivers + +1. Launch ScummVM +2. Click Add Game... +3. Locate the folder for Shivers (typically in GOG Galaxy\Games\Shivers) +4. Click OK + +### Disc copy of Shivers + +1. Copy contents of Shivers disc to a desired location on your computer +2. Launch ScummVM +3. Click Add Game... +4. Locate the folder for Shivers and click Choose +5. Click OK + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Shivers Player Settings Page](/games/Shivers/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Joining a MultiWorld Game + +1. Launch ScummVM +2. Highlight Shivers and click "Start" +3. Launch the Shivers Randomizer +4. Click "Attach" +5. Click "Archipelago" +6. Enter the Archipelago server address, slot name, and password +7. Click "Connect" +8. In Shivers click "New Game" + +## What is a check + +- Every puzzle +- Every puzzle hint/solution +- Every document that is considered a Flashback +- Optionally information plaques. From edb62004ef22cf3f2a8d4c5917c4e2cd185e88ec Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 25 Nov 2023 11:12:13 +0100 Subject: [PATCH 223/327] LttP: remove extra default = False (#2497) * LttP: remove extra default = False --- worlds/alttp/Options.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 0f35be7459..a89a9adb83 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -102,9 +102,10 @@ class map_shuffle(DungeonItem): class key_drop_shuffle(Toggle): - """Shuffle keys found in pots and dropped from killed enemies.""" + """Shuffle keys found in pots and dropped from killed enemies, + respects the small key and big key shuffle options.""" display_name = "Key Drop Shuffle" - default = False + class Crystals(Range): range_start = 0 From 8a852abdc4e36d3ea3fe8b4a5154ae97e78bd17d Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 25 Nov 2023 05:57:02 -0500 Subject: [PATCH 224/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Migrate=20support?= =?UTF-8?q?=20into=20Bizhawk=20Client=20(#2466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removes the Pokémon Client, adding support for Red and Blue to the Bizhawk Client. - Adds `/bank` commands that mirror SDV's, allowing transferring money into and out of the EnergyLink storage. - Adds a fix to the base patch so that the progressive card key counter will not increment beyond 10, which would lead to receiving glitch items. This value is checked against and verified that it is not > 10 as part of crash detection by the client, to prevent erroneous location checks when the game crashes, so this is relevant to the new client (although shouldn't happen unless you're using !getitem, or putting progressive card keys as item link replacement items) --- PokemonClient.py | 382 ------------------ data/lua/connector_pkmn_rb.lua | 224 ---------- inno_setup.iss | 8 +- worlds/LauncherComponents.py | 2 - worlds/pokemon_rb/__init__.py | 21 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45893 -> 45946 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45875 -> 45892 bytes worlds/pokemon_rb/client.py | 277 +++++++++++++ .../docs/en_Pokemon Red and Blue.md | 7 +- worlds/pokemon_rb/docs/setup_en.md | 43 +- worlds/pokemon_rb/rom.py | 4 + worlds/pokemon_rb/rom_addresses.py | 222 +++++----- 12 files changed, 439 insertions(+), 751 deletions(-) delete mode 100644 PokemonClient.py delete mode 100644 data/lua/connector_pkmn_rb.lua create mode 100644 worlds/pokemon_rb/client.py diff --git a/PokemonClient.py b/PokemonClient.py deleted file mode 100644 index 6b43a53b8f..0000000000 --- a/PokemonClient.py +++ /dev/null @@ -1,382 +0,0 @@ -import asyncio -import json -import time -import os -import bsdiff4 -import subprocess -import zipfile -from asyncio import StreamReader, StreamWriter -from typing import List - - -import Utils -from Utils import async_start -from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ - get_base_parser - -from worlds.pokemon_rb.locations import location_data -from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch - -location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} -location_bytes_bits = {} -for location in location_data: - if location.ram_address is not None: - if type(location.ram_address) == list: - location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address - location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, - {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] - else: - location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address - location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} - -location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" - and location.address is not None} - -SYSTEM_MESSAGE_ID = 0 - -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua" -CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua" -CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" -CONNECTION_CONNECTED_STATUS = "Connected" -CONNECTION_INITIAL_STATUS = "Connection has not been initiated" - -DISPLAY_MSGS = True - -SCRIPT_VERSION = 3 - - -class GBCommandProcessor(ClientCommandProcessor): - def __init__(self, ctx: CommonContext): - super().__init__(ctx) - - def _cmd_gb(self): - """Check Gameboy Connection State""" - if isinstance(self.ctx, GBContext): - logger.info(f"Gameboy Status: {self.ctx.gb_status}") - - -class GBContext(CommonContext): - command_processor = GBCommandProcessor - game = 'Pokemon Red and Blue' - - def __init__(self, server_address, password): - super().__init__(server_address, password) - self.gb_streams: (StreamReader, StreamWriter) = None - self.gb_sync_task = None - self.messages = {} - self.locations_array = None - self.gb_status = CONNECTION_INITIAL_STATUS - self.awaiting_rom = False - self.display_msgs = True - self.deathlink_pending = False - self.set_deathlink = False - self.client_compatibility_mode = 0 - self.items_handling = 0b001 - self.sent_release = False - self.sent_collect = False - self.auto_hints = set() - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(GBContext, self).server_auth(password_requested) - if not self.auth: - self.awaiting_rom = True - logger.info('Awaiting connection to EmuHawk to get Player information') - return - - await self.send_connect() - - def _set_message(self, msg: str, msg_id: int): - if DISPLAY_MSGS: - self.messages[(time.time(), msg_id)] = msg - - def on_package(self, cmd: str, args: dict): - if cmd == 'Connected': - self.locations_array = None - if 'death_link' in args['slot_data'] and args['slot_data']['death_link']: - self.set_deathlink = True - elif cmd == "RoomInfo": - self.seed_name = args['seed_name'] - elif cmd == 'Print': - msg = args['text'] - if ': !' not in msg: - self._set_message(msg, SYSTEM_MESSAGE_ID) - elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" - self._set_message(msg, SYSTEM_MESSAGE_ID) - - def on_deathlink(self, data: dict): - self.deathlink_pending = True - super().on_deathlink(data) - - def run_gui(self): - from kvui import GameManager - - class GBManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Pokémon Client" - - self.ui = GBManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - -def get_payload(ctx: GBContext): - current_time = time.time() - ret = json.dumps( - { - "items": [item.item for item in ctx.items_received], - "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() - if key[0] > current_time - 10}, - "deathlink": ctx.deathlink_pending, - "options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled')) - } - ) - ctx.deathlink_pending = False - return ret - - -async def parse_locations(data: List, ctx: GBContext): - locations = [] - flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20], - "Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], - "Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]} - - if len(data) > 0x140 + 0x20 + 0x0E + 0x01: - flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:] - else: - flags["DexSanityFlag"] = [0] * 19 - - for flag_type, loc_map in location_map.items(): - for flag, loc_id in loc_map.items(): - if flag_type == "list": - if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit'] - and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']): - locations.append(loc_id) - elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: - locations.append(loc_id) - - hints = [] - if flags["EventFlag"][280] & 16: - hints.append("Cerulean Bicycle Shop") - if flags["EventFlag"][280] & 32: - hints.append("Route 2 Gate - Oak's Aide") - if flags["EventFlag"][280] & 64: - hints.append("Route 11 Gate 2F - Oak's Aide") - if flags["EventFlag"][280] & 128: - hints.append("Route 15 Gate 2F - Oak's Aide") - if flags["EventFlag"][281] & 1: - hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2", - "Celadon Prize Corner - Item Prize 3"] - if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"] - not in ctx.checked_locations): - hints.append("Fossil - Choice B") - elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"] - not in ctx.checked_locations): - hints.append("Fossil - Choice A") - hints = [ - location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and - location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked - ] - if hints: - await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}]) - ctx.auto_hints.update(hints) - - if flags["EventFlag"][280] & 1 and not ctx.finished_game: - await ctx.send_msgs([ - {"cmd": "StatusUpdate", - "status": 30} - ]) - ctx.finished_game = True - if locations == ctx.locations_array: - return - ctx.locations_array = locations - if locations is not None: - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) - - -async def gb_sync_task(ctx: GBContext): - logger.info("Starting GB connector. Use /gb for status information") - while not ctx.exit_event.is_set(): - error_status = None - if ctx.gb_streams: - (reader, writer) = ctx.gb_streams - msg = get_payload(ctx).encode() - writer.write(msg) - writer.write(b'\n') - try: - await asyncio.wait_for(writer.drain(), timeout=1.5) - try: - # Data will return a dict with up to two fields: - # 1. A keepalive response of the Players Name (always) - # 2. An array representing the memory values of the locations area (if in game) - data = await asyncio.wait_for(reader.readline(), timeout=5) - data_decoded = json.loads(data.decode()) - if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION: - msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \ - "and PokemonClient are from the same Archipelago installation." - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion'] - if ctx.client_compatibility_mode == 0: - ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested - if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]): - msg = "The server is running a different multiworld than your client is. (invalid seed_name)" - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]) - if not ctx.auth: - ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) - if ctx.auth == '': - msg = "Invalid ROM detected. No player name built into the ROM." - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - error_status = CONNECTION_RESET_STATUS - if ctx.awaiting_rom: - await ctx.server_auth(False) - if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \ - and not error_status and ctx.auth: - # Not just a keep alive ping, parse - async_start(parse_locations(data_decoded['locations'], ctx)) - if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags: - await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!") - if 'options' in data_decoded: - msgs = [] - if data_decoded['options'] & 4 and not ctx.sent_release: - ctx.sent_release = True - msgs.append({"cmd": "Say", "text": "!release"}) - if data_decoded['options'] & 8 and not ctx.sent_collect: - ctx.sent_collect = True - msgs.append({"cmd": "Say", "text": "!collect"}) - if msgs: - await ctx.send_msgs(msgs) - if ctx.set_deathlink: - await ctx.update_death_link(True) - except asyncio.TimeoutError: - logger.debug("Read Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.gb_streams = None - except ConnectionResetError as e: - logger.debug("Read failed due to Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.gb_streams = None - except TimeoutError: - logger.debug("Connection Timed Out, Reconnecting") - error_status = CONNECTION_TIMING_OUT_STATUS - writer.close() - ctx.gb_streams = None - except ConnectionResetError: - logger.debug("Connection Lost, Reconnecting") - error_status = CONNECTION_RESET_STATUS - writer.close() - ctx.gb_streams = None - if ctx.gb_status == CONNECTION_TENTATIVE_STATUS: - if not error_status: - logger.info("Successfully Connected to Gameboy") - ctx.gb_status = CONNECTION_CONNECTED_STATUS - else: - ctx.gb_status = f"Was tentatively connected but error occured: {error_status}" - elif error_status: - ctx.gb_status = error_status - logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates") - else: - try: - logger.debug("Attempting to connect to Gameboy") - ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10) - ctx.gb_status = CONNECTION_TENTATIVE_STATUS - except TimeoutError: - logger.debug("Connection Timed Out, Trying Again") - ctx.gb_status = CONNECTION_TIMING_OUT_STATUS - continue - except ConnectionRefusedError: - logger.debug("Connection Refused, Trying Again") - ctx.gb_status = CONNECTION_REFUSED_STATUS - continue - - -async def run_game(romfile): - auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True) - if auto_start is True: - import webbrowser - webbrowser.open(romfile) - elif os.path.isfile(auto_start): - subprocess.Popen([auto_start, romfile], - stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - -async def patch_and_run_game(game_version, patch_file, ctx): - base_name = os.path.splitext(patch_file)[0] - comp_path = base_name + '.gb' - if game_version == "blue": - delta_patch = BlueDeltaPatch - else: - delta_patch = RedDeltaPatch - - try: - base_rom = delta_patch.get_source_data() - except Exception as msg: - logger.info(msg, extra={'compact_gui': True}) - ctx.gui_error('Error', msg) - - with zipfile.ZipFile(patch_file, 'r') as patch_archive: - with patch_archive.open('delta.bsdiff4', 'r') as stream: - patch = stream.read() - patched_rom_data = bsdiff4.patch(base_rom, patch) - - with open(comp_path, "wb") as patched_rom_file: - patched_rom_file.write(patched_rom_data) - - async_start(run_game(comp_path)) - - -if __name__ == '__main__': - - Utils.init_logging("PokemonClient") - - options = Utils.get_options() - - async def main(): - parser = get_base_parser() - parser.add_argument('patch_file', default="", type=str, nargs="?", - help='Path to an APRED or APBLUE patch file') - args = parser.parse_args() - - ctx = GBContext(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync") - - if args.patch_file: - ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() - if ext == "apred": - logger.info("APRED file supplied, beginning patching process...") - async_start(patch_and_run_game("red", args.patch_file, ctx)) - elif ext == "apblue": - logger.info("APBLUE file supplied, beginning patching process...") - async_start(patch_and_run_game("blue", args.patch_file, ctx)) - else: - logger.warning(f"Unknown patch file extension {ext}") - - await ctx.exit_event.wait() - ctx.server_address = None - - await ctx.shutdown() - - if ctx.gb_sync_task: - await ctx.gb_sync_task - - - import colorama - - colorama.init() - - asyncio.run(main()) - colorama.deinit() diff --git a/data/lua/connector_pkmn_rb.lua b/data/lua/connector_pkmn_rb.lua deleted file mode 100644 index 3f56435bdb..0000000000 --- a/data/lua/connector_pkmn_rb.lua +++ /dev/null @@ -1,224 +0,0 @@ -local socket = require("socket") -local json = require('json') -local math = require('math') -require("common") -local STATE_OK = "Ok" -local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" -local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" -local STATE_UNINITIALIZED = "Uninitialized" - -local SCRIPT_VERSION = 3 - -local APIndex = 0x1A6E -local APDeathLinkAddress = 0x00FD -local APItemAddress = 0x00FF -local EventFlagAddress = 0x1735 -local MissableAddress = 0x161A -local HiddenItemsAddress = 0x16DE -local RodAddress = 0x1716 -local DexSanityAddress = 0x1A71 -local InGameAddress = 0x1A84 -local ClientCompatibilityAddress = 0xFF00 - -local ItemsReceived = nil -local playerName = nil -local seedName = nil - -local deathlink_rec = nil -local deathlink_send = false - -local prevstate = "" -local curstate = STATE_UNINITIALIZED -local gbSocket = nil -local frame = 0 - -local compat = nil - -local function defineMemoryFunctions() - local memDomain = {} - local domains = memory.getmemorydomainlist() - memDomain["rom"] = function() memory.usememorydomain("ROM") end - memDomain["wram"] = function() memory.usememorydomain("WRAM") end - return memDomain -end - -local memDomain = defineMemoryFunctions() -u8 = memory.read_u8 -wU8 = memory.write_u8 -u16 = memory.read_u16_le -function uRange(address, bytes) - data = memory.readbyterange(address - 1, bytes + 1) - data[0] = nil - return data -end - -function generateLocationsChecked() - memDomain.wram() - events = uRange(EventFlagAddress, 0x140) - missables = uRange(MissableAddress, 0x20) - hiddenitems = uRange(HiddenItemsAddress, 0x0E) - rod = {u8(RodAddress)} - dexsanity = uRange(DexSanityAddress, 19) - - - data = {} - - categories = {events, missables, hiddenitems, rod} - if compat > 1 then - table.insert(categories, dexsanity) - end - for _, category in ipairs(categories) do - for _, v in ipairs(category) do - table.insert(data, v) - end - end - - return data -end - -local function arrayEqual(a1, a2) - if #a1 ~= #a2 then - return false - end - - for i, v in ipairs(a1) do - if v ~= a2[i] then - return false - end - end - - return true -end - -function receive() - l, e = gbSocket:receive() - if e == 'closed' then - if curstate == STATE_OK then - print("Connection closed") - end - curstate = STATE_UNINITIALIZED - return - elseif e == 'timeout' then - return - elseif e ~= nil then - print(e) - curstate = STATE_UNINITIALIZED - return - end - if l ~= nil then - block = json.decode(l) - if block ~= nil then - local itemsBlock = block["items"] - if itemsBlock ~= nil then - ItemsReceived = itemsBlock - end - deathlink_rec = block["deathlink"] - - end - end - -- Determine Message to send back - memDomain.rom() - newPlayerName = uRange(0xFFF0, 0x10) - newSeedName = uRange(0xFFDB, 21) - if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then - print("ROM changed, quitting") - curstate = STATE_UNINITIALIZED - return - end - playerName = newPlayerName - seedName = newSeedName - local retTable = {} - retTable["scriptVersion"] = SCRIPT_VERSION - - if compat == nil then - compat = u8(ClientCompatibilityAddress) - if compat < 2 then - InGameAddress = 0x1A71 - end - end - - retTable["clientCompatibilityVersion"] = compat - retTable["playerName"] = playerName - retTable["seedName"] = seedName - memDomain.wram() - - in_game = u8(InGameAddress) - if in_game == 0x2A or in_game == 0xAC then - retTable["locations"] = generateLocationsChecked() - elseif in_game ~= 0 then - print("Game may have crashed") - curstate = STATE_UNINITIALIZED - return - end - - retTable["deathLink"] = deathlink_send - deathlink_send = false - - msg = json.encode(retTable).."\n" - local ret, error = gbSocket:send(msg) - if ret == nil then - print(error) - elseif curstate == STATE_INITIAL_CONNECTION_MADE then - curstate = STATE_TENTATIVELY_CONNECTED - elseif curstate == STATE_TENTATIVELY_CONNECTED then - print("Connected!") - curstate = STATE_OK - end -end - -function main() - if not checkBizHawkVersion() then - return - end - server, error = socket.bind('localhost', 17242) - - while true do - frame = frame + 1 - if not (curstate == prevstate) then - print("Current state: "..curstate) - prevstate = curstate - end - if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then - if (frame % 5 == 0) then - receive() - in_game = u8(InGameAddress) - if in_game == 0x2A or in_game == 0xAC then - if u8(APItemAddress) == 0x00 then - ItemIndex = u16(APIndex) - if deathlink_rec == true then - wU8(APDeathLinkAddress, 1) - elseif u8(APDeathLinkAddress) == 3 then - wU8(APDeathLinkAddress, 0) - deathlink_send = true - end - if ItemsReceived[ItemIndex + 1] ~= nil then - item_id = ItemsReceived[ItemIndex + 1] - 172000000 - if item_id > 255 then - item_id = item_id - 256 - end - wU8(APItemAddress, item_id) - end - end - end - end - elseif (curstate == STATE_UNINITIALIZED) then - if (frame % 60 == 0) then - - print("Waiting for client.") - - emu.frameadvance() - server:settimeout(2) - print("Attempting to connect") - local client, timeout = server:accept() - if timeout == nil then - curstate = STATE_INITIAL_CONNECTION_MADE - gbSocket = client - gbSocket:settimeout(0) - end - end - end - emu.frameadvance() - end -end - -main() diff --git a/inno_setup.iss b/inno_setup.iss index b4779b1067..4744fa2b72 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -140,13 +140,13 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{ Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; -Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index c3ae2b0495..31739bb246 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -101,8 +101,6 @@ components: List[Component] = [ Component('OoT Adjuster', 'OoTAdjuster'), # FF1 Component('FF1 Client', 'FF1Client'), - # Pokémon - Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), # TLoZ Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')), # ChecksFinder diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index b2ee0702c9..d9bd6dde76 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -2,9 +2,11 @@ import os import settings import typing import threading +import base64 from copy import deepcopy from typing import TextIO +from Utils import __version__ from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType from Fill import fill_restrictive, FillError, sweep_from_pool from worlds.AutoWorld import World, WebWorld @@ -22,6 +24,7 @@ from .rules import set_rules from .level_scaling import level_scaling from . import logic from . import poke_data +from . import client class PokemonSettings(settings.Group): @@ -36,16 +39,8 @@ class PokemonSettings(settings.Group): copy_to = "Pokemon Blue (UE) [S][!].gb" md5s = [BlueDeltaPatch.hash] - class RomStart(str): - """ - Set this to false to never autostart a rom (such as after patching) - True for operating system default program - Alternatively, a path to a program to open the .gb file with - """ - red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to) blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to) - rom_start: typing.Union[RomStart, bool] = True class PokemonWebWorld(WebWorld): @@ -141,9 +136,6 @@ class PokemonRedBlueWorld(World): else: self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival") - if len(self.multiworld.player_name[self.player].encode()) > 16: - raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.") - if not self.multiworld.badgesanity[self.player]: self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] @@ -621,6 +613,13 @@ class PokemonRedBlueWorld(World): def generate_output(self, output_directory: str): generate_output(self, output_directory) + def modify_multidata(self, multidata: dict): + rom_name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + 'utf8')[:21] + rom_name.extend([0] * (21 - len(rom_name))) + new_name = base64.b64encode(bytes(rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + def write_spoiler_header(self, spoiler_handle: TextIO): spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n") spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n") diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1..bee5a8d2f4996ee1db54d2b7812938cc66dcb922 100644 GIT binary patch literal 45946 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

    ^aKX)H zf2^rllq`ZwYU{J<$w0b_BT^vYqoU085ahDbK$%4-3a3p)WiKzHjC6%^qd+42bqDovu5*r<<8xzW_3w6VSwGaso!nQ$rhu(5DkKWeH!8w&WugcT#{ti6b zwjU3Dvmi#ykXGt6NLs{-AYK&{H)V^$f}I>$%2Bt?p(j)j50m34L_rBz5-JWdr5*KJ zjjl(U<-;66?UB}0%0k&Tx0)bk05dm{h4)=Xf6{&N+m49uhBRzmRDX%j^1J_L1b@{c zG?7sKB~S_?0@#QFDW_R#Q!|W%6WlvTMJh-eXUTc#R;>Wv@k$PcM#{Er&kLC)drLN$ zqpj8RYNp^+L|2M|85Bp*dWZ}vLTm?-*hdX5hQbvwY0?-Ba@%TPrl@AQy1I=w_UU7? zh+8S)dC`Fb^?04>Qg$>DMmRCT-xLI3IhJHF^NJ#m|KjdQrwS4qsJvL;h%!Pk-O3+oR1ciqmfb$sXtpwLt8@Eg5) z0A9cV000004o6Rlx7O_Udq>Udz82k@RPMQZ6H2oy+ii+b-se(~)$M(&%e{S>a23YO zzRkC6?rz{OX};^PpC_dAuf6x1?ArOh?drFnM2HEa5C8y%m?lgKguoLCri>;`044%Y z0WvaV#9$_xWN0uGO&T;OrkDVlX`zG+Oh%bB8e}pjrcE@!P>3L!Gynu-$)hQNG5};W z#AwiHCYUmwfCSLfQ)!x$Xw^NYgs1ACp_(-&n@FGOPf_TadYe(TJf@S>$?9z;n5+fg+18Y41l zGzgjrk)t39q|i)EOoK%92+c|DDYHdAG|8G1Oqc?jBp%f^qY0SRJg2FqH9V#=nrx?$ zJrmPHVq|)QO)-)^CYmw;(?O=14I5FXsiuLas5LzS2{a)wG-_;9PezpTnWCSPdNPfu z$kfT1Pt?@fj7=Go!fCP-2*;#lG|}lj0%RF71Jq(-c?s$n1`+88p&2v{1}2R%XwYDT z6G5ht36KB)Kr{eT(WXr>CPPM=6D9_NFc4uHXc;t{A(U#LiL#XZ6KOPQrl+Q+$ijxx zQ`Buq=Bec$r9DkHr>WwP)imCy^u;vVdL%LrYFOnu%P09TwOMYIoe_wM5THU(=|T*z zfA3w%`5__$#*_yAtcPmFp00$IA9qr)-thO)p!puA1)@?8W;^*25g`;!>xhB8Ie>r} zq;pVUssQ(w@GubLjaV(tXl5dUW8)AIXdunXv5_qO!;Ipv&sCja$^0zjc(-*hvm;K_ zpv)dqIP*Y-sFg*oiZD3iL4*|wAqu5JpvKVPi3kCDcw0^ll-Aykce=}FGLB|4xmTB& zitwKo4cWVru4}I^ookcQ$n~J$o(MyJn#R;~EH9zWc&)VJi_%cPug8rg`Zy_gI_%ok zwcrm`)y35oVZOq?r2&Wo#P4JqWB;aycaENwlYreck%DHrO>nYO&ZD~d3BlM5DnI10 z2oHgW5bae2A&g;>KkC4QJ2xCfp9i@=3Pn11S0RM}|d_0wF|=dj6wHrL!nOJ$0jrYNdqSF%$%df>&J>gnh41d;vByKN2z4u+6ClFqp%>eiLFSgkBd;Vn zLvl^Er;#>=tnz`={lwvYw8*gvsnxDg1FrD2l>xvX->$%ro+=}cM8g|+&m9==1W7_^ z7fmciDI~C~&jU8}0D=_~h>@nxtq}u&0kW{=+S=oH`FYw*cl(I6WSRgdRbtAh_y2}X zKt;7y6i;R+{sfx=XhIr#mNPQJn_Wy6cBxU9cNFPEha3t$j`3WL3Jz>I>2wSwydXG4 z=dWMl%8(E;^aMbnAqX+IR^6nuw6g)EJcF$v2Rp^pHug4>eS8s!?|NlSCUDFP`QGkK3w2m$irua->f;HYv=cU#DXqIm^j#?62p-AzhHmM zw%IVMt_sGxRyZiylUZoeo1fW5M7xVT&ZGI~S2DUYDW`V4Qt)xnujx!p6ZC-oc-HXF zA%WD)e^kFQ12j8#=T{eVD(>%Z??)z-lEVFVkmi*wscQE>Gj2tJ;mb5-&$b=`{@&E{H@z!+T!XVq}V zt!nFEIbQ1dTAp?SrFyYb4@^>!e*q9%g#`xp$Fbkg*2SUmXid3yT9`EU{sD;diUtIz z(|ayexQA_D57R^sdSTMSZC_mK7ZH)1O!V3 z(3LPrUu211PA%o>N5OeU#*xZ}1h)a}*u5W|%D4k=%UN#qRCd*ni%r9dB)C+N9E73} zpb>D~+psCMtNY%VUKFShMABV#aQl5aaWw7i;=v^nhV%6FFyBi6kG&P~O-kI)K%%R4 zL4&U(@ zG@;ZJWCc$T3v}H*{g%n1-S?FE8J?y1c`2usmLIPNWQmgG;zg&m1_{n++VN?HQ*X?R zIH2UPuXrLWrpNp15I9DFq3i6{AYgfQaJLaSdV9Aff?+>sPmIkNoeor;N$0c!Cx8;a z`-j>;7Q=K78s{04^=_L$gd6Cf6e}QIZLz%TCn1QC0AXc4Jw0!0KZ{?rJ$G}QT(vC( z?3iQ}?QxCGf@3v1&1tkUzeyg>fy6Dl5AdX6ba3efRq2RHW`GjR?%PNMpadZ^a+EMhl)xBDZ6Dv@L$nt5@U89g@ZMdKou;Txkcc4jcNyHec5*l9(5u&ITVNnaKYa556RKa-`Km(#j0>RZGF?;vw3RI zV1yp=C^A%K*+d`tRC1|>xr8pas4m(^ru1)Q5S!aw-L@fp9aMcW z5D=(Kz36@amgU>@(Zt;56oruiYU{(+rikUttIJiP&_Axzua8@tH5W*rn3e^h0Z!7d zKObm}E;o-9&%IG-5LDvGN{uao&H?NJcvnP0zB}4r3RV}p*nunz z1Sjv;x-w1AKPO3(Z(+^N*C354M<)v7i%rR^E9vpS9MPY>JB>P4=WB{GmQfV9i`!u| zx9YVh5P=1gPC5o(zwunUXdt2(9=_iibVJus=Ft;5#vFN}@>10jLG3nkxeJt9K>(6T zAdpBTP-2q8pSYk~GG?0LTItAhZD8Nh#5ru$|NFbCZ+mgmZD2s52 zPduC=D5Mfi0k?tHZmby3Dg{tfteSDk?I&DQSYOquIza^iA~{lnjAf9LiZ&{a)Ucoo zSqxA~KtV(#l6LQ?U@2~@2H1cgVl5&ONhuWdgfR#Nl7xT>*K3s-rB%i#q9Fw1Z z&!mdnxMkGM*P`l>3&0Q^IzIC`IqSEW7Q$N2=S=DjFcgQs4vCwWbOWyKbDn0qp9?k* zaot;;zlZZ-w2!hy_3oEnVu3Dr+j={a0B{Xj4zAt6w`(F$4owJaeI4df;sM|~>^VJJ z{4O`yXDC9CMqV(?PnrW5l`p(u9=2g2gK$h#yXru-lC&}m4JBM{_KxY_k;!THA(4!+ zDH=RS5%Y7_6rv!4K`4lXl1M~CQ2{83FV#W>0F*!pHl258b4Jj;);-5>XA#}euQose zV*^IXvV>W`iQwoEWpkeup!Mog!C4z;6aq{G`{8lfsulwUsAK9iJeP<5=$9{#T&4ew`)<1zklM(k zj@Z-gRRs!QwJi8jV0?dE4HIA)1x9estc@AFwSinF5oYD&7rmWbM&5K-P60N1WKPUfNS-MB;c|r6U>s4ZO!nJ;*y#FW4hD9 zsffS3{O6AJZ~W}R^1e!Ev0gz7f;;aQjJ9UK9zBl{F92&c5+*aNicB9O=V#qCNyppC zMXSE#U#yX00JdvwjFDPWn_TbZwBcMYc3c`FCVfudn7>f=@Mio>siB{kmAE1Zjx2ag zR7zMAEUzWo5$MB&SrL-*uTeSSktOutg$p2&ojC#MVOwzGgjUo|JuaDC%H0$)N`f!=X?E{~*c zq3hTmV~3z=Io&ZVH0}FzE^&#-3x|di_oz|e;kl|XR=kzmR#MU;0AW}$s$fnwI-GyX zJ;IaeozV(k6C2)?xCv#iWtq+<5CB{CDz6=D1?)RKEMmDwcB{QW5{ufFFK!~}05O4w zkR+$S9xdh`({033&aH9nOD&fQR;LyQK0CQsIoSw30Z2Dh;XP`-_mBl{vD$n!M2izH z$I@}Js<_1PVBcm?e)Gni5s;Y!qycU;fHIl|A=)^V-!337oumzfpwM>i$h61Ss+Fxg zEQST&^~#Bq6y3e!tm0t0a*z7FMj~LE!Efv;zZzV$;%zl` z+pS!&fB>7NWO$Td^E1D6nu|FPq3o}-kM8l$(bgEo^-f2PZggxH$jd1wQztSWPt#c^>WxN1nbYBMv|=Rpi`-W zWLUW%sILnLY;a{ZE)~f7V!^s-YM3a63MFrtPzWbeuDGlO@vMfz`XF-AYQz*?N1}=P zbkJ4F9J#_g8A#Lx{g|>|Tj$_^+mT$4tSm$l5)BHLni9 z>}Ogb zOlc~virq_~aHeprW?~k6>rBg*{>X$4eETMQ2w`v&9vC2Wn%`?3^T#?9qMdGKa6ywo za`MWk;5*y3hm?WFG2O`0?V;7z(jk;FD2f@NGD4VSXOxskSz?b5F0&0T>{dt+$g0k$ zuP8uBFszVfRfvg3L@1I~QW+UtBuQs5$Yo%&l`yhYv6NYba}MF5STiVssO=2~CJ;L^ zqY}`n^DPEs!?d!iGzeEnMG^>q4_0|f!mVnqOT&eChe#XTp$RyJ!cy)FiE+eRNnsQd z!>p8JvTFi31~bD7fKd_(DdAWErWxaP#TxHZ1s=0g4MlXjluGnm9Sp}$q)GLyk4Gc1 ze%l@4X*PBP-HxLRUoFXS?Q$}&@?>q000Z}ELQ%t30JmzFb0jw&NWrMBoVV<2i7c%!e9P!qa50DTJ@SyX`4t^4#hA8W_ zMJ*j2=M^y4}{5TSG*3 zxbVag${6Znx01flGE#*kAkQeCbwHZI793h(_l6)42zJZ}!Ts>#1-0qNQBcU2wyRI+ z!H~_>&q)s@-4ia-UY1Vsz*W_7lQa)?$EB~vJUAX-jH0&EWU6S#MB)FPGmy&$Ejma9RUNH0qx#28Cc=XfQ-ZI+Y=F_F;TSu3M*Dk@e zi<_rO)Uyx*sYGHopyZ~a4GTry8B%aBH(L#Q zN;Fne91N)%W|1&cN!auCIvoI>Q9wZmh(QE`gpf%f5KxpPAs`X}B?%;L-8zDM$}vx5(@PRAo8Cmb_+&f+uLi zvsNjvR6=ZVfY&4>^k1~L0?(<9t6aYx<9H{2^VVpGbWckdzP{cP3WsLL(hL75LclY& zuctV0AVPsWnM(=GfpU1X!dNzLlGLSX>56Y+LGU4TJ7sM>4G-%p%1H(wj0~yUVJ)V4beE>)w&K0&T)?)ZXqi(`%f~w^LSd!8JAA6X<}A-wsG0;cL^jUFkPB>Rkj z&a9|tSWD}i$;7{sriMT9Z-y{wAWM+yXv&yGuFelwR*18^YaA>uBaE)tF|mML%^U@GvZ{ zB@nwK^P$nzRMQ{EmkD`zg|wN0zKIv7$4Uy@;1hq2QlU@D$@Oyw^)x;@k%sQMThA{4 z5EVG`=>j?}GBG%v``Hg!B~+;+eu;P4+0>g(I)C%EcmFI&B3({=RFH(w08qwtf*d9n ziar@;$I`nhH9Pv!RlWuQ00v0U^rL`wD$0;clXX@jx@m?Rd1#sJsd-9ZNxf{`(*ax> zs$nOcGCB5-)j@npz^}0W2h+t8L)Dm}{R z3D0-T+ffAwpx9je_dRQW5~pE$;($F|SHQp8C_{0$2b_p?!#{S+WB0xTce=-Zy$8Kr zP7)G7$%`F@sW*PzI^x^gi)(3Cwc#@E7qZ#dIHPy0d_Kp z&3)?<+7}x|)=|l_^~{}F`YO_{O4@J?troLPWkMXSRK^4E0;!|^e+9pA2?-*ZiQN7&m=&q@pSH4F{0qR1WJB2=m1o^RfP_ zAC@Xb=6M!P%hn$U4(Y3_&S+r%J4br$`8a=l<;wmdFYh_SU8A#Y=*ay1yg4dbd+thk z>}?B}08;a+v0!7Q`8TNQ)P3G=LycqouV-23`Q=jH2m$6DjUL3VJ9EPwiJF)ZMSAq9qnsSZw z%wl637tF9!eiExdTETBotvM$@G9aZmFr*rY-C176rw0H@W=2o z9e6P_#q4TSPuZh`N`?@CP$nGQO3i4h2v8t;AaoQN-z?m>UZ<-+lMk1T9r=V{!;Zr= zhthSezj(mKf}G7yPeadmjj8*_?v3;gsPdw=tagLgoHV?pBtLAjH(9bv7^3Xro2Z~m8U|+zJ_up+B z;E8;}O(;N^jI%IQ&j)k3gz^9yE}j42$j0yP^!NhN3b2;RXv$m9&VV4y7=x)3>36W* z3Z`kq6q6$b^(sOQ1?g^nDn4r7K&JP-cSNujlkYvmh zAhN3$S63LSvNJOUN@y4`NQN+q#foU99zy|8TDjFsB2>#uuuzDtvlsjSnObDYc_20g z(yWYCRGLl-s8NpoGArO&Xokv)RZ(iHqUeT-g#r?qYlB8kdT=cGl_`5EB#37=adttH zP_0!|q}2kl1Q6>uO3d9tkfBgnnQl_30Ys^L%c}s0P?A>YF(_RHX`BR#7v8PF_W*t8 zkGi(yBEdyo2z)(GuR`6zvUWjkK)(&J8ouG4)*obQ|H|V>1?8wX9+J-*v}c(vnFOO0z%b?gzt{t9Mau z1?TXlxJbks2dt8j_MIWufX2@01PflJVRbrQ7Z>Ya9i-c1e)4X18;h8EnXJpZ)9$WR z!Be&G$bkckO>Rvse^0>Qw_8d^Fhdf#soNGh zGEz7T192vitz68WUl}ZQYM|jkORY&GhM=h40&`QMR&2(aOLB3^%S7=M?%_aLxMKht zjc(b6H|lSN13v66wRF%KJS7VHaL9gtaH!#`&!I9e&)CoEqzu-anOFZ$RR9~7r)GO) zBYVKL+Bz&0(Lr*?yV=Gi(3 z3oA=6T-$~Oz2o=GN@$k7$$%g?mpAmx(f?s);&>~4f1A=w-NenVE&bT>*YLV?UgKF5 zy5$y}&+kG$0G7u?El@$i5)TXY4KJW}LcF=+b)CAH*e)m-Ad!3cLYn$pZnW43{MC|Knk)ww)&nh_e5eje36`Kc!K^$WyxbiPmPvnD`XzB44pLUe|3`2mPPNT(mt_pQ6*@-VFTx z=>xKMhD@4J6j3BOsKtyLyu8Q(>=IG;(`O22dx_LaQ$V(cIXcL*ZI+;X3EmjG8IY{= zGITr?Vc%3sFM?jNND-nRi&dNdeUVnDKq@%Np1?Q|w%+EL_>Gb^ZT7 zVgy-_WG_F2*%<`UfyI12#J%H}@wJz7;Ln*l{`D!nm(|bK29;ajR&?*;QomEvQ@KlX zZ+iwjZBn1C2<-Egj9G7&Hn&@~dZ5?Gxvp045s`V9RjwLsusJgkf#78dYzP^*!ccim11yeIigLR2lWbJIXn)V(DIdg=A{pvC4H#i9CS-mI8jmjZ|YWp6F2GJy@@~y1Z z{Fd0og@o0yG1=(UTvmeBl}2yU4M|;{0I&Th;C6pY7t}PzTph#64L{}Z zatw>=G=SVi^(Nxq3phG`Inxu~5F56dkIs3};1P#7UR&PR@tmAvo1(_dmOQucO=jfcL1FCE`RZ8ywV3_^R7JrlToEzCorF z^x^c;)RepqbbD6Dw1!N%k zjaYhG+V(A7Sg5+3E0kjbnBs(qRJh|*Z_5!VwjX$Yvpl2sXX>zIbaK=vT52w|*uI~+ z-Y$R7GxPU|>3z8W+Rrq5UhS{2p{0d08}9dTfs8=4A?^6?^))0vpzVTf*k0lCYt-Z0 zn(8*{=S5>9myT!TB+}qc&8pve_ZhPhUodWLZD8xZol45jMMMZ*&i{POT{?cP+A-7v zqys(wi?o3T&v%iMi)jhQ<2O^;R&0}#XRnoxWaGOYffk-@$u?3#tFX1?*q_++3XLA@ z1+J{lkU!b z4c++1?}3C9LBZ+w4udm3mLf3F82;8Xc2q>zAVhO2*qiCM?f#yhjVcL1d`?fNvbBN`u3))rHA;L;I#4Nbc(VSWN=?_~Y9jd@!v zlB{kMvYiU&4)a6L!rSw_?u%h>%6QT;6$5$Q0s*8<<#P`LEC3O&IP2>xm<9EB(LSRT zFtTr^@1)BEu-in$h~n?;1WO?>8dKgql_71J`PtI;)Ek~2r@j(o3JBgeDjo;L(x5|w zPi5+QF2C%A{MbD&-j4L?cI_V^g>K+JEz=Uq(ZT`5ejxGYa+BY#d|2r;uKBg+7Jp9T z7w+HfOyeCJH7`Y~>ks=}vZ_Z#tCebS;y$axOMSf*eUI_=(%HUUBb3CnHvhP{(y8F^ ztiBdiD&+gNX&T8CX|zkX)-}|&mTLL#RIRlWoVER2L{Qi zo1y16s-RpSnEsa8g*3D6rF)b2?4_a8ztc27BHHEi3wKXAVeA>GX{C%6b??_c$J4dP z%lew}KHv7F>|%zG0(BQx!1NCwAT=HFsT$*_9Kj5<)BgNBmbZtRnp&ObXPg|)k`|iH zChwL-EgiV3e5yC_8LEksL>th@j?6Nx(US8&$ef~&y_D;xJFjkh(d`FY_eSaabL?#u zc380%8W}Q)Bw5y11hzz#;;MUTiEsNk&HcuIe?PKlW4zS6fU zsjjul;()sO@Hgu)J2AQcJKa;DTwHHz4X^=;szg)Ms+*5ljka!oUaH5kN;HfYv7`Ak zf};^J0`2L5lE;IJHy6k76cwTu07AL`*h?XC93H&BIHytw zvbov!)h?60Mv1Z{^^&mb?a1WQaxOBPkGi_;2At)S6}#6J%gZ))Z?>_XTri07O2Y1D zwJ*Gm!kpywJ>>-NsnO9hDVk`L@is!1-Vhoy)obMkTVirR9~;DD0kd=KhVY_zvF(=o ze~E+ja4^m&bpStbF zgmIfJERt?okH2VvfgS2`oQo%Sd9OwSA}}%nK|nQ-R$^s{AOJo&MtdRZ9Z*@{5V)w8 zc!h)@4U9crb6G5$STW}QbwR|p9gIZZZ zMT~VwPbngAsmp`3-q`xzw#shLuBRSzUK4=B_l7p6Ma$Qo6E4VWj4quAr(FIh%1AN@ zaeEV9AH8d@wPD=|*ZyNZif{TL1duOmU*+H=-N?M=7P7#Xl1iV!*TSN4O6qS?+1nWx znAhgi8aK==>);O@u4_&rMmh24Btr>oTW*er;++9H(IJh^<*nMZ{@42SO(eki$z~l~ zUGxg=(;k2gvH8u*{$$Jew*RtsN6Qvy1d(KaJwN)z>rr}q-g$gNxY7Yn2z!Rq1U1Y! zVFuR7_h3E$&+>>8;;_B6`}?GginR34NgxBumXvrD*B}+O(Bc6~jduz-ZQI8OLAKjz zBti_Nw!>{*c`;jg0|GKseTRO}I{{7|*cbvEZnfbHFc_N9`JP#`p#XqB)Pn)-euNeJ zD`K2F-oMyq#}t9jKT#}1u05B@$j;zPq0z#lzrH-_MRWa1I<(4 zC3W`a+ufg>@ThWZ1Q7Sh#`kyI3j2)>2AU0(q7;xvM>buNeg^oJfG`>}g63=b&)d;ph8u?~0^1#d8Q%FG^);BzHOF4CFe3Vc=A1INJL=H8Pgzk3ZUr^>DlYmp32vet9rq z9?_I{qbe+vkr?4^456^#iN1;M5{PY`h(IKC&CBOfvSC(ZD7$K|ZBNXA5c}Z#ALC3- z9q+gZVPCU&IE-jMZZ-xwYInbECYQ~4JKu6wW4_`G3cl*F^f4M^gGlYG=DpEjLH{%x z%|t%dI!w>2bET${x~ACH`Az}@r1mp8Gx3BhqmSftidS!$j_xo;W7Pj568>xf3aNN? z%0l@ONmcKDU*N$2yKBqrey|$mSyz((S*v5Tqp1jfj)U8}i2IVy!L&L{U@~~1L>C4F zSnm698`^7m%Z7H>lBz$J=&*!(Hw%^D1jOR^%CoJUhACn%rO(`%hE*2GK5C}~!hCX8 zSx?IdkY_!7Ucad@=g*ffuADn#X=RfGaUa)kQuC4HhsX*(NR~C~(zKMsP<~#@r zJt&!ft9_Z{eDy#S9bc7Jv>d>f^6U@~PWS*ly?Ae;4nM{_=1}DcX^*OQ38%E7p#-L| zb0Svv;#C)NOh6r{B{KFTgaMAp{V;pR(DY0ZG(+^4qCz4|>$qRl$f!TE4b+bZs6MB( z|8#P}p$|aE1Mgi%z-Y*^RXv?t)XkSW^1oRI76*>eq5mWireKJ?Te*Yqu< z5=eJ?#uFJB41lTRzX#HBEm2wrOQRAUnu#M@0pMw{6zSYzqE`#k6m;yO-W!;~6Y8KJG)hXNoH`9SUD2g>o8-*k;JsDGcE> zmdX*5nE@6%>Wf~nlG7tGlake?1Lhz1cRJtc1Lb~U`@i$V&8-_zLa2|9EwPG{5(pFC zAYtcg`Q8^3iRXvV(Ce;)WEJ*R!xtB79g)cbC3$Sj08CPD{Sayic>_3T9$R&GBj@9J z+yx046YY+?rw(_}-*{Hu4}snF-Ki#9vW_>qkl5dh>MW4lIjiRDChgeEoJ-*4-kkWwR*v|5tFKzxrnbvi}X zbM=xZZ#4dN9Vbf~^88^ck&RYMvXpH+i(e47@Qt<~*@FDMPDVF+u&o&dWYs&PZ3oqZxCQR!4nmmXFv|LuWL<|Z zwMaw(eT*?c&2AKdY6KCrmg72{|AA?jddB0SwdikoMWh@ zG!O6)z$947kO!dM`~youM3&{Z6_VWR*eb^LAnWSpPrJDxEB}JER2&xJ=pGTg1k^1p zVeo|Xl^dk8fo2O`AsO3XY%)UtFvD9KpcXwXi!SG^#6l1=Cq@oELe?;Hy^|>op_4ND z95Y@Q(tTi}?SGhD1tp_4t#1AM8mU$H^T+%)gW4bq8a1oay8N~^mlAaAb91EYQxs|O zLo6K!PoCDW_TlR>f;pCPi9#(6L1&8^&`T@g-^sFDISYkcLg23mN=-w;r=W9_6}YiQ z%6tr*nDxvj437BE9z%&g8yV@gZfuGt#4v_V0qrKTdSgIF3^UKbJ3{*0BGJWnKo zBZuI=ukU}`93BKACsTn-O%}bDra?a* znW7+EL{4!)xLJEnauq6qUJ7)|V|&%s+VN1*UqexdjG)Qt4zrI!mUEhhQ$Y#Y>zm@a zMvRgM6(rp+f2_O0qk*^Woc!MyuqOs3PnZ;pBE@SPz=x@cPpVVwSB*mZQqX?={Aqb( zD-ImD+N?N!ju!0Y=2E^|n3=OVz&b=o3fj7cafXr^&m1cJJWmYd#-{^U_?aJ;k>`Vi0c!L>j0SP1BI6e83c(kd{d`)*-G|t0L1va@Kj^Hc5U3h5Hv6 z;_-I$vlE-cLhaFRt%GE16*NjnBT_>MHYx+IFXYf2;HeYHmBSTpDdTN3U&wXepLGeG zyxIH4?X|olfIW2g&M@25*3H>rq$`dco1}yqCDdP7 zS9XF^&bY9Qzv!8~nMy4Hkbr!m2P6t&L`GG`%-rm8v9b=}+}&yMFZ=p{3dAJRmOnz0 z-Qfoj;;y0!taRQghsc`}ejr6HR*g4U!Uno(&1&Nng=-aW@D*CBMZy;)v9s37gOhHE zFN!cQsTWOCGW)J`LbmE*Wsv9C>8sOfbu|?YQ7 zwt-2}x>Iii8jhWev!IN3uV8YUVUDIjAqwQy+sZFkuO7Gsr;m07D+DCXWFp>T5;rSc zp$8@qJIMnkcIc!orb>wbU~=Y79Dvglt5~#*P-_Sh%sSQ>5tS-P4;)i)Sivitq_}-Y*ym<^77=Sav8UdXQWq;lY#^w>V60P=~ncL-OWp~3H zLeyG?7{wmOZDlNA7{oeWx^OU}JV0eZ!Ak+(W0?*dkcdfQ25gaP^$NY}wt_Knd(#8; zN4v%u68kmT_@|a>_Tu=+KoZuSoE-}ZZu|%6Yqy6uso|Iz(8~om_!SG5Qyge* zR&scKxA?z(JD~ZnfzK&EQ$&$lvP!{K5+8G&hyB`VDc~bwr1M~hzLJmJyzbIK?pGC zd`IG2we#K!YWuC6nqcDGqXFD1Yb?7`Qi`#!P1cUkvdv#$M?H4Ub6h*y`7ICQz{Jm; zhOb^^bGNK;(%y8TLlLQKB^QQEGLjJnyY^rOe_O0LY-=bIfd~qE15dnUa%zbSfu~qP z3ntZ2bp%Z*U}U<@g9MX6QXnI)hnS2_doP5S9eK~~){Wh(vrsh%A*Dvt>yc4fK;;EGb=ue(65V>EQcyYjqe~|p z^FHcW`Wc`eh;j!Ll;76w z$>pR9zPzN!o}H@pN%~zA*eYcDuH`1(rQRp>ipFyLVw(EBi&F9=*QX(c8=<(4iac5yhr z$tDzHB2j5oP#VT`BUKw<_sJRh@k>aCr8LE??b2_y$OBNFq(ej_npDPx*VCoIY#+yw zOWWB_en^TrQy0j?O1eA*6t*n1;Po7ew3kI-MJFzjHgFokp8p3*@V2&Qzmc54-y0$j zPxnfbt4eN8{kI4#>q2xi7FjCYS+N3BO14aFRs?HMe>lv-Rz3(0opEHZJXp84uVbp* zR^{0Ip>tiq2R+fYC1$cq_Y@3m0&#}9js>>9n9Dh!~kBk124e4vWCj}`2<=ohz_$1Yo?5_tPu{|Jn z5a4_ulQ%y(IX7=Vk{guc@+cq-qn;n<1OZ}`yYgCKWkm0?s;Iy=z?jI+eBC~6Xt95w#yw8M1&{;{QS`2Vm)_T?=F_ANf;*#>Yi#_|e=#O(8FaUG zoXG;$BYmLe;7j&j57VzH5!AoQgyaNZ zl(4FaFg-Qrne&yBQuO+HUwnQRCevm&%X)8lNB8>P#Z_d@HdkBvQqVcA@{7nDDh17k z(r}Ea;w0zJ{_ix26Qp4KVa*GRLC-&Q4WX>n-CEPLB_tqUZS&}~Edo=PgmoA@ zrZZTvhm)7`_NOT^z44D*2@P4jHb-4KGr69@{B293U%iZMs6!l5^7Im37hw zoiX`P*dy|`jPU&(hZ-h1O<6)f?+OdkG@xp#1t?7)UB=q!{?%S3SR#~!QPrIlydH{x z;ZJS_<3v#52-npZQ9s30N+=YB*eanUf&l=2A1OjGN^2SQ6anUtfrOzzgn`8U|DRb6tZ)s&0XbCVt|o7jd#i7^0+ zN(>M#ke%@W8gt?x*dP%Ti9{Fb;6$J{-e{qL>GnhJ-DEI?vl`=d7224X3rpw(JjZMC za@Z+Op1xU(`q_>YO5@F8sAKQ2(N+Tir&cyf`K%qAk11)Y9yHsKozV(8-onkwQLKep zYdI`zeTPe&#e{jvTSxC18f1=Q1p)dUGHqbM4-2fEoUW$(onG^ovFujA%Tx$Rbp1f+9$^~IxL1$xOJYCFK>|a2$G54GpRL%b zAQdTEq@uX?`1qNRLeG923Sm0{*R{gqj47lb$WoaVLrqU~Qp{i1WNaUZ8uE_fh*cU8 zxxdz_?Y_#-BsZ&cT}{=t0?twDw;r?Bou+)};^!#k`OoEJaJg~rX`sf<2WIn={MpT0 z-3vTUIBlvoh6ZE-&8bLIQ-q;%Cg4UeKmiF7Y0ul$w;44xNs?w)oqinv`f#1D0Q000H7`&sDo(BKVx!#iuP&b&`+PysJOYVXPj3IK1f%tOZOgmFO%7UA%4 zUvm;N-#S%y+i91>pjh)^z1}<5b(`@&l>eapE{4tFP~)1_G>2VI?FvEk-y8@AV8AynRuXB5)A;Lm-2P`|f*uAl+$x zV7@4Fn4+O@V;5iysiwAx4p{vADYN?lz}Kl&CuaC`T~a#U@qf|kjmO(T#-Ee1r&g`a zWJo(Y1*k!KNq)H46pHPr`c$<(0)-B@h#)8e$YtP!E8lk%g;3nKCH{&SHrLTEng~L@ z-}$!x6zSf0hANP$CkS941g<`bo&in}LG}fYpTD%>>gg@)n(riiHxx(K%>ntavA(ZM zLMs=cP97B+V3IfjPnz%_Y+_Swl`Vy* zX>8P}2m}yF-9<$3ez4%$TP9}Anf z8do{j9;t^wmDmFHjpumY)ZXL5;@!;X2!-Ub6%YwD&M{E`mUu!t{ zq-#3j@++j_!(@HqF#O(LQ;OuJ|E}x2l|84Xx69Ug`@VO*n?%Mvu14Y8q@q7v?1<^e zhaOVWB&{-1iKwn74v)9@Ol_u?W(LE-^E(^=FVFg%3{N+I%D2R)==(PMTaJGR_4)r} z%jNxl7ehn(+2>^Nw93m>ZW@l%Rq~LOOQ8Kl5=}I^a#KyVRGQzbJ(=`Y<0hoE)qmco z{Y6xMs<4$-LW-*^tg_21(xlI;@@G!DcGByvy6ViCGJ5PqVTL2H#AxzYVR>t>EruAF z(+sykx~nX*X{e<|RaHmIs{vJ1NktWHWvA4ryv6T1P0^{&p+1e>Hkx#UrkZJ%S(`TL z_3LdjqQfnBfpX0>-db!*A=D$~5JS4Mp&C@(>Vnrej#6Xv%GxznPy+~tyP zV@`>P35G8HKVqT-Zcv(iJUEcnSw$1A6Khtj7gt{$F~=V_NjqB{%w*5a2CF|iTRij69CJkzAgu%w zc^~SEC~JHwA%=eLiYS&#z2-b8nwX-*MND+lO)tMX*LOZjdSPsGcI@-O0Lj_e1I8*` zS#>FSnHreLV%O&#;vCAs^VMgxk)IC@+PH~^36p4oZ!}IQP@zu6XNd04<>RYS5YL{D zNaJS9TG!mvX;!ht!_eA2OY%OS=<~i)8fK!2Nlc|A!)6*6`JQSy^XX2DMJy@phn(4T zXs+?wx*A<1+EVAbb#(4$)f8t&-*BrC{3i;`+oargdu+_a?rweeRa$rM|J8SU&R+Ag z|L+6RclPKw#p7r~&itXS0LsalqWzcco@aeGlP0%4sE#*3h3ey6dbuEy4R@Q;=x6r- zX|vq-J>QV;KXdLt7;&pyF1v7#++J%B=zNQ`%lSz39fbLj_+k8?Ha4FOTi%oG_WRN8 z5Kk&4K3~h|5p~g0Q{E($3o0Q{(Uz(d6(k?M3ZR6LsFrB&o zqrH`|ZIEtjg52<&fj#j*3hsQi*JA&f zGleirG4HRD27$*QB7y`H=MJRi<6(ub@XlqGKc(YF(@V{tJqHQUJ5oH45xa)qCP;^T z@Sm!fB^v`1szL{sbLs7^^wt`xRx{~9fnsTDu1Wn{C{Jt4S0)^0Q@-!7R&HI?DzpXc69t7(ATVWJbbsso$amz zHlpj&L8Sh4-n`8qtfPyx>!rPhGIDXGv9qL;V%v49cCUuTXeI_~g8m>HC-At!W@Yd)l09 z8(o*cxpY53?w*@Q^j{+5sEunElOT$vW#{v?0E>3$0YLfq;-^E5U=oGNOi9Nu7~e;t z*Z=SC?>&7w)(xwniueQ10JnD|zI#Vi{#-@0Png5je6E!7sScgv#;n#Jb>znfu4asV zl}Z1Kp?=er979BNDt`Kazg;>TpoQ}wa%kx86G~g^&g_7r_cJ{?IDXAnOFJmM&1aH= zW5fh=orrCfPw$u1V<+^86*=x+tyhXHUH=QEdGdIIKvr_cXVm}osiT^Y z$NJ5{pzb0a)z;nAE?yRy`g1OuJ`32Q7f%EL3NzH-pMUS)z3_FGBmr_J?DuQZa5|2tMe~1H~W@oItPZ zFS|iNZJah0`V2u2=wp0QzKoCGwzIXVHtmKQYeBB5&Kee&>#P1ppb+CyK z=y+T17wmtBK(;FRKR@teG0|(DtjUG2hca*4q7x6HbJO^HR@iRr{a&qY&)%BA&FVZC z-CiJnuX}qfSmwwDNJxXwOH+W;hWULT_hzm}u7z1=7hY_bsK=vHg>hK%VJ5^$^=7+p z@vsISTeZDU{a4ueH~Z<(yf^w}m-RVsXu@~j^MZ&V3*|L@(m2;QzkFfg=kH_5H}MXz zBDQetX7xKPvI^W@e#m)+4K#X2E_UtfeZNj%6b{`?{Ra7s}>M>Yv6Gkvx7(1cDs&#R1rV4j%FbH{C)VT0NsW#;_l z;QTwwA)l5m$8==uHXi_ui;0DWBJQj-ZqxH3NRH)jV~-c2thTL}`*DybDSs4HXOzh29*~ z3_0~PImoI6G07Bo1MH@BR7Ne`X1K6Ry`FTq8FZb|3L3@s$mMiAO8bmQaC4{1l76ku z$DknP1xwaP%hwh;seu=@v&PPm)a@D3QoqQnpU_?DU!=~vzf|(T^;Jf(V@Y(x(nRcj z25fyVfu7t?;_o8CXNAie_+c3wnki26B0}-OPTKUjmHq(||E@30*-T8D?VA+w#-{W^ zmQ;4*fs@GHxa#*8?LNYVbFd;X^TU!jlUS3vI_zer%Q1?O&}vu)ie&%*1wjG=u{T{u z4BAbt%l4tE(&{xFBIKm+U!b19u0@3p9|<-r|oPwDqD!V3@tR2=2* zFezZs`|yHALDa>O5C>~TZ`JaDqqW%p!Z)Qz?uhP~i+rjcBWy#b!9)W>{l8(s2#mu4 z|5H?gfP8CxTic2?-9;WfVZxjgCjKSlZ;Qg_cu@GVum8HnSmQZ?hF`@gBl||mXl%pW aPYw_^<-D}`Ke5VR1xy^y6F+*`;eNOs?i4v(3Wqx!4#nNwp~d|u?(XhVw765;wNNN-El%6_{r%rd zUh?v0lbzYgY<4!A%1vHmT(F>KXxZqe$ox z_mCyI90&wJMjr54!HygoGJlU&5oS(~7?8zem#f8uz(Z&a3E=0W*S?`&q)_BmmlD%j zo_r}?wAxHiMZzyWLXrm1t9;gyUY_4{5GVyhmQ=BUp+yL1b`S*ZO)L6E2swl)DqQij zxXk_}I%YTtgLJ#xomQBayx8nBqlf+uGS*L5o8|WgtiD9a8HgEGS34+ zlC+avVNsQnw6mHg2g)%?0&~d!TLD0Z6mwt zVcUTmT|X0kC|2Jt9U9~K#t&YLshXQY@{?x1-hN4IZRxHw%)dR@xda?ViHrJin7r4l zU>6My+uvKAln$+>>z(bM!K(3FQxf>~G@FW0C1}6)6a0DM!Xqv!uj!NA)G z=*$nLnRYF&r*jX_v~3Coit-asx+96s1^XKYD{jn5?>ImOW<#^b+3N%Ey;{Cfy}GPs zq6eS*d*^=9mCjWekhE|%8xaT1$BuRSVkgvm&p}0!_m59&C92Grn#OWwC2Ys(ek8D( zqkid50!bXDJvdIX^6XeJdZiE%|3h4|0Onmd>`;OCh zz&IiXf9&GR^!6UB{@T!(Y!KX$5yOTj@s;pwo}Fz&yusn@M1SM5mZSV1-!P1sS7rWl zLc(KAW!w4G4en$8b-r+ZX4}@{2SYVAvT0@{l#589wh=t*hE%j9n2VpfulXFC&W!B^ zi_JW4PI_Hy0wn*41>-rA%_jhFhF zD+p<}S`nV!T(Thqg*e+($!5ow$3I>!Y!% z?K_&#pCY+@W6xI`&xio5n<#{g7&Cn?{jeBG1_8SjJsn*!iNIfe7$NuEOsUnwbRd;G#5D9wwf9eQ%e~5JHVJ=i(1eB-9@p(BD%C|Zb z$H>A4NIMNzI~)$x(Nwj;QYp; zgt8Cfb#N^=LZoHLMrW;wt{WOjst9Q{K2yQLSiFZOncOjxfbh4O04b!_wSP1gOSrrwgz21R$%nG?>B`Z@IVQ4o#Px|)FS)Ig71tT! z4rQ`@78^Q%Ck}YHkE0dy5$C63a^X+B4`7cr@Mg^IKX{X2#yR~pFhlLJO46k_>oQeH zo&2&5)DhbeljVL0L<>7yYvxHXU%W@QO8G{DTb$ETKk|$}4th`Zy39!5V8A-tAFT&B zrpix)T2&IIi8WNYALRXQe#tqIC-1Qfd3$&?aMob==5Mr*kJ3L&prt@1q0}pUs6{jFI~^HV>H( zn72!0azrH7O|l?kX^EtNuVHL09HuW=B2Utd;v6ltIBu8qHP1RldB#NOS#CuI+CBak z0)HwN{|)5J?)4Ytsv1#0qSbdBw%=8xYFVN>^?t%yQTvr7Xkls{VTCp+ab9PTbSP(x z;a`S|!Vd`IUMCaR5@(&E9dX`|L1;6#b^7d6l5;SaG%*trW!GFkMVo^iV;%t0Y|$E- zIdhD4I6RKa2=fb|)8_9_=&^?+V+k?DYjQy!>JK+_B8MEJhGeHYljGoC$0&Y36b|9A zqV=aQ^!B;a(#>^>mi%=rH3^h}XO$>BV2Kz3!`|ZekL$!oQH(!RTbv87Kz0qx20@UX zNql2TR-ezrA4cWNN) z(VSLUEpljF--FGh%V=Cv({duv?$jaDPh$9p71*u`L>xSJPw>&z5j;3R|1gB9fkBx# zlF2Cb&5pFjTNO|NGIgBxRI{whx}RE8{H%^qU-}NEy9EsL-(I$fQ28x`{m)y=OZQQT znr`VF0i7Xb=7pOf0@bO zVm%?(QejU45i8qrIevQm@?zw;g!v%jX>JF8bpkP0RpvTv9-G24Ot-Z! zdb}y$Y-l-eT1XVuJJl4oIb9Ub`i6{dI=C{rvaQ~!X)?2_wad17w;!C0@)z)byZlVH zhCcw{0Mi^4V;zwT!IQ7qPqs#t9}jp*a*>s~(GUS4bbum6v@k#z8X6)Zn9iJZ6nO!T zf}9jPyx%3_6y$_DroOT=D|K|+PG(?7l&8D?5l-H3io|{?gpACsT86gU`;}1i>E=~J zzUA`zq`$*Ar%iL z7caC8`2tfQ2^no40H6dQ(}xHF<_nv@j&U7>oiX`WZ3MwkutEFRE*(`{nzoP z?I(e><(XA0+fyFE*%<~My{5J$f<6i&&y~pZ&sSnQUuL30-Q?xqexvYTsLZWr?uU<` zPzxN^tv5VP9al`<4c*bv0g_-a02K`n;L)b*Y2SE8UNRAyHxSMa>(CC~o;@Sr&;&6k zXdu&~A>uLZRz*S6Xy4|%qYAz-dpErH{cJe>^7Z@q!yl5{ujcLNkZ$Mx5)EvD^d1>* z3Z(EOA8{OtoiEJqye)@z-Sh8zZG9AHDU_ zS(X4coW(y@4NQT|iE?%Cpi`+C&aMPhTws%ClHwD31R--{QkDUd(qd&KWvrlyft(q_ z4U##eMH=HW%nXRWwKMGK0LHK1*`>4)5VP%-=)|7+&#W!v&_Pg&X{$fQ(3ZACrI-|Q zP#mI?4E7}U7d5!gd&{XeWviYs^}tz&Z_+3>**L`wzCEol?!ILr{or;C#$O-SG|aw5nCG2t>$1W$%3`;4Wwg78Rl9$*{XI*&y%)cJ3sexcfiGq9QN8>7-d zEGU?(Hlk0;yxFzCt9sJu*poX-hI#o=2;Tza{+>DEUlzN^r$F!RUj~SmQEBb5Q*>;z zav3b?F?G4HZQZNut^MVpsBm36HcbYhpkp zg(Z@5mMy)6dBcrw_2A$@9}R(`^R->ZadbBGTb@y^_n44DBtgt^+Y-a<2Vd z7ws@VH%^9D;H#6>2y|9&F38D_42M}%t&Ge5usG)UW0)da;bZIiUY*1A)9-_FPvR-q z{>4y4$1t66AD3lJ)%xw;u`b=yrY||_r(fUrJIpNOYTr2>7wTvjWsK5Av?&{v+Amc;&p0sL$IQMtKGA*HkQ?xVqWOcf6q zd1y#k;4z=48=b;QuA*6>F%HaPq6M$J?#@@xv738)TR$goqD`ivvuh&-%1h}WE`F)_ z-n@0iuRj$fgqm3*$MTKL)(4Eu7kwO^F|0U%^Qv07$Io&`Tv1S{y(aH zLt$;E7cU*n3*V-j8k9N&RWc0#!hab%fO@UC;R}k!ep-*2j-M zIY(-}JriZW+6tl~z&Vf!M5~c`pn@48Q;yXHP?Gh>W)3f}eX6vUS?ZGZZd4BIGkN8y zI@3Z?Wva~5td?AkKl2IeNnx2)!t%YiN#+0#rW#hmzo~h)V+Dkb7)wgj8ucsWS=% zBuRuK`(k9f5T+b*@O(j7VG%h??h=#M@{-DmI5V87vihZB0}X!W5KvhDUY0ii&g-mV zr$P>gED=hhEvakZD%b^NWkayZA@~2`FSMof(vtR-RSe5Al3;1M2>T)}s{y7OS7d>t zdr0ZM7`@`DSoNV&fU}mvM^&wbvbR)papvlZEJW%Mer#@4U=GrveGyPvP7)9h5&$Qs zt_VE~PzA#CP0>Y>tC;^%W0 zYvNnZauo{RM*s9)lKFQo=D+ZI|3=EKJdoo$YquR5pDd~C6wNtW7iRJ= zJ2fZrgHNL1=Aa4nCWSz`$!xLQatk_}Qp^Y5S2ICQ2js9o_XyQ+iE!-Dgo_oo9k!Ie zF*u^$N+ecJpEc{I&2fDXzRkwo_AtlL(tTMSUx(2ADu2P~{O#}phg$hA@5ib1XxPvA zm82&8c}=>IB80(%VMqsalex|+mxuE~kx$Rm7!vET0*c$6XU7mOY z&Qf$WLlYu$Z@#}PGpVPFewz_AIVNRy@_iv5>%bHuYa_v3&NCv2pNQH}sCDnb_K_W+ zl-Lskz*`WltLcwao>;`YI+S7cI7PSYI>Ib(0P zL?r2*We4H(d#r>{_-W;eNGGz4;p3x<#~vHJ_*FZj`9s$ulRmA!Z?R(wJF`XW`v16MvWCi-Rc31PS)!6{gGyT`)0j*dhD2nHI9O*q%3JO-E8T z8Gr7GvlQc=eq3|y(9*DbAv7)xI}3F@{PFzwIx&q|fW(bOp~2AfAd2KvoKkImXUKr%YpctTf_S>jHAU%;p1CX7uhF=4S_SZ<;I0BJjJYx>14t+#M;oPm z@zzv=dTE>^_20Lzu6$={8U)8JK%V+RL?r3@bYs<|rxZKYYO44v*-C?`)78!z;W9Da zA1!J^&D%fmVE+Yb1J^s;ziWp3+5$+(lUP{ulW16-s8_)kM7O@()(i z>fJzILbOm$=)vj9nFNfNk5@wBLYX3i^Rww70ygb|_vqh|{VOGzI`c5M&e*Z8kAww$ z-s)UC3KWD5NYY3gz{8u=Nbk|v)>0h-ot5JR(EZajCcf}`kMkFdOn&UXRP~ki>{pkk zzfRnIje-4DL5e$7XLz_m)fF}lU_^)S&w-F!sS9MH7%inOVz}Pk1i{1C}y&u zLzaV%Q2lm`;YwS9auIt*os75oD~1RCFi4Ht)@DrDg_qE~Q%Q^}rzkBI8g&6>bQx{U z^5IK}4aXvzw27caAwWj_faY*=J`l|mgu1z=C&|^UK$5<_;3WbzCfeiXVj@bGgT(Bk zeW6=G9U5pJ6TNr!u1y&z)x~+btoPe>Vpkc>6C6VR$aGmZ>_V&u;CF{sdK&6lA!l#bU&&`S6wZ1^2pVAyS65teHB{1ciia>p>i7rK!hW}d zieZQ&NjGfz$JWmiy~SB1IeMGE-t1tW#Ej_~o_?cyBO8uEQZ==U?!af4?c|~jIOcCU z?MPH(j(M3KVazGTaez6-P{nGWmZzHuTeD}w9(>Vo+?Q*)*NCm?TyD&&IEz*jJ)@ib z3l7?T?2IXQxXXy^9XT9FXdg?vRFT$N&qnDEn#|MN2$ywf4BBtOgYd>Sd?9_PC7cR( z`{Ff28OF+<1HH5K5#F$aS|&ClerHk-P;*qyJ9M3;P72EBa_{W2K`*Rj=ChM(BBp?I z|FQLH*IoS`wXd;7DYDYIm-LfmF3ouce53`Z>9$PTX+gMjNUKQhKixl@3b)(n^>SC{ zISK0xjjMxei9x1w&gwoXeIG}RGoTP)WUU&xh$T!)_%TEuBNK|mD@#J(^G8CzA5eMM zGU+g(lXUSF^1b(k=mtt`XTx_6Ty}inyk?H4xxH^TtiS-OBZ9Wu|U}`$3#qV z=fUc)$%5`UBK)S6*RrucW-IyD;8IIG0fWQ*Xi_%SKQ)cW{qiBZ{kU1VJT@ zUGmzCgwGMM5X^H372nlq!0T!sJv)5Hb7x{K>Uzh6Fh=l9qf z2@Mi2`jjB4r}J6|Jb>Y!D7|WX2Pg63C2MXEj*NgRzEfQddNu=(ul%3xRZ~D)-|`Ij zixa*Z3(ZhPHX z1h#hH+7DN*s1RNEMZlWy@o4hCuU)<|Evx(}w;6qY-*xjUYu68Uvk_qu>ih+2zFPab z%eU&4ojN22?K5R+R~XJbqactcK=d!2>`@RZQqUPO8WTqecW5^UcFN3ERR76DRTzFY zAf&}?OTWjd!NK9PdSsX*3R0>e=~>8IERssKY~Yx4G91G?KYO>tR)C|JF;;oPCOGXr zJt3revEGRyQ8sQ$Z>};_&g(b?^5OtF^T+0u9!L&*8H{_U(SJKHISxwgpD3)_Uu-ZJ4x9nL3)lpd4ApcGG+OxoIJ-O zaz3L%;PG@l!}bY#E6=mW#lN=1;|gpk@Y>pUc}NfAiE>Y?+A5G;s<%8&zr`i?2S${K zTEoZt%k}NKtxdR5MpWH5qX7g_Q<^U9N`d3|E&+)9B1XNTEb;|rQ#9c&6D_zM@`wSp zxxO7UHuo>uvsay-ZgXJkOzng(S$cCLnvbum65<GarE`@ca>OwOUUFf>@xA|8=iActN@2+ zmET|nt8*Ij7CWi@;{uvu)yiGYoHL*h1ABkWuwB2Wfw@ZG9P_&*v>HxR%wm`&V^oMu zc2=D%yDnKXaJn~I2^OH5cApl`GW8E*DO}D@AH^L$IjHnW>a^W^@6t(dgq@$tiI2!{9qN6|x;&Q|0bLD33z zf(WD&f2#*{CYu#n)|B3-t>SFxX=c>rEJH2jI{0*(b=c<*zqoIVYh8(><5*3so=&bZ zUHxbwtgy&4RwdQQv@1mJ?hTIoX-J#!i^IoOmutI?VC&vk7cb7h58>)a zKRs}t2Y-!6`coVbt zHot#xP4_daCy-r9M?p-*YA`o~;#v;1h_&RApL__K@1ggGz2vJVXTR@b5!gv+ah zt?cDh97%Vp_RD>}(SwwCX40o#z{tA6zs-xbE~V(f&Fg1^scP?SEB$$Rz|&=?==N>p z!D|okRf^j~|HrW&dbrL`#(vpM#lPUepg_IXKoKFX+C#z%R){yJ$yJuJG5zikb(0a!-Z?^j{g&%)|Cl!->>0xWEjDZS7kj?9s>Ak#A|MyZ? zOa9wP(=r+cv#8@@pU3u6-RH1#V)4ny3&otj?f=8BqE&XCV~IB+8H&cCZ8^n7oEXn+ zz9WF6cqj(Q#ilh7k!dEt1Le2_O2?QSu6u4gM(SEt3^z=zS3J^J$qNPAzx-XTkv`|p zZYqRKr(@f*pgaX?W_6g-85mKkWl9)qbF_`xuo8t&V*PzJ9h2KA%rVEe z;8@g|1EwNvO{W}XDt83xNzCK_{?I6*SJx{fncE)6V%S99jN(K!m$6#!84omk(dZZT ztMu?N!z!%FacuJac>A0gZdS?FI@gGK?P#$hc#y{~%k}nKoviHqb&kwDNcu52BRt>7 z#oFC$H)Vfzj-+#QFS6lW=DNNiSLnBkQPW8dxK`9+;5{sm%rotFBYaEQ)HwswY9_{3z>z);;%XT37>=pfu;#y(!!uC-&gJ&XqMyNoKps(>QU zt-dr8^5&|c|NNXym{3pLKVR+y(nm@#wRL7VMwraWW*b^-E&)<@t+qIlf`1FIA$y`? zcDs41INaqkWsj9&Tnr4zJ`=<}9l{0L>2{)QbEvyWemp|9=rfz}_+sy6JY`4aE80ex6mH3N_D>SvW z9{62?k6mASZEo$f7{IYxVFobOoQA0Q3z&sqsu67{55lm~SeFT;Ns0f{S9 z+Vf=pFA~4;4uiNMD`x>!17pg@wy+#tRE~vn-A)w1WsAp_){NB-R+6bY5+zht*}QCL zKy}-%+2{c1tq~JEf?QSQNmuzL4qn9dLuMK$&@lVV99TD z39TlNOWc;n((u}Uhr`k{0sJ9(zwqDCXFumgTiumN#SH;s5t@PC3|w%DZze+CAM{Js z_88HEpehNM#39;y5m<-RZ}?n1uCrrr5^r4EX(0SKI~a5$4yyv<3Y#RlnS~^h3Qppf zsFJ>H3hk!rLF~rcbp=^(^>o<@+B0_*Hq?GWlR+x{gPn+G3?Wsd`xuOp;|NNNA&7os zFv+CbZU{zEeuW_$TXb<4W)z#<0K%ypOYPhUpxfUqO;)ccNxCJH_7Li%ypLIp+JRQ` zh=8IpprC+1h;Nb{_HNz~QsU$@%a&M%ETZq_gg;XjqmoV!zFVE3-4TU^s8SN^wBbi6 zOCrr1Cez+~T%@c^@j$~Si52<0@TwAs3<>b9FlvaUEl-ViZH1NWCRj5Rj2LQ0Cy4W= zBG{E8%7$80fe53`8Y!*0w5cNH6P8C4+ELBuekCXjNRR0<8KG4L#%lxJxv;`$ZN+pT zxhp|VdB(6zNk2-K;;oi8lCHX?HS&Fuu(LjgKDb);&?qvC-FR8`w4arLG%l=JA#Tuj zUdcwD6}u?(Tb9+{n*VsypD_$P^Y*Aen|paz?|ADVe%x*MG(2f^ug=O_x~{4-@dDSQ z6ZB|mA7i&#>OD1{KMVMXGzlcC#(~?B&}{jkEWdY#Yg)}%yBI58O0|(!tDMuqG+}9q z@$AdW)pG7~I?RjErH^cAb#V#s)n$>~t&ua^QiLf&$(t$@_W2y4!+)>EF z5v(XYGG+X6gHl9kk#zihtiz1ldvTnYHiZIJ=B0!n58;~moo^=HN4^Jf^~>y}+?r%4 zC=TQ4he-b(oZ8K_6hNoi7%j%IMOd?rOKk}o`8NlN0^eT;ixFRwwTGh#m5KdyKxJn( z)G7)7j2OujRv#)Q)(PT&CiF*{RZ>-dMQ`2^+drgZFR8KOZ*Q17qZ9Ke8mWM z4o7dY7>yd!w%ua<@RAXvYA41DeK-U0;4;tE$^}sPTA~`raz9@>ER{1uE|Ps{cTY~W z@hzJhmFiLQNX-W0yFH^WehEw zolCY_y1r9kvsR}IsbG#idq(}Qkgl4}%+TSO)#6DLKE;imrO4&Kq=i^fi)7W*Q**rz4zBOEqh6hWwzSwfo8pNZioi3f zgt)9+!SNomBRo_3;Pz(gi$V&j{JEWT_<<9-h+}bSP8dL?2{l%s9Uv*N9z*+phe9Yp zMsZr_tp`%nj71x2abD+oH{ZmT+x0b?myWeBxEP-0|+Lfcg*HeVA z{Fw#_hb4vL1oNMa!_1Va#TxDv@PyOn((#BTiB3w;@}CQT)Yp0gDsce)4z%{dpptwB z;0B9Vd*It^2w=$U2hqtDw(}H`3snvZwjG8-n9v0*=3EB)Od8Xqp-|N%mxVXH-8%my zhbFBbViui}TVJEIGdycnPC25QrBrNyhc9K0--gZ_S9bNGnt`JxNz^2=^T2h`%XQom zF+goptrc2Ctt1)w#6Dw%W7!d15F*qXcsAfH>-b4AA1K9%vK;a~){{Xyb@)!GF40SS zi;295>S7X)L5u}{-8NzQmiO@dSNu8Ktzw+5 z6ZYijQjML}5Fu@0$eu`(Hz)qsZ>+kH zFeD*7HN3-DEI!_4nDJE{-Oxt>`gVQAMX67EUxn#;`uC9r_0QMuj(2^RogY7cQv7`T zOg8b(TF@%eZ19|C+J44T(9-MsG`vEZgkITtqIZZj$f!Iv>WfspLDa~Y+Q2{4G^^cZ zqPjs{EF^AC8$=-VTQQ;wA2&i4AGWo_O8-=!uFc-Z8rwVUC4|f2P)XJ{BpV0rC_FM6 z-GIMA$3rquPo7^Kk8TQSD(Rfvf(L9Ik}QY}O00?k0zQr2vKONr(ShE|itR~E2AnCX z>Yp_-qte3Ay@^xKh&Q<_YLz*s41-2ico~JwD=_e7@m1L(wJh}vnktb?VCFekuMr~& zC*OQ@J$Zsmh~l4d)vcYOF})kLmJAdjUq)Tj%H0q9;5U>>gdA1AeQV59PsTr$BPaJw zgBw(ZLc2KmUb|71)I1`#ziURUPd3$0X^l_Ti`2YY`~7mB+?O=jRF#6FLgvl zkk$Er8ASY!xCQ&kdC{uu4zX5T!CeWzh%POt{aUNE+;fY4Y=gan&n|{EzsF~J-_;P< z;B5C}rsAirhgFXFo(tRngtSNdLkzX^Y^oyl^YwXkD(k5Zi`sb8CUyNYR&;PGtBV0! z6)38tlnf+?5u=2>w`9a$YhKE1S;Mu=9Tul$M@eMj@Mgp@;4|FtTL-T+q5T$!W3lXz_PhDryePl2*Zz+9WZg%H0?qIwl5MhlMEV z6dVH}vS=;nz>V%t@d~pRh8kMnicvLEF+iC1BX0J`RxDni8NdzvyO+upuZf48@_UNF zlL&`0aK9k`SXLkx3r8*U`v*RA-*+s^tFCpqHLR#UM4GKCfbIeP%ad(39iiFT$luTK zv*K?@L<*r-f5=|n;CSTy#$TN+x(_lt)?=0RI+U6wc9mr znJS%DEc^%J>{mVAzfMb**UmDFrrl$GE5FVKm8P#aQkz%;#4diywluuU_TVS+U%9z< zjE5VT|2;Lyws$mn5nN$4z4Xbf>tC_NH<3-(WR~JRhyKYA`zcS3bdhQA$tH|d*)-er zR+$Na7TANK1AX$^N9vSaJrwCTFYVgas)<}KoHL(K%I}}Jy?>zOtF_<4P~abvFVALG zgn!ns=D{Km&ZkiFkDIE=BU11}lqdlxQiNFnVML;TZAowvv?lR#__Pcj`~>Q!Fz4;k z)cPK2g5qPw*uYgsgCB90mb!(`ed<`A4OIJa-S$OFSceyCHAU=L#G)!CI^BbggQrls z=eQapp&7EsiegScZmFna)r&lu1=`b&i5k11RP9La=Yqzqy@sUUT5DF@lQ^PPyQyn_ z0^hb&E_pLf_|5ZR%#}N7=IJ)f+xwb$HEAs>MZ!hJuL0HUPu`pm^{-OULfe<^#|`~@ zKA(IhzkJe#mDD`HiiKk$DY3XT54Dq+w3hPhg$CJJ&&6%_b2gK>a{2StALNUf)_@Pf zDN&+Cl07Mk$WF^Xs8C0YP&1*LA9*PezqJJAjtfOHYvGoWs-3mgf&3LbA5XbAW6IG6 zS%IgySTb-KzE*^djAq|OO?o7{Wkbspxl*-bN+lZfqVA~sII%E1lgh*!;%R<*{vGCO ze0)=Zmh5CC>rQ=Ebh>uVXA?f#FOns8AZ@0RJ&6XPua1TpsRN6Zt=ec=!G!K%8{(_} z4K>l9`0tD!v<|bzx;G?JCF;)?lBxx1S4G~~`=c>=R8VuFJQ=CN3VhA8GG9XZ>KGzy z>z;IbLIO<-owlV$k@PyxmIF?3HmO+3NTOciJJ)X<_6dUx9p-qjG`dkFT{nyIC`!;u z@*e4$j~YG`HF5FFj;m75K{G=p6crefp}5y=xk(bf9J?e-7d!x+3$+NZI=`CgYxh#h z6Mj~&n*LuVsGnQqVj3K5Tf#ATxR)(PG!;7`w!kg++V;a%q$rn`Qk-t45|m<;@j~5c z;~BpF3qEPN{#BbSe1&RHA_k05O-ZmEo=ilFl(i;wN?QZ_QoI8c!GMG{Ov}K0&K1d^ zEz1L|pmS5UiLu6N8;W3!56jVVa8=>8PGyBy@34By)U#laWumD-2HLvO<53=5;7>ws z$_#NenZK8d!{yRlxw;3y#G!kWJTe2)xaF)wh$msOb|^A@ML@e9f)hj>tmPUUJ9;$k z?~)jJT7jU=@Hm3>>KYxM4>vI(wzgB+CLJ=>3h_};EHGLMMll#K1r5gwvC2A>ob`m7105O*y0_savBJXRv4wHd+xaia0nWBYz2D zoWEca0kFg%>pU!pDE@S2$65(Pk&z|?$WuyHG3Y}t@0>#4A&<6jqi9gn7?xRs`K34u zcRwcfvX-!vNb(t?BQ8KO8IqN2kgcL8%~V89TV^pCh*?O{Dof&C>|)&kR+s#&TB~zF z1b@^}bcIXu6zTJ)wO`FroDrIuT8^9uR&U7I1}$;%KbnkP_l-zZKhUy3$?Pi@=QXBS zU>HU}%gQ>7%Z5TE7vWNmg|)TTDptfa^|JUHBQm_n7$2Sg`=DVj6~S$61Q0exZUg8y zlE26PGezhqzyPrrBqjeCeq;&yF$vm7-SS zZ@I1l>fOzn5_35mdLPv3DKLp?lxk2f37_g7#4-4DYl3NEHh*V&pXF7``_8K3@$`A= z(aKxWx_hc4l`=}=SlYE~R8VwM(zUIdh;(%HphIP_EV2=3Y1}MA#xF^W3we^fABj|W zcb_7!{~-^Sq6@T0Z79USh_6U3&g9_=)_{q{)>oj1nSR!3!%_#!DhrFPe^e~%Paqyv zaRkB#Xt6XyvSiX?4S`0|Zit}*21`>%HnJ`nI~L)dk8?YZtgJ>OAyPa~Yh>;sAorZO zT!Hv|iYpyjQy88c+eF;VO?SCvrZmB39U1WfiKP#}Zk82-q<&3NxeTnZI46n9L1~tP z(pbu-@gae80Z;~FS8nP()q^M|CrkIdie&yeDPAy8#6ET6#4tyUNyQShNiZhKp^YoE zY8;>cI(EmVcFufMCP2Ad(^OasO<^JYB<`l(D_QRV8Z>zEmnwBaovkvws8nj>1(R7D z6ZHT35W&RF4MWn1)J26w7_arQ%Ql8}}h@>fB_DhEyL znbz8SqWRg__lZ&4+6=cJE558CADh`_k+Ii=JD~I$8P-hg{8uERMV3hr#arZ)i6T2L zONR#sSy}^F>gUN5vdy%J$taIw$hlM5LP2FIDd`#{afZ-!m~2`kEw%wG0Hm+btPNtM zRoE`k0)% znPORmux85vl|yA1oE?V@x5>KLDQQaWfFMam{{+OB#Y##k!A~==rDmrQ->Bo$v7)1* zo5^wD#${qxJJZ|^jEaM66B0~laVYgP+YdWAm0}G8dM&Y;K!&4)N3`)uX-kZnp)OI_ znz=n4#B3-fTM;}uSQa`=cpXs|{Q4xMF1dBa8{WR5@sW5#Y&$x%PFbV+<>i=khVv^J z?5e*kw(KVs+kPlm+BoBnYUUJ`q@3xmuvv3RrA*a!*Ya4#%5KQQ52ib6W-PWXGHN1} zI%;5=igqkIrM?y;HQ(6Q&@y(2bf7wtH4e!!Ej1uk6>|$5%yMbc3OfA_^Nf_Shz}-C zQ(dyz%4}hIJJ3`#W4YppQ4{W|ZinB!Ke0hXusA*Ta!XJQ5Ym$gE4L`M2JJ9TYOi!! z7?~^afVG4z;IRCQ6m#AZavsZ6rO2G)if>ckael*gfV0#JZ)7N}OVnq0pfvb<>tIfSUmS7+=++pZanWat#HjRg(%;nI7ah z70XT4(Dg7vRS?Wt_1M@&*0uHnXF8S2Aquby7Kt$R1)8$z-U!`)9TR)~d zA$jr-%7a?5ff%)REg66qDtin?-hm+uVz3f4g~r#*P_hhh37vb|ZIrR80)>%*u3E*= zfsKRZno*f3KCakkXopud2Cj(rCG3%hJUohXrn|~Yf53tVaH5#TnG&?>!GWPvjbRmq zOw57%RSqB1{ML7YR>3;?$Gj`SGz*azbblzBq#$-Oj^uI0Zun!@AbS0>3<8t|4-sEk zvKW_SSBd(JI4Ajy^o43P$DH8DKMmD4$`MSR6z-!l^J3w)5n*@K8>V~ zOELJgEN!(|q@}RJ6q*D`_{)+UA&H3C@RUr)r!^3{-zI3}s!Pf%zsm9@iK518`4K#e zh|{@>u>!R`_7FXwxk1@ZEPct!65B(1w>&2#Y4zvppJ-pU+0Lho;e;j8QnprvS0)LE z1(Tmsl{Rkq8b9p3ojG(uyt`L^=F~fxTY=0H$ncTau8Afa@5|E$ebl(U{+u{Jq@yYA zjduC(Ge2kwu^}<{x(*+{HD?DoJptGmu-HM&tDO==w;IV>`fR) z7d%S1KQoc=wmyD(;yGityI<3np2%+bR%>$S2%X2Ja*Az;nc2;{qTI`B4mnkDj)fyB#1g7eKXVHR-KQ~igEpzD>HtcnpA!XDz zyznB1Ha-WA`OjDvRCZ^^$W&Dmh*NsAeZ`UqY?k6G6-kOhMO4MRhWe4@v$4cTFyWtJqRS?rf>OE(3xei`As7T+AaxsG*kFE>J z=eb@)*~zfxb5JiZS5|Bvu?3KjPmrAaeT@F*=%H)9FA|*+fFj>6 zg#<}p!Zu<2j8ku}>h^3tw7bz3@F`Bfg&{Gb$+ebBFbpBWY4 zYK4X9a$`G`6Y^mXj*rL@f8app(V8aa?=)W)_5l%DB~9huqqL##jPJoR77U(rbi(esAO_*% zcV+=b=SRjyTLtIA%~9Ke3O&mIaybm)Os=V$nCy*ynXi) z`}6Pp(m~Ky(>w3K|5~4eucM9q{rz2E-;+GK2BXo`V{s2-Nvw@eUJE`LthoMH^jx&u zK0c>ii9q@QwDsZLnr+SA(%C(5v$+VsQ#khK+0U5&UdN}nCN<^bkG3Euv&%^>9C|ln zFJwnDaQSqvlX{ql^PmqMVw`*v@rn;IDou#?uty%C>)*yrLcHYMbmx521gY+W??UZIGFW7}{O~dLV zmlCR_bACb?M{0e4dJ&{CRFaW>*%WyyDlOH%>-wium>7$CUB6?{)!FjNo5r~!w)c?s zw)&tF|NY|eHh`bAa*b3sqmSgGu0ovN*vQ;J`k*sY+G2(Dsgs_KYnlSsR#$ZWz5PU0 zKlz|Hnn<214Cw{3Mg%_+MYQ)?zyw2BSrC`I*x@PrW zTGfO|8e0d^4S`XHHz9*{ofeBButuMa4a_GaiSyUX3Qyu2(iM0hB;d*MFJk%z0rkw~ zg$tMf9J(VoT|QXfU#=dHFP@yAa?bngL!{jQ`u;MMMF%-9i$(RePniE4wmfjDrxw3#Q@Om zW=QMdKw&FLwy@q`H^>vvPcqN{eu{LK>?``T?(kc`#B3`EKJ5$+?|$7WlL*CoC6(Xa zrS!6Dc}v8LpaS0F{cs7b-QjZO`pyRefbbPl2__4~7@f2C(IHioW%Es?P?Hf^^b$qC zBhj51%L94$^Bwgp>1djCO)n1dcMIPq0*q6u)w1vIb^f}bds2~p7TtBO+k&kQ%eMWs z^TLC~h;2EqW2qXy6x90S8Qny*g)^f^%#Q8sWZ3cgZ%buE=dKTNJa&4}Y@f=#%By{O zrG{zfUxF^i=d`QUWFcG}NS7e%2&hI3M~+tCXTM)?*pgwyEPTOPd9!9^?P&RWs3kdw z)5;jpvTVF=_M>D|0ChM=K!5}Ka#T3ncBqB*lRQ;t9_L+RqM0}XhO)tm7$b+LEz3T1 z8%qKf0!VvAL!EQ6^nSvjhszUJajg6tBeEPV_`WE_nGgsdfGJLgX3kJV8JIT`5%3Qq zs1yB_@9bEmeOll7@cH<@es7<%()>N2rLXX@19&SFovtEC6uQ~Wjj7dQDR3^;adiR) z87fZ$!pPxeYda`wZ!T`u+^HEFdIF(hM8b?fN)#R@jCsEG9uvLp;*~L*ZXT;tIwheE zsB!%*n=;|WF}2A}3A&80V|4ru;!VJOfn2;*teMpC;x~wMM@eI=H@3~9I<`C1+9~K} zh)B!(5D(hiWG6; zC$tls+RxCwNZ_ZU4)i5Qe`3Ok{NzPC4pva{eB5Ts?{C-ZdLG|aFv!mq!a-wkDcQw- zTtUGyaOB-vyDFGYs{U~D3I-YBNqYk+&b@v89qAn1-u2P+Pkd#||~e7e3T>qM5siCNi&K@86FQissT7im%f7 zm$w7uaHdu%W0insV;OauT4B_|Y@hjfvt|RMKGJ$yAq7Jjv!#2XoZc zWyP~mt3O_B5w5bdpt`v9#AgUR?I*8P$R`450bRDdY~71P4{*9Ro}rTk-7%>(@ebzu zzq`t7k)3x`qmc=;t#?xkcI+0AZr%1LF?KH%+m|qlV?jjk*-Fz-R$yzOl?4x*XLlYp zE!mnJ{4nd*uahPqbh?UE>BIK!PMfT~a77QB?0UZg-}@X0oL3z#{yj)RI7uFC6n3D7 z)IZ7|DsbW_5DCXlD@gGILUb}jaDqS%2tZBpyONN25)o6e5Z^^vE0e&iA%<=?VBwq) zq{Rif6n^#_osZ%CPmy`c@%(EYY`S8{mRR)leW;H_Tgxck?ksN$1a78XXQ&OGMt?iF*-CWcCT>zRFjQps*WXM8YxU0xU@|0V1)&CT--9u6oD^Bw(`rzx2*FmI6edwJ2> z4~)3K*Cf2{kueY<9&U^ZvZgmK~bU?q~Z4 z{<@{UY$U$^KC`eT9DMgIvL2E$tEPD8{*Fa)M4VMrs5uK_#oALN$b35|EBTWOjEROt z&eWMqoL_)k+49j~dZSOO8Z@W%PH& z3s8VzocUnwR5SqEAM`5AXb~_dD&&gU((x7@r9OWF&}fnJXQaaHId_|OvOCEQnP*Y@ z=Is|^7v$-!Eeh92OBchc6^J!^)l_$rW8cN0-6)B}abRzdE(5D28?qdX*Bjc`UPyR4 z?`>m1umnlfT&fZj%%IEA>e64erPzU~?QG6W|DCKjf4`in2_PCsQa@d5 zbrp|ttIg+)Bc(QobJnYRs89iIYT(A-Du8-;0EZqR4m>%DYAr{BH+@Fd8(oC>Re#H; zzWL9xx6AjF#IS6_LzAgOYmpHH*Xz?Fh-HGR^D$5zj6+=RYV34ff{R0SdSm6+UwKE9 z^wupVjumQRCPFpRUN)U6vJ(o~o61QN6R$DR;73stjv$Ud_!x@#fmyL@Yx%&pSdm)cx+-Q{uCO&MLf4YkkBgDE+x>{w+lbw~PC0c^B3 zr?AkMrBZ8Duz?ET`4`&TM^>dE|6Q%A`c3UldqC@1S})>b0J#t4>0jl;rTz=rJ?nB2 zT58M1xP%J}=fUXhtp)-zDLGXTf#|XjE49;Oqiz`dPwK4tiO!j1lWIxD!_troPbn;X zx6XUKe#=RSv0$-QDyYR4F$H1*?D!eRH#OX7i=slBNl673n6j#+BE^tZiy)}5VuHa) zuoYGUW`dY3ix_{}hvpzlXIgZ6^*mWAe%6`~KMi=PXd~S~3XjjvNe^2Dv_ReM`&&A3 z{OABe^Cxc-}?2=Q37$0%rN}Ji`GrXPF6YE~%v4@X!8tcaQbhSL^ z3G^6FPrC0_-AjII*>xz z+Hw@Qae@(01`3-$PQI0%3)G#a!p<-X4+bnQ8ZitD10pVa51^hF9?EwUVtHu{A?P7) zUF|X)@3{I~d`HYWNQ$wHC8^se1Fw>xd2dSx1P*a+;?-K#KnpJ&!FnEdy5T7{IwSQ} z4Len}m*>~Cn)Fv|r;YAGC<~2BlvG6EsG9K)CmZwQl_44nRZ913SPqmEZdZ@xCxI`7 zgW?u9(FDDG=ol1$0EvO59g{+5A()0kFg>{}XoyAKMCp?wn84ZG#r5}p!tlX!on7s_ z?2KUYW2{g%$Pb;B=ncFmDZv*g?+|z0*QeWFXg#kvQ7QKPZpk|9cww2jD6P1as)5 z9sCztJA=N*e~rLUV}kd0QR4H8ZK;;a+Qi8X?up)b0w$?npH=xaM{x=E%+WuZ$v<}Z zIs1PP0vo6<+)X%#m6&3A`FCTI&$!Q^z8zFt>EGX}WVG1f(3EWrnM5UGj3JT;$Yqq0k`yvyKKCD>@c#bKu-9ja>vf?EMTme@PwzaE1Cbbc zIe=fXy!sukyhG+u^2`TWP--W=-L%+_V%-f>>joA&j@MDJTiLF1of3}1;T}>4A8K%1 zPKj}HOhjFRK&K3%Y7`hx@$^+CYM5ZFfwRNP<5dzS?LhFb#w&VSKL)=E^?bUY4Gnvg zPKUMcvu6r6cP-Q8uLnk`0%v}*8kic_m2A1LoOg#NXGsBN8ykL?Aj24^ho59mfLmn zP9PG51}dmBF%->y*{~EMkWwiTk|-zwkct3vq@-9WB9bVuiRwMxx;$srRUmL>KE}$0 z@1nd_OowOO*JqBr-ycU-Et_5yN)G$8|8yybh1q&*h*-t{^@Xd0%hAN)QsFges3(c& zr8#8mXejXOMkeHUurJBpyHChNw&SL!B0ur6PtHww7=j{rw?-t9bT*2U6BpG!kyc^%s5OCFtV1uUuqaWJb6sbEN$m2G15 z?<;Ys3`coaLGCb8x!Ca4Q&6awE~)b|QR8m;R$c3M%P#8T42s zNI@*{HD}m4v1Ew2iV|*QM$t>`;gUPNdc_w}=BJaT%*aPs3}m7N7l6R9ncFSx`=Ye7 zcPfpaDD8pS(Y*Wc1%kXLQ?@@`f*zrXaNlaNog&1qHuToKR8~dnROP*az}it^nNwM# z2RZ9eKr=~lI>gvfiEXR)kxp z@Ib9f&e){Zif2UuFhm^o=+y(AFXmI|IP?kfcM-Mf48T zt8Uzki-!%!UeY$K$E>J1w}v>Ey2RfD>nxBvlOw0|Tl}Q=j&s;F*D@KL?A=F*KC>7d=iqWmt`(Lx5Cc9$l6ijju0GsONg*r?)V zd5xBp2su$yP?#VI{GN!7c)jrz>`_GIC$mxk}ucQ%g(g;@=B zC_kvrL7BnTcJOv5(tK8(&xXUkr z<-27lI`49${fL+~DPN#;`;|+E1p;)9!Y*klG{;c~koij@a=)yf7CUIsSSVGILi}}P zO)@UcQ52U7Dk7qQXfmMc{x{GG@4V&{4sfL66gD*;Fqm&-gJ~_H|jW&f%?w zIJ}wi_RwYLexE%ULkv={+AN*)^bp{~^0lRi{%frD`I=d&`*rc+C{jffk8hP&9m=n8 zp+W#X%@l1gL4Y6>J4H_TqG^l*n#J=SqycdpZI-L-%z#D4DJ~>Syws|qg)p0$xmmTw(qD3H7DZ9iF#;G9K>#T; zHYYndnax}7PA{&*O}lJ8+b1qsSG8fDNrDq{TOrW&Ovg((&vZpG=vIOivfXs~gXr&Mg-p^j?ly2w) zsi9!Pukk$@gyjg5?HS}>CL^1%gqDpni5NU=9W>W z-n894!3jU#Seod&WnOc+cTYmfopBh*2!o17wvwP6OgrYM$XFDFZ`kMr)kBG>ON`su zK`)SlnpmPkbKIDrZs|DFsFjn!NQdsN>Q<>&ZZPh-okH(%r`#=Muro2uNgt77ef2eR z%t|{G7#azlBEZIh1i1(xHRn8`G#g6CS20o5MA>ywSmriX4b)h^kKRsLZWJsI=2E12 zEjGX8!<7odqspeIq05fEPrm$pev5AVMavEZ(|5EWR5%gay0GhUa(F$a3_hQR&#`7U z)Y;}$d{1_II1Z%vNOd4F6rOjjzVX!1ci4SbhPv6o_Fp~y3T+uO_MbPkMqhrKN5Gw= z@T-!W=LrGoWvWXQ0VoxYg-QlqR{7)Y$N6)WA^;Bb~fbu@praY<- z*IhjAeRr8bYNk2t3KQif5=rn-E3%4U9rjW$^0;g*R5RezL-r@9ieZBB;uK$}Yer0w zDk}(irs{?;|401%K=&9-y7)PFoSER2r4*^Xp%bQ3wqHP018R)o#1YRZqud^&r@LW3 z0)e5y!F}(GE@0wG-Fdm|)T^UDKJu4#I{X}VRgO+fxmk`l;;^z_Q~fnv;-0+E6Fj~@ zpyyw86aNy`@o%eEm{c#>^}Cj~*5tqCpG$dNOfY&ZYkNVyd-}TmZVfXe- zZdNy9)EKQFDkDCo%THNyXHd0Jp{{B)S;`Q7TZ&l~$^0lWst<{@o=Ead2Ll}FUaS645@my4+_wu;i-PKMmi0()$y>He0FN3}qzv(ENt zC&k3rFuH4Xjc^bu%RPemugW3?Yg;w!M;I$BO!=T zhT%vKlh*5=Vkwj6QjW_@B5LxaCZZtnY1@^}#cDw0X_4V+a4c|d(d}_UjhslOCRFtu zS7nNokv?cDhB4V^ccI2a2OVz2!$PDXoXbV^LGo9CUwh<}!cprJq_ZMBH zgBW|pdL@If*O6<%n+$C;#*-(9k<$sL=~&a-$9Eb>BgQX5Uk**;mPj%xM*?h|(rw&5 z)E>?BA?_*>p;?y2thA@wW@uPMAfKW_J7#!eqM-$%IY;Use*;0f{h3*4&QzL++K0Zd zb`Q{$huqXtq}h55LF{`+9ch6f`ZX)YH^^J+cB0s=;#khTedA@sZAI#UnZ?tH3VmfC}N?!u{+9gfQ z`n>MEDxr&_8aY7jwvhO&0$QX4>%25cn-XhN4QTn)gfFTc$B| zZCeS<0#7H=;#oY*`0Op1gzr|qT}nb7_qxP>`ieXmuM24WO$JnyRu}2_YQP>t!+A9M zmq9Q)lh;dunKNXP_SIF!0t8;(i2}zMgDe_@<2|tOGvPj5|D!6hf=hT zoN6@?W91tJ0{B@X!DZ(1?I{~0TfFU5(kWCvLNmd9QUoh0)P8aUEfdSbrOHd4!2?@O z*!Y+=;xKayo_G4S?7c{R7e{L(=yKsvc_*fyp5dL&^>`mE%C9l<5^wL&d$cE+uS0z= zB>8L)pW@%-e(soD;6L@V6lJ7xe2(5r4-VJJE)Rkpw+c2 zp#%ACON(ljK@6#eN%v?I)baYk6Ktxo=w5Q+gzsb-(dwNoRyFtU1 zOU+NJd!k_el1#E5_Ye>387)~1p_X&J@7~pZt^7(V^snSF{0=!Roo@YAicqKAGLz0D z0j96LVCL}{AvXGDb6UZH1bgzo2Zqc%V^O|BdqRPa#qZa27$`p1=eQ0>rC;^_prCfu z#f8Rp@l%(2pMgP7r0aAZNYbRj9~nO3^ipLEy>oi=0mON`)((l%tFraAtRfia29;A6 zFu;RGf<&YuqF_ieQj1d!xj-UZr)vdHdCnI`O4?%eG0r88gkHX(jQI}a@)#UFryr-m z@)Lrfb8}J6uLkFHmCIgkviZ7uIO+)eoCU)GrRG4Yoz?$oA158r*Q+HWpkmy)uh_nZFF+Dc7B$oiOpHJbsa6N zyK6RQHZbVoEmitTj%15LG3B0CLLZSAiNw<>*hg^~&0MH47%>RFK~NZzS?1S?qs~Pn zH3jS^N)?-lTBY)qo8}}A@Qji+qj)YFaAj-t-<~2eo!hx5|v3 zV;lM&h3`xq1&O*q)cF(3K~Yu~$c1OsL-(8WZNt6n+={`rLnyMu$U|s`g2O4ig>$Ec z_-*^*U>S%3VG0^B+MR6LD>o*Nj0`my!^o+ov_V2d)J455OT&h#MiUqmP=_E`4*;NM zQAOpu-MX-5Dkua&zRCt6MDKGjnYSAn74-jVW@RHD~aW38=0cQ=}) znD-k-VH`lOEXfRrdhw6~*F?ruBOvM2mp#?lu@-lR3ZMPVfo}r_WB@}5eCE})vxnK% zPgAd#A%id>e zvm9uo5Gw$oBGCarVC0ejrG!+3$}%kmXJMgO1RF40xWo|XCJC!Vg$O$nqP_r#vN$4C z=CC|$mYgI#^8TWrd5hhnhx99FN7As>!f!&}r1L)0Hh*TotQA_+XLXfy<3;JBhscjd z6>&h_z+HHv4@DILRPhOn@f;vw0`N4#{7Cx0ON=BmWkV|iDYjFUDC#ja=C;-p1nb?# zO38XLEAf0WWzBPYLY%iZvw1hV{*1sg!y&=SGq z71`4AKo5FB@aJ5@0U~!jzQ-HGh>7i#c@+&tJf#j{;LXF^)LYYt0MUX4z%jfq6k%?h zgJH~*lo3T|agyBiUBh&0( zxLW+<9{NWNK57B1vi={j>q5U}_VgY#yu1G3*tvmStcymxifB$@qB$5SV&3=jppfHk z+@Rn}wzygwRE;B>RPBBW^vHJZH#r1nxSrm;Sn8PrWbIK~YFA|1Pjf#@ZG86;*Wvxs2 z-w)i=2g~;L`){$+Kv-cFc%f`N$bPx|$E*Bbm2lVVsOReHx@YA7dO-)r@RB4LusNaT1OmiL^h`a7TtgGp!p{lmi;*A} zBp*1o)**@4jm5vFU<~eQ`RH6DK%npdNqp8of~2`SxGc#ekx$w%1U!bYJ1^K;uC7-F7V7_sAT0`P{r`pBKqB%62WLU)v!cZw zUBk;mCRsCO2hT)ju`}8R;e(pY|B$t)b4|gI8#}1FoM2f9H({+SVZ73LkmRz|K#@f# z4Y8w(hQ6x#AzdDIJes%W&krc(Tn8k^#4u> z2OxUGmN8{wce1$thwXDc*JXZ*a%KxN`s%R?A_CZm0X4UpZfjHwgfy! z^OZV#`%4b3J>}B{HA6Me-Rm^p-KCDoAjK5g(1}104;J9j$(`l%D8g*S9`MXCf#GlQ z!sTpT-1f6Gxj8%8 z*Pd_}W7>qeI(Mw~yk99*U9gcOa2>gzp>_ZO00E!?7ckOZw%bnczLC#&!?x8`Ggq51 z1cyw4M)!N#QdfHSHTClP^X_lF`a9d^?)P!uXLhx?;Cvrz-M;TzR-4~f+vncTyL#7Z z?(UC>kqMAA003zXGGrJDiHU%iMwpr~00A;!CIrC^JpfFN8ekJfMu0E^8ZsGAB=TTQ zGGuCcWHAgTma05MEzVqgS%023xAl>C}KOw`EqCQONq36gp- z6V*QxDt;7cwKVlHp-*ahr<2reQ}s0-)NF|KnNLR2N193cr>VTBr3Rq{$N&=;BQ}RtQ6Himr zdY`I#k0kv>(`7SLO*HURYBrN?N$MFqnkLi?ng(iQG9I95pwJ$pMu(`;ki;6Efe4xt zKnbHJ1kh8{3T-Fp4N2*VniC3sgv~_s%~Q!d3V2ONso*K(!kRrp5unxq%t)1 z1Ih=e(ds=w27ojG13&=K4K$EOfB_m}XaFVwFijdTjWTG^H8G}vFcWHOY(k#enw}KQ zr1aAoM$xJ0)Wtkg%|c^AH1dJ^lhacsN1)X7o=NIywLLXGO&+1@XwcJTDB(EDhx}LA zx_1gog2Y6EP$4L8f($QD?b$~7Um^pv8b-)S#_v@XMBecGd6UYI3hL4|(#DfTt&6+*-offIWov(r>AT#?s1M6o6y*uaI~4AUm0=W!S}wHdgOX&Pkjp zmBP(;C=$0Tl8LX3$_fHdCX^Py#wp4PZcDg(=c5CCn8fQDw~I>~io)!{m- zQz%}fhYe>P0c{_e?>!8}q6GyGdq}doph(f0P{!VE5WS@^cv2Bfo9q4D_C`TK268(k zp3)=0YZ+$?f*mz%fPkDL0tEy0oOd@{%S=uZL%x#pd#njek;K%$+}uoL+Bnv6X-jZ; z9j6+cOoT1eQywPgNW>I@(L&J?0L6^coHCkop8-M%J&a@z!q~%Bn+>=l+TAin8i*u0 z3w-=Q(%wd9ievWo$+^FQCpg={uw{Svb0hv4$xeoYE-VoPj&?UQfwmu~9@;8*sExY@ z@ND6_cCj4@k;I}bS}aB>B(STlgDXS;K>C!TBso(kK|t65Yian^b$U)-pHp*#$AwE6 z(g2{wIMLOM`9?CJqKd~9g!#~a;6`9HP(rDWaoVO1+2g-uvMO~-N@rVc05g5xB_91O z@9n`YkZ7C21CfS^&bQ0{AP z1?zERlmW9y`efsQOj7x*ir@t#Cobx?4HpuFP6|7dpM#f;ba(-C!ek790yK|^2p-xP&GaPzGHBV##@7vp}l=N?nJ1Gfg*~2wwtg|&wS-5=KBAQ-XRBh)H>bL zBzGVC-r)A@Xm4=uHH?{qp_(E2JB%G<>&~C41{$<7yRSnMqnZ8(kjw|Xp9JnU?hZGa z!)EtEdZm%mIEDvTG5uop5Ho}B+&c7Cp=W_^<*~_jvLgxqAmRk~6TPr65UUzi4BjJ_ zrZpyN3otAvVK9_Z0+jp5`*moD1?nM1d=PCvHp}{6yz?$!0uE+C-mTo&=QbteZH5B0 zU_sXqsOKM*YUTAD)w;_ys`w99Eu|3iQzauH{@D;$1qB9oe@Ayoj|!9XYb97Yj+YeK zoZ|)7)C*|v<$GLei&raRs`WM#FYvAUsk3~q-S{60! zsEpZCKyC7e!jK}~x0t}Du7B?Np?9&~K?d%}#k%9)rSAUyzC1V)(J6NR?>hDRQ3(Cm zZ^Y#*V@QET2C+j2a&{#JKC4ph`^AL}yoxrDfm&qlQ(_S`x&-f}5M$Go2|xSPXF}Wk zy`w1s97)KEC=^@>kV=XSPKEA?_%-NRJUW}>7nj+)2$FF;!Y}u%>&Q%0B^x&8V3en5 z)xc>;y-xp4mW9{vb-R}dld69`uR7s@%^zdH&b(0WF0XR8+n--#fyH`o*=ZAvMy#)s z%$=Bd?tw(|5nJEnKM?$U8dVM(7uz?plca$RT+lA#E3xa#+4^uyd;wIZxat}Ih>&STX0E0p)H)_=@$*r02qLPkrK8$uTYxTiKL|3^}dB}KF@PeReK6_ zl$L=366n!0u^3ha0!V?Z1(cN~tMbBB21#7(6qwdV5|a{Y|c> z!NKF`x!pqSJFZ@vL?HKw^JPYU0wDi=WS20@phD~VqN|Ab6~6TyC&6UZ7)RTQH&R)N z35Pv*s*Qs7Y9T2IfP;`{Ex!6VHpx@0h)1ixuqg`Za{Ba9zj0r>)@gVq<a&RF63@WYC24|7uB%6T)2mpND3@FfRc0(k!vYnwgOywAH=>zgo zxe`I*Y$K(bXERSdNX5kUrn`ermpKtUccnE;t6Cd3j_ zppZx)nG>zts@i2Fup$$xn0tz7{leBq50ZR2yHiQ8Ih_u8al2RGCA&5XElq3L7 zMna=5)o@BEf2*Q%06fdLP-(J4Dn0OS=RBuOIzp;ROi0VYbR0GK2NAQF;H zXRTNpAXnn6KEG+Z$J4JQboyAF52@1VM4P9Lz|#wuAlHko6PRA{aM!jiz)$6~nTOE$ z%i&*-v0Atu4DRwQBxU_Ho>cN7ZOr57KJPRR5Fq=P{VV??rr+6oYZTj8+bvy<$tLIr zO1)%d2OQbsy3CGq>_@#9F`Mt-sUKjAJ1dP{1PNW)Z02?Z0N5WE%gyq2gt&sHkS)%j zW^%Ca*O(&(ptZT?(7l1`!rPFRt`2~Dop>oov4wOUtn5-jP~Pe6`%#a$kUJKKA}=#P zwAt5q6M6^gVl2vKx0`VXbIzjdb}tU=fko?hl^6n<;H>3st|;kv zd%kTP;fUpd1|bG>zIx<rtdLx*J0aiD-3;fQE32VGR#};eg8Z*lmS$>aJzw*mqxlc(N=Ctjlwpi0^UUQu(@AZ4TJ)_s4Hg66# z9+@XlNPJj$VJOWQ(s<*1?^cUe9mv{j=hn}fQnE9T&QlEof^r?Bf=5E1_bi0Q2@WUX{2F{Rc_$dxZyeSGsDt&W67n)|=o3_RTPpM30SvBp~W zwhDY&Qz@>ttj5oQ`OP`I=~B*?cyHryqRYaRFLGZ1P&xwvjdnO%!VN6SSm~FHE%>qh zz9F2Dro$mxUW#Q&dRu9>ih9$np#1BFsMcn0Oaiii$gg4*JE}m4)3G@&HykQHosa>} zUn%gm>-Y$dH1$Nf4Z^l{uCHaeYpD7Mq1RZ}l6N}W8&hMl_o`qA>(orV=O3DLH~Ahm zKab%1za|Hqb}i_XRa}hb`URHbh~BPV`Z#6w1`G}8nF`|@s323#MvFVT_?!U8T~RF% zxmkyA!!0bb2j>;x1r&&c>jpdn!(ZBTx?`{aAWaHTSEoF`YmaDtX>Msfwg)HAP_2Kk zELWfqSTq|$y)UJ0yCJSeVK|4Uygjo|>dJu;_1HZNU1$LwfkY_AAnsgA^JeluUw--A zq?EGl=GRBiM!Aw+J$XLQFL&l-ShHH%Kph#ND&bN)uDb&l@$Q6|tqyDfIuWy}8cG7hzmbw^VY?YzC zt8!DQLEDO6|3Q5qbk+y6>yrLKf=jee(0kd)aDM*?fWdC&AuR-v0yMg0&lkn6_lvvqQkiLwdryaRvG{*K0~aF8W}Jk4|EbwIT6|ju z#xl6YwXZRCAYfHqbBCXFJyNHtj(*LW6AZ7cKlPvr5`^1!D~Q6(n42s`DWM@Zya z>~XhBb2Zb&=mL&2W1{qaZik{wyex=vG_N!d)5t&M>x^T2CnMnNY0JTejPjD5a+0l~ zT=CzV>(c6U_C4JaXD;4uxm9&*IB=?dIU<^arZN7sRBuEW#o!!E1*Pvtx zI*ihi1fg*qiH&PiZ8$uKT|yM|(6QttXkpGc3S}XWt|gTNBMyjOY*h~lqm9+P$P`1o zQ%E^0kUU!7Lzl~9yY6r&F;G^G#&AylFS31Zr!0fbQi zXps}-KVwOHMQvqDpf!}wJ{1q1L}C!DWy#ConHMeVUQz;EZxZr~kzoozP%+srglQ-k z`hy!!Txn(Z9&`vmJ2I)Xj7g&-5MM`P-L`gp(WQXl<%KK3K!8T`aF{2A+#eu|iPEP5 zHk#bUr*0`xR{GywZR52H>by^7(McA_iiHw?JfIKd&8u710(ewLO`H(j`L&QNAWN`u ztZB`zkrGP?^N)OmbUJLLv|PK!AfibK^`c;qS(}4s+n*Yk>Yh09PhGRfO`kv*0m0)( z)=;56@R$a9=E;MEJ8+kDbdr?9<#<9*--kMePa=M?3zBYBEGy;NwF*6a`c~h|R7p|S zjH9sH@=I)`&2oJZfGmeX2zpkXJV|QgURcvI1Qqn1zwXOV@870C!AvoerPsS^>*v-_ z!KW1uDa&?oMO!u+tGb>2Hl=PtmI$2X$ld1QIL+~Q+ouDqMC%CM8s`NWoTRJGv}|$d zW9-F2lci|GX32(_9f~Bf5i<$_nvGYw)q1ksN{)?S#2H(`K^127RIsG1=Bl(U2PfQOyJis>kr;!+@7T=|^Tz zz62V&w3(h&#~J?h%x!W97{_eHj)Ev0f!z}pXkuuJ8K5#km}F;~O%fJtQQ_w6IBC1F zS&$)-Rh>~@Xh29XtdM6lVj^hBg%V{|A(55RVr87eA)3KwDq&_+v6?L5T*J6%Rt(A@ zsyjnLiNXhFRAO2cUUO(OnTKhzt278#NJSF_KT$?`r6E?7S0&-XyTi;cI`1|kVso!` z6Nr6meZDo>Sb13o7LE(!5X87}%ooOVDbcV2Of$wt{B53w3Oy%74JCB3$|X1&_-~w# zWRv>l=Ce7z&bqr(8hJTZI5<}`q(PAZtZhwvXuDDX08XI_2{!o00B4LxoF~Kw$?9a~ z$Y6_%CAY7Aas*hiJ4*CHQ`(zrUH#%yqHVsPmyM(Sr9@3}6$0`UCbGGNEOOQ2#NZWD3B`G=h~U|Qy_5& zFLXNDlHYd)?Geqipp>j>n%(CW$fzEWqA&weRVK*?AG(5iGoo2Z+HYzGSwQ@NfO1L? znox7JYJvcptJ?{?ZMG$?k1Oqn+M0q7c~%rf87;b<@R7ol&8FRdi~P!-_th*?d!M+a zorczYdHFj*u91TTK!i}mQx&c%{ZM12B-9|%6i*s(OkoNHD1*`{5FyDjsBDW^Dr^xV zaU$j#qH&+B?%EZtR~;__w38m9Yup+lP)_0Lac+8S6LyNdVYg45PSdMM+_>8xo|?s1 zwig(4>B;(!k=wWC9eQ+~Bm^hhvb7W|xvk3X-UCQDkr?-WP5|^dO%+bBk z)@|qGP~d6l@apyT(4tbH0(nGYHO)df}*blCgm;?=Yh=BWb;CFiUJ5kLI@-jB!Wo*f`p+V2>_4*C`ly= z1py?VS_>)@!C8O7rW2BTXc6KP3JEN@!`kw;)3(BvYtLxhr)MOjKnWxuKsaurnvh^O zps4^8Ap7}g4d4`jA}jEG&bkChfvBG@yJOO1zP_5avAcVh8UO%F2CBWjUswxql>19Y zrGfx~!(vpc6w|69G<1M!(ei%d`l`SxdOA#H)A#lJaVyMrr4i2w*Rt97e2mxePO4{2 zYjp8kj4madd-qPM9ZRQI2H#vw4ZOi1iwe~=+Gbv{QMp=y_A3&O5=?&Aiu<_$$wIh3 z=d#g#EU!07ZJ}y*`LtU0t`ZKwz~#jxeCQDEXMIAh6a)-p2sS`9I3W6O@t=Z)NyFhW zWii?d1q#WilbSK7AEIRN6Vr%Tv#XykAdvzI?^g$y)Zx#iZmIOdex*Geu3Qk;Q%n=L zt}tw&%at*?`|hWJL^J{ptD=Hv< zW;T#?@k^tg)Dyi3$jG2u(*kg|!~nX=G7%ZB1l6Hrz&@;2cpd$34g11Ap9VVuLMR0J zIs<-~t?#?;NE_CWdRu#4d^mC)7_@t2U<8!>YCBk~ha*v5=aU{7JbzH2Ny+p`V`76u zHOptkpuJ1?v1}xX5hP=k9KLt1ncWuWm|^O>?uInhV}y#Xs8J3zeCLSjI596k%Ynt! z@w&I^2RoVZwKVlSES+6Zk=5;!V_YZ6&(Cw6Hc_6bSY~$H^(heZvLV_MZ@6ciJsPiB z*2-$mw4CD*@`t_a?xoeX)l_m_un665|4Fq}K6=XP)I=m;pU}+vVF2h8C3J_Qjs^L9 z-o&UyJ}C0kq?CnTSi}`0Or}1JDb~nWsVRsGpjsvT_x>IOzdHusa4?Y^?u)8$-S)15|SpoIGZGzGSnhPZ4s;J`U&Gr}h7B{T`iX zf<5IzV_C~|TU?H@F#9VB>?K7WT?+b1Nj4(8xD>~?q_~qSCv;`z}DB*L$k7}ST(TkR=%uDeB@a;RnFbz4a#5K@&xco?cl%q z_{NA!LjZ&z!WcjR6X}vd5(y;<3?P0|0IDD@S5{WyHAhtVe-G#LzXyDlc41`2n<=>a z#jaB(;X<>sL08LCYOQuK8V%PI@PiNqU^R;vhC*nk)BTHck#-zaBT?qb zwSKX4H`(iyOFT&R?gORYBLdrs_e(Wux%_gBzO;|P`>@)r;G=1Nn)X>cmY(mHf6mt> zKUa)dH1;lmxBw|SbzcYA$4R!MNaagemy*&X?|Wa!KPL6e}^uDnydN_TgVj`FMqveRcKOccuUX4S@gT! zJ%8EK{oU0Hd;YAg`77ziuZ2++Wgc^qL=(GO*KmvVPB6E!b_IH!Dtp}e`2mqdB#I0# z1Nr!eUVJS9VeynB_UND9pu`{-EUSZYNo_R|2*eKr4g!BV=$jdK>S?Ec=uEs`c63DS zSJd<{)ysWj|CBsk6&&-FPileH8M?9wX6I_?aO0o;q?O6jH5)#f6O!z|d)S{Ky(!Lc zQp^UG#?i3x@V%&HYP$F^ycrl^EI1AVq#yg%@e)dY*>GZuBDQL2E)`>yiUIG5k5zI@ ztOo{qBNIR*NMZsiSb!h#!k41HoVWyqPT6qy@AY$wg%==S3T^z_N^Z9kPJ@I{d3gS3 z_wDOQQkznkV3NWVpwg<0!dVcKiiu(bl+V(85c8C&O9-Gdkgw8@59Ovt-zo3;xT|zdGX+Uw7%)hNFp9;BVo5y10YF-qRZJpK&8F-WA}eUco%i(8SFc$z z16o}u$i-Djl|*g$jbK%VA0&1rmkqo2vkbP?A=}7Zfh(fnZF%zKyUr z!2Le1i(hpJl!i}?GOljx)$USGGYyCy2mtSQ%v*VQ_UmA*u_*5MoC#Tb-R*66%J@6! zOZyl0Gsz*&w@u6yU(vq0nRYXW#=m9khbK^h)%#7j?ZG6FfH343=s7|1T3#?!;(#Vr ziHHr=1By(tN1EsbvK6J-N& z`(}%WyNdQ6t6|tynRl@xWyR!p`0@BVwDzDAYIOpOJ}jzJV_*H2iEK~5fcIs|#Q--O z&d-4_c+~&~-y99qT&No!5`cI(Uq3&!P~_{eXvzye>}~s~18*tGvY&wUKmny&rzuoU zE5r)j!=oUilr;&$uOt!7)|}}$eKT^N7U+lPQkM$kZXOHVOZ8PN;dwo>UP@=k$vsNr z+nIBiF9C@uu(<>@1S^q*6n&e2ZOWd)-z$`Dth>!`$tWT^iv{-RoS8S3Dl9Zeh`=R8 z6RmAN@IXIh^CalXsVU1#`1qxWNZr-nP|;n!;_utcbo!i55)b+A@5rVar}`#?#nNs; zFr-G7h_`T)0^qt&HV@n4S*A6uSOR#x+%K6|Vlx3=yI|k@MJAi#BX3F2^Z7h)ZlCW& zK)+Qs&$fyX;-9}8a`_?>?8AY>D$bp{P+jo2H8->fnuS`~de$CGnOOU*h2T+NU|=id zo~O*nRaRAZne;8CFf2ZOH6@HTeI~#_Z)R`rh{yTB&BF85-{;(tV(;%aSeL%h<8NT~ zWz{lZG@ zS|_5(hye!MTEbdhy`XNXh6Vbi*2T#f^AD~8>|bcmq)c(kY_;`@I31Z~-B|3yjF%$9 zl3=LH@boKssd}d#RN&T14y!|aaBiTc>HG$-S9eVIl;=-wkKO8!Kjx;})J$YO?RQp!R+#72( zYLAvox&%2~I3A;0L`N-;-_3VE!n%qz_A{EcwN9HIe`o2i?&}_`413&AQg5rmqTnjF;R(sRet)WhbO;JW8>uoH^^lBwbVMqWF^TjCrYinL)1y5KlX zz_K8lYoGbH8+5m{+z_r$?L9SN#d%L)>3tGtmE4Eu0MyZmwe-YB$lhs1<#Zyk*B3{D zBR$Ycf{5p?QDZgv;Y+i6uA(~<%O4_RwUaG`cS^iQxRdAg-P^bfDZXsH(VYPSu>21b zyxe$*9@cNbqj;hP*H$x&&j%KG5qLSlg4$&oyW+DM>(ZZsO_&&4!mf~_7R!Zzrnog+ z?sbNQ8A-{d7MQ7pn%Iik*du$UcuFOIu{+h1ZHD8%-lMOBuB$1_sgBhCKLY8pC&pJ!mfvt8$YlQ=AX$c%bm-1!;OnACTa;35??t*`4KZW0Nw zc$v)J_eE=~rL7FdVxr%+D+QYl{$lVr$vw>=t+W(cIr^VN3I|v6}&SK&>J1`f#yoX12`gUpLd8zc~%X4x;p_`w>sd+k+lj`$fL@nK2S+qSlEqjDh zm!`+2AAPk^`vt~F{D)@!5f0B!W98d0hLGY{bfTl8Tp4DJrA91Zz+x{QeW5!8`T5d= zMK^(!y_9PgAxhE9POZz#`Q@ET$DvqtQRwt$~o!K}v+k=Alzcl-n z>umc%io3Jle16nBQYRG&Ni!zcox1)Y*_qi}7zC&~k4omrX(O8yFy2m*RE2_eO%Mvh7cWnmu%jL=b{xi+qXtD2Fd~)Y5Zd@KT zUXLTk4)u^#1jKN4dB5xZ|8y{r<{-V9TQ6PMFk5{Q%#%j)?$f*ab$X^E`HXUXH}&-X zi|qbaJH7mTg)k0N6eB zyC-pI%MAV3d>UsP>-<6v6aItv@4n@@`7U-5?v69#s^aV* zUaY-Dm|B)Kyk1J3bF(qx^_@H=&U_@Us#i97kdgSK>i2@6l_DT8YOtu4=z4hPkC? z=S0^T7yg&*Yq!DaELT*cqW9r=+?VOKI1>4HQ}q1DhWjo`KR!)$SY~Raf0u2kuD>3? zwx(guHeO+z z2iredZ%2Km&p3;(=ynHFH(>w}cxH>TNCAcmS@quxD#JIA>EScDKGJj6@wz*E{~ywb zO4N%IMhR%`MK`6ElUL2@p0`* zav+N>p`~}WLOZ9;&fg_Ey!anZ`CL?Q>%Af+G@A z4}mkSb?tH#=RSqYDu`Os`eyQ(#ygCYL^);>14Ulk1C^mJfCfymF{o{rdavO92va!6 zD#UaA+Q5UIQM|>9p)@?3#*Z{md3M!8^8gSV?;rxUp2%qTXSRaah3yu6F*DN7QyO*2 zx>1JKSXo95n(re)jEpm!(+cl^lJ>181hpMu!e(Nr4;y0U$Sjs_2B&#;CI|({F)B9F!2H6ej0#uAs9f{mve* zoAW>QeO35mw-Dy@)9dd)pBU*GZ7m9!L&TWC>8IGc}il=dl>G7Kw z<&=sj#8iChk|lM$m$;eKZoQg`TK$bQNo_hMfus8{e0m<|m*A!^!Zt<}^!?_^{3*IX zF9>mbH;!*u3ok7ov=L7-oJ}3)d_Wt3HMBL6^CYxIdrO(u`}$Gaxx4M03Ff`r>mpkd zLi*D852wFlTjFrQ9HHhJFtST%A09@CVQ_`In$XUqKK%=(t+yK)WXOy}AZ!g*M(&49s{s8VLy@xEt%B}T$yx9(E1bM&~@I!9V6GtSF zz`zU4Y$Vb^XgVul2Jf4Lhf2+ATA!cWZ}p%94`=9SiRD@XevYY#38Y@WIfHFsg}GWZ z+{7JBgF?q#X5?yP@lAwKZI@2uH+)UX+5JKKzU-ckCnv{mC-ZyrBJptd zj9C5L(_F$&mRT5!J}NjOs4~Ro7({X>IjCfhIECUFSZ{n&wFX+tNhjn$2!1a!yy|1L zV_*S`Ygzh;)evqt*N<;$6i!U~!a5E+Y5Tv|E6F9X?$a@!iGm#cqv&CKjqr9PTISpdQ`i-=F%03&+gq+DR|c$&Y3@gC6o1-%am3NQA)n zhme5eI;^9s-CcmkY(0!{;=L1hD1ztY!jQbAq8> zK`k(!{Kq6}P+^z1_9z_=8z+6JL)b4u`U=;(*!6KJzIxlcjl2pMi2^&@uM85nCw2#U zNrI5Ol@StDGYS6vv%q;RL)_6zJH(whV9qaXw2`0Ze~2~3nX8trxH82>bp*uejCXR- zexz;HBV}DJPR{O^f#?{37#x12`C0)jW6(f8jiMrc7HdaXEq}Z@Y?<1R)!z2jKUM#( z>-JV`M&Y8K?1G4!^8o;OTvLs02?zp8lTfMbZiVKt6BIk+T0|ieAMRj2N1Xg@lUtbTU^--}LwfV3!op)&L652S$+ zFMNMLR|zDD_X?!nmGOS$pRaoF!HM9aEDCpp5uTQK+XNu^ESLy*uEL0sY@>++6O$Nm zde5EAph)u&L5ET{aiWOgdGW$BNR{~`M;u6Nh4m<;0qZy?r~-wvajaf}Q$O5uab-AZ zWj;c&wI4yT@M*j_ds?W=g(?Wsd?r^mD>QQ^`PTATDlxx4Vcjsqm6?t-ti6p(#)6d? zFEy$|I81e_gk+WAMNZy=wo)%va~KTG>eD*Pn}5H*=s%DTANR-P`}^!l5b!A{h?pX? zNK}xIJelMR%~nq{-}%PG8K!#BuP(!DcH!Kw{_q@R}az7Yp}UksAo~ zw_R<@gocUy!){Y~FT2h7)j7G3t9~>SAYrv$y=eYC&AKv>Kq+|bhXnd_j!G&*gjrOd(n)#hBc|L%2B!VF?)jMV;fKSHCQoW!-7a&+)LufgyS^C zZz;Ze$G*9Jc&wASdT*r1uI@`dzQ3t6Bfel-mV)zao#bZ8v>(MT?ttX2kR^?O)~ZHY zM;}cDAq;xw7#$KPi6B*kCaz6>wcUr&s>6DPepOyo?^Y(-Ja7+~uQZUna}Eizah3)V zz?wAz=760X}N(DDw+{QkVBvPL`d$fiOk} z2s(No-()n?4+jj-uZXO^VuDv513F?>Qo%D`%*KT0RDX#Stx0|$t+{_Bv$yfM} zP>g;}b&}b`agNxXHoJ538@9IzXf16@udAb{-FG*)QWRdJDH${+=;n(hW|hKrJjvl;*4A$j*~r&nfs4soTy#Q4rz7 zl*Yts@nf+QHx?h#!{Hr5eOqhYiTi_}1E@&Kw`z=o4BTXZ^@|89XNgjksQ@$31=92i z4<5w;@aJ$laBAz~K*%i^deE{PGG>JqzaJzCv0Wiu2H93iwS+KC7&2tTkGDOx(!hsI zD(dF<0#Zs6v)}8ZJ5j9giXh?}QSo20F21N<;JW4MSn>C?<8?E2c(O3LDz+vXQUuN` zzOKGvq!wXGv(ofXp2&7h{JL=61g`K?vI(;CXLWjq`bQl3m2a8I)KzA+vegd}@FhwH zc)@yqtP}$0*y(ZEo577ir7&PvfXcT(GqUPrK?9GLarz;KHX}}5uB%+jhvsTLNHjb5X5Km)<%~U@k zR0)@%6tt}k9lHn^($-r`D02+Lr%>vmk-H&;9F+ma(syg(P6q`MkqF($zGAl7H8dYP zdO@h$*riCZHpX*&-7GY(JqtENhfgw@d0h1c42+D3G)wA+L>VRkQt~^0(M6P`g^?Xu zf)OQ<*)tFda7OrVb5)vI&#donW1E<477g`{Tkjvr>RFdDdFwgvxYy73x5eyxFL3a- zN%^5QtOt;ozIPs9#fMCFeXNeM32K2M? z6MJe(+5u7m+ntGNSkSbk1VI<*mYet!72$zVEwtd3`VLfvMoIbG>K}}gkg@iHCG3*8 zFeXaicMh0t-DPWW_7r9iw*+V0y20v~J2H@_iZ&{ODHJY3pi#KRL9~W3v8C=Yh5bOk z#MEa%*gb`s{7C?VWITyr);0@5up&09)qt!bsQnm3|jqHYI>eG zwc~h$i_z*0K!b{9J}Yg|s)5BK4E9_pl%_ysL9UepnU0zqIUx{|#0(drwDyX-G^`PG zi!7C`(KYLJb|twQWOG|PyhgW4%4paMi6!vPg~!!lx|T7+E}|Xo5Ewx~T=qe*5Gn0# zLu`s@jps_1?z)H?^|nH@>XLixI1TxY@a#;k!ieR~db(cvczfnXL?4>mBFft>R>4gO zU@~#pQi6Ps-Psb_(d)E#KRCxtx(Qw*aYY(;YkL5q@|%gwh~8d%KO^g2f`xc%fn-o8 zmg;3T&}xG?+${-PVHa%<7~2&>Ddkh-@SoD=>`K}|AV34%B5O)7HEt~GWzkMH+y1~L z{F^Oyrx=9*Xux&0d9EfR2d*3@Q1BvD;(V`D%MCB@jGV3~q2{Ry4AS6JV$z{xp-l@pn~v85@B-iX+n1C zGaeX@9pbeq0|mxH{q&W7n}4opiVj#O1p8>3q%P z5tSD<63$QyV_d-wPGbael$no#DB%SHs798up{OjLT^JGy>`HauBRThSzt4e;Q`6pEIxA~gk)Nqx zYvk!)1pG1MVP(%TEXajzAu|3edn$NRl)837#tzGJ$M`O&NxLYHF19{|?hlPY;~8xqpX-`;dvQ zADVtDQMeTx&$~$M7O_b7iEJWS-U~JDVynbqWO~89@&|f?QC5Q(XB|)hk<1 zV{xgg5J!42@hgoStS-JDob#gWjOjGDpjroDsvYDF%^gaj3F@I_Pbg2Ub)zCKn(|_^ z(Um7Qn+5^uV)N_npo;h+B>L6m;9z?kI_ebk=@*6;a-w zEG=OfP*u`I0x~C?we$Kf1mKNdjveqwgkck9o^ z`I)Tu)M`BZo-X}B-nkHy?nEO>w|t!7rhtIZpV%^2W`6%8 zRi25a+F*xCi|g%b{U{Y2r%)sP3snpS5skdYi6~)^7uGy4tZ57e*Fp@5bgu~*$dU}M zMpPt~OT;KLB$6B3qL8S?2vatqx|6}fiN*M0pgHw{SLCcR!lVXB8H|8JJlghtjvKVp z_U&!DKGlV#-SyVq`jYIX1~zj3U55-(UVl5--L=}^ufpLJ6s0T0dFV7{y`wuCp4ef0 zFa>HnQiWQ~;32@fyut9hGCzl9_~_UEU7}`g@3e~+EcRW117HuWot-8?vQL~+{XQ=r zsmw$?G)$&0=U)wVx_b?Mwp0LN4e-atSv>Y_J};iaAtdrt4jKn+jx;_mV4d=koGOq` zmhZs0M@>1d@K`SWDhJ7YVq>CF)>v``AKd7gdrRtbkjeQ)!NEy)&h@Yuoc&S}Y4+=? z5QNd)4zyU=)0c|D?xj{YpVf1Fqh{#sa#h<)gZa$x{BK_i5xef|)BKmU=;HjNxy6}Y zO!BEv(=YQ9@+$k@agv0TxOzPh(8UROyZ~;F!(9s(THMtTi&J~QsNmbhFa{}2pFy#4 zSV+D0rbyS7fjgY`-wJ#u^XgfB-hZ3~Qt)Y>nT9n@E)N<89HuUvxd9KBQ7019iZ-aE z!)b%_4zDd^?xliSbI0Ieq+ASRw^2vt4I6?t&LNKl@M6ve>WAYoES1QF$;ssekpR3r=1q!LI>D^MnXBP>0mn|ouu4XX zN_>#qbb&l^-F0h({^#@&m2E(qpsLhQUma&SlfQGIR{Sx6L6S*z^({HJ3oP&dgczK+soQylvMks5spLsVNJ^Z;KKYQ@6$}SkOT+ z6hk3<1nVFX8ZC;pW&=d&%&j6gu^3fr{Dx7OhMYv2 zW;*Np>6p*NTOe*wV8m{N=)axnde`(70umfPSa1%R7}|8RlifbV{_^YpsCR#K7y zLoLRJBAm|0Gwiv^)1j@lA*5J1?B(56(F!WJ9{zXkOFNsw-t%bghc2gt`;bCkciBEK z=D^PV7d&C!NaHz21#ntRL0?7-euXVuG_;;oD3)s(JIBwlg|ywnYM3&=2Y5A&Y48N| zeWutrkCG^P3S}OF+>S`xsXbsl@cjJK@PG6J|R?oM6Gx3Vk?XIs$ydDQ9hMyMc2$#f|4%>UzhShUR>a* zhL?-0E06+ndAA2W_ZZ2ag-fj1e*blk_xpe#cvX835Mo3I9onNqkfb3LIDM}k{Hb&? z(W{3-Q!4CkSwJR+KHVNanKV=GW-LhAn;^|Nxu_fW4-wqL103;EvFEhYA^3BDpV*qH zwYEMhmz(!SMe7q{3JMZCu^G9hLHr}sc(2bXH{jcaAc=V8FeX>sS=ri+WA5m)IB3Es zR=i%Fx(Ch7YSbrOKjyK68=-knVwlA(a$ykk;_EyUeQwy$`h*-itZkBuElqd)DUbSt z8^qLn_#(3-Yi(lVxm;JrE*4HNEr9jiLWy2HZnrwWaS*70<&A|}h?b#lGu zoa-S=(R#hP1L}XBj(>!W%t7iB`x6$QZMS^8^Pt-NzvUMsj(#{J!lb?E@$sr*C4|WF zs>lzsfPK@r7(D!No4wt3F76B=X=(}*nmcJ4Bu-lg*JnFH6RpW`Hb31I{>7&-Z};5x zL}WSaFP{4IA19J7zBiT23^5m|r|YNK9ka%V+gUTpA5vBtv*Ii_D_lv{f~&HuX(s{! z1QIr|kvp%RI~uCQ!`|WVAUozhObC?-U_P1?VwD!pV;0mgEJ;jL6&VRb{Kh>7d+N+O zzi{+UpZ;Tu(j_@7d%8;4Ey5`(8VY~Kg=ZwQR~6?9GDfGdC`c2j1QB}N?V}+S_%V-& zN6Fh4ty6J>2aorO!MgTzTUL5kxAxvNuBNW93QJkr?(yWq+$FE>ardT1C`V?=28ZX! zXXi)AD97js#gI`D(Ggb_=RSEOj_9^oQMaE@--4#_TS6|>K}j-1Z|pHydhc@(&=VJQ z#qOs1Js+~)ODo~(*Pgo^b)T%fHKVMjjS*#(RaHFMs;cX2QFqy<-8zhTO07Lr;&E1p zvg<@$cl8ah6$SmFQ5RO_rrJ(^pM3F6wJ7Gd-4Pw9`}^ zX}4Z>%M388t1V%5*IF9u#a(rjS!LZ}igqkBL7Tp7wJda=r&6tV98Cn=amL$jjTlhv zy5oL3O}gb4sL^hX7HCnTqZedn`#m+;~$LT{1p8vao_-@* z-c>m0@M%(|LwRjRjY>6@Z8%l7Qp+WGqWd}~-$l=?ZDFd@>?+;VkLA`&QaX*aylho! z)2B+5OQlMbUh11@wXO9v^DS+b7-dYV*x$^x%{0?Bvc(iCsHIEd`g6}Z*UFuYn<9DV zl03)4%>>fXiY%;DTSko zH~h5!E*HT6ea9;&4@cW>uf%j6ry{$#&U5rBJ>$DSa~&dxhI}$|M+*h?l1u2MBqWkH zAaavL`|rWnp}~SkZz57%l%$?*iKBm6=6iWctFm65hf7#n{22294j+qCcC@UnlxyU@Z;ZABqa-)uCppOTaUhyamnLs>9x8aQ4T4_sW5|<1F#qw~lCgtE)a(O>vW#o8xU;zcX5d-AO zx-)#&=SY7WvUZ%`w2uq5pLRTu{hxmwN6Nc<ykJiAdNQu9B77T9ebdPGZgf7T;HlLe)5VQQxa$gkEiZc`X_|aoKkQ6 z7Mk}iaWN(4E5?4DQ8ddlX~!3(TcQULEMl1Q3^g3H=8JzioW@HUR7=!5QA1+ z5&|e7Ju+DEWwoTe3tK6Cf0KtILJr0t98x}C$ZIjHMAC{|H$;s@VHm0w4^W_i@w7cH zqgRS#W36=bShwy1tPEjhLAmOQ))tR9**CVAjDIWd)`LH6J-Q!cej&d(zo5PO9DM*a z?NIl9KU?WshMgkFfIX~212^m^`y8-NnyPa@%=gUdd(A>&s}5T`Xgv}NUfDhpCtM2{ zcUvu-baR2rnj}h=7O3ZE!KX1xq+6N)m+}Kqx&%|It4^goG!->6v=oSNBuHS3xOcv) z&XcNM7#r^1w||+L6Q<0Ov$69i!9oEQ@QHUj>ix@s&5J=o%oWN9As}fnp7XH4sd+>gj_!^0DAPAFo zF|`1CfLA}f@Se<@2i}aeNBdBgqDJt_)EVNwR-;LinhT+OZQMHHdMsC_$3Kg$bY^kd z7eBrb!8_gqSTFO}3-CA$#&Ks}t>;t(<9_>O=;3&)MEv$Xi?yjCDGUP)%PJAY5HimH zWH8O*S=1`*KYnVRSay64P3pzmQEl-1m~ z-j_9IDd|T2zdg(`sUs6E=Fw1nNnub}($wwuM<$vZu`axhINQ$)T<-aFl}w3$DPSb` zoCO8|#OG9x zKZE$<-u72IOrFG65TMo{w`VbJ&SaU-vr%%_e zBu=5fn~_C1UKp`0?SR5Oq-N~rSJc|IrvFb$datb;t-<9=6WU-wL7ib?4Lci32_|QS z#Z5J>Wd41Gm!pEcMB|00H(y`EJ!fGFa;!~6a~;JZi@u9bewOv=N1YzOgW+vg#FMyS zlXjPYQk5&WNbMh1JL&krL8ZYw12H_so0ZBP0QahHvw}N9Zu}Wm#$SUuY|1 z4$0@KiC;{;b4ZXIN0g-g44alH`x&Ae{ypejTpY@8NxAf*_16QRkwQ!ak1iwWOPk4D zE)WyQpqGkJ)9q<1sO^OQh`VP91@tm1>F=ZplYo(WEVf)vZ-@cK1oJaTCEI2DrCr}g zuL@n^bdgSmCNR*OCeoQ@0!{y;d*jqzj+lt4JR*!u{PqPJWOQQ!0xmA5&4S&gT6RpVyN0BH+u=6&~~SQ7s} zM=>V&r~EFn3lwKf1jX6Tuve;|!IIlrV4q~8B3@LKxKHtIbXb4sTb)%eI?5$S5)4rP ze7eK6KVk)AAe7x;Yz$+cUdl0Y?B2)|*8=lya{V9B-gj8<<7m^JH2rd0e;7vBh0F5M z>+H*PYl02DI*2^u))CLe-{9HqB=E4C)@yK?nb4h=MxJYI3CukwG4CR83;EE;FiVAh-o1-)L1@j#NFlMDODP~%k(c>w?H37*?HiGtx7S#ks z^c*yPqJ?jCL^0$B;_wwE_L{r~le{pBk+9st42vc3011R3O07=w&pShs^cKeFM{&68 z8vk7bk$Xe<@P!uKu1Is9Y%6E&aZ(lpv@8LP5)3+TLx=FZ4bM2?xu- ziv$NKLTl%EpNryUfWkA1k=v2mu@==-I!5S+4?PeJt7g3R!{RXu$L_SG6a)R%eW!B> zWPGI^8UuYSC{5%WIDXeZlh1*{jcfkYWDNG{?XL^h2{Q58d@uLo}r2p-?cV1TNrzL`w7j?f?MJ|0ocB8$m8fJ$enjrE;qg04?d4 zfB(Du8Tik(aDer0ycg*I6U+st%qz2%PeB>TTb4=y=WkOCfJ0)+Rfe~jim8Ha7gm+8 zDCKfHDszJ|K~}{pQp#i+G|L|}loDVzlcG`*zDg<@tG>7~m9}7jqm(R+8_W$r4pR!Y z0os7EP%Fc2)09=9G3VJzyeb;gzM@bZpfYTzIM=KYpFXm3+4h7_DHn{o%>Y|bhCSc| zp;9W0bGR|lk9?I$O5rQYaAgvABv#o+&5oFaWN25kdLCCj$COL#UL&(-vkQBUK;xkR8r;fd6F{UC6wwSks6?ZbOM;fB1W-aX0pR1))5qsxA`T(IQOh%b zNmD2)mA&AlR7C(Sib`N1he3nEq7py=;1LV}f|rW1uwZbP;QvDYpWA>ISb#||0PGh9 z+snvpj^H77h0mQfl%Y>i9+~NtW|!SG%#xl3gG8s|F_5FmGSZ>SzRG$aIAnI-wnM z68;RgoGZB`e56Qn^tHl8jF7mZ%SPy9P3?v0EHUr=V@pSW(~WiUehow@DUKW2n{533 zZOoIqBimk#f}I8H%*!*+v!;Ho%%qs}smX!kMS3x2yt4``zUHv7ETr_5hN9irC&cD= z?I(Dpc`o^SJ^_I;8Y`wF&wD%?mGA-qTsSzjZ~@mrM5EbomgCW+GiDD>gLroaJk6N8 zZ*VInO5moec=Q>SUMMZdM>-YlJKS-TG`T7|Ba#JPK{z}!)adq%9G7<5FgndePj8se zpx#U55ZRw-`+7sGaK3DB{9@qn^`3tRp@}bB??cnBkb7h?8RNuCDt1}|;dFh+8|0_7 z8&Vy~hie$6kdlEMzU4Aw|AxZ?T4WZYB2d^0i>8MAEr0C7No}Edw?494dvoPRJg&V_ z9$4+r&FkVFOFTr(*^d)@M1TmOqK9OMV^eVIM8k2N{Qg^ETe59YWbL{tlucefI6$rT z+PkT0DdIrn7AWiKQTSO$dLbGFXvX2f8G(lh_5067`_1ci5#2@GlWjf_qe(#bk)Q#r~YhyrdKcRdu;2jw8EH zB^y{7C8Udrf{N2*WoEyyt$9ip^AhV?`EO!R<(ytolWf$SHQ|hjO-COx}#+*|AxaA0N9xX+M^t^DWtOm`(16^U7Zepy&&dqVsUKlm%$8sw!$amm#vwOh-Zd~ukw_9))gMnK8=A6F zAOE9O(tD;f)@jIkL(vG)p+}e{j2@>gr9gyHTyAAwy;oj1W{cpeiO0p#ZR0H6{@PN{ z%L^Gv>|$84lnX;~0w;B>+D?;!AmtBsz}F4@ePd^lRv)7Wp*xQZe!jUm=mw+T?;THy zmfTclOso4(^W&@Ed1jG^W=#(DT*SeJ*Vru^*=b-NM zRE_T8ux@Fb0n9wi-?3(I6TBz>Sr6W7|2?;lpdGfjmd2HxHYgo~U_y2Sv zEJ&wdVT)PouRmICodM}ws98(d;!MFg`>AYy^i)17T<&q8V>p89g7)mcn@bNII}iA(M$KW;9pv+P;U>6}veb-3NR z<+i&9%*rD}S`?`o;t*@N>5+MI5ZI)D`DbTJi&s-Jg*7&RDJ-Eto)amwKxWaVpmIcq z6U>iDVC1_})W%tMk};2LKu`C?uvU_D@xuUos!>hN%SeZ{$}v^9j100fNwPPxD0>b22wMWpxxtuYcZql)<*TQY0q`;Ai^2tsVTjJ2PACU&`p zgSy8Ek(i}#w>ek5nCIcjQ2#6Prl^uFCHcOTjb3}5`l=hF&wA3Ro#~8JvQ%{Li2g^l z%td`)W8LWn>g_fR4eP3Xd3GDlAmwM0JLp_3BWr3N`Ni4Xl8ngVo40q0(e#W-0l?qt zcF3$@yXd3dIGwgocI{jthkn&mmU!d8f-9a4XxzZ@Ii^Vhie$+d;i=-~Bb10~5yPD~ zaT-n6n-eFoU%3AX0C1?X1xyWmP29)YdLO`7iG+rO?yct`56?~OavJ>f%ud2mF|G4x zBtbT%4R9Xm-FI%9%kFpgjz6%R8GoH8_fg7@=A&#`MmriCmr-WUx}2A%IWZC|yWjt; z>38wo%JOX>4~!}Svnn!@88MhXZc`YxtYuoa><|Tg+o-ndAsu7y;oqe$Cn8M$u-gw+ z!Azvma^0NH;G18b%4zmWzh8eRdCQ?*S1U4~ORmi*HO<$b=FQT`AQl?vo&Mq}kN5)w zr-p@*G)Cj$XmUjcXWA;t@%+=;3V#GfOb!3vlAuKdpvp;5q2i$8A@=pU!0{;~(n?B~ zYOCd5_|6h953J}lDK=bx>mMLzY$OJS?gDz+NI?I67XS?>8Wk=afJcaNmCZC-Wj+A; z2B;TyH~n47z`%EHw!$2rRNe-2_QB$1ubwipqzj6@QWmdOQ|S<;e@PH|n!TphFVy$w z>KF`PpZpjz7y^fi1~evjJ4VwsKhHONs}%(^TpCPjeAF2DtKA2gh!W}3-0cvYFiY)wVg1p zr~nAbxECTOA~gg+5&p}znV)Okw13;BdqFt(t>NzP$3yM-pCNZ!CvH!vj&0kTU@3`B z@*pW(&xTXSm>Iw3@Pl7EJ(w09zB8v8Y6;nVX=c5Hj!*6)m?~v+KEk#5r5<@EfLs#6 z3K99xv=np{#280Bc~&x;RC!uW#*=@%KXK6*OHiZvsXVMnyaps|4?xOg!fxaXbCw|lXh+_iiq!dwfX$o@{1nizwBO5dqCIwh4_ z^|k#K{h!3J%w4Nn(+wO zQ+Fyjdh30OcC zIFz|W>Y3E=m>3fSyni()@*%RsvOTxw3s+>vtM*0ePAj$vh7zJ5DSJxr?H8HN8d0?@b|=9SJj~GyzVupL+jj z%Fn$(8kW>%!H%1ouv3$L7GnJN{m(-Y`Jt!XoAMl#(2!vwYR;V^(Z|~4%`L-Kv>j4; zZPRLqrWEhLu^PgepbadN1T@|7y5kj=;P2joBUor*m>ql1iKq6cV>p|I zAKp7C1MQ7TNHO9sYVRL(jTtfMLnG^Z>gs35;@i1pDr`JUS-iO;t#~x?1Y5^IUEoUAUqfj)dH&&811qWh1Og$ zgoO={+)U0SWrOdC)&wwD=G!JiJVYhyn?g)pQSLZP*%Yfqf~Egh3e$A`;}m{a=DWVK z>4Vnp2|of^F_S00b)s+gtf0%N=9L>Xe60DDuvw60IkbK@ASy2O9oKH|r=4s04$x^t zYemwtT?9$cc)b?O;B`K`;z?IC>d^3@UQ+uNxqeSRXP)TyPL+1*;!z8`Phz^v;>d>v z`~FsZEwl-Fwk)|o!_l!6i;rKR%KMd@!i1gun>wscq04x~ojKQcn~p zJcqAwwIK)hm-OO8ncilARq%`VKX>PTWyr$x(aW_EhjsW~B^Mj%uo?KzkAak0Ga5^= zH=gKIq;b0%?Z@&v7QP;hcKyDjJzuX4B55d_DI=9rKZp6WGygy@5o8Ku@6(zL!@Otf zX1W{f>Wy`vK#F6n^F+|NC*vU(n;@Rgx#UM@#=o)Mlurb-U|Z6r!T--sfXD-dfb{79 zm7aY2f0hG%L5VDPVY~91BGcVbBF`MFx>N?|3zZwjuF5GD{KwO4{v)F#xH5<_Gag> zv(V=T`t;=SIw6wRD7@FMU*Ok$G$UZZG;3Dxx$4=!*YCft-sfk~eJ=Xb@uJf{c2{(w z_@(l_{^Z z4-e9R&_#Jg!InJo{nB$%O1W0ik62)frFQVB4Q4JDT$+*uc&gY2E_%@~0aNNf9))K| z4Fcg7(@RfhhHi(mvvJb5s^G9Wt_u=)T7<cto`@0U54D^3#C*j&Kt=6k)g$=jf;rAql}RLzXl+DITe5nRP6c|@W|^|H}WUaa6NqrQQQL~j@p z3rOtC$eOu7lqmt_-+x{v5~+o&*HHxI?lq+`6EQ|_v7impGnxAf%x(wQAa+PqRUkH+6mcT5_J`y2gna` zd?DXLI|}^%+I%Hy{{fTs!S+?x7xQ`Q&3`Ra7q{FbG=(psu30Zh_;c%fepAS_R>$It z@)4t&M>1g<)pEBzC~IhYZVuAWppxctOD&?i!<*p`jU&_Fl;0A@>9YJKaREF=L%SL0 z{Cff|{!aF2^aX?gn?|$sc~o#^hDNUOcGuxNiT+I!{T9;s+f0&heIBcq`UCyvIUVlr z=UiwhU!(hK$Nczj?DNclo#5c;JO}^ZxY@4lN@i!w4^=NG@p1MndSu*458iVm zlNkk~?HM!J^voN!gcvZoT^?m{#Dhx= zc+CUK_NQc+G^l=j4x*R^{#=rLtvBRU<+eI`y<2@7a!4-YqWUeTHro_WBpl;pDf1w4 z!&J1|W%c0bL@F@Xb$#Uiv8)FBDs*=}07ym{Uxn1}8UH))XDM8Dt*C>lB)S@p&;dwY zIqXUmIn}tp(Noeglz_IMds0%yLXBNuy$Llv)Fi^mPBrp{Wb+dnmlEB>We#9Gx2fI| zn}m_g5})lnzkwt{|N6&wJYoY<;a@>Ayz%n%+B0oLCXfP{DzQYBn1DCSbg93#~IvUV~omGGWi}6%6oXW7L(GJ+9`5 zW06%l*fSW3O@|ZoVY#@ILDoT)^dEK?Ah%olnt7swN|7mbQEt z62N~fDJcobRjKNERH5&;ac2Lljyra6~h-hgm(TPU7hmv1>?qFI<$iEIPLhd8*6Mz z`b@x`iUw#FaMGP79tY4n9sQZBY|wbtVCTmAy0?noy&)4fH60pb)^~agPa)FLyV%Bm ziT|)gNZTXo{oLF)OhlNdIN;o6xYrYsM-TRoE@$CzHMH1OB|PHeb<5p;oikcL6tE3i zp1?GQwGb#ZR@{C5F_fbn%%Bku&0K7R_$f!RF+xe_O-EZEW)u1T(s0Wq%1E3=p6p@m ztAAHVcb;DNltGu{2pUdiRuWq34$joO71aR;YgTmskp1Qi;6iECjEcPBZqvHA3# z$rb2~G?ZqrjNEK%1Nnjs{<`(P+T8-(t3QaGSobbMy*nj_~aBjpZ& zkS$<3C4~D6BzmghoXG~>tutNy_6W7BFK@$5tHS89D}?H!@+n^Z3}=g)`UT1~u4RC| z>^%E{UQ@}m^BmUHh6I6_N{C0NQGEC<3?8eXCW*siOFB#Sxkrs5Gr@bmRyL=4$AnQ~ z^5{T@!e|DGp3Kd*-mt^=i!Re)3eu&1wB+q%NGtm29V>plvsXaKvaZdq4N^tEv9EoJ z$Vd$15JVy%REZhR@z+Fvc-_)$=$DtZh0ZN59O;%AxZtBJz);?dhp@Z#q8rx*{qIcH z2&B=Fm__n=52UYDD-*=!n)|+!1cV*;`n7-c{_Rh1qgxf5WZ=5yl2XAXglzvSeC)xx zUweh4e=rE_)LTr)nVUMM_@FT*EipfD<)l4M54BSHBygGV)WDjgQUebZAG?SP3NPH3 zAFNRewP}s0>8PX-LS)E0DXH1_ZK}Ed0!3-4Hc3%&9qa{ULVC&X#sYRMB-ek~8=bG?gd7D>BD6A`VX?Gvm{~laMnzt)dh6VL69yTN$@S z&hnFt%tvLj-?1@q>k2;f)qw`5f4^6rvUvT_X@#FKI84OH7`;obbceRM6zpyawu0Yf z>ZHG`crJ`m*f^Ku5c^8}rh_>V#Wrb_Ea@OQR_rSXTA5{TD98B}i-NtLiNlDZB!;LD zOYodz-Z{*+#3TJO^5oi^6GgZGe6{THv?8`*tMNQc%IBD$e4*!$^NYRg*P)K7-bv!| zC*!BK;Va?Jj{nBBqTu!{-+fC@TLLmgPc2-Jzq4oij6}@a;SiB>!(GQ$wo?<+)g!Ck zl#)#)iAEGt-#h+bBYj4^gJYSMkZ;wTQ-u+-*qoo-Y}higk*Te;0RJ^Kw%{|`n1f`~ zJhbOvrdJe0K(l?F|2p`UmJcbdmOLbjC2X)HBb_@tINQkY8UWC3rZTk9gL zgs9d*GKr8(nf>>~c13)}_@BY1>0BYr#?$1pypAY=iaOq#{wEF+zS7@+_PcWx#Z^qI zSHSDm*Vtd^3$>@m*Qqv@NLqytXN*{n>s$re={PF<${eye6iTI6Y4~B$2e_0Hvnghy z&S81;VTI_QVdE@i@a0ijTeN=?bM#e7f#ljP$Ksstsoz-3>D3rB`o5dF|6IgMPtPn- zY4l|Jp2CECimj83DE7my}Nq)zrZ;V{+gvvFB1|4QhnjP<)Ry~1PA} z(87rCv;%k$IHeB4CgHflbw?Z0Z@R7*I%KxJ3=7HwRKd35rXdkGN=Htr&{{y(eH{gp zab}gc{lV{f*_FOm&^qHe^(F~lZzHv*=RQID!5&vV{A3s~o>|0)*B)jX^h<;8Xg7=U zSv%=(-5Rc_uSOIWBN#7IiCvzc{RS^{w4{QWjEBzxji#!14PxolkRm`EFnC#&LJ^!;XgeOylw{h>Y$0+0OM|nXsUrp@-qD!TyzvwT&gH*zQr8;H1E^Y6> zwz8$TT)W~X^+eDaJtA>pMzQ@BSyJW`{-~p1cbEQl1YQ0W4VO{DDsLvF5nF;|JFq(N zlvhdAG5x$#;x^wOGXS;7?Y!;zF#(mT4l@-AjmY5|LKn<@OmI-QWc1oM&8hv~QJr$) zm~7V{)&cuixj0;(u4P7|X3)g=8SrOCb{onehQuv?Sg!q~+o&LVsaIY%*_K#c{Ieu0 ztGi)S+UXa>feY`PPTtd)^x=D4cinb6Hyhu3#Ywt9Dnb3%-FkNc)O9HPG#69L1g`6H zm#>TiLWHqo32VbR5#>F4^04Yy000AZE5Fr&=H%?IY_Ugx;nZW2ZP!R?_kOER(r z$Q;BJKWy%{G^~9NLxECogD`03_dI8_GD6-$e!cKa_Fx zaHXni8{4QMwlP(YE47)0jB4(}=3&kd6rGY;@lJZ7R&+W0)WO_y1`aMA-&L7! zk=dVXfyzo#nsp%-&GlP3vjp=6v`6}9#g3Tj9zrBUkjp%p$zy6%IvkPs2)XDI=_%*% z{6lV4@E_p1=sWJarCJ**gyFmmU6A(^dVO=$AKj8oW*hFM$c(5-tK|xtWyS)*TrX_q zE|_Oj&c~V1&Dq}`Q@vf9FDg_DScUeLiaQLVY*nGT|KQFkAJjG>teUk((j976Aj|q; zbDVNR-zj9+NH z)|PEm;jYAH`#b4#9`L(z&V3K<+}Bg?*TGdA?yNCTf{~tN_I&ZgnQgmI&pB6Dl?nYP zOB!zmHBQ?6ahl|tD;M@RbL9=TP{w|e;%q&}7Gdi%&>KTup?A09pdF-M2@1@C zh!;6+0|%`c2Ko@rQ_A(@k9_dlj?UkpEoZ)MtQ|OEb!`~wlcQ&?zX=tb<{#Dc>g+Jw zrq_AbJ!#31>Af$Vw}f8V>8P>Vq(}PldFt8Po^fVm$maPdOC5J!uX%gci_Y4{=scp= zGj3?6hT|i+849Ii_cKmCTfT>dzej$nDVW%;xueVF*|5}VA0JwlGE@&falWSupb+)7 zOT@0Yy7*R~W4M9&&*(z4S70Tq{?603hImOidy`0EbnnOWO=O(^qlb50*+E(jW0TwZ z*Ri}tKT_1S(fqwv4!SrKNzD^TQOBvixC~~O@<(XKk)Z8!-=|LVeb|_48x3AZU7Eus zU7_F3Q}%B<9HWJPclT{Cz|kDye{E|?Cc}-FOx8)z8e(*81a`(_6Mst^c80b^IFeFW z5zQCCwu7v&64l(+6B8eNGxb+|>Crj@Qe}k*VJmjGrQv(t_lDYEAzE7YM$G>Wl?b9w zCs@xd_kQ@k#Q)mP#>^nkHHPILH~g1xmzE`|nLIviJMTb*?=4V@&8)g5qAjN+>Rv}4#VkNMnIoZnJ`4iHdg$Ujp z%W-Pe&tCh$ChWcYY;4W7?l^^0*GBjDjVouN%7;QP)$!A#xo+di!6hV8JtVkaD%yey zh6av?SB{>VEP-rSCztHk4!3N#Rf+A5%Xa}fjZ%i0Z!u=ee)*k(!u>nDM7&x7KTXUO zdR{%z#P{hv!s?YidJUw%2AQ7y;jd(N=&Qp|t|iodhsQ?Gm!~Zw{L7+4E4@AA6tQ`$ zBvD#kB>$5{Qoa>j?Z=93?rXeGvb67@YmJV{$`9hiEwJ(B2n)~tNg8VGy&=Dd?x?*| zKCpdR&g4NIZOEbvFNVrrxVTuP^h|v-))g!>VI11TXS`6bhf>bZQd;4fhA2H@O zQ(IK)8`pRUu2g+$f*k>PnmIOTyMCc6@3+s~(PE{XBz|*Jg3tGgZ5bk~*ceSnBQe|V z@cquhmQ|8X9&bg#--H3&pAV~lU#p)zi#1HYS@;SNkr}Bh8d^?c322&yU&8lk!Oi=# z*b=}(eqv58(^yo|Kv<3C`(s$wey9zoR7IMhAN_HenJL-`t`9J5%DVB$Hpv!KeN-`| z7;)6`JZUrM?=U&^FtW7j<#I4+lGQqFk(H@P#a^nuIdDQVxj4>l0EDN1$mhWJk0e9w5|O zP@ja&=07-uh=>`)-uL~K8xF0VUJu(DiR4X)e?LP9Y%@`%&ChhZ-oECpYv3rE$+WUq zj)afpdNZEYM2J8xDGehzkkfhqe85SNL=s57x*UuR^&+)tN|ktJMMdETs3^stHtgbH zYK(`MGWX>m3Ztr1lF*M10gU8CyJUzb!wI=Vs6ft4Be=#1p&UtVw$eA7HA2Za7p6;` zvdpPeJT*|sg;$v_Y2hlXWVAr^RorkT9Kb8FYG`15FKq{ol4@~_Osdizgb+8h7UJM4 zh)ys=%ROU((bH!XK6Cy}v@WvR#xhkwnGmFoXaiHQ$R=eVMwQo_OP)$IGmIp~qB)u5 zCYrr?f^uzb*z)pyV>e#_U$`NywEtKgxquZ50< zqNiKyM=B9k{Al8rq3I9FhOrPt*AWLImQNm1wc!Y+Vdm}k&b>t)ACw@I={oP#zv;O8 zX1Foke4|UJe%8;b*X!ozac0}ioO`K8G(}rj5GK-Uu%jHADQKv}DoZnqW|J5uol}h! zC3d-}+eCMgxBUuRgX66svjnAUR^XXUKd4`-<*Q}L-zT?yiw$L=E~Dur7Q~NPUdWIk z8e@*XEm4l=%FW|i|Ewsq>|SgW!LV$~7CPdSLQ-0121^gUUftX>=U2dDXY?;6)(#`Y zQnP?!t{C)b_thytGMPgYJ26WP{Q`E8dc;aetw{1bdo-D-bhcKtxim=C^x88tKgQnh zFx8;-^Hs)V>B!rGhHy1qO}nRUQAmoPKlz?lDz%|UaggdyW>pQ`TGNlaVi)j2miig zJI9DNq$^t6U5Y(Bj*$KJ4``TZNe}73q#ueS;Nvb66Vh5383?OK31l=LgG5pt++8je zdhKlvt*a@Cnd`S*$j=D3u#XA{{6P<8_2NC(DsBzKB;m4|Y<64&lv9VsezrbHC$c+< z=;3WHM8vJst1gkyVEyS2c5A_4SeEkaWzEl$woYEG|JA#@x}q=I;^FTHo-_Hm=wQKn zTEV-l*3Wsolc)YqO_p%EgWhMggQyHeA1RN_=M_$a+4~$_*SRNtsD0+^yJmN(jhsT{ zt59wQzCs~vv+j30)~P-wH@D<&65NCu8a%KG366Z3iT=3ZbBQD-W6fCXjb}|3p>}P% zCjs6*X1fka=`&v1r}=-}_xzO$^BXGdL!RSx+Tx`(w9*<$W=T|oW60=;sgRd6I|IBI zVXaEHEM`K9oSrhpKJDQeYDpY{l>E!;064X;9y|?^Ul)8$`i7RYdX!Hiv+nd?@RtZu z9gDLh=OhFCus8`Uf~TROvf$4AkslXbzeeV z0(Zi!oJtrIWT_k7q$&q%l5R9BU(Lgj7~$W@FKp_Po{vVl1?-RXw=LOD~Ia5 zil;bs;<104ZM#49x?B%C@Ucpn!MMmeae*Vpf7*8MTbV$V1{;g-x0{2u!rIlMe5tih zj7~q=m_C~ukl01pR@r~bU39h06sem4&e74JzTl@0UdZ1P({apLG>Ug@$~Hrv#Wqe1 z;dmne@a`&bbx@4v_Ju===~?fVedNFLH%{CCSa{W9hYoK9oW$`{QgQ`_9kDI+ zzab#`hDA80k3$C=9p`P<&DH$>wir_}u-VpHp(mPs_R{hS0jH{_}#Jw5i%%GKYu_CTf*v5)45x9|CLq(V;{AMais_&UPi zl}VM>--zQHh2pm29I^K=Gt=xrN-YMoxrO}@W1u=J-KxGK%)n+ix1P0=$>FU^b29xT zGMiC`Kn7exsGMMAHJpzZBOhtQ^J4{%r$}&1Z+7gLk2c3qKgINR1fI$d%36NfK@z^t z)hot_g~*mDLhu$D*!R;O-x6+w=d&;h&N)rmr>b!H=_;MsBrfC!O85l{O!;^^2)og9 z-pL-BkNJx6e0~cZAB~{d!EV;r?qFp}%-pt0ONSC3ft7_`iKp3A7tJW$J=i*7RHZU> zt;zE5v8Cv}>I9Q+L#f%8XO`va=D#(>54BIx(t3|coupLe^BDW|r{x^AbhoNAHk%Mt z{rgqCle*q=HX`ID2<{gNlxyvLSf*P~t}d)m(qFEJ{iuCZg}-w~@T4Sf-$0hT9v%8V zZQiOl6r6QkeDmFrzFE}x-rb{0#Bik^_AYeVbF!WYwd5CSO-Z$R7DrCdBS6@vI`Hud z>M;UmPT8KVF@E}vP@f}dF$f!%!3{0_WKYTeSU9(6Gqrv>(xV#a`JB3UQG?E!COwm> zP?KZt$b7f4+A@p0`y}LEp5`;ATO)14iX*8S#6F#{p*Lh9PcY=MiPX7g_k0^F13w2* zTDP&1l^RxE+FzC{k%Ny;Rd=tT)fKfyb0E=YB@iqc>$5JsBd=qMrP(t5TYjT?u#<9i zQ9R8gL=uEwhTWkQ-yxIBkB72agGt3igh7altEu|H+`y@{m$jm~xPkpPuuTR^}qLW3x4P}iDg}j8= zM*MXU<^9%)-<0%}dO+{4*P%{f*CE<3gxhxgJ^5-fn~y`!ta)Gl;~6Xa*Xsyd`@9<6 zVQvYtU$hEgTmi@t0w=!yj{Dcy2vVLs9)>kP3FC$u2KMw-dght>irq3_zM!(c`G9D!k%{SA7|@W` zN&C~sz<=+ftLL$GjH@j6w4JmN8?9C`U--L8&{`~br^f#ScVm`h5gcF?$l^aw}S!Y z=EEL=2j5mNVI7qZ1|z}4rA!0!wQ7RrjKXuDjGT^MSpNW#dKRb~BOsYFi1H#{2qd5p zclxJKAI_5>$SkzLrt&qAY=6pcKYorT|4H>z%FCge@Fd&nFHBQzZCVk^QS<;d%`@J^ z$`)^?!(yBg z#Ja7CO3}d9zLAb5T5S`&$}uy1<>2mHx6V_{`<$|~_?7i!gC-2S&EVo@CZ4(Gg&=Tb zcS~=Z@1syt6}4*Nu+{nQ%E|J>uR53Nj%i-eHQV^l`Pq)C*#*GM&&xhSz+UxVULW0d z7s|h5)8og`x(!XCWAQiWj~Ef)*6nuDL{lN%za_!_G@hNR4(;xy%{x8SfPi*9pU@#n z^@^cBSfnpF--I1teGGAwE95$3)waU*%=Vh7IRPuSp<5z-cSc&+e-)29f8cL$C3v<3 z6%?6FwPO#S)z{jrE%U2ucZ_90gqwWx>uYTxVmR|{HQV*Qt#*;!8|JaerP)AHu>{#> z36GN5SV?cMOZIQX-a5GaYgLN(_-3XNR6M(-IB4Oha&qo(xjSF`zs?MHtq}v4Ht(6d zhE87&JYmk;a+apTa&SqGOZbQyhD-8RrITsS%AA}b0c|Sj!zCOnn1sWC7QptQ zvSFP3d+TKef}t7}U>X7;m8g`8+maAyW#Vqf2nIoztAq3`JB7%HfU(O0Gz^FV)!@OQfr3wiHGh<%Tg76NP;kaVOcNN9yCYrerLk;+ zDfyU7f}aTp9MWT-UdzkzH-;H4VhK4mT1x#m;kf|8kvYqnqa-+!A((z^-Lyq|$2#W* z`u*X-8mMH_sLFL{iWZen#7dkB38z@*nrc$T?~8;U5cM(umnJP;OKE-Cb)TmTJ?hM~ zCNuFGBB(r?8xNhLPO`_mAP60)FIe31gD5ULD!lydtDVY@c()0qJr?dZ7?1K_!pFO# zP<>n(8Rfb*q_o>bUaRSfxI>ekZ~YxYB@TM>G_m0&sFsxtp^JJ-u^MF)rE+k1iymM2 za(mKOgTI5Ceru``Mjj%V#N9EK;g%_2az1&X3RtOUlu9~)7e2UVVL?u3EIm4h?hTc3wO+G%<4;aGO zOrVjF%-iCR89@)z&Va5~%1GcA5?HOPW!k+956A+7a9DjfRVjAr?E1>1{0Zo{PwUwu zRfeVB5#KX`b1cjEjPF$HrV9c4g!cVfGSRH0xZ&zs3Pwh&!xf5ZG6ecHTJ-QWQwlZQ zo9Hz)wW-79>e-~Dkh0i0xXgPAvuj!6{NqG?>n|sYkIYGx^zpRYQz(|yOmYTYrr{kQ z(37X(QYx&!wZ_4ZPKbK+nkATpMtW`9q!yZ(0;WkM2#3|x2=xH!ujTrn91v1*38-{< z+X7b?RL7~(_`|WCOGZYuku+p1duw>^0-*dDr(TZXGR2>Qs4Iy|Luesmum2EEIMC!nf=qm$1PEprxtxj52LPB z22L0Ggy^p_g||R>CA5p-3Zx3n3o zff4Uj=VMt9+U9F~!p{LLPutjUc-BN#5E;%01tkE>-^){dzTkCfN$gLQ1&t0bQz(TC z$5bp~@=Qa_Q;SK?e`)W^VD^ccfpX_{vo`h4thDlr!jY%{Pl# zpj8pcY*g$ua3hwt=d+js>5{HPrL@sElKQbmNrZ8{DkX8MY{>*C%A_b?K2(K~7H{bP zD7i7Xr;~~j#osP=_018b*PSp|)q>CF5RoCIMjuQ3CCHVho?wqn`XOKjvPG3%V51<3 zYhJ86uUf9mG2-T`a&TI}s2bO(<#AI}lU>iQH+o>$x1^I}?tz6xP{FCBNTt-w{9~*N ztGOkf2NV8RJUCm;o#p=q~%h=qf=e4F6ZxFA(rW)g=rJ%sXhJ@bcbgi4AH>Q7xw^LdJt3*(* zCW&J%fuxUbmc%+`n-h#G#%`HrrjCaj8aqsP%j76Bhvx4=(l8B5V5VF;@!So${TzZL zGDw=G8PgZKW7Kd7>_-Ob&1x;NPN~N=c00{%O%)``O!?Vm+MTPZrrf1KF7q^nh}_f4 z`4b%T+3++LE9z#M#pQCF|Eh8fRWZbm+F|EEj9Yt+CMpXapbBQG-#@c@R@SdClQF zCJ|gM+%2Pa0aGu(K^YnR*t^HVz^MHBi}mqglnftJrF4Rx_hB?AX) zU(GHpb?mUe9=mLrKP7g@govpnHMD635%!x$Uq*h5GnpRKh5oeaeR6il)RfW`fD1x63BdRp-uY zn1mr(&-_Yt=UC}TDmS|$>omju7aJ?U!%ZU&#D4&{WLxYXFyB?M2}|wt4jmh*S)e5T zWOomF5^5#4dLCyQA?$7&$|*eBj)8)>i>t~kb!LrRg71H$svEz~usb=A z$z@&9Q@gI&mhnzbl(qNiZ~Qr`8jO^;wy-tvU&m#>4Cj=fsmXCm1hPb`Xk=0N}FbKHIjxfmfB33{sGId)?w^(LNJk0Y1LVK-4jEe*L z&piSiWUi(QzNe+6?zIMcAzfB%jIHy)B zLqyf$CRF$$PV}#8fi+aD%h^)HwC{`@Wn-qyRaopKG~Ocx!++-obOK$(iR>{=vYRpc z#!}rcQzM|1+M;=B1&b`ed)M=V!t<)|XYIBs9!r#0{s-aJbQa8>j~|1WcK?+mVu^zd z_g%S0eILEEaqV*_LYQ1J8;m96tJ=?k!~_F^RMHz1yv~IArI=QG$AqVXxVn9&Dae)5 zyzGfswMZjuy>hPF0+QTAo<+V$g^j;{PV;>h@7#O(@+^*lfqpje{P$VwWCZE@zlKCD z&^fI2hOZOI>&guqtDfoIdQx-bV@9w*$=ixQeeyiLWpQp1;OYEGPb!k%t%x&&as1ZP zkjuW+g9r-n!0+QIzFyBs~na! z@8*tFVREqfquz~HJTDvANPlPlTR5mCX{Oc_`fhu@JC~qcH&N@zQ8K*ZePgAz!=sQW zbFj8rpO)ayp-ukwB!)JF>)n}Zqf!J}6P*H!ND4Kwzc~#X7}k3Lq4EB z(+zcDDH$bt#ugrB+cAhJ8ssxAT!q-y(sN%J`YuPcDV;p^+r$4o-D}bJTMUE}7s4eq zGO8vFnb~aZ=tc1I7x|5!dB-Nx8`hrEu3 zzbLHm#P!YvJ!1KFAkU`v@$WDocz9Agd&s&wD9|x6$s1lquhng^=V;l_3`b=Lx#HBS z0M7hJjZ-^~V4)nJ`k5oAi2;Pak!FWf^IQ=*1mi3Z_Q{&tTWjw|-qSd_{6>QGxDHUo z?t0d(rc5Ij)fQI1)wlCz+vKkvHV77OaaRbM)%P2rm(6T86a=+Y(j>uvgHP*0gzQSF z`upQ$RAjoV^b$t1wdPKJx(4(RRdeXpr;^KpX?Phcb#;lLp#BfRDZCX z_4;x4@8)3UTw!2%*xBM!T>LToEq;s5TjX%y1;iw3AVQi|v*`W8!$pJ~YV6y6mNqND z)b3{dtl08f(lYXfo&7|A4CyTy_KX262NUO<@&E%IURGbS%@EG&XNazd=xgaHq{BD@ zxmYnr41K@2ExMZi7_*Uq^Mv-htJr)2C4kW@D%>I(RUZnGnK>0v(Vl?&mQ6 z!Mv3&J<^yP0UZpEM*=@#o8;>{$Zd63cnfAsTM79&I`?tkASDZeqZpep?ftI9-aX|g z)W+J#cf`088VEy%8-qHt8rvCWIy~f{s>&zrt0xOmrC}H%Kp4hN?U`qmCS= z3UZ`5AxA$EJ(!!_jM^9^jr8t^2qi|ia>9x9#6>vWRzT=F+-FeTv#-?U?KLPJu0?oS zul8!}N>p7D>ynyQac{!iZLxoylNBLa*5bT|i)TL~b!mle%j979d(TTppv7Gbzkh{U zb29jLFh3tBi5yCOhdqC2!$ld45YdLiA65ii>=Ej`MGKRK z-=&#>WN^CdbChqzKxI;uW~%eQn|msk&)&(94t`$8JIvb$yHLi3abE8qf}GmE>(>5M z8E#%oc=%gQ6!S5!S0|59SD#kUq3RN~(&$aK?G9sKU>z#J9>{<-%vixU^@ZYewT9H$ z9Y*3e^H;{430`s1v5CX&^_BH)(kdYhyDCm`a^H>jWL0roXcQ;ZS4Bylsez(mGZQ{l z{4Gv@7WByu95C9n`vPGDp@6*5__|}$bocTo}LJ7E$?O>y` z1U6yvA+b2nQR*EQD0+1UQjaO5t&92w|L7%p5WS zhAU)I@L|I1HoZ6SU^?ChBF8#iF=NRrb#;N1kFG7dQODY~z8M`*_f&4ss0ZfTLF?8m zt!$(b_e{)-MVcPCr$f~489XntqgFtuNCFW*%IkLkWu_;KSa$@ zAcPuUSMbayvSBJV={Jh8q}QNebAQ|~?XZJXfzDKw5}xi3s=b&{r?m8T&veTQV8HwC zD;vR~HOIS{C-;2z)wuDM&$+N|S6q_(cPPszFS2Oha6opL_Zv)3X_+V}ZxHb7%8kJI zi-+=}mYs1WA_ODKXlzRQXDr$g_7JvbdnH2x#|~yKvxBPG-H!R+0MDBKtta;~`L6qF z7i^SCZM=Op+)Er?3l|&@IT=yRJM#Zm1+<_|DkxeGLe*0C)?|1e2GPdeRJwyAQIT{Q zk|`5Q=$9^BX1AV*PvlpHy*TaC(u9LK_MhbwntAOf=A8|X&&}UoFAtdV&n=8d@Cr5p z0j4?f!Prp22FUkCS!{wP3JSSGwsgQnr((}#!E_pge5z+*emdL@w6i z!;CX@>1Ud?&L@-hs*1!ld-YUzn`m$((eYGBziurJC6JKtNDGuI}jF5T%1xj#+2kIAvvu7f>R49d10LCAKmD6 zgIwm*u8{@Bj-*6kpBj)AKedRNGEtgH!XSCQ0)$Jk(1DKX5ivk{yv~l>8Q2BFU6D9n zXE-jPq_$S*SOiVgT&fZj(4dE*)ug|7rPzU~?)GOUe~+vG~xS$mddc4#;lrb7Gg08)K*hL)Q34;=FO?vv* zktROH_`Jo}j3hT`Y-PoXM|4NF@9(t8N+?l$hHluh4quN(iH@40a`RT&%d<-r%QX9% z&aTZc5fUudnmgP*mEYNU*zI3~J34?5_UI`D4_5#>gG;J}(0u$2WmS(ZqFaQ5P>sjdgj;gtnslJL=`@++K7^6kR0(rNe#15^~kp zu*zQQ2l|vko>m4KXl_qUp*pbiG7vK&Qh*8vNp2%xV&?@Ps_Q<_9V~Nz>eWhpEFKdR z{azn~E@z)?2fJnhwTngI*mWR4Kmei5B`7EZy0ixTt`+q{r3fUr$W5-3lXRS(jEIPp z84v+Mk$a34EY-cB5nd&3TV5LcU5=jbF&p;yT`kolKP!=34;jd^A<@BA(faD!oIP44 zR$EcZf{DAgm!G@I>d>d1^)v^sVloKtvC*qCW}@6$G@*1E5k5>247 z1}H2Pivd+&CwhXIEQ=Ck@s^+)84gczDT#K-ErJ3kFLrdiuUP01%CmysYAo<)`I zpDpfketXrf)l(nSvH09rmedB}SRC87q`2kva@ zN8LaJ91woDte|ZJXK1yG?x?igml+94J`w;cDo;YiTRjbH>_PC^OG#cH`;mNJY9Jyi zOdI>;?&B1v2KiX0Y(~{ieY-ku2uQU+QzP&2t;9ZdVKRc^AJ>8(_XA6Rv-$eID_04F zdt5oSrAoSS;P2?oI+*t{Phk_CH?l>Ck*wPlw;yW}zC0Zo5bbU#Y|SiGzio;DvB-Wh zcI*Sdo<<5kVv?LFk{(6Y-ZGupbQHd*?ltx-hw4BNB?aK+s?sUT9oX=p!HD&6Q({vQW97e^-B5| zEDu2geqW>Xt-G(gVfBYoHLNS}HJK1O00Jcj5N1>kO&q znNSV%D?*D{DJUGazO8v82|G)lBtT508uS?xjnOFg#C%_kYhob$3vh<= z5_rry5oGoqSbYZ{3w6(}(NihvGI4%ZdM+GZ7z-mZRSQc}!v=)u$*h5!v(r&nwwmSa zjYfvKxc^zS=-$_!6&Ze}?u57OzJmu4DOJpFC~wpR=z<()5C9nj#=p-I3x)XF6 zOAbU&eEy(;LV!~ArBH$kkU3>Y3TiSTUep9$N0{u;-{hu0i`SJT76^*`=C6da0>Us? z6iMKL&|?T>f-)IpB&3B5n9r$?ukZf$&w;Mb4mV=Ap=?DKyn%QvjD%;UwuAy z_CfQg`DO=it~r`kd?vV5Lx9OL@&+rmg}vnW*DrAYhXHwK;W;6P(%hkKEd;T8Y($-c zK~9{aYZMsI$L~}La5nh4T~tbc?`52-F6_Eh~SG?`Ol}G3!YA#vo#v=O)b8K_uyesjMQger&CnvU~S>I zhCigjgcdMhstk-z8n)AXEu?W*rzeu0wS)6vwGO55R5@ef5WoULAt(w80y0Gf08$Y^ z4XG&+ASoh?2>ctZyEc7I(8W-~>M{>u*U-BgBHdLC^ucZe9rn)l1~poN4TvBoLQv~ zTmtxL%}lxbH(wLgd^*vSJv3PoAL^m-Q`Xx`C>ig4b(6g9VIW@>P_A8mjB@Bwc?s^} zelNwbqpj3_1=#)-F1!X9{$AG$c`c>QJbLxCDP>R_hoG!ED+G!O*2mD`Tif+OC_BY+ zfS)%dL`$lgs-lpk1f5UPqK;em%!#g;`N|ta@?FNhMsTy|FIxmNS({4*_L#uBo?&{g z@Uq$V5M9M>9xfeQu!ipzku~T%lkzjQSP{}f_^dpF_svS|7R~D3xl_A}#@BS}dDGI^ z>n#Op+!F(?9=ZqmMkB#(Yf`w%^0e8^T{oDC3g|M`gA;+VV#cbcO45fqO7$uMt2N5& zGigR8sLF(bg{zxXc9%AgjOi!8@9DU7pl$U1HIq(kz{}Rq=^B(G-9MHEYgT^6CbUyN zC<%N)!L3FZ28p`WK!}Mt$WRGDlpqQJO(!LQbgxt{)f>)rE~rf6UnJ{FHQ9oo(ZOOD zmkQS*?^c7CV7~J7FE>2;Yl;o|eyc-h%WZJB{w}UHLK;fx4oeZ?hA03n&M`d|&CfDO zl7c5&tAvm+Iwa9Ke;$s*JB%mkosOx|$#+WAX*-Rea_xEUX`0!5qmx|AFI(QJ-a1AkA{d+DvJSRAhAGHRtph)zMb7Q^e=|LkM^CDX6<=?!+*Z% z;o4h;K#GE?O!VW?#ib;Gq5e{$ilCdwQ{kN0)I^Y?wBLQ^A4T8hP->H69PCoFR+wOQ z-3Lqv)FbE7NkR|&7*8dl)z@e2SH`nF0+gXeBim$F2c%l=sG$SdW#Cxy8&sF?DdKEh zV1XH-Pn(OvH-?8IKC#$=P~H^amP|=G+H!H!OvaMuDSVT$l~Ttmphgl31P=NgFvcp9 zP;)A+ZmTjX4KX|$JP%4m4cA}EO%NfPucVq?d2fB36Nq>#Pr0uT^Su8v8(v0rcr{l-m}nEjN4bM` zeKjRee(oPjdbaq1zth0Oz5Q=dV4KHw;#(gc@ieXU^4MT?2Ub!R zpFY{G6tQ&uPU5;6muAEBW|gULcWgT+0^XizPUa3Tl~=>ChJ*z<(A?RiH*<6r)BRe@ zGp8P$K-pAa*+lcM{95L6q&_Qd)bM_Kjt_^mPDZq-4WMqA@*%YX)c*a(@mVWJk7Zo7 zz-w&`$9j@KD#ZLNb$mh>mZBX#Tz_h?J2jZX(-o5Z5fmZXBMAp{qRDMc6dO@E`}(_HoUgfjx7bYv zOubK))ka@cG>>^YlfL7ozV~FI^}83-nl)z3lI>=xWL#7Z|C8!x13$+3)v)*5uSxo< zrr9k0#i@4S=zHqCi3m2jF?F$*&tVjieMbfq+iS7u(eR+NlXvSeGF3!7nu;F2BobTGon;im1o-g?)#Qqz&bM~{!@HMZF_{K)9~+i209^b2;q=VrE6 z%q{yP;n=p$>zs>)E?_rHd(BP$-)mgqMLSR-O+IrSe{0{izQa$pb&oqkS$erMsMt`W zv}O3ymG}&bk(1Qq*1f=D<)IzsZPrvxlP+{aH(&PT;u!Yp@b6LPXYXoGi)-I{Y1+w) zm&%(=$$6}S8i?|rMaLZ{RuY6*7E1joDN_`hhH2T{CWZm<{d4?(p2ekBHlhJjM3ofV zQc*#Oqd3ngn-(bjM!B^bvppVur^9iAreYmh5|4@2QSG##RaxcwbQAOVn+6w6Zn3X* zf$mq&bN{EWdPCcH)ONnI*3APPWI7vH5-Fe0RnU3e9UQ|>p`l6Kp@P*e1?3?!RV1v1 zuiIPVFTH8`aUXfNUM*$op#xEdM|EYsxLnVf)Zu*DC=o^~q01c>;{}yaK3A<_N^UZ< z-1Zk^nK-tjFJl&PK72Mv6Bn3QlY;Yoz#>9a$*l9@ZN?zFLo>-I7slt&FMak0oz1G_ zVg;^(Ue>p_K7c*PUO!bskxv;{VP*@X=H)vq>FPwE@9sY9S>dwGp7xWmYvV zlFpyD$iTP2AeYpThO;}b%|LqQjac}PKT|Qb-Kf=A%2SfWD?{E}+jFxczVc;6rzKjG z5aDz}9>DL+0lq=USJg7J5)V0#O6+-sc` zh}*e#cGn7$(QNCop_bDbOC0#<7zxY#-Ui?QvwmevT^C+3MM&XDak{6dWRu@HNT+cQ zxB3WovtFAvA?*+ekdd#j!qZ7AB?$NMpqs4V$^9Yh3JyuUgkk2{(6U~xu$+0kg84M5 zw>n!YxUky5(KLN#1`~^n)iGu|FRvM9_GpJ3Xk451r7-P(%}=r2M1GH*U@7LdkWD!5 zpj#+R+D_ESS&otN?X{}`>PY`Wgf#S!7TnMaWT}{?2$A`jtZAXHirke6-VwGuqMehk zGQ}o>6f^Z}W^o~Y?eWcFaw&X11@zLbhI=#_$t)0DTXpC5&_}$Z|w44eupD)+4g%MAiTzx4nEQ z6?oWS7^9#~C7$6?NC-0>B8ShAqjL+?Yp+$NrrPRyV$IsR0WvtVFuT{&C?&MG4aF=p z(@}u*Z|&FxH>jvsE}XGDjv2l}Z?%+R^I7SZ1uImzi%Bvm5V)hOTP3Y3jYJuBjfw*M z46uPPi~L)Vn}!_hhdN`ri6$xyTT;3ZJ~gRvZBob~ zk{3mMoC0`|`$irIUlzYlcID8#D=VO)>aOT{tTEL^Bv0cr^K3RD56C}wS1ZWPbf$_0 zuXLYq-;&JDDw=U%zd5tFj^{OV^VP>dL{!GKr|Gue?(~N5na1YLC_K8ZvlJ?SQ9epb z%}9Ee0QgLns7(;dIi6?ls=llJ)fI2$p<36_+^inTPTkjuJk)C8zFr+T@lfp1=Jr3!&(%{r)jQ^!eA=%^hp0&9hV9 z&{OC--3NeaQeh8>pC$8BWeo50eJ%MnR{e0;9#PY)q0QN_L^94DTBa^xfgdRZiAX|` zf{dUc;MzV`h!K6PuokXRhu~83Tjq);@j@T{tQSFD)Fp z@NRZ_E?V;Sm&?)IK#bHvC%y@t!F;^G7eSvdM>oCn67KptH1@jED%tSVA!* z2Rc@T&lUJq&PnsBV`O^~=gq02a@K9#M?-ks?=il1sw!;+ zy%hKsjZ{l9Ddm_~fhE~H~8e}6(?K|IezP%c-0tofS-xk)Pe7c705C?2LqKy&; z0xKYqxIUDiw{0kNx|V3T08Gq?FUjdWdd{4dga1hle%;O4sweK6sLWmu;CH3KMnH(4iD|?ql2lQjiinyR}Vvf8~2cn9A zs(iBwKtxmSh-B=I^)I_r(jdl1|1r-XdwYTH_NopUQ9EXe{8eBaIFbB{ueq(O7 z2nLMrI=wbun8;-Mgc%AN?M$T(`1F%>+scc2aR3=10(c~MWCkJXIGd(;>-!#Lx|7B; zJ(`R%FPOd0qSach&rXU11Lx*KwoXQ`Q(>Sgq}$fAz?vr~;N29HB*j4ohT3PpB)O}Z0$WJcc#9iErR%U!Q@ILv*G{2WU%ZqXe? z-M#;-UoeMa!*N4*q5B%KbDec)q*H7 zw`1jBEV2#RT^$MZo%r6meeHdRrKHcLpQ_KDV>Erv| zPgU5=YcF`3>vZDsJXlxpA_PT?{b%jcEgU$A2Nlge()a@1(jo-+Qe)6EA=~)3AQUY> zq7--?{T$6zGgEtvgw8Vh<}^%cj3!;{CdVekVap?$%eY=c0ehV8&)IUhl5yJdXXbk8 zNCHqf3cOIZ9kf3c`EO6@g=>2ue zfBI)j;X}q?YR*lXc^~eS#x?c-x@r8sr@T`8FDEagU@#G&g^ph#)dKtsB8DgeN|im- z^Q0(xWn_=L^r8@be_0?wOL@r;ARq|{I<}KO>>c9u!>N=f+Lt0=EQmgFZmdQ}UN=*R zSim&Bt=~oBGZY@E1fTdaf`k=I>b7|trxL%gR}jBmqsFyXYTXJYQ2Un3oX+KEr2B+~ zLS73%%tOdxgK_nIO?^rv?WU8cUnETX4 zeT0u}8%51xvj4ehNj4TiCN;Jh^(3HQr6X1#;HL9C(1#_Kq6Eq*KyGZ7DJc0|>?}Ln1F%OgH#w8Se33?sZOuHXkf|@GGJib zMzChG>LfH6V@y1+DZ~BzAJn1lxPimm%53P9iNjH%!ab@#i7sx(wUGD@jC72d0qCWae0;1|`bb0_2TXxxdLtry5V+-~wN*MX&!)DH*; z?^vJ&NM1h`mEsh5Aw&Q>-jf7YTqF_tyD>EkwgocO|Axr8GsDV1Yv!?*IDyngm#0PJF$@`ACoKM zdH>JK=Q%6&%abr{FT+hi6rea1paw@tuE^a4pv^HYsSSF}J-;rRa&9;Qzu^=e42_j+ z+nzTvO7@m*F-LvX^D3s`RLoh5f-)$Nr1g*(R)pFQ8?%k_T@8RLV$`TG80FKlm?^3m zu5P}wP5r7^?4lORc;0kiK>kPO@}2I~-HkL7jrn153Sv+n8xms>I-)6m{x0N-aG@YH z4SzgBT4*^jL0KkKSw9EBumC4*fB*mg|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0 z|Nr1SKQ-@nRcI?V(_c?s^)*b`o`rgOZ(YmX+dbI5Z!S)BdGCAK-tcw2&tOD1ZeES= zTj?mTL6IkN2X|TkU4Q@p0B8UQ5w@Py-*KMhK0fQdeLZN>xv72JZkpMZ(%7XN-O5rN zrSWr*5pWlL_j}lAzT4L0f%2XATKjdf*}dEAx#;b?*y`)uutbOm82|tXX^EyrOa#CH znrOmg(*T-aMA#D}BTS4;CYXkX04A9<1||VAX_2Nt#Kd4DCPOBgU`z!FgbAZSXeLHX zgCV5JnkEKAAR0{2(qIOfGy-X)@@a_ClSU+CQwfzlqbWa3ny2cUGHFjH(A4!Bo|{zl zHmB-%r1dYi$kQROl{ zCZ0?v@l7_SAo8B0YHd$XQaw!uN2#=c)6{9|Hlt0bJwO5KX`lh=9;cEtdV^EY5dK>qNOo8PC z(g&z|f$9c;8Z>AFKmY&+fu=wKrjZ2D1YjmcfS4wYF*GqTG&IvDkj9Lf6!gL}VH#;M zlS3fVdYWL9VKl`zpqPn_2+A~VG-`THl=U)XdL~B7Gf9)wVHz}MrFI*vsSAq!uMyId zA~6vn6bMQiz=IFg_3h;R526FYlm_jrhhoK=u7s5zZ&^%kH`|!dd=F6q!6*kcPWxzx zkCaWrh=IH6us{q#N6r>0z&d8t?XmhpRtVyYYO!m0 zzSB6L8s4(^RT^e04N&iy!p979G7s8=LAylYP6e|uOnrH(6tX%tA=_>wB zHyf(aU$>&qXPtWr3&yv(egl`ai*eVlmfZyLzqS?e;Dk5ju5Cvi(;7U7t7It{U6PH9 zJSg&@BYdA1x7Sy<@qixcS?gyiLuqkmGy@O@S9&3jzLNvHq@HRiuv#*^w=a5=dKC&- z&Uam(COScc=iyj{2ta9K9h#|RKsO|AJI^1e#m0y+_eg*R;=~SfJ4+pwJLCQ(jA z7N-$GY0EG!q_m%DnY=tSUA)tgY&Xu*NVPm7uC{Z>1WJMhQACY%3sKS2WTX|KGpgaO zHljcV!Iv?3A>(AoXbHq1AW$D@>dn2rm!`!MiS?;Wy`fxEIQ1vObik%fYBkE3jKna! zY?#nsoF+&SW8!=)79gA+6z-7_4P=(P(-g%#SZWYe?_{8a7qPdF#~lv5B2}_$}}0U^*eLx{jT?BVC7l zHj!RAv7QK$MOhZDEJi6Lu&d7lS8xD8{Njj_s^(INfxrOEr_|V3YC8-)ZQcYfJX!Kg z;1nvcWmY}^!wRqwV@ZV*&58O1n$ciF8aRw*Eo(MdkXP8HV=X98f(XD)={D-*SME6w zplWgwA z`@EAXtdImtE#$KYhADiuMNk5fQjsR={kI*yqWK*Io&%SKB=7>~h)65|0UD2x5$I}w zVb`Tj=CMLUP&K{K)3M0C+Rko}9IX5U+0HFqs7}_l7us2lMf>OtWExoA&)ASfU-^T9 zdLb+{|D(PiD~6FNr>lx#MW1pCIQNtlY^M00vI-*`RN-@;Id20u%8pGb$J`Z_e1#vI z8z4`N2g@2cMG(O1hCiZTIRiF2_71WxX4da6>|Ks6Ip~Y`oHK|L(3kyyULkV|HT3>1 zo!sjRst7IswXDNoD5M1`>>tCgL_jam3M=6TZ@IMgQqEU8NCFOiU~pCMY_yyT`aTX4 zlYkf42xm`c4O%N-^cC7!t5d_aRIZ#WG`Gbl2wEgTXcQD1j^?{}J6#rcxs@%=a$V5O z<`{P_C>kkIrSLe`h!6(6hzqKTPo&A%>@?)1&k^gQZNyewQ@z{qxqZCd0&D+B0!PPL z4!|MzC0xW%HZag3551UyDil21(9dF2`TVFa-Xsb4Uysg+M{vM{u_CO4gYY;nON5!E zM0sZ_jX?@fmKRAZ@>MI##lBLMd)Ii_(l?<&EwuITUXQPlZwB5Ls@(NdjvCao+Bl+1 zg-HR&)IKK!Tjw(v6xdSoK44w!s1QWEweWAcoAlq`x2cN+lu6nC_cHntR)mD~SVD@G zwVHuNCgTPUxa^7wdfi$3P6AXe=Dz5!TTr9Cs0nMZ?wZ;}&}b~AOP+ov8halvQ-*YdFF`0i|A zVwHrYkem|af|Q~y#EP;Rx&C`@A(#0``g|VzxfY;cNx)ka9|1^4mzkJ>`tA5~SLx>K#U}0Rcy)rM zZEv6 zN`+Qm+leq1(gY#XW3iNc^Mde(3~)e5>1QeEDbVlmXJ+|dI?nrPx~XA*CcdpEAV5WR zLTfQsTh>hy)rjPrd29hl@U$9G;3~12|u9_~+yD^S73+bFk|i4xMMa zedf8VT7x7Y{A8lhQJH>-KeDT}j4~#Wxt#%W+B^DfXQCAUDd(Vfz|fsXQ9>3q3_7^= zoodDv0x%XzHFd4u;v`C@FxpHQBM|Q1qP7XhrmByR&Vv0A@{Z}U>EF=ejYWb7rX?X# zz$r8H6-O0AG$BGLAku|FjU*r-j}XZKnkXjZ6jGp&NFbUMwcje< zX+*dp6JoZcBM2mv1{SV+a=~^(%mMWemON7QvZC(FYfLR-qofc}5+ju;I7X<1lu@x% zbmfHrV#s2GN&*TYB$Js5Ljg-gP&R}C0}*Htib+VPMne#QNhnAFoQ#D=UMk=eQ4oa$ zkbsoh6|_|(jdB7Xgy58$&;aBWAt*^BDxp*)5&A?>=13 z@$2Jep0V?^j?LCXosmh)dEj0G4tt)=Z>_P>K^R$;tc9>-CmLd^HymZNZ0=BX-JUzl z$mPF{2d3_^&ZnK+!)YH(jSbD_O>zXa;bu?U5Ced9_dZWQELV#vwZnxh8a(W56{G`z zbXPFC6n7dPvmD_H>t0rX%TI^{7?m!9zUqcGA!BSvX?BwU{t_V&u*+4Uw8^w_`^)g%lxxz+@7Lh)E=bL?sXsh=S=!dshnrh)e4zH!!f*ov16JF#t(K z0Dwv(LREhICVWZ31-0U>ZqFKa&&8Xh@D|>Uy=0L>)}jHk%cDi(I@vk%=$wl#$=*U; zH`vX`lcL10{Qps-ryO4^WIK-9v9o2WCfIg+odjd3@9v*xrSXQdO(J6Y5Xo=kz~gre z5^?r$5N6}wtn`vBJP|n|@u)>aia0am*S?KNSmT?AG!tCcrjE@oDnO#>kK%<%I_0;mxL3`s zbsHP(%x>~OJ*HjuD|c#kVnoqJW~b1-v3UY{uow($wCnLmH`t1{npO7V+i(ZEXKk-L zQW?I?g(t{)i18D9VE@&=v76uHu)@o>?hjne(N%LLc3p2t z+PXejwuh3z3636NpyyV^tTjC;FMq#Lvo?feV3rNQeZU_gCW+qA|<jE>s+(HQM2F0!^YA_ zzH#;u@ywqm14j^MHFfB=-X-ab5@%6?yO`(jgzlrTAV4k5#3i_rAV!m{OyPqbkPeOg zTWj{o;+%LKjO|)b>mW;{FQ{R;KXkjFXqD*`M-CsA)#Qgg{0#jYuY~BP`^f3erw@9= zi<=gXA@?V4X{Ya5i!ZIlI6rfQk5x-vOcvYZO54N z=SiU{9(@ad?0zM!F+y;04Yq_O#}vm9`q0CiZxqQx9b5}421XqUau_KcMv5lZ;~q&4 z;Y}Fhu0ZwvwjYGfUfWE9&p@4)UY{%tgVkQ53jk-T5{g2UVeCUxq5x`@N+3X%&MF`n zWe^6EBm8JgYG09OWnQQal;|H7AG(xU5UX}6)@G6t`;&+t4Tgpwa{%b- z#V4{UB7U7j7IsH&@DDimNLE9d!?mE~*xEF*#&u4?#K{JkS40#7}Qqel4!@;2xAy*Ybo@?`GYs`cwL;@h5c&jrMERtr# z!`F~PT!}JB7Pk4rFr{}UvuXkW97hI$SOqw?a*eibgFZ)}A1wFV;T6m08q$V&tPsP% z#Xe-{MXP}DM#ac`X_%_5+ihjy&Q2Rv*r7{=Ow+gHYUVl(=IyW@!me=|0{rl!YsyOu z`Ic$?NlM-z{|616t5#%oZ-)+8Bqkya+f zY$aZO{R|5DN~{WMrMNazCQ~@-qNPo%!+_VcAp<``p^d5`905V%0tA#_-o<|P-i-Li zWKFUNG3d`?VOAUGn^y30&^X3BSR!bmg1Y$VWeiFphG-0srWqOKB@z}`qr=Or!%Mpr zk_0j;v#Kk~5E2Y4BpH=rB2keFB$ZT#MpsD^S;v?{#IL76b^EUL`{719wzf*-c4Jf&e)wO1wK!n?!BUUQh`5Lq1VZCg8S znyggdOsv;NC~KCWg(8HkVgN>f!w~Sw02ISKX>N^ose+GLsfMDuT;&qHekO)fr^-q4 zTbZlY`nyiRbQz5vtFG?;#-<_>9<_s|sf7nxKmZ9+AweZ+3~&adVp43I@Y=r%8y>2( zi@Y_odq@O$^16#eK~t(XS{?S4DWYcipEFr|)+!=pkz6mL#coylmYJ+^*cV2u$iuLB z0WI>gX+G>6n%A`D*!#6uYo|n6-5Z@Y>}q0_&!*SIL@MT)bV!JROtWuQ1J~Khy<6w( z?^@kDw_KgeOXerT7j_#qSOehc6ku>x_tXdwD9Izu;jHXD6i4=a zH(B%4UXXxi5P|d1Uvihc;6~f+ZU%L>228EBUc1kX0I>45TE^NkFDwC{j^hyp|0Cw@ zWxv)KE)L%2l1^iGJVwVs3Q~yx4QW)LR1UkdhI_g)Eo!#g&3Nbv2t)(}ttdP~gO_@! zA&L1J!iIKse@L3W7hUU#SoP!{b&ydcY#ezzfmgto%p+^Fky%zlrhxDZxww2WTff{L7GuKD!`i2793h5>y!ck4`RHF-dT>kHa13^FD-W& zv7b-u(%rFd1C)Oa#JpFqmz9ByNGd7(e;<<%@-Iu<8s%iw%$K07Qj$=4TVLN*W~15! zNFIe)ze#f1Qk?Rz3Ye$f817Jpt|X~j?U5O&7os#~NccvNF$d6CayHnT1iVd+)LlGg z6(cv%))Fbyu@C@83_@ZznB=CS4660dGNkCPGwEdkw~8bpR@GMrn-qOPyw1J9=3Mfu z?B|XIN%PiI91N_DS9Y2-yXkp&xm>`V@jyWch(QE`gpf%f5KxpPAs`X}B?%;L_sZ=daUlhZ$oY;(1Hu+Ygui{RFDBAiV6YMLXNh9fPhqh z37~!2Yz@FG07P5b{9}iq0iT$6;&V5gbbnt7xyV+#MDPFwDFN*EIG%tNhEu#5j6Xa+ z54TK8^V>Bd zYe*Qpi#a{R*R;=Erc%C6AhkDhVpujU($1x2)fvwfm|c>i!;F(Fx9uf2&jm|?sCF)1 z`wl(YsftC2WKJa4>j6we6H+>$OL+|w5Fg28jNtc(e7A9au1g-uB#)zr7VcFxqDyr-1G5lszdiH zwsALjM>k<%5uQ7hRSoil;O8I*9?DarCd@*tsiROHxHbJR9_N(`0V@**QvyXO1!Z3( zZ_`%h`?=%+y=o7!pLNj2ja2Z}*JX&H0gPqsXzn9!=>g4+CpJhIROdfH9qal+Au<4r z+vl~0bPvIUaF!OoP>YJMm-%U{+3a#yenY<4=E}(ABCDztL!RDS#B@CbQ|!$v+nvRh zhETv?RLQC+O<{qtQwA$+Q z(`wAdi1CMu@z~cJUwWz-F_>DAkhRv|$yT%9DzL9KLXQd`A5(etMF4a~oVQ)f$^gM1 zd#yAEyb4HzLaGPnLv4@@zWfNtGkt)pQd1Bb9{JL~soqWJBcXdjjxdm2eWn3NX&}Cf z2F8xVF^yE)qtc3v-ve4p zVdnbSb|t#}G|CdXPpqJUR3Zi}zFA$#V5=#KQuR%XrF8aV?H+i-?*l$lH$qNxMx({i zcunLguYLmp1$w3z$a?1`sBQh zjkjBf^Tfs0ZmVG;m&X~oHPFiU%$jeatB#%|tg1bCIWtSYZTwV|R)0074s>U3}p_mHUso|B4+6*gz_m}`>0bZ)5&i1zI} z$>1w@OFSgQh6eum?)VnOq6%9tKzY|Fjvz3!zU2@%ey)uA=SinWVkQVVY9C>Gz8_M4 zpzh!)A8xq!40~||$gy7A_CLp)*7n_V=)XTCJ6mhu?Z?YxW}u7SwI!4)uhM#_gWdOB<3%fCV)^h5Diyzpsj#C{tg{qDJeipF8;W7gQXvTr4~hj7y^-~l@+OMdwd>np@Fh*R`%qBhYxx7yK}_i$#QhyuuB5QMa1#MI8ws`G_c zl^r8-P1sXc6V0=@MmqpMMI8=hWywytexwZfn6gE|^2+K=4xe z7TMjM|HJVhpXc?RU<-W874N(RO74liF%aJOb$!q*6s}NLG1Y~;5wtfsm2o2>iZKir zxdYg-oLJvy3+B$CnYA~`NPiH3PlcP?ipc1w2v2}^AaEBX<}B34ijIC;9s?%^A2XJ@ zdk=bH0jA?oa_Mz``zMx|n1h_@4M+NhVVY6nJUu>}ZlNa^*o4@`JoGDK@Jp^&5#g-P z`dFO2u5j}H?WMIoV5X$=IN5u4LAu(Zyv*TG#?1wn+-0nD{Qn_4okyHxf|C$w#;h)CpXJRwPZb5Zo(}9AoOOE8_}K}EouLbjFdzg+hvE7xk<-b5auGcF zH`jC%FjY%^quf4cI%;pY=LmkWJC$ib_o)qt^7VCA`A$_fZ!?7|J|=4B$v&OJ`8Qo>89I@t6nFF|QS$n1R2%|> z$Jcouk3P(WDVkFl5?Df%8kJF)OI1lK#aI+7JbP>4T|Ld(V z!s#G2<ZnnV4lq}|ve6Bd6sn@tRYk~#iiH9anrnMT9(r#q_?0Pjl#)a< zn>e|UVJKFrs#0o!Spo=ko8@M1P^2hS7G_(DRRAcJFLb)F2!#nHZb6Ac?wM8ujKtjp z#)TLct=7|T?wHXb6Kz9rd3yy;l1b*E;6YLwuq)l-tA`HW7BZ2clxvRw2Gvo*Q+%ZF zB9fKY+3KDTus*>F@qnLyxJ_tr@}3lrl)=n8d-4j$4%4oO;eCf4dhn7+o>iLvleHg< z6Ikw|&aD+Dyg@|HT zAt?EVPL7A`ogO!kiC*4Z>fpJF#d>RO#O}Q0P3mfcUoH5<;;c_r~|eggOxn~yYz7dj;T@NB?(dt{*7tjV7sEYHb*?HCuRH!h{)J5T^? zR+W(O6o&GGw|wBlC!`Gp*evov6v=7Ud2UqHuaUo-%62Pku14A7I@(HEPO=Vlx4ln5 z`OuKlt338~pbmUy1iJz31S9kwXu=8F8(&|sPhEG5#V@SC2;+=YM0D0;wx=B#lBpC} z=Ma$qN{8nfx;_wo@P?%f7Lw`DR;^WALSI@9u4fhq-># zk-##EN%JQ&Wq-Rp2eMl|oRt9w6i57D>-=_k$qJK0t<`UxJZrY&>##<*{h&zw>=feP zv(Ckh3IYwL)r6$2YXIC%uu?B^S7N#(@-Ghg)CP$*`Rgrr_tr;l`!12|NN5y8_V8(@ z#&GIOGQX^USfa+539>+{<2VHbngdTe-X}vYJomh(c@q8UxqNM1JH#o$tYU)fogs&i7 zoTuhV4wTOk5O;**6|hO&%5wj026O+6?(U|elMr#qQ2EB}xBXAITBBdTa0lJH|3yALEFH5?n-gi`J36xt(-o(MUjHps zCfBKogD2OM-f}+HY*;+0CSVBgkBaToYSEUNPnikfqDP+@%!uM3L}CMvY&h}nk%${@ zDSVFch+dyT#r?eAH|UG14Hd)M_=1!KJ>N3f_Q)t7tU{&7|o7F<`_bxa2zE$ zd;{|O$F>^$Z4KP=pj4;Oym2qN)W>i0wlT<6EpNZ@Y3IV(MEW6m5nmkzsBC_jjvDaK z#5M9`QML73`v$58ACdO^Ks}M zZ)`Yluif0Qp;rMyi?*y~4f_Wshv9g+;d{n0=q~vyhI#ZS-jv}3D}Yo^l|a~WtQ4B1 zFF&7Iod8#_C%$UuNi5XD)YQM4+%!PVtNsdnR!i%*oY85!c)Mz`@3e9gx0NZ7!Qcwd zBD?oWt1qfHS00XN6DoI_i`9se6A4pr8W33LM_Sjgia* zK+@j39(i$o28bJ!x4mmI#jWJfYD$*t`T%bNS}dcke+-NPveLf-r=mjtsd+1V@=MCV zS!}$O=6h!0X_boZnA*7!J;xDpNfj`*@|h7echU*tGjNNo--f{Wz`k z-71q>A7^kGwJNAlZi(f~BYa*_geK};PY?TZk@w@hRS#~3c4-75UDf5GXF=3f%j&Td zSLc{sJjtF&oJB`IX7m~Mb@}<`xHvD~jO7z(^OBp}$sL+loa*d<$FxG;zkNB%>J-^R8{Y~xYG;i8F$+~xv5ZD+TP+t%$M14NH{asj(@Ht(X_m;UG z8kGKnTSs?tq;OUi9cbQKBczh}a%bdtqO!}6gT@B7mZi!kgNWGPyc`IH^QWgT2|$^x zY>067;P%OL>F=L%SuL?K(CfK;-fLY2Wj4`1lO;@SI|n()>Y<-NbR7p!#Kh&KFJ7%8Vve#*M@Yp3#&F5hDgJ=;|^vAd(1 zqdY!)T|6t!DP!{i!P(;WV=Fp#1{A;-|21+|lSJ1a1aBJBm+lw>~3J66*`T!5K;xc{BA zMn=;FYWr-gXJ^dY8a7$W4R^(n1<%`;2%9b zxr81{V-&x>)A@**m&w!6(cl~`+alr+sNILvu0N=?4j>#yq6=KEnq$qMrwJZ)&n~3e zx9{Oz(DVLho1x*PW2Uv7U^EshDN#`Tk*w{!r-di}r{~`{-v6E)EE^+{x@*eAaJ(x0 zx;`fsvlgUovr4!yPTp-aKCof3WLUPglz1>vxsg zK-5*7-^GzhMQbXV8i}L~bfn3l4ewvVV;g;FT+%~I3m~J#ChF0?)9IZecQHeuHsPS( z-rH}sm#>u>STYGCR?}4BciwV)c9A%Lff3kB;b9tp8;qw2KRju6)fS_oQr+@%OlJn0 zYK)3jijFtb`DNb})`fA&eZNb~K5z>Q4UFNq04*aphIVmBW%4b!%g&;utE43iMfc{* zr6PWv10MM0@Oz5FIq z_cc(HnB1t&yhE!OO;MJ!H}U6t)Hx#ngU{u&;Mv(+9=*&|2wX7=&;3=@K;<^OdhKA` zNFm1N=Uh>^O3Sjz5ht#ZdsF8lk3G+_$ZI^G*kv%3RV`7#ZdmqKvN8U3%f-Klk1(tD z@<&$V)Z{5XjJ_E`IqLSbOU7lGBmGR!I*cR+h4|O{!N$OxPzTr8h*&nP{!jjdFHRML zzk~N4dbEZ5qBFlgnJY)o!04c;yu0b6*Z>e4A|wQV+Qe!08AG&pp00vBn~ycO0(um?_`S(^D92=MEoA%w0i$7?_LKz+KdMqh7rXmY4Mum3v}M=U;S zNoN-4Jo3Agr(gp-DMq!wYE{cKK1y5^z_9=!3Mbu}^F!%N`#dfwSw%L>0XY!lnrnz; zeLXl*zo2tR7lzdO3(FgfIN7uL=mbiC@_P(pa5;=3cKc*-t8^Bqt$>?F0D;iz4;LNnJ$RPdYjtT`h zM6SBO6f#1d`T2TY%+sI0VDEC^d!_NG5NEKzSz6HFf&w>HySH*l0PLH-BE>5yFp4}7 z1)7nJ#%6$&E-V9IOFzfnpZ882P+(20kHM4T^PR3(Ifw1*ZV0JB=$26xF}0(YU-;wh zp1o1eLX*LE`-SpV08n+E^(t_30$o*BpdL_-xwSp(mM7|;|_1c_pScqFKq4- zt{`>gN7(10oy%p zP>seQgA9SQi3A4|j*UYkZ?Bd#c_KtD7yi!p1D4NCNClHVr}%XCzZpLC>gUvO)YN?? zV|qUQv~uv?96jx1Wz3cYaNjYeEuQWdOE0Z|a>-$#>f}47Xri+*(52XDR|8R747fGw zLpV(pvV>%&0E-?4MXs!2X_1u4!qwKg#{cs5%$T4Z901wTd0#^;u6l$8)&p_B7;(J= zfQ*cVW-tPyCk7%l>FRhV;Azg7F~%l78BWK)*5kVP$i!y3phIH(FECONcBRt^=4Hvo zKgGTB_lgoS3d|iw6Up4`q;BFeFP+4|Bw|Q#PB}R?3`#A@L4+MKhMP&=8WRoQ>aV2@ zX)~y>s~7~9tN^GJ2>e(gh*=|lG*IsaJC!Am9oc}EWayr-v?GrKhNqQ`d&Ev z5CU&-vS}uqnPW!Y9O(~tqD~}jG6C2?$9*F^MDn6HBqk-z&tu#>7^Func-3YE!0@_L z>QgSO$kj=oyVLZcUh}aotPBN{F{?^=F@!En4#;(G$qX%NCzU8rfRp~EI{~7dymXi= zI`;%`?K(R}@DXe^%s- zGL9aC2wV(0hZr3K2Nob`5T$A?{I^-1HgO&=KPjxPgZ+FcKo`$4N0IkLw+~ITthG1? zQ9xxMK!yP#x=esM?cKmHCrFaqM(Np9=IMh+GwaFO=<{e?=gCgTYQ1C~7d-S22;Bl| z7nr#G!Ash%Q(aKIC9;@~;=y8?oB*=SXkvg^=%>+M|6PEDAYx8EE&SpvDR6s5%o!~T zIo&-ulOsG#8UckPdjfbdN_fRpZX#0^BB>>kJ`G^>$^e-uX49ko6Bae>c5>zL9R*sdescwAjwnG^}TWgH1yCidC&(X<>M1Bcv|YO4ZF1 zNg#;fCznn7{g2hK)|YLvo&`6LV437Wl*w%+!Bi!-Qm@NtZd0I-)rXjV9l8N5y$OqjJi!k=o)dN-LZh!7x0 znX4IiF(yCM;KcPXHzzSRo#k$1GWg^;5`=80ewc_by@-zGF%Xaj`mc2~L2TY&EN7DsVUC2|$27bodT0+SJv{;a{RFjs6>?gc-}I zyHr=DL7rjKrHR&y+{Kr1BoWvN6od#t35YL_R{IlQs<&?hxy7MI)$L9qfCY*YSxWB? zg4^DQ5#p|*3QTm|Du_f&i8lNqma9efjf4z|Yb~pQRr?sLb)r>jsTu?>RWY+xq~|5u z5Mc@MFtCUgX?f===}CSTwbneSk+S50ofSg8If{lpvzV1u#c%`zW-f5V9ky zFhVG_98)3zPB`=9b5&WXto00MvL~P?LD08!t|-m<#vqDPP$r4p7GjOcLFin1?PZK@ zh&^-#Cq2nctI%pXce2icG2cCb$ZdW)mcx7h-WHyz*buA`lQocwbi^cW z*0}->Tp)Lv4pU|A>X&s@InokDB1MgS+Zp0p!)BK-N;OW^wr~;Ssd(Nxj9@V-zS2$0 z(wEc=5ElJx>sv8TTS`D{nh4|;y+i|NhIWC|X8>bl zgjtLR8xv(MT^@mgaSNj(g?94?-FJpkEdgC@s3Z|i>?#Eti=-P^!&R!8xW*Ut1^%Z~ zpATaO7IcUOSOgn!;!Fm~z*;PVBZ{rH7D(1sz@?7)QOv(9<~`5reQ5W?{Mzzfov)Zme5awa@$^* zHsG$v>N`}wjnIL`we%LBOq08tDaj*rdXr3ZA~|z!h^`+)50KE%gU#4QrT1Q~mb@5X zH1t_gqIy5QxGBup*)Z0`^3FdZH#rC*V)OHYLehJl90WUk(ax&)h6YG#!A^hzg+gps z8XHNRUH4m8nj_}Mj5A816zb{adE5vm-{%MW4ak%+zIUGlbO+6IUSK7e@}T766) zzh|TK*73cMqu9Gkg@%|ow()@O6*X2}sVPNR*WW`9?6SRKFe9F0Wx0?#>!~deuwY_q z=4n;yj4oE;jv5*K*WrlNC#o{smSrR&4R`Rs3jbBAILvD(5`hQ`dXrCB$mG=$7V}Pl z2rQcwLCzv+O9Lg=Y#1b(0+9hC1g#jEEYOKAI`f|Y4%q2MN{R)=Wj)9u+$|wMN+_{Wv8tKoJJ<)7ZBhYW@(^&NS8>-dD0^X_;3Tz zkxLCHOpWwd|9jHCrS)iC;!&_EukMVjQiMlo#Kb>3DW2TV7^s?}@P z?!w=s3I9(yG7$^@vVJ@`K>S-IE-vaf(4}SX2VPLw$h2oZRo3?@;t~sdd_TxmAxypW z=cmzw+_{GWWie1I1VkiWbD3U{6Ra{A{EXCkF3+;>TAR+4^f&Ju4jC!Dm{$(_2hC(M zKEoc<0OJ!ulOj^yKJPE-6e#cCz?AU!bK?0slpOxz0-lR@%d>df`@P$n9^Xq4Wk}A7 zwTc07Ww1u8+AvBfJ81YN9U!1p6lB`iY!-S-up|}Rq}$nC@pP(-=FYK~YZ#1RFSbVy zlPm>rhJ+E0Gzp8oPfwj7kUR|p;IrvyBa=gY%12f{o9!0_E|geFO1K=Ur@xNP|4IDv zUyMXa?JBARSj`xkt*t#|jNQ{ZSOU z-el(Gh@+J;b__IYpQb@eV#`h^JCjz>fxrY%a@iwefvqX)Z!aDeR!qs_W3V^4!h9#( zl_yw~T$%dx2rKEn=x8jct8HGy2}LPXV`8u)T7&J4RqC=3AUbjjC3u3ly-l5VuWL__ zUDyj-Hv}BFZ`$p>Dpm4Yp#qW|2pU6bni%lxvlMUTf&^Um)@FQ$*;c?aX@30Z!plcWzP1L_9 zfp3e0jQ%h^1Z)x~bshg{na+hHMb=Y?icokkD*@?z zyly*pkArW71sXRJ|55ljHdO0tNm18&m)M_*YWjT#rs*gu9sT{U=AELS0fv&jF`kMg zpyii4Ha0HSchMW9+x93R42ZXU%diDGzZcn6ZG{S*g+VpxrR=ueo4f|cNbG|gCkO!8 zLhrXz%9wzjfs#ZDN{=wAyoP9LlPGGu%uIdy?=-IRSA0}44%C44O8pIg>}0h?V%NY; zPc2k}N$cwV(5v7nHmLZ}@>S=`zu3_mZljW^2?01aSO^UPsQydXhx+=RcL@^@HwZi= zef;Y`uMWemc>+S@Rw!U7jB*Avl7=w>J<;TSGf-tWx)5kfrMU>kgpg^9GNB}@U_ygJ zNg=&DDGH2Gg)-GwQh6D(xXBDu2R`T(e&pxnIHo8EL>b3DC?muT`rfV~&XZoc8T_uj z3YKrIJ=ot#vQk^e*&E@_-3jYV@h1I^uS;>d-4qn1EfUeACG&CU>?Pq*&lmmh0-Wvf zLX7HrZt))SrH~@8ic4hha=AOP z2(WOKJbmsQTs=F8xRMG00d!x%B^_r~`K~*i`zC29PcW^h_r;KLnI7=Jh{aY49Hu@W z)QCtP>La(v7lxc>GAP2sh|bZ7cM>ia(BtM)8zI-mV{O`Ck!^8NjqT&fkUG+WBwd;G5^&UPiRJ-XoQ+yoN!}CGcfVjve%k}ze$d*<~n`+KKyz!Onj&Bvg5Nnlbbe% z0Gy~Y5Sq5WD?ke=Y}1fnG!2}|(WAeC08P}8F)quJFI3`~z>Y#$Z=ktCj*-s!fX=DR zwZwGLhYOaDLO$}Y#Aw@fY9z{i^%4p|KqRQG5m?J58|p-fkNt7qFP1SC3qaOfTZ8r;CIA3~{#3y^{YhWB-8S%ukl zK9vaop{E01IZkI2naSqOdn;dZ8pXr2XD;flh*4Fm6f({33N3@mD4J(phW9w#*o8T@ zrr~}Q&Z~IKIAM;COL-agfH@{;G`K=TnTT1V zdBd?QydHq;g2M(zT3JJhV2V|v8M7K4G!Jj&##fJW{hM6ev#F+B32ppRkQkd`;b;74 z!@I*zS$RibaX<;n!R6@>4i^~9t)G?9{Vva4{heqKe2X57zZkKBje4lj((%|Z5W+WY zlRl$TERnvmErN^I(?P5@5-6dHmcyFkgX|a8Qx~mwc240>+HXs}FLkj&N=$7E)fF>7vyQbgb(@`gbM@+s{|A+3*xlEx!Jvn36T!`1mQIvI6zEQqZZHqyou!grh*D$_dV;^%+J#0PIR@?vE>| z$(y7X!}rGP1N(p<@Giy=YrEaq<8I-^7djA7l)bu@aEaJ@Eez&zNKTgl0D$}mrTrDB zTGDN~6GS9A3v067USZ>L1db1t;f2?O&ri`y&K*O>cd<$nNP9r6HYdMW&e^(^mO{g( zvT9ZW0R$2=Vxo3`IBzU0g!=vTG{2{@)XJVqk!BT^!8x z@dWhX@cj_<`4JfX2?YraaavdFj5zL#Xv%HTR2~QE?+v5^_c>ZRKUuFIAHMCDn6m#VSBk9tSZb&9U@EF=D4#x3ia64$u@Wne6{HI|a9%S-c@U3I*wZF{~n`T6%Pu)_>6wAr(0FwACIWT2a$;gL#_`4g8wAA@($B4-N(%Llk&so1N@hR9S`Bh&-m6~ap4a!JwnnPs!#^J~A z1EgDZVi5db`Q}2OY}J9GzwK@LE^0k(q$VrF;#1T;+3O?6eHtfbOq0rDM*#8F_vreE zgq_GKHs7x@XTs}eK+p|T$dLdtaF9hiTCdzL4M1$Dg0$e1dgu8(3N9>PBMAR3ZTiq! zIQkt5Aj&u-1W-VNY{A}4ZU&SspW>Lxlzv}v8v3gmY}6aqKClml z6hkWIedUx8IyS%PYh1FvG`W%~!GmgmDxk&}aSfuNmt9zLh3!*&%>t*e9fduC)*Eu? z{qnr~NV|Fu`-%PEYv1&%2E0Y70DD+S7N@fd&g_DBb@Y2bd9PPu@bMuE`xHzXk(1C+v1rw(Niiz2(6^^*7Lf9rC$G;uo?~|qkc}45n^)4D`3Iy^giEP@zh1y^Fpyxr^rxEEhjZFs zx_Cj0wr- z4#D~VmapB78fW+*ov8QsZ{9w#es71J1Vr;n2l8ureyjrb+egN6LZUfOMKT|q_>z1- z-N3o2E4ilGx+^kwBpf!ZZQ#ub7#PxQmIuU!B?XNQIxhI-E(M}$x5}p5m1u4BN|1^Y z=QWOndg0~`N3B%+EodRq#3C4UO>O7}*)N3=))&^F-sjo89ab zq5|;@LU59?Em%HLvs7l=RQ(o7)W`ah1(W~{i>X@?wVu`VV5+XI{Qdixuf6+YiWh6lMJZHed1i`rZudA+i- zi=dCF0ZbC8dsMTw%(o8LOmi_7p+79FsWq^8#i}jP)SpCC4c#(p)SVd)%{}$7!;;}1 zCyBjx9$xKwPINBplM5FS?C0I}yda_o0{6{d1dbK0&)FJXF28XeNxG|Mm`k)^`a151 zQKo@hjmhpuno#x-f-9D6Gtg&9O6X<{tEn(MKFPF^Jc02T_e4by0H87daN~Yr2ld?9 zc|8oYbk#76V^QjC>?#*6kWK*Iz{W55Wh6M#;OqoH!OWkmmp1Po476am&5mw82=QMX zn2)3`b1Sj<_Z*&jQmC0MX?tk@vqqgl>JmGvZ{R-{?ACBHIZOhwTtH^Ejlw`0iT+ z`ap6UajUJZ>;j4T*}1h&S*8(oJ<$nLlN*54hX+iBNX)?HFECr!?>Au_0e)KdnO1d0 zdmq7x^4+t%KR=+lr5AJMf?L)nP_$7$aTTU^V4l%I>iSiwFpqsLuB7@K+()mta;3H+ z3V{3KKOLz0iKI$EV!NNO8nNQZ6xCL$<>e5muWs|sTan4Jw$i+XuAweb`j>sj%n-7I z#q*K**@cj60rlPeBq*A#rOSF(bK48+ZKQrStrnD$&Y8RLOb)8VEmhsYbf zPY-V6tH-)&^HeLFq9YFsIU{{7i8G0jfYj!g#YpJ4QG+DQd;kCGo7Udyz+oGLqQA}S?Z>T2pNUE UboM_(lt27k$rRy2K>Qy9z{TtP>;M1& literal 45875 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 diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py new file mode 100644 index 0000000000..fb29045cf4 --- /dev/null +++ b/worlds/pokemon_rb/client.py @@ -0,0 +1,277 @@ +import base64 +import logging +import time + +from NetUtils import ClientStatus +from worlds._bizhawk.client import BizHawkClient +from worlds._bizhawk import read, write, guarded_write + +from worlds.pokemon_rb.locations import location_data + +logger = logging.getLogger("Client") + +BANK_EXCHANGE_RATE = 100000000 + +DATA_LOCATIONS = { + "ItemIndex": (0x1A6E, 0x02), + "Deathlink": (0x00FD, 0x01), + "APItem": (0x00FF, 0x01), + "EventFlag": (0x1735, 0x140), + "Missable": (0x161A, 0x20), + "Hidden": (0x16DE, 0x0E), + "Rod": (0x1716, 0x01), + "DexSanityFlag": (0x1A71, 19), + "GameStatus": (0x1A84, 0x01), + "Money": (0x141F, 3), + "ResetCheck": (0x0100, 4), + # First and second Vermilion Gym trash can selection. Second is not used, so should always be 0. + # First should never be above 0x0F. This is just before Event Flags. + "CrashCheck1": (0x1731, 2), + # Unused, should always be 0. This is just before Missables flags. + "CrashCheck2": (0x1617, 1), + # Progressive keys, should never be above 10. Just before Dexsanity flags. + "CrashCheck3": (0x1A70, 1), + # Route 18 script value. Should never be above 2. Just before Hidden items flags. + "CrashCheck4": (0x16DD, 1), +} + +location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}} +location_bytes_bits = {} +for location in location_data: + if location.ram_address is not None: + if type(location.ram_address) == list: + location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address + location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit}, + {'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}] + else: + location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address + location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit} + +location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item" + and location.address is not None} + + +class PokemonRBClient(BizHawkClient): + system = ("GB", "SGB") + patch_suffix = (".apred", ".apblue") + game = "Pokemon Red and Blue" + + def __init__(self): + super().__init__() + self.auto_hints = set() + self.locations_array = None + self.disconnect_pending = False + self.set_deathlink = False + self.banking_command = None + self.game_state = False + self.last_death_link = 0 + + async def validate_rom(self, ctx): + game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")]) + game_name = game_name[0].decode("ascii") + if game_name in ("POKEMON RED\00", "POKEMON BLUE"): + ctx.game = self.game + ctx.items_handling = 0b001 + ctx.command_processor.commands["bank"] = cmd_bank + seed_name = await read(ctx.bizhawk_ctx, [(0xFFDB, 21, "ROM")]) + ctx.seed_name = seed_name[0].split(b"\0")[0].decode("ascii") + self.set_deathlink = False + self.banking_command = None + self.locations_array = None + self.disconnect_pending = False + return True + return False + + async def set_auth(self, ctx): + auth_name = await read(ctx.bizhawk_ctx, [(0xFFC6, 21, "ROM")]) + if auth_name[0] == bytes([0] * 21): + # rom was patched before rom names implemented, use player name + auth_name = await read(ctx.bizhawk_ctx, [(0xFFF0, 16, "ROM")]) + auth_name = auth_name[0].decode("ascii").split("\x00")[0] + else: + auth_name = base64.b64encode(auth_name[0]).decode() + ctx.auth = auth_name + + async def game_watcher(self, ctx): + if not ctx.server or not ctx.server.socket.open or ctx.server.socket.closed: + return + + data = await read(ctx.bizhawk_ctx, [(loc_data[0], loc_data[1], "WRAM") + for loc_data in DATA_LOCATIONS.values()]) + data = {data_set_name: data_name for data_set_name, data_name in zip(DATA_LOCATIONS.keys(), data)} + + if self.set_deathlink: + self.set_deathlink = False + await ctx.update_death_link(True) + + if self.disconnect_pending: + self.disconnect_pending = False + await ctx.disconnect() + + if data["GameStatus"][0] == 0 or data["ResetCheck"] == b'\xff\xff\xff\x7f': + # Do not handle anything before game save is loaded + self.game_state = False + return + elif (data["GameStatus"][0] not in (0x2A, 0xAC) + or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF + or data["CrashCheck2"][0] + or data["CrashCheck3"][0] > 10 + or data["CrashCheck4"][0] > 2): + # Should mean game crashed + logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.") + self.game_state = False + await ctx.disconnect() + return + self.game_state = True + + # SEND ITEMS TO CLIENT + + if data["APItem"][0] == 0: + item_index = int.from_bytes(data["ItemIndex"], "little") + if len(ctx.items_received) > item_index: + item_code = ctx.items_received[item_index].item - 172000000 + if item_code > 255: + item_code -= 256 + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["APItem"][0], + [item_code], "WRAM")]) + + # LOCATION CHECKS + + locations = set() + + for flag_type, loc_map in location_map.items(): + for flag, loc_id in loc_map.items(): + if flag_type == "list": + if (data["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << + location_bytes_bits[loc_id][0]['bit'] + and data["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << + location_bytes_bits[loc_id][1]['bit']): + locations.add(loc_id) + elif data[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']: + locations.add(loc_id) + + if locations != self.locations_array: + if locations: + self.locations_array = locations + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locations)}]) + + # AUTO HINTS + + hints = [] + if data["EventFlag"][280] & 16: + hints.append("Cerulean Bicycle Shop") + if data["EventFlag"][280] & 32: + hints.append("Route 2 Gate - Oak's Aide") + if data["EventFlag"][280] & 64: + hints.append("Route 11 Gate 2F - Oak's Aide") + if data["EventFlag"][280] & 128: + hints.append("Route 15 Gate 2F - Oak's Aide") + if data["EventFlag"][281] & 1: + hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2", + "Celadon Prize Corner - Item Prize 3"] + if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id[ + "Fossil - Choice B"] + not in ctx.checked_locations): + hints.append("Fossil - Choice B") + elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id[ + "Fossil - Choice A"] + not in ctx.checked_locations): + hints.append("Fossil - Choice A") + hints = [ + location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in self.auto_hints and + location_name_to_id[loc] in ctx.missing_locations and + location_name_to_id[loc] not in ctx.locations_checked + ] + if hints: + await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}]) + self.auto_hints.update(hints) + + # DEATHLINK + + if "DeathLink" in ctx.tags: + if data["Deathlink"][0] == 3: + await ctx.send_death(ctx.player_names[ctx.slot] + " is out of usable Pokémon! " + + ctx.player_names[ctx.slot] + " blacked out!") + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [0], "WRAM")]) + self.last_death_link = ctx.last_death_link + elif ctx.last_death_link > self.last_death_link: + self.last_death_link = ctx.last_death_link + await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [1], "WRAM")]) + + # BANK + + if self.banking_command: + original_money = data["Money"] + # Money is stored as binary-coded decimal. + money = int(original_money.hex()) + if self.banking_command > money: + logger.warning(f"You do not have ${self.banking_command} to deposit!") + elif (-self.banking_command * BANK_EXCHANGE_RATE) > ctx.stored_data[f"EnergyLink{ctx.team}"]: + logger.warning("Not enough money in the EnergyLink storage!") + else: + if self.banking_command + money > 999999: + self.banking_command = 999999 - money + money = str(money - self.banking_command).zfill(6) + money = [int(money[:2], 16), int(money[2:4], 16), int(money[4:], 16)] + money_written = await guarded_write(ctx.bizhawk_ctx, [(0x141F, money, "WRAM")], + [(0x141F, original_money, "WRAM")]) + if money_written: + if self.banking_command >= 0: + deposit = self.banking_command - int(self.banking_command / 4) + tax = self.banking_command - deposit + logger.info(f"Deposited ${deposit}, and charged a tax of ${tax}.") + self.banking_command = deposit + else: + logger.info(f"Withdrew ${-self.banking_command}.") + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations": + [{"operation": "add", "value": self.banking_command * BANK_EXCHANGE_RATE}, + {"operation": "max", "value": 0}], + }]) + self.banking_command = None + + # VICTORY + + if data["EventFlag"][280] & 1 and not ctx.finished_game: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + def on_package(self, ctx, cmd, args): + if cmd == 'Connected': + if 'death_link' in args['slot_data'] and args['slot_data']['death_link']: + self.set_deathlink = True + self.last_death_link = time.time() + ctx.set_notify(f"EnergyLink{ctx.team}") + elif cmd == 'RoomInfo': + if ctx.seed_name and ctx.seed_name != args["seed_name"]: + # CommonClient's on_package displays an error to the user in this case, but connection is not cancelled. + self.game_state = False + self.disconnect_pending = True + super().on_package(ctx, cmd, args) + + +def cmd_bank(self, cmd: str = "", amount: str = ""): + """Deposit or withdraw money with the server's EnergyLink storage. + /bank - check server balance. + /bank deposit # - deposit money. One quarter of the amount will be lost to taxation. + /bank withdraw # - withdraw money.""" + if self.ctx.game != "Pokemon Red and Blue": + logger.warning("This command can only be used while playing Pokémon Red and Blue") + return + if not cmd: + logger.info(f"Money available: {int(self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] / BANK_EXCHANGE_RATE)}") + return + elif (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state: + logger.info(f"Must be connected to server and in game.") + elif not amount: + logger.warning("You must specify an amount.") + elif cmd == "withdraw": + self.ctx.client_handler.banking_command = -int(amount) + elif cmd == "deposit": + if int(amount) < 4: + logger.warning("You must deposit at least $4, for tax purposes.") + return + self.ctx.client_handler.banking_command = int(amount) + else: + logger.warning(f"Invalid bank command {cmd}") + return diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 086ec347f3..b164d4b0fe 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -83,6 +83,9 @@ you until these have ended. ## Unique Local Commands -The following command is only available when using the PokemonClient to play with Archipelago. +You can use `/bank` commands to deposit and withdraw money from the server's EnergyLink storage. This can be accessed by +any players playing games that use the EnergyLink feature. -- `/gb` Check Gameboy Connection State +- `/bank` - check the amount of money available on the server. +- `/bank withdraw #` - withdraw money from the server. +- `/bank deposit #` - deposit money into the server. 25% of the amount will be lost to taxation. \ No newline at end of file diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 7ba9b3aa09..c9344959f6 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -11,7 +11,6 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst - Detailed installation instructions for BizHawk can be found at the above link. - Windows users must run the prereq installer first, which can also be found at the above link. - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases) - (select `Pokemon Client` during installation). - Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these. ## Optional Software @@ -71,28 +70,41 @@ And the following special characters (these each count as one character): ## Joining a MultiWorld Game -### Obtain your Pokémon patch file +### Generating and Patching a Game -When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, -the host will provide you with either a link to download your data file, or with a zip file containing everyone's data -files. Your data file should have a `.apred` or `.apblue` extension. +1. Create your settings file (YAML). +2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). +This will generate an output file for you. Your patch file will have a `.apred` or `.apblue` file extension. +3. Open `ArchipelagoLauncher.exe` +4. Select "Open Patch" on the left side and select your patch file. +5. If this is your first time patching, you will be prompted to locate your vanilla ROM. +6. A patched `.gb` file will be created in the same place as the patch file. +7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your +BizHawk install. -Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished -(this can take a while), the client and the emulator will be started automatically (if you associated the extension -to the emulator as recommended). +If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the +client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue +below using BizHawk as your emulator. ### Connect to the Multiserver -Once both the client and the emulator are started, you must connect them. Navigate to your Archipelago install folder, -then to `data/lua`, and drag+drop the `connector_pkmn_rb.lua` script onto the main EmuHawk window. (You could instead -open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `connector_pkmn_rb.lua` with the file -picker.) +By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just +in case you have to close and reopen a window mid-game for some reason. + +1. Pokémon Red and Blue use Archipelago's BizHawk Client. If the client isn't still open from when you patched your +game, you can re-open it from the launcher. +2. Ensure EmuHawk is running the patched ROM. +3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. +4. In the Lua Console window, go to `Script > Open Script…`. +5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. +6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk +Client window should indicate that it connected and recognized Pokémon Red/Blue. +7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the +top text field of the client and click Connect. To connect the client to the multiserver simply put `

    :` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
    : [password]`) -Now you are ready to start your adventure in Kanto. - ## Auto-Tracking Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking. @@ -102,4 +114,5 @@ Pokémon Red and Blue has a fully functional map tracker that supports auto-trac 3. Click on the "AP" symbol at the top. 4. Enter the AP address, slot name and password. -The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly. +The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It +will hide checks & adjust logic accordingly. diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 096ab8e0a1..81ab6648dd 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -539,6 +539,10 @@ def generate_output(self, output_directory: str): write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) data[0xFF00] = 2 # client compatibility version + rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + 'utf8')[:21] + rom_name.extend([0] * (21 - len(rom_name))) + write_bytes(data, rom_name, 0xFFC6) write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 97faf7bff2..cd57e317bd 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -12,101 +12,101 @@ rom_addresses = { "Player_Name": 0x4568, "Rival_Name": 0x4570, "Price_Master_Ball": 0x45c8, - "Title_Seed": 0x5f1b, - "Title_Slot_Name": 0x5f3b, - "PC_Item": 0x6309, - "PC_Item_Quantity": 0x630e, - "Fly_Location": 0x631c, - "Skip_Player_Name": 0x6335, - "Skip_Rival_Name": 0x6343, - "Pallet_Fly_Coords": 0x666e, - "Option_Old_Man": 0xcb0e, - "Option_Old_Man_Lying": 0xcb11, - "Option_Route3_Guard_A": 0xcb17, - "Option_Trashed_House_Guard_A": 0xcb20, - "Option_Trashed_House_Guard_B": 0xcb26, - "Option_Boulders": 0xcdb7, - "Option_Rock_Tunnel_Extra_Items": 0xcdc0, - "Wild_Route1": 0xd13b, - "Wild_Route2": 0xd151, - "Wild_Route22": 0xd167, - "Wild_ViridianForest": 0xd17d, - "Wild_Route3": 0xd193, - "Wild_MtMoon1F": 0xd1a9, - "Wild_MtMoonB1F": 0xd1bf, - "Wild_MtMoonB2F": 0xd1d5, - "Wild_Route4": 0xd1eb, - "Wild_Route24": 0xd201, - "Wild_Route25": 0xd217, - "Wild_Route9": 0xd22d, - "Wild_Route5": 0xd243, - "Wild_Route6": 0xd259, - "Wild_Route11": 0xd26f, - "Wild_RockTunnel1F": 0xd285, - "Wild_RockTunnelB1F": 0xd29b, - "Wild_Route10": 0xd2b1, - "Wild_Route12": 0xd2c7, - "Wild_Route8": 0xd2dd, - "Wild_Route7": 0xd2f3, - "Wild_PokemonTower3F": 0xd30d, - "Wild_PokemonTower4F": 0xd323, - "Wild_PokemonTower5F": 0xd339, - "Wild_PokemonTower6F": 0xd34f, - "Wild_PokemonTower7F": 0xd365, - "Wild_Route13": 0xd37b, - "Wild_Route14": 0xd391, - "Wild_Route15": 0xd3a7, - "Wild_Route16": 0xd3bd, - "Wild_Route17": 0xd3d3, - "Wild_Route18": 0xd3e9, - "Wild_SafariZoneCenter": 0xd3ff, - "Wild_SafariZoneEast": 0xd415, - "Wild_SafariZoneNorth": 0xd42b, - "Wild_SafariZoneWest": 0xd441, - "Wild_SeaRoutes": 0xd458, - "Wild_SeafoamIslands1F": 0xd46d, - "Wild_SeafoamIslandsB1F": 0xd483, - "Wild_SeafoamIslandsB2F": 0xd499, - "Wild_SeafoamIslandsB3F": 0xd4af, - "Wild_SeafoamIslandsB4F": 0xd4c5, - "Wild_PokemonMansion1F": 0xd4db, - "Wild_PokemonMansion2F": 0xd4f1, - "Wild_PokemonMansion3F": 0xd507, - "Wild_PokemonMansionB1F": 0xd51d, - "Wild_Route21": 0xd533, - "Wild_Surf_Route21": 0xd548, - "Wild_CeruleanCave1F": 0xd55d, - "Wild_CeruleanCave2F": 0xd573, - "Wild_CeruleanCaveB1F": 0xd589, - "Wild_PowerPlant": 0xd59f, - "Wild_Route23": 0xd5b5, - "Wild_VictoryRoad2F": 0xd5cb, - "Wild_VictoryRoad3F": 0xd5e1, - "Wild_VictoryRoad1F": 0xd5f7, - "Wild_DiglettsCave": 0xd60d, - "Ghost_Battle5": 0xd781, - "HM_Surf_Badge_a": 0xda73, - "HM_Surf_Badge_b": 0xda78, - "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcc2, - "Option_Silph_Scope_Skip": 0xe207, - "Wild_Old_Rod": 0xe382, - "Wild_Good_Rod": 0xe3af, - "Option_Fix_Combat_Bugs_PP_Restore": 0xe541, - "Option_Reusable_TMs": 0xe675, - "Wild_Super_Rod_A": 0xeaa9, - "Wild_Super_Rod_B": 0xeaae, - "Wild_Super_Rod_C": 0xeab3, - "Wild_Super_Rod_D": 0xeaba, - "Wild_Super_Rod_E": 0xeabf, - "Wild_Super_Rod_F": 0xeac4, - "Wild_Super_Rod_G": 0xeacd, - "Wild_Super_Rod_H": 0xead6, - "Wild_Super_Rod_I": 0xeadf, - "Wild_Super_Rod_J": 0xeae8, - "Starting_Money_High": 0xf9aa, - "Starting_Money_Middle": 0xf9ad, - "Starting_Money_Low": 0xf9b0, - "Option_Pokedex_Seen": 0xf9cb, + "Title_Seed": 0x5f22, + "Title_Slot_Name": 0x5f42, + "PC_Item": 0x6310, + "PC_Item_Quantity": 0x6315, + "Fly_Location": 0x6323, + "Skip_Player_Name": 0x633c, + "Skip_Rival_Name": 0x634a, + "Pallet_Fly_Coords": 0x6675, + "Option_Old_Man": 0xcb0b, + "Option_Old_Man_Lying": 0xcb0e, + "Option_Route3_Guard_A": 0xcb14, + "Option_Trashed_House_Guard_A": 0xcb1d, + "Option_Trashed_House_Guard_B": 0xcb23, + "Option_Boulders": 0xcdb4, + "Option_Rock_Tunnel_Extra_Items": 0xcdbd, + "Wild_Route1": 0xd138, + "Wild_Route2": 0xd14e, + "Wild_Route22": 0xd164, + "Wild_ViridianForest": 0xd17a, + "Wild_Route3": 0xd190, + "Wild_MtMoon1F": 0xd1a6, + "Wild_MtMoonB1F": 0xd1bc, + "Wild_MtMoonB2F": 0xd1d2, + "Wild_Route4": 0xd1e8, + "Wild_Route24": 0xd1fe, + "Wild_Route25": 0xd214, + "Wild_Route9": 0xd22a, + "Wild_Route5": 0xd240, + "Wild_Route6": 0xd256, + "Wild_Route11": 0xd26c, + "Wild_RockTunnel1F": 0xd282, + "Wild_RockTunnelB1F": 0xd298, + "Wild_Route10": 0xd2ae, + "Wild_Route12": 0xd2c4, + "Wild_Route8": 0xd2da, + "Wild_Route7": 0xd2f0, + "Wild_PokemonTower3F": 0xd30a, + "Wild_PokemonTower4F": 0xd320, + "Wild_PokemonTower5F": 0xd336, + "Wild_PokemonTower6F": 0xd34c, + "Wild_PokemonTower7F": 0xd362, + "Wild_Route13": 0xd378, + "Wild_Route14": 0xd38e, + "Wild_Route15": 0xd3a4, + "Wild_Route16": 0xd3ba, + "Wild_Route17": 0xd3d0, + "Wild_Route18": 0xd3e6, + "Wild_SafariZoneCenter": 0xd3fc, + "Wild_SafariZoneEast": 0xd412, + "Wild_SafariZoneNorth": 0xd428, + "Wild_SafariZoneWest": 0xd43e, + "Wild_SeaRoutes": 0xd455, + "Wild_SeafoamIslands1F": 0xd46a, + "Wild_SeafoamIslandsB1F": 0xd480, + "Wild_SeafoamIslandsB2F": 0xd496, + "Wild_SeafoamIslandsB3F": 0xd4ac, + "Wild_SeafoamIslandsB4F": 0xd4c2, + "Wild_PokemonMansion1F": 0xd4d8, + "Wild_PokemonMansion2F": 0xd4ee, + "Wild_PokemonMansion3F": 0xd504, + "Wild_PokemonMansionB1F": 0xd51a, + "Wild_Route21": 0xd530, + "Wild_Surf_Route21": 0xd545, + "Wild_CeruleanCave1F": 0xd55a, + "Wild_CeruleanCave2F": 0xd570, + "Wild_CeruleanCaveB1F": 0xd586, + "Wild_PowerPlant": 0xd59c, + "Wild_Route23": 0xd5b2, + "Wild_VictoryRoad2F": 0xd5c8, + "Wild_VictoryRoad3F": 0xd5de, + "Wild_VictoryRoad1F": 0xd5f4, + "Wild_DiglettsCave": 0xd60a, + "Ghost_Battle5": 0xd77e, + "HM_Surf_Badge_a": 0xda70, + "HM_Surf_Badge_b": 0xda75, + "Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcbf, + "Option_Silph_Scope_Skip": 0xe204, + "Wild_Old_Rod": 0xe37f, + "Wild_Good_Rod": 0xe3ac, + "Option_Fix_Combat_Bugs_PP_Restore": 0xe53e, + "Option_Reusable_TMs": 0xe672, + "Wild_Super_Rod_A": 0xeaa6, + "Wild_Super_Rod_B": 0xeaab, + "Wild_Super_Rod_C": 0xeab0, + "Wild_Super_Rod_D": 0xeab7, + "Wild_Super_Rod_E": 0xeabc, + "Wild_Super_Rod_F": 0xeac1, + "Wild_Super_Rod_G": 0xeaca, + "Wild_Super_Rod_H": 0xead3, + "Wild_Super_Rod_I": 0xeadc, + "Wild_Super_Rod_J": 0xeae5, + "Starting_Money_High": 0xf9a7, + "Starting_Money_Middle": 0xf9aa, + "Starting_Money_Low": 0xf9ad, + "Option_Pokedex_Seen": 0xf9c8, "HM_Fly_Badge_a": 0x13182, "HM_Fly_Badge_b": 0x13187, "HM_Cut_Badge_a": 0x131b8, @@ -1164,22 +1164,22 @@ rom_addresses = { "Prize_Mon_E": 0x52944, "Prize_Mon_F": 0x52946, "Start_Inventory": 0x52a7b, - "Map_Fly_Location": 0x52c6f, - "Reset_A": 0x52d1b, - "Reset_B": 0x52d47, - "Reset_C": 0x52d73, - "Reset_D": 0x52d9f, - "Reset_E": 0x52dcb, - "Reset_F": 0x52df7, - "Reset_G": 0x52e23, - "Reset_H": 0x52e4f, - "Reset_I": 0x52e7b, - "Reset_J": 0x52ea7, - "Reset_K": 0x52ed3, - "Reset_L": 0x52eff, - "Reset_M": 0x52f2b, - "Reset_N": 0x52f57, - "Reset_O": 0x52f83, + "Map_Fly_Location": 0x52c75, + "Reset_A": 0x52d21, + "Reset_B": 0x52d4d, + "Reset_C": 0x52d79, + "Reset_D": 0x52da5, + "Reset_E": 0x52dd1, + "Reset_F": 0x52dfd, + "Reset_G": 0x52e29, + "Reset_H": 0x52e55, + "Reset_I": 0x52e81, + "Reset_J": 0x52ead, + "Reset_K": 0x52ed9, + "Reset_L": 0x52f05, + "Reset_M": 0x52f31, + "Reset_N": 0x52f5d, + "Reset_O": 0x52f89, "Warps_Route2": 0x54026, "Missable_Route_2_Item_1": 0x5404a, "Missable_Route_2_Item_2": 0x54051, From 6dccf36f8853abdbf07df1bec2f22cfc69cfba71 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 25 Nov 2023 07:09:08 -0500 Subject: [PATCH 225/327] Lingo: Various generation optimizations (#2479) Almost all of the events have been eradicated, which significantly improves both generation speed and playthrough calculation. Previously, checking for access to a location involved checking for access to each panel in the location, as well as recursively checking for access to any panels required by those panels. This potentially performed the same check multiple times. The access requirements for locations are now calculated and flattened in generate_early, so that the access function can directly check for the required rooms, doors, and colors. These flattened access requirements are also used for Entrance checking, and register_indirect_condition is used to make sure that can_reach(Region) is safe to use. The Mastery and Level 2 rules now just run a bunch of access rules and count the number of them that succeed, instead of relying on event items. Finally: the Level 2 panel hunt is now enabled even when Level 2 is not the victory condition, as I feel that generation is fast enough now for that to be acceptable. --- worlds/lingo/__init__.py | 12 +- worlds/lingo/data/LL1.yaml | 2 - worlds/lingo/options.py | 8 +- worlds/lingo/player_logic.py | 298 +++++++++++++++++++-------- worlds/lingo/regions.py | 48 +++-- worlds/lingo/rules.py | 112 +++++----- worlds/lingo/test/TestPanelsanity.py | 19 ++ 7 files changed, 330 insertions(+), 169 deletions(-) create mode 100644 worlds/lingo/test/TestPanelsanity.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3d98ae9183..da8a246e79 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -55,14 +55,14 @@ class LingoWorld(World): create_regions(self, self.player_logic) def create_items(self): - pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS] + pool = [self.create_item(name) for name in self.player_logic.real_items] - if self.player_logic.FORCED_GOOD_ITEM != "": - new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM) + if self.player_logic.forced_good_item != "": + new_item = self.create_item(self.player_logic.forced_good_item) location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player) location_obj.place_locked_item(new_item) - item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool) + item_difference = len(self.player_logic.real_locations) - len(pool) if item_difference: trap_percentage = self.options.trap_percentage traps = int(item_difference * trap_percentage / 100.0) @@ -93,7 +93,7 @@ class LingoWorld(World): classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ - and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.PAINTING_MAPPING + and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping for painting_id in item.painting_ids): # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings # go nowhere, then this item should not be progression. @@ -116,6 +116,6 @@ class LingoWorld(World): } if self.options.shuffle_paintings: - slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING + slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping return slot_data diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index d46403e8da..8a4f831f94 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -379,8 +379,6 @@ tag: forbid non_counting: True check: True - required_panel: - - panel: ANOTHER TRY doors: Exit Door: id: Entry Room Area Doors/Door_hi_high diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 7dc6a1389c..fc9ddee0e0 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -52,7 +52,10 @@ class ShufflePaintings(Toggle): class VictoryCondition(Choice): - """Change the victory condition.""" + """Change the victory condition. + On "the_end", the goal is to solve THE END at the top of the tower. + On "the_master", the goal is to solve THE MASTER at the top of the tower, after getting the number of achievements specified in the Mastery Achievements option. + On "level_2", the goal is to solve LEVEL 2 in the second room, after solving the number of panels specified in the Level 2 Requirement option.""" display_name = "Victory Condition" option_the_end = 0 option_the_master = 1 @@ -75,9 +78,10 @@ class Level2Requirement(Range): """The number of panel solves required to unlock LEVEL 2. In the base game, 223 are needed. Note that this count includes ANOTHER TRY. + When set to 1, the panel hunt is disabled, and you can access LEVEL 2 for free. """ display_name = "Level 2 Requirement" - range_start = 2 + range_start = 1 range_end = 800 default = 223 diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index abb975e020..a0b33d1dbe 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -1,10 +1,10 @@ -from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING +from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from .items import ALL_ITEM_TABLE from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ - PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \ + PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel from .testing import LingoTestOptions @@ -12,10 +12,29 @@ if TYPE_CHECKING: from . import LingoWorld +class AccessRequirements: + rooms: Set[str] + doors: Set[RoomAndDoor] + colors: Set[str] + + def __init__(self): + self.rooms = set() + self.doors = set() + self.colors = set() + + def merge(self, other: "AccessRequirements"): + self.rooms |= other.rooms + self.doors |= other.doors + self.colors |= other.colors + + def __str__(self): + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" + + class PlayerLocation(NamedTuple): name: str - code: Optional[int] = None - panels: List[RoomAndPanel] = [] + code: Optional[int] + access: AccessRequirements class LingoPlayerLogic: @@ -23,27 +42,45 @@ class LingoPlayerLogic: Defines logic after a player's options have been applied """ - ITEM_BY_DOOR: Dict[str, Dict[str, str]] + item_by_door: Dict[str, Dict[str, str]] - LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]] - REAL_LOCATIONS: List[str] + locations_by_room: Dict[str, List[PlayerLocation]] + real_locations: List[str] - EVENT_LOC_TO_ITEM: Dict[str, str] - REAL_ITEMS: List[str] + event_loc_to_item: Dict[str, str] + real_items: List[str] - VICTORY_CONDITION: str - MASTERY_LOCATION: str - LEVEL_2_LOCATION: str + victory_condition: str + mastery_location: str + level_2_location: str - PAINTING_MAPPING: Dict[str, str] + painting_mapping: Dict[str, str] - FORCED_GOOD_ITEM: str + forced_good_item: str - def add_location(self, room: str, loc: PlayerLocation): - self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc) + panel_reqs: Dict[str, Dict[str, AccessRequirements]] + door_reqs: Dict[str, Dict[str, AccessRequirements]] + mastery_reqs: List[AccessRequirements] + counting_panel_reqs: Dict[str, List[Tuple[AccessRequirements, int]]] + + def add_location(self, room: str, name: str, code: Optional[int], panels: List[RoomAndPanel], world: "LingoWorld"): + """ + Creates a location. This function determines the access requirements for the location by combining and + flattening the requirements for each of the given panels. + """ + access_reqs = AccessRequirements() + for panel in panels: + if panel.room is not None and panel.room != room: + access_reqs.rooms.add(panel.room) + + panel_room = room if panel.room is None else panel.room + sub_access_reqs = self.calculate_panel_requirements(panel_room, panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.locations_by_room.setdefault(room, []).append(PlayerLocation(name, code, access_reqs)) def set_door_item(self, room: str, door: str, item: str): - self.ITEM_BY_DOOR.setdefault(room, {})[door] = item + self.item_by_door.setdefault(room, {})[door] = item def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: @@ -52,21 +89,25 @@ class LingoPlayerLogic: else: progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name self.set_door_item(room_name, door_data.name, progressive_item_name) - self.REAL_ITEMS.append(progressive_item_name) + self.real_items.append(progressive_item_name) else: self.set_door_item(room_name, door_data.name, door_data.item_name) def __init__(self, world: "LingoWorld"): - self.ITEM_BY_DOOR = {} - self.LOCATIONS_BY_ROOM = {} - self.REAL_LOCATIONS = [] - self.EVENT_LOC_TO_ITEM = {} - self.REAL_ITEMS = [] - self.VICTORY_CONDITION = "" - self.MASTERY_LOCATION = "" - self.LEVEL_2_LOCATION = "" - self.PAINTING_MAPPING = {} - self.FORCED_GOOD_ITEM = "" + self.item_by_door = {} + self.locations_by_room = {} + self.real_locations = [] + self.event_loc_to_item = {} + self.real_items = [] + self.victory_condition = "" + self.mastery_location = "" + self.level_2_location = "" + self.painting_mapping = {} + self.forced_good_item = "" + self.panel_reqs = {} + self.door_reqs = {} + self.mastery_reqs = [] + self.counting_panel_reqs = {} door_shuffle = world.options.shuffle_doors color_shuffle = world.options.shuffle_colors @@ -79,17 +120,10 @@ class LingoPlayerLogic: raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not " "be enough locations for all of the door items.") - # Create an event for every door, representing whether that door has been opened. Also create event items for - # doors that are event-only. - for room_name, room_data in DOORS_BY_ROOM.items(): - for door_name, door_data in room_data.items(): - if door_shuffle == ShuffleDoors.option_none: - itemloc_name = f"{room_name} - {door_name} (Opened)" - self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels)) - self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name - self.set_door_item(room_name, door_name, itemloc_name) - else: - # This line is duplicated from StaticLingoItems + # Create door items, where needed. + if door_shuffle != ShuffleDoors.option_none: + for room_name, room_data in DOORS_BY_ROOM.items(): + for door_name, door_data in room_data.items(): if door_data.skip_item is False and door_data.event is False: if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple: # Grouped doors are handled differently if shuffle doors is on simple. @@ -97,49 +131,44 @@ class LingoPlayerLogic: else: self.handle_non_grouped_door(room_name, door_data, world) - if door_data.event: - self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels)) - self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)" - self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)") - - # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also - # create events for each counting panel, so that we can determine when LEVEL 2 is accessible. + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. for room_name, room_data in PANELS_BY_ROOM.items(): for panel_name, panel_data in room_data.items(): if panel_data.achievement: - event_name = room_name + " - " + panel_name + " (Achieved)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) - self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement" + access_req = AccessRequirements() + access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) + access_req.rooms.add(room_name) - if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2: - event_name = room_name + " - " + panel_name + " (Counted)" - self.add_location(room_name, PlayerLocation(event_name, None, - [RoomAndPanel(room_name, panel_name)])) - self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved" + self.mastery_reqs.append(access_req) # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER" - self.LEVEL_2_LOCATION = "N/A" + self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" + self.level_2_location = "Second Room - LEVEL 2" if victory_condition == VictoryCondition.option_the_end: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END" - self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)")) - self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory" + self.victory_condition = "Orange Tower Seventh Floor - THE END" + self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) + self.event_loc_to_item["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: - self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER" - self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements" + self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" + self.mastery_location = "Orange Tower Seventh Floor - Mastery Achievements" - self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, [])) - self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory" + self.add_location("Orange Tower Seventh Floor", self.mastery_location, None, [], world) + self.event_loc_to_item[self.mastery_location] = "Victory" elif victory_condition == VictoryCondition.option_level_2: - self.VICTORY_CONDITION = "Second Room - LEVEL 2" - self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2" + self.victory_condition = "Second Room - LEVEL 2" + self.level_2_location = "Second Room - Unlock Level 2" - self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None, - [RoomAndPanel("Second Room", "LEVEL 2")])) - self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory" + self.add_location("Second Room", self.level_2_location, None, [RoomAndPanel("Second Room", "LEVEL 2")], + world) + self.event_loc_to_item[self.level_2_location] = "Victory" + + if world.options.level_2_requirement == 1: + raise Exception("The Level 2 requirement must be at least 2 when LEVEL 2 is the victory condition.") + + # Create groups of counting panel access requirements for the LEVEL 2 check. + self.create_panel_hunt_events(world) # Instantiate all real locations. location_classification = LocationClassification.normal @@ -149,18 +178,17 @@ class LingoPlayerLogic: location_classification = LocationClassification.insanity for location_name, location_data in ALL_LOCATION_TABLE.items(): - if location_name != self.VICTORY_CONDITION: + if location_name != self.victory_condition: if location_classification not in location_data.classification: continue - self.add_location(location_data.room, PlayerLocation(location_name, location_data.code, - location_data.panels)) - self.REAL_LOCATIONS.append(location_name) + self.add_location(location_data.room, location_name, location_data.code, location_data.panels, world) + self.real_locations.append(location_name) # Instantiate all real items. for name, item in ALL_ITEM_TABLE.items(): if item.should_include(world): - self.REAL_ITEMS.append(name) + self.real_items.append(name) # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: @@ -201,7 +229,7 @@ class LingoPlayerLogic: continue # If painting shuffle is on, we only want to consider paintings that actually go somewhere. - if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys(): + if painting_shuffle and painting_obj.id not in self.painting_mapping.keys(): continue pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door] @@ -226,12 +254,12 @@ class LingoPlayerLogic: good_item_options.remove(item) if len(good_item_options) > 0: - self.FORCED_GOOD_ITEM = world.random.choice(good_item_options) - self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM) - self.REAL_LOCATIONS.remove("Second Room - Good Luck") + self.forced_good_item = world.random.choice(good_item_options) + self.real_items.remove(self.forced_good_item) + self.real_locations.remove("Second Room - Good Luck") def randomize_paintings(self, world: "LingoWorld") -> bool: - self.PAINTING_MAPPING.clear() + self.painting_mapping.clear() door_shuffle = world.options.shuffle_doors @@ -253,7 +281,7 @@ class LingoPlayerLogic: if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) - self.PAINTING_MAPPING = dict(zip(req_entrances, req_exits)) + self.painting_mapping = dict(zip(req_entrances, req_exits)) # Next, determine the rest of the exit paintings. exitable = [painting_id for painting_id, painting in PAINTINGS.items() @@ -272,25 +300,125 @@ class LingoPlayerLogic: for warp_exit in nonreq_exits: warp_enter = world.random.choice(chosen_entrances) chosen_entrances.remove(warp_enter) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # Assign each of the remaining entrances to any required or non-required exit. for warp_enter in chosen_entrances: warp_exit = world.random.choice(chosen_exits) - self.PAINTING_MAPPING[warp_enter] = warp_exit + self.painting_mapping[warp_enter] = warp_exit # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves). # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall # is forced to point to the vanilla exit. - if "eye_painting_2" not in self.PAINTING_MAPPING.keys(): - self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2" + if "eye_painting_2" not in self.painting_mapping.keys(): + self.painting_mapping["eye_painting"] = "eye_painting_2" # Just for sanity's sake, ensure that all required painting rooms are accessed. for painting_id, painting in PAINTINGS.items(): - if painting_id not in self.PAINTING_MAPPING.values() \ + if painting_id not in self.painting_mapping.values() \ and (painting.required or (painting.required_when_no_doors and door_shuffle == ShuffleDoors.option_none)): return False return True + + def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld"): + """ + Calculate and return the access requirements for solving a given panel. The goal is to eliminate recursion in + the access rule function by collecting the rooms, doors, and colors needed by this panel and any panel required + by this panel. Memoization is used so that no panel is evaluated more than once. + """ + if panel not in self.panel_reqs.setdefault(room, {}): + access_reqs = AccessRequirements() + panel_object = PANELS_BY_ROOM[room][panel] + + for req_room in panel_object.required_rooms: + access_reqs.rooms.add(req_room) + + for req_door in panel_object.required_doors: + door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + sub_access_reqs = self.calculate_door_requirements( + room if req_door.room is None else req_door.room, req_door.door, world) + access_reqs.merge(sub_access_reqs) + else: + access_reqs.doors.add(RoomAndDoor(room if req_door.room is None else req_door.room, req_door.door)) + + for color in panel_object.colors: + access_reqs.colors.add(color) + + for req_panel in panel_object.required_panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.panel_reqs[room][panel] = access_reqs + + return self.panel_reqs[room][panel] + + def calculate_door_requirements(self, room: str, door: str, world: "LingoWorld"): + """ + Similar to calculate_panel_requirements, but for event doors. + """ + if door not in self.door_reqs.setdefault(room, {}): + access_reqs = AccessRequirements() + door_object = DOORS_BY_ROOM[room][door] + + for req_panel in door_object.panels: + if req_panel.room is not None and req_panel.room != room: + access_reqs.rooms.add(req_panel.room) + + sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, + req_panel.panel, world) + access_reqs.merge(sub_access_reqs) + + self.door_reqs[room][door] = access_reqs + + return self.door_reqs[room][door] + + def create_panel_hunt_events(self, world: "LingoWorld"): + """ + Creates the event locations/items used for determining access to the LEVEL 2 panel. Instead of creating an event + for every single counting panel in the game, we try to coalesce panels with identical access rules into the same + event. Right now, this means the following: + + When color shuffle is off, panels in a room with no extra access requirements (room, door, or other panel) are + all coalesced into one event. + + When color shuffle is on, single-colored panels (including white) in a room are combined into one event per + color. Multicolored panels and panels with any extra access requirements are not coalesced, and will each + receive their own event. + """ + for room_name, room_data in PANELS_BY_ROOM.items(): + unhindered_panels_by_color: dict[Optional[str], int] = {} + + for panel_name, panel_data in room_data.items(): + # We won't count non-counting panels. + if panel_data.non_counting: + continue + + # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. + if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ + or len(panel_data.required_rooms) > 0\ + or (world.options.shuffle_colors and len(panel_data.colors) > 1): + self.counting_panel_reqs.setdefault(room_name, []).append( + (self.calculate_panel_requirements(room_name, panel_name, world), 1)) + else: + if len(panel_data.colors) == 0 or not world.options.shuffle_colors: + color = None + else: + color = panel_data.colors[0] + + unhindered_panels_by_color[color] = unhindered_panels_by_color.get(color, 0) + 1 + + for color, panel_count in unhindered_panels_by_color.items(): + access_reqs = AccessRequirements() + if color is not None: + access_reqs.colors.add(color) + + self.counting_panel_reqs.setdefault(room_name, []).append((access_reqs, panel_count)) diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index e5f947de05..c24144a160 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -1,11 +1,11 @@ -from typing import Dict, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING -from BaseClasses import ItemClassification, Region +from BaseClasses import Entrance, ItemClassification, Region from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda -from .static_logic import ALL_ROOMS, PAINTINGS, Room +from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld @@ -13,12 +13,12 @@ if TYPE_CHECKING: def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region: new_region = Region(room.name, world.player, world.multiworld) - for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}): + for location in player_logic.locations_by_room.get(room.name, {}): new_location = LingoLocation(world.player, location.name, location.code, new_region) - new_location.access_rule = make_location_lambda(location, room.name, world, player_logic) + new_location.access_rule = make_location_lambda(location, world, player_logic) new_region.locations.append(new_location) - if location.name in player_logic.EVENT_LOC_TO_ITEM: - event_name = player_logic.EVENT_LOC_TO_ITEM[location.name] + if location.name in player_logic.event_loc_to_item: + event_name = player_logic.event_loc_to_item[location.name] event_item = LingoItem(event_name, ItemClassification.progression, None, world.player) new_location.place_locked_item(event_item) @@ -31,7 +31,22 @@ def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_ source_region.connect( target_region, "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic)) + lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) + + +def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, + door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): + connection = Entrance(world.player, description, source_region) + connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic) + + source_region.exits.append(connection) + connection.connect(target_region) + + if door is not None: + effective_room = target_region.name if door.room is None else door.room + if door.door not in player_logic.item_by_door.get(effective_room, {}): + for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + world.multiworld.register_indirect_condition(regions[region], connection) def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld", @@ -41,11 +56,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str target_region = regions[target_painting.room] source_region = regions[source_painting.room] - source_region.connect( - target_region, - f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)", - lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player, - player_logic)) + + entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)" + connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world, + player_logic) def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: @@ -74,10 +88,8 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: else: entrance_name += f" (through {room.name} - {entrance.door.door})" - regions[entrance.room].connect( - regions[room.name], entrance_name, - lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, - player_logic)) + connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, + player_logic) handle_pilgrim_room(regions, world, player_logic) @@ -85,7 +97,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") if painting_shuffle: - for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items(): + for warp_enter, warp_exit in player_logic.painting_mapping.items(): connect_painting(regions, warp_enter, warp_exit, world, player_logic) world.multiworld.regions += regions.values() diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index d59b8a1ef7..ee9dcc4192 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -1,23 +1,23 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState -from .options import VictoryCondition -from .player_logic import LingoPlayerLogic, PlayerLocation -from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor +from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation +from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor if TYPE_CHECKING: from . import LingoWorld -def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int, +def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld", player_logic: LingoPlayerLogic): if door is None: return True - return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic) + effective_room = room if door.room is None else door.room + return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic): +def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): fake_pilgrimage = [ ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], @@ -28,77 +28,77 @@ def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: ["Outside The Agreeable", "Tenacious Entrance"] ] for entrance in fake_pilgrimage: - if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player): + if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): return False return True -def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld", +def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): - for panel in location.panels: - panel_room = room_name if panel.room is None else panel.room - if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic): - return False - - return True + return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) -def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"): - return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value) +def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + satisfied_count = 0 + for access_req in player_logic.mastery_reqs: + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + satisfied_count += 1 + return satisfied_count >= world.options.mastery_achievements.value -def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int, - player_logic: LingoPlayerLogic): - """ - Determines whether a door can be opened - """ - item_name = player_logic.ITEM_BY_DOOR[room][door] - if item_name in PROGRESSIVE_ITEMS: - progression = PROGRESSION_BY_ROOM[room][door] - return state.has(item_name, player, progression.index) - - return state.has(item_name, player) +def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): + counted_panels = 0 + state.update_reachable_regions(world.player) + for region in state.reachable_regions[world.player]: + for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []): + if _lingo_can_satisfy_requirements(state, access_req, world, player_logic): + counted_panels += panel_count + if counted_panels >= world.options.level_2_requirement.value - 1: + return True + return False -def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld", - player_logic: LingoPlayerLogic): - """ - Determines whether a panel can be solved - """ - if start_room != room and not state.can_reach(room, "Region", world.player): - return False - - if room == "Second Room" and panel == "ANOTHER TRY" \ - and world.options.victory_condition == VictoryCondition.option_level_2 \ - and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1): - return False - - panel_object = PANELS_BY_ROOM[room][panel] - for req_room in panel_object.required_rooms: +def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld", + player_logic: LingoPlayerLogic): + for req_room in access.rooms: if not state.can_reach(req_room, "Region", world.player): return False - for req_door in panel_object.required_doors: - if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room, - req_door.door, world.player, player_logic): + for req_door in access.doors: + if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic): return False - for req_panel in panel_object.required_panels: - if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room, - req_panel.panel, world, player_logic): - return False - - if len(panel_object.colors) > 0 and world.options.shuffle_colors: - for color in panel_object.colors: + if len(access.colors) > 0 and world.options.shuffle_colors: + for color in access.colors: if not state.has(color.capitalize(), world.player): return False return True -def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic): - if location.name == player_logic.MASTERY_LOCATION: - return lambda state: lingo_can_use_mastery_location(state, world) +def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld", + player_logic: LingoPlayerLogic): + """ + Determines whether a door can be opened + """ + if door not in player_logic.item_by_door.get(room, {}): + return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic) - return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic) + item_name = player_logic.item_by_door[room][door] + if item_name in PROGRESSIVE_ITEMS: + progression = PROGRESSION_BY_ROOM[room][door] + return state.has(item_name, world.player, progression.index) + + return state.has(item_name, world.player) + + +def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): + if location.name == player_logic.mastery_location: + return lambda state: lingo_can_use_mastery_location(state, world, player_logic) + + if world.options.level_2_requirement > 1\ + and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location): + return lambda state: lingo_can_use_level_2_location(state, world, player_logic) + + return lambda state: lingo_can_use_location(state, location, world, player_logic) diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py new file mode 100644 index 0000000000..34c1b3815a --- /dev/null +++ b/worlds/lingo/test/TestPanelsanity.py @@ -0,0 +1,19 @@ +from . import LingoTestBase + + +class TestPanelHunt(LingoTestBase): + options = { + "shuffle_doors": "complex", + "location_checks": "insanity", + "victory_condition": "level_2", + "level_2_requirement": "15" + } + + def test_another_try(self) -> None: + self.collect_by_name("The Traveled - Entrance") # idk why this is needed + self.assertFalse(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertFalse(self.can_reach_location("Second Room - Unlock Level 2")) + + self.collect_by_name("Second Room - Exit Door") + self.assertTrue(self.can_reach_location("Second Room - ANOTHER TRY")) + self.assertTrue(self.can_reach_location("Second Room - Unlock Level 2")) From ba5327814799aa3d7983d5d161a4e9d50d923b3d Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Sat, 25 Nov 2023 13:53:02 +0100 Subject: [PATCH 226/327] core: make option resolution in world tests deterministic (#2471) Co-authored-by: Zach Parks --- test/bases.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/bases.py b/test/bases.py index 2054c2d187..d6a43c598f 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,8 +1,10 @@ +import random import sys import typing import unittest from argparse import Namespace +from Generate import get_seed_name from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -152,6 +154,8 @@ class WorldTestBase(unittest.TestCase): self.multiworld.player_name = {1: "Tester"} self.multiworld.set_seed(seed) self.multiworld.state = CollectionState(self.multiworld) + random.seed(self.multiworld.seed) + self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py args = Namespace() for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items(): setattr(args, name, { From 9afca87045a02fe60db07e25a82f40ebb46d8f7d Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Sat, 25 Nov 2023 09:22:30 -0500 Subject: [PATCH 227/327] Heretic: implement new game (#2256) --- README.md | 2 +- docs/CODEOWNERS | 3 + worlds/heretic/Items.py | 1606 ++++++ worlds/heretic/Locations.py | 8229 +++++++++++++++++++++++++++++ worlds/heretic/Maps.py | 52 + worlds/heretic/Options.py | 167 + worlds/heretic/Regions.py | 894 ++++ worlds/heretic/Rules.py | 736 +++ worlds/heretic/__init__.py | 287 + worlds/heretic/docs/en_Heretic.md | 23 + worlds/heretic/docs/setup_en.md | 51 + 11 files changed, 12049 insertions(+), 1 deletion(-) create mode 100644 worlds/heretic/Items.py create mode 100644 worlds/heretic/Locations.py create mode 100644 worlds/heretic/Maps.py create mode 100644 worlds/heretic/Options.py create mode 100644 worlds/heretic/Regions.py create mode 100644 worlds/heretic/Rules.py create mode 100644 worlds/heretic/__init__.py create mode 100644 worlds/heretic/docs/en_Heretic.md create mode 100644 worlds/heretic/docs/setup_en.md diff --git a/README.md b/README.md index 2bca422fea..b51fe00f9a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Currently, the following games are supported: * Pokémon Emerald * DOOM II * Shivers - +* Heretic For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 6231da8232..c589b1333c 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -55,6 +55,9 @@ # Final Fantasy /worlds/ff1/ @jtoyoda +# Heretic +/worlds/heretic/ @Daivuk + # Hollow Knight /worlds/hk/ @BadMagic100 @ThePhar diff --git a/worlds/heretic/Items.py b/worlds/heretic/Items.py new file mode 100644 index 0000000000..a0907a3a30 --- /dev/null +++ b/worlds/heretic/Items.py @@ -0,0 +1,1606 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, Set + + +class ItemDict(TypedDict, total=False): + classification: ItemClassification + count: int + name: str + doom_type: int # Unique numerical id used to spawn the item. -1 is level item, -2 is level complete item. + episode: int # Relevant if that item targets a specific level, like keycard or map reveal pickup. + map: int + + +item_table: Dict[int, ItemDict] = { + 370000: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Gauntlets of the Necromancer', + 'doom_type': 2005, + 'episode': -1, + 'map': -1}, + 370001: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ethereal Crossbow', + 'doom_type': 2001, + 'episode': -1, + 'map': -1}, + 370002: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Dragon Claw', + 'doom_type': 53, + 'episode': -1, + 'map': -1}, + 370003: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Phoenix Rod', + 'doom_type': 2003, + 'episode': -1, + 'map': -1}, + 370004: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Firemace', + 'doom_type': 2002, + 'episode': -1, + 'map': -1}, + 370005: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hellstaff', + 'doom_type': 2004, + 'episode': -1, + 'map': -1}, + 370006: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Bag of Holding', + 'doom_type': 8, + 'episode': -1, + 'map': -1}, + 370007: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Chaos Device', + 'doom_type': 36, + 'episode': -1, + 'map': -1}, + 370008: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Morph Ovum', + 'doom_type': 30, + 'episode': -1, + 'map': -1}, + 370009: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Mystic Urn', + 'doom_type': 32, + 'episode': -1, + 'map': -1}, + 370010: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Quartz Flask', + 'doom_type': 82, + 'episode': -1, + 'map': -1}, + 370011: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Ring of Invincibility', + 'doom_type': 84, + 'episode': -1, + 'map': -1}, + 370012: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Shadowsphere', + 'doom_type': 75, + 'episode': -1, + 'map': -1}, + 370013: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Timebomb of the Ancients', + 'doom_type': 34, + 'episode': -1, + 'map': -1}, + 370014: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Tome of Power', + 'doom_type': 86, + 'episode': -1, + 'map': -1}, + 370015: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Torch', + 'doom_type': 33, + 'episode': -1, + 'map': -1}, + 370016: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Silver Shield', + 'doom_type': 85, + 'episode': -1, + 'map': -1}, + 370017: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Enchanted Shield', + 'doom_type': 31, + 'episode': -1, + 'map': -1}, + 370018: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Crystal Geode', + 'doom_type': 12, + 'episode': -1, + 'map': -1}, + 370019: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Energy Orb', + 'doom_type': 55, + 'episode': -1, + 'map': -1}, + 370020: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Greater Runes', + 'doom_type': 21, + 'episode': -1, + 'map': -1}, + 370021: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Inferno Orb', + 'doom_type': 23, + 'episode': -1, + 'map': -1}, + 370022: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Pile of Mace Spheres', + 'doom_type': 16, + 'episode': -1, + 'map': -1}, + 370023: {'classification': ItemClassification.filler, + 'count': 0, + 'name': 'Quiver of Ethereal Arrows', + 'doom_type': 19, + 'episode': -1, + 'map': -1}, + 370200: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 1}, + 370201: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 2}, + 370202: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 2}, + 370203: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 2}, + 370204: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 3}, + 370205: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 3}, + 370206: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 4}, + 370207: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 4}, + 370208: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 5}, + 370209: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 5}, + 370210: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 5}, + 370211: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 6}, + 370212: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 6}, + 370213: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 7}, + 370214: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 7}, + 370215: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 7}, + 370216: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Yellow key', + 'doom_type': 80, + 'episode': 1, + 'map': 9}, + 370217: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Green key', + 'doom_type': 73, + 'episode': 1, + 'map': 9}, + 370218: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Blue key', + 'doom_type': 79, + 'episode': 1, + 'map': 9}, + 370219: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 1}, + 370220: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 1}, + 370221: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 2}, + 370222: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 2}, + 370223: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 3}, + 370224: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 3}, + 370225: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 3}, + 370226: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 4}, + 370227: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 4}, + 370228: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 4}, + 370229: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 5}, + 370230: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 5}, + 370231: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 5}, + 370232: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 6}, + 370233: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 6}, + 370234: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 6}, + 370235: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 7}, + 370236: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 7}, + 370237: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 7}, + 370238: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Yellow key', + 'doom_type': 80, + 'episode': 2, + 'map': 9}, + 370239: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Blue key', + 'doom_type': 79, + 'episode': 2, + 'map': 9}, + 370240: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Green key', + 'doom_type': 73, + 'episode': 2, + 'map': 9}, + 370241: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 1}, + 370242: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 1}, + 370243: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 2}, + 370244: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 2}, + 370245: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 2}, + 370246: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 3}, + 370247: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 3}, + 370248: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 3}, + 370249: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 4}, + 370250: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 4}, + 370251: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 5}, + 370252: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 5}, + 370253: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 6}, + 370254: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 6}, + 370255: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 6}, + 370256: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 7}, + 370257: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 7}, + 370258: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 7}, + 370259: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Blue key', + 'doom_type': 79, + 'episode': 3, + 'map': 9}, + 370260: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Green key', + 'doom_type': 73, + 'episode': 3, + 'map': 9}, + 370261: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Yellow key', + 'doom_type': 80, + 'episode': 3, + 'map': 9}, + 370262: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 1}, + 370263: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 1}, + 370264: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 2}, + 370265: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 2}, + 370266: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 2}, + 370267: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 3}, + 370268: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 3}, + 370269: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 3}, + 370270: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 5}, + 370271: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 5}, + 370272: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 5}, + 370273: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 6}, + 370274: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 6}, + 370275: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 6}, + 370276: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 7}, + 370277: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Green key', + 'doom_type': 73, + 'episode': 4, + 'map': 7}, + 370278: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Blue key', + 'doom_type': 79, + 'episode': 4, + 'map': 7}, + 370279: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 8}, + 370280: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Yellow key', + 'doom_type': 80, + 'episode': 4, + 'map': 9}, + 370281: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 1}, + 370282: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 1}, + 370283: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 1}, + 370284: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 2}, + 370285: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 2}, + 370286: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 3}, + 370287: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 3}, + 370288: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 3}, + 370289: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 4}, + 370290: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 4}, + 370291: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 4}, + 370292: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 5}, + 370293: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 5}, + 370294: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 5}, + 370295: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 6}, + 370296: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 6}, + 370297: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 6}, + 370298: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Blue key', + 'doom_type': 79, + 'episode': 5, + 'map': 7}, + 370299: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Green key', + 'doom_type': 73, + 'episode': 5, + 'map': 7}, + 370300: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Yellow key', + 'doom_type': 80, + 'episode': 5, + 'map': 7}, + 370301: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Blue key", + 'doom_type': 79, + 'episode': 5, + 'map': 9}, + 370302: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Green key", + 'doom_type': 73, + 'episode': 5, + 'map': 9}, + 370303: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Yellow key", + 'doom_type': 80, + 'episode': 5, + 'map': 9}, + 370400: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1)', + 'doom_type': -1, + 'episode': 1, + 'map': 1}, + 370401: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Docks (E1M1) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 1}, + 370402: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Docks (E1M1) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 1}, + 370403: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2)', + 'doom_type': -1, + 'episode': 1, + 'map': 2}, + 370404: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 2}, + 370405: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Dungeons (E1M2) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 2}, + 370406: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3)', + 'doom_type': -1, + 'episode': 1, + 'map': 3}, + 370407: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 3}, + 370408: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Gatehouse (E1M3) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 3}, + 370409: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4)', + 'doom_type': -1, + 'episode': 1, + 'map': 4}, + 370410: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 4}, + 370411: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Guard Tower (E1M4) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 4}, + 370412: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5)', + 'doom_type': -1, + 'episode': 1, + 'map': 5}, + 370413: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Citadel (E1M5) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 5}, + 370414: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Citadel (E1M5) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 5}, + 370415: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6)', + 'doom_type': -1, + 'episode': 1, + 'map': 6}, + 370416: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 6}, + 370417: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Cathedral (E1M6) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 6}, + 370418: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7)', + 'doom_type': -1, + 'episode': 1, + 'map': 7}, + 370419: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crypts (E1M7) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 7}, + 370420: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crypts (E1M7) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 7}, + 370421: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Hell's Maw (E1M8)", + 'doom_type': -1, + 'episode': 1, + 'map': 8}, + 370422: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Hell's Maw (E1M8) - Complete", + 'doom_type': -2, + 'episode': 1, + 'map': 8}, + 370423: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "Hell's Maw (E1M8) - Map Scroll", + 'doom_type': 35, + 'episode': 1, + 'map': 8}, + 370424: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9)', + 'doom_type': -1, + 'episode': 1, + 'map': 9}, + 370425: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Complete', + 'doom_type': -2, + 'episode': 1, + 'map': 9}, + 370426: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Graveyard (E1M9) - Map Scroll', + 'doom_type': 35, + 'episode': 1, + 'map': 9}, + 370427: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1)', + 'doom_type': -1, + 'episode': 2, + 'map': 1}, + 370428: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Crater (E2M1) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 1}, + 370429: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Crater (E2M1) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 1}, + 370430: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2)', + 'doom_type': -1, + 'episode': 2, + 'map': 2}, + 370431: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 2}, + 370432: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Lava Pits (E2M2) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 2}, + 370433: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3)', + 'doom_type': -1, + 'episode': 2, + 'map': 3}, + 370434: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 3}, + 370435: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The River of Fire (E2M3) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 3}, + 370436: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4)', + 'doom_type': -1, + 'episode': 2, + 'map': 4}, + 370437: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 4}, + 370438: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Ice Grotto (E2M4) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 4}, + 370439: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5)', + 'doom_type': -1, + 'episode': 2, + 'map': 5}, + 370440: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 5}, + 370441: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Catacombs (E2M5) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 5}, + 370442: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6)', + 'doom_type': -1, + 'episode': 2, + 'map': 6}, + 370443: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 6}, + 370444: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Labyrinth (E2M6) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 6}, + 370445: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7)', + 'doom_type': -1, + 'episode': 2, + 'map': 7}, + 370446: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 7}, + 370447: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Great Hall (E2M7) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 7}, + 370448: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8)', + 'doom_type': -1, + 'episode': 2, + 'map': 8}, + 370449: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 8}, + 370450: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Portals of Chaos (E2M8) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 8}, + 370451: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9)', + 'doom_type': -1, + 'episode': 2, + 'map': 9}, + 370452: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Glacier (E2M9) - Complete', + 'doom_type': -2, + 'episode': 2, + 'map': 9}, + 370453: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Glacier (E2M9) - Map Scroll', + 'doom_type': 35, + 'episode': 2, + 'map': 9}, + 370454: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1)', + 'doom_type': -1, + 'episode': 3, + 'map': 1}, + 370455: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 1}, + 370456: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Storehouse (E3M1) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 1}, + 370457: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2)', + 'doom_type': -1, + 'episode': 3, + 'map': 2}, + 370458: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 2}, + 370459: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Cesspool (E3M2) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 2}, + 370460: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3)', + 'doom_type': -1, + 'episode': 3, + 'map': 3}, + 370461: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Confluence (E3M3) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 3}, + 370462: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Confluence (E3M3) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 3}, + 370463: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4)', + 'doom_type': -1, + 'episode': 3, + 'map': 4}, + 370464: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 4}, + 370465: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Azure Fortress (E3M4) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 4}, + 370466: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5)', + 'doom_type': -1, + 'episode': 3, + 'map': 5}, + 370467: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 5}, + 370468: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Ophidian Lair (E3M5) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 5}, + 370469: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6)', + 'doom_type': -1, + 'episode': 3, + 'map': 6}, + 370470: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 6}, + 370471: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Halls of Fear (E3M6) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 6}, + 370472: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7)', + 'doom_type': -1, + 'episode': 3, + 'map': 7}, + 370473: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Chasm (E3M7) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 7}, + 370474: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Chasm (E3M7) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 7}, + 370475: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8)", + 'doom_type': -1, + 'episode': 3, + 'map': 8}, + 370476: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8) - Complete", + 'doom_type': -2, + 'episode': 3, + 'map': 8}, + 370477: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "D'Sparil'S Keep (E3M8) - Map Scroll", + 'doom_type': 35, + 'episode': 3, + 'map': 8}, + 370478: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9)', + 'doom_type': -1, + 'episode': 3, + 'map': 9}, + 370479: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Complete', + 'doom_type': -2, + 'episode': 3, + 'map': 9}, + 370480: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'The Aquifier (E3M9) - Map Scroll', + 'doom_type': 35, + 'episode': 3, + 'map': 9}, + 370481: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1)', + 'doom_type': -1, + 'episode': 4, + 'map': 1}, + 370482: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Catafalque (E4M1) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 1}, + 370483: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Catafalque (E4M1) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 1}, + 370484: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2)', + 'doom_type': -1, + 'episode': 4, + 'map': 2}, + 370485: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 2}, + 370486: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Blockhouse (E4M2) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 2}, + 370487: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3)', + 'doom_type': -1, + 'episode': 4, + 'map': 3}, + 370488: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 3}, + 370489: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ambulatory (E4M3) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 3}, + 370490: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Sepulcher (E4M4)', + 'doom_type': -1, + 'episode': 4, + 'map': 4}, + 370491: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Sepulcher (E4M4) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 4}, + 370492: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Sepulcher (E4M4) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 4}, + 370493: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5)', + 'doom_type': -1, + 'episode': 4, + 'map': 5}, + 370494: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Great Stair (E4M5) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 5}, + 370495: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Great Stair (E4M5) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 5}, + 370496: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6)', + 'doom_type': -1, + 'episode': 4, + 'map': 6}, + 370497: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 6}, + 370498: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Halls of the Apostate (E4M6) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 6}, + 370499: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7)', + 'doom_type': -1, + 'episode': 4, + 'map': 7}, + 370500: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 7}, + 370501: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ramparts of Perdition (E4M7) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 7}, + 370502: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8)', + 'doom_type': -1, + 'episode': 4, + 'map': 8}, + 370503: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 8}, + 370504: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Shattered Bridge (E4M8) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 8}, + 370505: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9)', + 'doom_type': -1, + 'episode': 4, + 'map': 9}, + 370506: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Complete', + 'doom_type': -2, + 'episode': 4, + 'map': 9}, + 370507: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Mausoleum (E4M9) - Map Scroll', + 'doom_type': 35, + 'episode': 4, + 'map': 9}, + 370508: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1)', + 'doom_type': -1, + 'episode': 5, + 'map': 1}, + 370509: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 1}, + 370510: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Ochre Cliffs (E5M1) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 1}, + 370511: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2)', + 'doom_type': -1, + 'episode': 5, + 'map': 2}, + 370512: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Rapids (E5M2) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 2}, + 370513: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Rapids (E5M2) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 2}, + 370514: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3)', + 'doom_type': -1, + 'episode': 5, + 'map': 3}, + 370515: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Quay (E5M3) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 3}, + 370516: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Quay (E5M3) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 3}, + 370517: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4)', + 'doom_type': -1, + 'episode': 5, + 'map': 4}, + 370518: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Courtyard (E5M4) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 4}, + 370519: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Courtyard (E5M4) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 4}, + 370520: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5)', + 'doom_type': -1, + 'episode': 5, + 'map': 5}, + 370521: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 5}, + 370522: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Hydratyr (E5M5) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 5}, + 370523: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6)', + 'doom_type': -1, + 'episode': 5, + 'map': 6}, + 370524: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Colonnade (E5M6) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 6}, + 370525: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Colonnade (E5M6) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 6}, + 370526: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7)', + 'doom_type': -1, + 'episode': 5, + 'map': 7}, + 370527: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 7}, + 370528: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Foetid Manse (E5M7) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 7}, + 370529: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Field of Judgement (E5M8)', + 'doom_type': -1, + 'episode': 5, + 'map': 8}, + 370530: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Field of Judgement (E5M8) - Complete', + 'doom_type': -2, + 'episode': 5, + 'map': 8}, + 370531: {'classification': ItemClassification.filler, + 'count': 1, + 'name': 'Field of Judgement (E5M8) - Map Scroll', + 'doom_type': 35, + 'episode': 5, + 'map': 8}, + 370532: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9)", + 'doom_type': -1, + 'episode': 5, + 'map': 9}, + 370533: {'classification': ItemClassification.progression, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Complete", + 'doom_type': -2, + 'episode': 5, + 'map': 9}, + 370534: {'classification': ItemClassification.filler, + 'count': 1, + 'name': "Skein of D'Sparil (E5M9) - Map Scroll", + 'doom_type': 35, + 'episode': 5, + 'map': 9}, +} + + +item_name_groups: Dict[str, Set[str]] = { + 'Ammos': {'Crystal Geode', 'Energy Orb', 'Greater Runes', 'Inferno Orb', 'Pile of Mace Spheres', 'Quiver of Ethereal Arrows', }, + 'Armors': {'Enchanted Shield', 'Silver Shield', }, + 'Artifacts': {'Chaos Device', 'Morph Ovum', 'Mystic Urn', 'Quartz Flask', 'Ring of Invincibility', 'Shadowsphere', 'Timebomb of the Ancients', 'Tome of Power', 'Torch', }, + 'Keys': {'Ambulatory (E4M3) - Blue key', 'Ambulatory (E4M3) - Green key', 'Ambulatory (E4M3) - Yellow key', 'Blockhouse (E4M2) - Blue key', 'Blockhouse (E4M2) - Green key', 'Blockhouse (E4M2) - Yellow key', 'Catafalque (E4M1) - Green key', 'Catafalque (E4M1) - Yellow key', 'Colonnade (E5M6) - Blue key', 'Colonnade (E5M6) - Green key', 'Colonnade (E5M6) - Yellow key', 'Courtyard (E5M4) - Blue key', 'Courtyard (E5M4) - Green key', 'Courtyard (E5M4) - Yellow key', 'Foetid Manse (E5M7) - Blue key', 'Foetid Manse (E5M7) - Green key', 'Foetid Manse (E5M7) - Yellow key', 'Great Stair (E4M5) - Blue key', 'Great Stair (E4M5) - Green key', 'Great Stair (E4M5) - Yellow key', 'Halls of the Apostate (E4M6) - Blue key', 'Halls of the Apostate (E4M6) - Green key', 'Halls of the Apostate (E4M6) - Yellow key', 'Hydratyr (E5M5) - Blue key', 'Hydratyr (E5M5) - Green key', 'Hydratyr (E5M5) - Yellow key', 'Mausoleum (E4M9) - Yellow key', 'Ochre Cliffs (E5M1) - Blue key', 'Ochre Cliffs (E5M1) - Green key', 'Ochre Cliffs (E5M1) - Yellow key', 'Quay (E5M3) - Blue key', 'Quay (E5M3) - Green key', 'Quay (E5M3) - Yellow key', 'Ramparts of Perdition (E4M7) - Blue key', 'Ramparts of Perdition (E4M7) - Green key', 'Ramparts of Perdition (E4M7) - Yellow key', 'Rapids (E5M2) - Green key', 'Rapids (E5M2) - Yellow key', 'Shattered Bridge (E4M8) - Yellow key', "Skein of D'Sparil (E5M9) - Blue key", "Skein of D'Sparil (E5M9) - Green key", "Skein of D'Sparil (E5M9) - Yellow key", 'The Aquifier (E3M9) - Blue key', 'The Aquifier (E3M9) - Green key', 'The Aquifier (E3M9) - Yellow key', 'The Azure Fortress (E3M4) - Green key', 'The Azure Fortress (E3M4) - Yellow key', 'The Catacombs (E2M5) - Blue key', 'The Catacombs (E2M5) - Green key', 'The Catacombs (E2M5) - Yellow key', 'The Cathedral (E1M6) - Green key', 'The Cathedral (E1M6) - Yellow key', 'The Cesspool (E3M2) - Blue key', 'The Cesspool (E3M2) - Green key', 'The Cesspool (E3M2) - Yellow key', 'The Chasm (E3M7) - Blue key', 'The Chasm (E3M7) - Green key', 'The Chasm (E3M7) - Yellow key', 'The Citadel (E1M5) - Blue key', 'The Citadel (E1M5) - Green key', 'The Citadel (E1M5) - Yellow key', 'The Confluence (E3M3) - Blue key', 'The Confluence (E3M3) - Green key', 'The Confluence (E3M3) - Yellow key', 'The Crater (E2M1) - Green key', 'The Crater (E2M1) - Yellow key', 'The Crypts (E1M7) - Blue key', 'The Crypts (E1M7) - Green key', 'The Crypts (E1M7) - Yellow key', 'The Docks (E1M1) - Yellow key', 'The Dungeons (E1M2) - Blue key', 'The Dungeons (E1M2) - Green key', 'The Dungeons (E1M2) - Yellow key', 'The Gatehouse (E1M3) - Green key', 'The Gatehouse (E1M3) - Yellow key', 'The Glacier (E2M9) - Blue key', 'The Glacier (E2M9) - Green key', 'The Glacier (E2M9) - Yellow key', 'The Graveyard (E1M9) - Blue key', 'The Graveyard (E1M9) - Green key', 'The Graveyard (E1M9) - Yellow key', 'The Great Hall (E2M7) - Blue key', 'The Great Hall (E2M7) - Green key', 'The Great Hall (E2M7) - Yellow key', 'The Guard Tower (E1M4) - Green key', 'The Guard Tower (E1M4) - Yellow key', 'The Halls of Fear (E3M6) - Blue key', 'The Halls of Fear (E3M6) - Green key', 'The Halls of Fear (E3M6) - Yellow key', 'The Ice Grotto (E2M4) - Blue key', 'The Ice Grotto (E2M4) - Green key', 'The Ice Grotto (E2M4) - Yellow key', 'The Labyrinth (E2M6) - Blue key', 'The Labyrinth (E2M6) - Green key', 'The Labyrinth (E2M6) - Yellow key', 'The Lava Pits (E2M2) - Green key', 'The Lava Pits (E2M2) - Yellow key', 'The Ophidian Lair (E3M5) - Green key', 'The Ophidian Lair (E3M5) - Yellow key', 'The River of Fire (E2M3) - Blue key', 'The River of Fire (E2M3) - Green key', 'The River of Fire (E2M3) - Yellow key', 'The Storehouse (E3M1) - Green key', 'The Storehouse (E3M1) - Yellow key', }, + 'Levels': {'Ambulatory (E4M3)', 'Blockhouse (E4M2)', 'Catafalque (E4M1)', 'Colonnade (E5M6)', 'Courtyard (E5M4)', "D'Sparil'S Keep (E3M8)", 'Field of Judgement (E5M8)', 'Foetid Manse (E5M7)', 'Great Stair (E4M5)', 'Halls of the Apostate (E4M6)', "Hell's Maw (E1M8)", 'Hydratyr (E5M5)', 'Mausoleum (E4M9)', 'Ochre Cliffs (E5M1)', 'Quay (E5M3)', 'Ramparts of Perdition (E4M7)', 'Rapids (E5M2)', 'Sepulcher (E4M4)', 'Shattered Bridge (E4M8)', "Skein of D'Sparil (E5M9)", 'The Aquifier (E3M9)', 'The Azure Fortress (E3M4)', 'The Catacombs (E2M5)', 'The Cathedral (E1M6)', 'The Cesspool (E3M2)', 'The Chasm (E3M7)', 'The Citadel (E1M5)', 'The Confluence (E3M3)', 'The Crater (E2M1)', 'The Crypts (E1M7)', 'The Docks (E1M1)', 'The Dungeons (E1M2)', 'The Gatehouse (E1M3)', 'The Glacier (E2M9)', 'The Graveyard (E1M9)', 'The Great Hall (E2M7)', 'The Guard Tower (E1M4)', 'The Halls of Fear (E3M6)', 'The Ice Grotto (E2M4)', 'The Labyrinth (E2M6)', 'The Lava Pits (E2M2)', 'The Ophidian Lair (E3M5)', 'The Portals of Chaos (E2M8)', 'The River of Fire (E2M3)', 'The Storehouse (E3M1)', }, + 'Map Scrolls': {'Ambulatory (E4M3) - Map Scroll', 'Blockhouse (E4M2) - Map Scroll', 'Catafalque (E4M1) - Map Scroll', 'Colonnade (E5M6) - Map Scroll', 'Courtyard (E5M4) - Map Scroll', "D'Sparil'S Keep (E3M8) - Map Scroll", 'Field of Judgement (E5M8) - Map Scroll', 'Foetid Manse (E5M7) - Map Scroll', 'Great Stair (E4M5) - Map Scroll', 'Halls of the Apostate (E4M6) - Map Scroll', "Hell's Maw (E1M8) - Map Scroll", 'Hydratyr (E5M5) - Map Scroll', 'Mausoleum (E4M9) - Map Scroll', 'Ochre Cliffs (E5M1) - Map Scroll', 'Quay (E5M3) - Map Scroll', 'Ramparts of Perdition (E4M7) - Map Scroll', 'Rapids (E5M2) - Map Scroll', 'Sepulcher (E4M4) - Map Scroll', 'Shattered Bridge (E4M8) - Map Scroll', "Skein of D'Sparil (E5M9) - Map Scroll", 'The Aquifier (E3M9) - Map Scroll', 'The Azure Fortress (E3M4) - Map Scroll', 'The Catacombs (E2M5) - Map Scroll', 'The Cathedral (E1M6) - Map Scroll', 'The Cesspool (E3M2) - Map Scroll', 'The Chasm (E3M7) - Map Scroll', 'The Citadel (E1M5) - Map Scroll', 'The Confluence (E3M3) - Map Scroll', 'The Crater (E2M1) - Map Scroll', 'The Crypts (E1M7) - Map Scroll', 'The Docks (E1M1) - Map Scroll', 'The Dungeons (E1M2) - Map Scroll', 'The Gatehouse (E1M3) - Map Scroll', 'The Glacier (E2M9) - Map Scroll', 'The Graveyard (E1M9) - Map Scroll', 'The Great Hall (E2M7) - Map Scroll', 'The Guard Tower (E1M4) - Map Scroll', 'The Halls of Fear (E3M6) - Map Scroll', 'The Ice Grotto (E2M4) - Map Scroll', 'The Labyrinth (E2M6) - Map Scroll', 'The Lava Pits (E2M2) - Map Scroll', 'The Ophidian Lair (E3M5) - Map Scroll', 'The Portals of Chaos (E2M8) - Map Scroll', 'The River of Fire (E2M3) - Map Scroll', 'The Storehouse (E3M1) - Map Scroll', }, + 'Weapons': {'Dragon Claw', 'Ethereal Crossbow', 'Firemace', 'Gauntlets of the Necromancer', 'Hellstaff', 'Phoenix Rod', }, +} diff --git a/worlds/heretic/Locations.py b/worlds/heretic/Locations.py new file mode 100644 index 0000000000..f9590de776 --- /dev/null +++ b/worlds/heretic/Locations.py @@ -0,0 +1,8229 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import Dict, TypedDict, List, Set + + +class LocationDict(TypedDict, total=False): + name: str + episode: int + check_sanity: bool + map: int + index: int # Thing index as it is stored in the wad file. + doom_type: int # In case index end up unreliable, we can use doom type. Maps have often only one of each important things. + region: str + + +location_table: Dict[int, LocationDict] = { + 371000: {'name': 'The Docks (E1M1) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 5, + 'doom_type': 80, + 'region': "The Docks (E1M1) Main"}, + 371001: {'name': 'The Docks (E1M1) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 47, + 'doom_type': 85, + 'region': "The Docks (E1M1) Main"}, + 371002: {'name': 'The Docks (E1M1) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 52, + 'doom_type': 2005, + 'region': "The Docks (E1M1) Yellow"}, + 371003: {'name': 'The Docks (E1M1) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 55, + 'doom_type': 2001, + 'region': "The Docks (E1M1) Yellow"}, + 371004: {'name': 'The Docks (E1M1) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 91, + 'doom_type': 8, + 'region': "The Docks (E1M1) Sea"}, + 371005: {'name': 'The Docks (E1M1) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': 174, + 'doom_type': 86, + 'region': "The Docks (E1M1) Yellow"}, + 371006: {'name': 'The Docks (E1M1) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Docks (E1M1) Yellow"}, + 371007: {'name': 'The Dungeons (E1M2) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 1, + 'doom_type': 53, + 'region': "The Dungeons (E1M2) Yellow"}, + 371008: {'name': 'The Dungeons (E1M2) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 5, + 'doom_type': 80, + 'region': "The Dungeons (E1M2) Main"}, + 371009: {'name': 'The Dungeons (E1M2) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 17, + 'doom_type': 73, + 'region': "The Dungeons (E1M2) Yellow"}, + 371010: {'name': 'The Dungeons (E1M2) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 18, + 'doom_type': 85, + 'region': "The Dungeons (E1M2) Main"}, + 371011: {'name': 'The Dungeons (E1M2) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 33, + 'region': "The Dungeons (E1M2) Main"}, + 371012: {'name': 'The Dungeons (E1M2) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 29, + 'doom_type': 35, + 'region': "The Dungeons (E1M2) Yellow"}, + 371013: {'name': 'The Dungeons (E1M2) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 41, + 'doom_type': 75, + 'region': "The Dungeons (E1M2) Yellow"}, + 371014: {'name': 'The Dungeons (E1M2) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 44, + 'doom_type': 8, + 'region': "The Dungeons (E1M2) Green"}, + 371015: {'name': 'The Dungeons (E1M2) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 45, + 'doom_type': 79, + 'region': "The Dungeons (E1M2) Green"}, + 371016: {'name': 'The Dungeons (E1M2) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': True, + 'map': 2, + 'index': 46, + 'doom_type': 84, + 'region': "The Dungeons (E1M2) Yellow"}, + 371017: {'name': 'The Dungeons (E1M2) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 77, + 'doom_type': 86, + 'region': "The Dungeons (E1M2) Main"}, + 371018: {'name': 'The Dungeons (E1M2) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 80, + 'doom_type': 2001, + 'region': "The Dungeons (E1M2) Main"}, + 371019: {'name': 'The Dungeons (E1M2) - Tome of Power 2', + 'episode': 1, + 'check_sanity': True, + 'map': 2, + 'index': 81, + 'doom_type': 86, + 'region': "The Dungeons (E1M2) Yellow"}, + 371020: {'name': 'The Dungeons (E1M2) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 253, + 'doom_type': 2005, + 'region': "The Dungeons (E1M2) Yellow"}, + 371021: {'name': 'The Dungeons (E1M2) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': 303, + 'doom_type': 85, + 'region': "The Dungeons (E1M2) Yellow"}, + 371022: {'name': 'The Dungeons (E1M2) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Dungeons (E1M2) Blue"}, + 371023: {'name': 'The Gatehouse (E1M3) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 8, + 'doom_type': 80, + 'region': "The Gatehouse (E1M3) Main"}, + 371024: {'name': 'The Gatehouse (E1M3) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 9, + 'doom_type': 73, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371025: {'name': 'The Gatehouse (E1M3) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 53, + 'region': "The Gatehouse (E1M3) Main"}, + 371026: {'name': 'The Gatehouse (E1M3) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 22, + 'doom_type': 85, + 'region': "The Gatehouse (E1M3) Main"}, + 371027: {'name': 'The Gatehouse (E1M3) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 24, + 'doom_type': 2001, + 'region': "The Gatehouse (E1M3) Main"}, + 371028: {'name': 'The Gatehouse (E1M3) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 81, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Sea"}, + 371029: {'name': 'The Gatehouse (E1M3) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 134, + 'doom_type': 8, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371030: {'name': 'The Gatehouse (E1M3) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 145, + 'doom_type': 2005, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371031: {'name': 'The Gatehouse (E1M3) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 203, + 'doom_type': 33, + 'region': "The Gatehouse (E1M3) Main"}, + 371032: {'name': 'The Gatehouse (E1M3) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 220, + 'doom_type': 84, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371033: {'name': 'The Gatehouse (E1M3) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 221, + 'doom_type': 75, + 'region': "The Gatehouse (E1M3) Main"}, + 371034: {'name': 'The Gatehouse (E1M3) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 222, + 'doom_type': 30, + 'region': "The Gatehouse (E1M3) Yellow"}, + 371035: {'name': 'The Gatehouse (E1M3) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 286, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Main"}, + 371036: {'name': 'The Gatehouse (E1M3) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': 287, + 'doom_type': 86, + 'region': "The Gatehouse (E1M3) Main"}, + 371037: {'name': 'The Gatehouse (E1M3) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Gatehouse (E1M3) Green"}, + 371038: {'name': 'The Guard Tower (E1M4) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 0, + 'doom_type': 2005, + 'region': "The Guard Tower (E1M4) Main"}, + 371039: {'name': 'The Guard Tower (E1M4) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 2, + 'doom_type': 53, + 'region': "The Guard Tower (E1M4) Main"}, + 371040: {'name': 'The Guard Tower (E1M4) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 3, + 'doom_type': 2001, + 'region': "The Guard Tower (E1M4) Main"}, + 371041: {'name': 'The Guard Tower (E1M4) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 4, + 'doom_type': 80, + 'region': "The Guard Tower (E1M4) Main"}, + 371042: {'name': 'The Guard Tower (E1M4) - Morph Ovum', + 'episode': 1, + 'check_sanity': True, + 'map': 4, + 'index': 5, + 'doom_type': 30, + 'region': "The Guard Tower (E1M4) Main"}, + 371043: {'name': 'The Guard Tower (E1M4) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 57, + 'doom_type': 75, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371044: {'name': 'The Guard Tower (E1M4) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 60, + 'doom_type': 73, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371045: {'name': 'The Guard Tower (E1M4) - Bag of Holding', + 'episode': 1, + 'check_sanity': True, + 'map': 4, + 'index': 61, + 'doom_type': 8, + 'region': "The Guard Tower (E1M4) Main"}, + 371046: {'name': 'The Guard Tower (E1M4) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 64, + 'doom_type': 35, + 'region': "The Guard Tower (E1M4) Main"}, + 371047: {'name': 'The Guard Tower (E1M4) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 77, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Main"}, + 371048: {'name': 'The Guard Tower (E1M4) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 78, + 'doom_type': 85, + 'region': "The Guard Tower (E1M4) Main"}, + 371049: {'name': 'The Guard Tower (E1M4) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 143, + 'doom_type': 33, + 'region': "The Guard Tower (E1M4) Main"}, + 371050: {'name': 'The Guard Tower (E1M4) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 220, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Yellow"}, + 371051: {'name': 'The Guard Tower (E1M4) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': 221, + 'doom_type': 86, + 'region': "The Guard Tower (E1M4) Main"}, + 371052: {'name': 'The Guard Tower (E1M4) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Guard Tower (E1M4) Green"}, + 371053: {'name': 'The Citadel (E1M5) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 1, + 'doom_type': 73, + 'region': "The Citadel (E1M5) Yellow"}, + 371054: {'name': 'The Citadel (E1M5) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 5, + 'doom_type': 80, + 'region': "The Citadel (E1M5) Main"}, + 371055: {'name': 'The Citadel (E1M5) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 19, + 'doom_type': 79, + 'region': "The Citadel (E1M5) Green"}, + 371056: {'name': 'The Citadel (E1M5) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 23, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Well"}, + 371057: {'name': 'The Citadel (E1M5) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 28, + 'doom_type': 2001, + 'region': "The Citadel (E1M5) Yellow"}, + 371058: {'name': 'The Citadel (E1M5) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 29, + 'doom_type': 2005, + 'region': "The Citadel (E1M5) Main"}, + 371059: {'name': 'The Citadel (E1M5) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 30, + 'doom_type': 53, + 'region': "The Citadel (E1M5) Green"}, + 371060: {'name': 'The Citadel (E1M5) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 31, + 'doom_type': 84, + 'region': "The Citadel (E1M5) Green"}, + 371061: {'name': 'The Citadel (E1M5) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 78, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Blue"}, + 371062: {'name': 'The Citadel (E1M5) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 79, + 'doom_type': 75, + 'region': "The Citadel (E1M5) Main"}, + 371063: {'name': 'The Citadel (E1M5) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 80, + 'doom_type': 8, + 'region': "The Citadel (E1M5) Green"}, + 371064: {'name': 'The Citadel (E1M5) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 103, + 'doom_type': 33, + 'region': "The Citadel (E1M5) Main"}, + 371065: {'name': 'The Citadel (E1M5) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 105, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371066: {'name': 'The Citadel (E1M5) - Silver Shield', + 'episode': 1, + 'check_sanity': True, + 'map': 5, + 'index': 129, + 'doom_type': 85, + 'region': "The Citadel (E1M5) Main"}, + 371067: {'name': 'The Citadel (E1M5) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 192, + 'doom_type': 30, + 'region': "The Citadel (E1M5) Green"}, + 371068: {'name': 'The Citadel (E1M5) - Map Scroll', + 'episode': 1, + 'check_sanity': True, + 'map': 5, + 'index': 203, + 'doom_type': 35, + 'region': "The Citadel (E1M5) Blue"}, + 371069: {'name': 'The Citadel (E1M5) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 204, + 'doom_type': 85, + 'region': "The Citadel (E1M5) Blue"}, + 371070: {'name': 'The Citadel (E1M5) - Torch 2', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 205, + 'doom_type': 33, + 'region': "The Citadel (E1M5) Green"}, + 371071: {'name': 'The Citadel (E1M5) - Tome of Power 4', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 319, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371072: {'name': 'The Citadel (E1M5) - Tome of Power 5', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': 320, + 'doom_type': 86, + 'region': "The Citadel (E1M5) Green"}, + 371073: {'name': 'The Citadel (E1M5) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Citadel (E1M5) Blue"}, + 371074: {'name': 'The Cathedral (E1M6) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 8, + 'doom_type': 80, + 'region': "The Cathedral (E1M6) Main"}, + 371075: {'name': 'The Cathedral (E1M6) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 9, + 'doom_type': 2005, + 'region': "The Cathedral (E1M6) Main"}, + 371076: {'name': 'The Cathedral (E1M6) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 39, + 'doom_type': 2001, + 'region': "The Cathedral (E1M6) Yellow"}, + 371077: {'name': 'The Cathedral (E1M6) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 45, + 'doom_type': 53, + 'region': "The Cathedral (E1M6) Yellow"}, + 371078: {'name': 'The Cathedral (E1M6) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 56, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Yellow"}, + 371079: {'name': 'The Cathedral (E1M6) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 61, + 'doom_type': 75, + 'region': "The Cathedral (E1M6) Yellow"}, + 371080: {'name': 'The Cathedral (E1M6) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 98, + 'doom_type': 73, + 'region': "The Cathedral (E1M6) Yellow"}, + 371081: {'name': 'The Cathedral (E1M6) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 138, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Yellow"}, + 371082: {'name': 'The Cathedral (E1M6) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 139, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Yellow"}, + 371083: {'name': 'The Cathedral (E1M6) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 142, + 'doom_type': 84, + 'region': "The Cathedral (E1M6) Yellow"}, + 371084: {'name': 'The Cathedral (E1M6) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 217, + 'doom_type': 33, + 'region': "The Cathedral (E1M6) Yellow"}, + 371085: {'name': 'The Cathedral (E1M6) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 273, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Yellow"}, + 371086: {'name': 'The Cathedral (E1M6) - Tome of Power 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 274, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Main"}, + 371087: {'name': 'The Cathedral (E1M6) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 277, + 'doom_type': 30, + 'region': "The Cathedral (E1M6) Yellow"}, + 371088: {'name': 'The Cathedral (E1M6) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 279, + 'doom_type': 35, + 'region': "The Cathedral (E1M6) Yellow"}, + 371089: {'name': 'The Cathedral (E1M6) - Ring of Invincibility 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 280, + 'doom_type': 84, + 'region': "The Cathedral (E1M6) Yellow"}, + 371090: {'name': 'The Cathedral (E1M6) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 281, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Green"}, + 371091: {'name': 'The Cathedral (E1M6) - Tome of Power 4', + 'episode': 1, + 'check_sanity': True, + 'map': 6, + 'index': 371, + 'doom_type': 86, + 'region': "The Cathedral (E1M6) Green"}, + 371092: {'name': 'The Cathedral (E1M6) - Bag of Holding 2', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 449, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Green"}, + 371093: {'name': 'The Cathedral (E1M6) - Silver Shield 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 457, + 'doom_type': 85, + 'region': "The Cathedral (E1M6) Main Fly"}, + 371094: {'name': 'The Cathedral (E1M6) - Bag of Holding 3', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': 458, + 'doom_type': 8, + 'region': "The Cathedral (E1M6) Main Fly"}, + 371095: {'name': 'The Cathedral (E1M6) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Cathedral (E1M6) Green"}, + 371096: {'name': 'The Crypts (E1M7) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 11, + 'doom_type': 80, + 'region': "The Crypts (E1M7) Main"}, + 371097: {'name': 'The Crypts (E1M7) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 17, + 'doom_type': 2001, + 'region': "The Crypts (E1M7) Yellow"}, + 371098: {'name': 'The Crypts (E1M7) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 21, + 'doom_type': 73, + 'region': "The Crypts (E1M7) Yellow"}, + 371099: {'name': 'The Crypts (E1M7) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 25, + 'doom_type': 79, + 'region': "The Crypts (E1M7) Green"}, + 371100: {'name': 'The Crypts (E1M7) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 26, + 'doom_type': 30, + 'region': "The Crypts (E1M7) Yellow"}, + 371101: {'name': 'The Crypts (E1M7) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 45, + 'doom_type': 53, + 'region': "The Crypts (E1M7) Yellow"}, + 371102: {'name': 'The Crypts (E1M7) - Gauntlets of the Necromancer', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 46, + 'doom_type': 2005, + 'region': "The Crypts (E1M7) Main"}, + 371103: {'name': 'The Crypts (E1M7) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 53, + 'doom_type': 86, + 'region': "The Crypts (E1M7) Yellow"}, + 371104: {'name': 'The Crypts (E1M7) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 90, + 'doom_type': 84, + 'region': "The Crypts (E1M7) Yellow"}, + 371105: {'name': 'The Crypts (E1M7) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 98, + 'doom_type': 85, + 'region': "The Crypts (E1M7) Green"}, + 371106: {'name': 'The Crypts (E1M7) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 130, + 'doom_type': 8, + 'region': "The Crypts (E1M7) Blue"}, + 371107: {'name': 'The Crypts (E1M7) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 213, + 'doom_type': 33, + 'region': "The Crypts (E1M7) Green"}, + 371108: {'name': 'The Crypts (E1M7) - Torch 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 214, + 'doom_type': 33, + 'region': "The Crypts (E1M7) Blue"}, + 371109: {'name': 'The Crypts (E1M7) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 215, + 'doom_type': 86, + 'region': "The Crypts (E1M7) Yellow"}, + 371110: {'name': 'The Crypts (E1M7) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 224, + 'doom_type': 75, + 'region': "The Crypts (E1M7) Yellow"}, + 371111: {'name': 'The Crypts (E1M7) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 231, + 'doom_type': 35, + 'region': "The Crypts (E1M7) Blue"}, + 371112: {'name': 'The Crypts (E1M7) - Silver Shield 2', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': 232, + 'doom_type': 85, + 'region': "The Crypts (E1M7) Green"}, + 371113: {'name': 'The Crypts (E1M7) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Crypts (E1M7) Blue"}, + 371114: {'name': "Hell's Maw (E1M8) - Ethereal Crossbow", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 10, + 'doom_type': 2001, + 'region': "Hell's Maw (E1M8) Main"}, + 371115: {'name': "Hell's Maw (E1M8) - Dragon Claw", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 11, + 'doom_type': 53, + 'region': "Hell's Maw (E1M8) Main"}, + 371116: {'name': "Hell's Maw (E1M8) - Tome of Power", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 63, + 'doom_type': 86, + 'region': "Hell's Maw (E1M8) Main"}, + 371117: {'name': "Hell's Maw (E1M8) - Gauntlets of the Necromancer", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 64, + 'doom_type': 2005, + 'region': "Hell's Maw (E1M8) Main"}, + 371118: {'name': "Hell's Maw (E1M8) - Tome of Power 2", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 65, + 'doom_type': 86, + 'region': "Hell's Maw (E1M8) Main"}, + 371119: {'name': "Hell's Maw (E1M8) - Silver Shield", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 101, + 'doom_type': 85, + 'region': "Hell's Maw (E1M8) Main"}, + 371120: {'name': "Hell's Maw (E1M8) - Shadowsphere", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 102, + 'doom_type': 75, + 'region': "Hell's Maw (E1M8) Main"}, + 371121: {'name': "Hell's Maw (E1M8) - Ring of Invincibility", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 103, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371122: {'name': "Hell's Maw (E1M8) - Bag of Holding", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 104, + 'doom_type': 8, + 'region': "Hell's Maw (E1M8) Main"}, + 371123: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 2", + 'episode': 1, + 'check_sanity': True, + 'map': 8, + 'index': 237, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371124: {'name': "Hell's Maw (E1M8) - Bag of Holding 2", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 238, + 'doom_type': 8, + 'region': "Hell's Maw (E1M8) Main"}, + 371125: {'name': "Hell's Maw (E1M8) - Ring of Invincibility 3", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 247, + 'doom_type': 84, + 'region': "Hell's Maw (E1M8) Main"}, + 371126: {'name': "Hell's Maw (E1M8) - Morph Ovum", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': 290, + 'doom_type': 30, + 'region': "Hell's Maw (E1M8) Main"}, + 371127: {'name': "Hell's Maw (E1M8) - Exit", + 'episode': 1, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Hell's Maw (E1M8) Main"}, + 371128: {'name': 'The Graveyard (E1M9) - Yellow key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 2, + 'doom_type': 80, + 'region': "The Graveyard (E1M9) Main"}, + 371129: {'name': 'The Graveyard (E1M9) - Green key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 21, + 'doom_type': 73, + 'region': "The Graveyard (E1M9) Yellow"}, + 371130: {'name': 'The Graveyard (E1M9) - Blue key', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 22, + 'doom_type': 79, + 'region': "The Graveyard (E1M9) Green"}, + 371131: {'name': 'The Graveyard (E1M9) - Bag of Holding', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 23, + 'doom_type': 8, + 'region': "The Graveyard (E1M9) Main"}, + 371132: {'name': 'The Graveyard (E1M9) - Dragon Claw', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 109, + 'doom_type': 53, + 'region': "The Graveyard (E1M9) Yellow"}, + 371133: {'name': 'The Graveyard (E1M9) - Ethereal Crossbow', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 110, + 'doom_type': 2001, + 'region': "The Graveyard (E1M9) Green"}, + 371134: {'name': 'The Graveyard (E1M9) - Shadowsphere', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 128, + 'doom_type': 75, + 'region': "The Graveyard (E1M9) Green"}, + 371135: {'name': 'The Graveyard (E1M9) - Silver Shield', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 129, + 'doom_type': 85, + 'region': "The Graveyard (E1M9) Main"}, + 371136: {'name': 'The Graveyard (E1M9) - Ring of Invincibility', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 217, + 'doom_type': 84, + 'region': "The Graveyard (E1M9) Green"}, + 371137: {'name': 'The Graveyard (E1M9) - Torch', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 253, + 'doom_type': 33, + 'region': "The Graveyard (E1M9) Green"}, + 371138: {'name': 'The Graveyard (E1M9) - Tome of Power', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 254, + 'doom_type': 86, + 'region': "The Graveyard (E1M9) Main"}, + 371139: {'name': 'The Graveyard (E1M9) - Morph Ovum', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 279, + 'doom_type': 30, + 'region': "The Graveyard (E1M9) Main"}, + 371140: {'name': 'The Graveyard (E1M9) - Map Scroll', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 280, + 'doom_type': 35, + 'region': "The Graveyard (E1M9) Blue"}, + 371141: {'name': 'The Graveyard (E1M9) - Dragon Claw 2', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 292, + 'doom_type': 53, + 'region': "The Graveyard (E1M9) Main"}, + 371142: {'name': 'The Graveyard (E1M9) - Tome of Power 2', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': 339, + 'doom_type': 86, + 'region': "The Graveyard (E1M9) Green"}, + 371143: {'name': 'The Graveyard (E1M9) - Exit', + 'episode': 1, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Graveyard (E1M9) Blue"}, + 371144: {'name': 'The Crater (E2M1) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 8, + 'doom_type': 80, + 'region': "The Crater (E2M1) Main"}, + 371145: {'name': 'The Crater (E2M1) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "The Crater (E2M1) Yellow"}, + 371146: {'name': 'The Crater (E2M1) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 39, + 'doom_type': 2001, + 'region': "The Crater (E2M1) Main"}, + 371147: {'name': 'The Crater (E2M1) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 49, + 'doom_type': 86, + 'region': "The Crater (E2M1) Main"}, + 371148: {'name': 'The Crater (E2M1) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 90, + 'doom_type': 53, + 'region': "The Crater (E2M1) Yellow"}, + 371149: {'name': 'The Crater (E2M1) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 98, + 'doom_type': 8, + 'region': "The Crater (E2M1) Yellow"}, + 371150: {'name': 'The Crater (E2M1) - Hellstaff', + 'episode': 2, + 'check_sanity': True, + 'map': 1, + 'index': 103, + 'doom_type': 2004, + 'region': "The Crater (E2M1) Yellow"}, + 371151: {'name': 'The Crater (E2M1) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 141, + 'doom_type': 75, + 'region': "The Crater (E2M1) Yellow"}, + 371152: {'name': 'The Crater (E2M1) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 145, + 'doom_type': 85, + 'region': "The Crater (E2M1) Main"}, + 371153: {'name': 'The Crater (E2M1) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 146, + 'doom_type': 33, + 'region': "The Crater (E2M1) Main"}, + 371154: {'name': 'The Crater (E2M1) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': 236, + 'doom_type': 32, + 'region': "The Crater (E2M1) Yellow"}, + 371155: {'name': 'The Crater (E2M1) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Crater (E2M1) Green"}, + 371156: {'name': 'The Lava Pits (E2M2) - Green key', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 8, + 'doom_type': 73, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371157: {'name': 'The Lava Pits (E2M2) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 9, + 'doom_type': 80, + 'region': "The Lava Pits (E2M2) Main"}, + 371158: {'name': 'The Lava Pits (E2M2) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 25, + 'doom_type': 2001, + 'region': "The Lava Pits (E2M2) Main"}, + 371159: {'name': 'The Lava Pits (E2M2) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 67, + 'doom_type': 75, + 'region': "The Lava Pits (E2M2) Main"}, + 371160: {'name': 'The Lava Pits (E2M2) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 98, + 'doom_type': 84, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371161: {'name': 'The Lava Pits (E2M2) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 109, + 'doom_type': 53, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371162: {'name': 'The Lava Pits (E2M2) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 117, + 'doom_type': 2004, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371163: {'name': 'The Lava Pits (E2M2) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 122, + 'doom_type': 8, + 'region': "The Lava Pits (E2M2) Green"}, + 371164: {'name': 'The Lava Pits (E2M2) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 123, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371165: {'name': 'The Lava Pits (E2M2) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 124, + 'doom_type': 85, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371166: {'name': 'The Lava Pits (E2M2) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 127, + 'doom_type': 2005, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371167: {'name': 'The Lava Pits (E2M2) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 133, + 'doom_type': 31, + 'region': "The Lava Pits (E2M2) Green"}, + 371168: {'name': 'The Lava Pits (E2M2) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 230, + 'doom_type': 32, + 'region': "The Lava Pits (E2M2) Green"}, + 371169: {'name': 'The Lava Pits (E2M2) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 232, + 'doom_type': 35, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371170: {'name': 'The Lava Pits (E2M2) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 233, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Main"}, + 371171: {'name': 'The Lava Pits (E2M2) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 234, + 'doom_type': 36, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371172: {'name': 'The Lava Pits (E2M2) - Tome of Power 3', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 323, + 'doom_type': 86, + 'region': "The Lava Pits (E2M2) Main"}, + 371173: {'name': 'The Lava Pits (E2M2) - Silver Shield 2', + 'episode': 2, + 'check_sanity': True, + 'map': 2, + 'index': 324, + 'doom_type': 85, + 'region': "The Lava Pits (E2M2) Main"}, + 371174: {'name': 'The Lava Pits (E2M2) - Bag of Holding 2', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 329, + 'doom_type': 8, + 'region': "The Lava Pits (E2M2) Main"}, + 371175: {'name': 'The Lava Pits (E2M2) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': 341, + 'doom_type': 30, + 'region': "The Lava Pits (E2M2) Yellow"}, + 371176: {'name': 'The Lava Pits (E2M2) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Lava Pits (E2M2) Green"}, + 371177: {'name': 'The River of Fire (E2M3) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 9, + 'doom_type': 80, + 'region': "The River of Fire (E2M3) Main"}, + 371178: {'name': 'The River of Fire (E2M3) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 79, + 'region': "The River of Fire (E2M3) Main"}, + 371179: {'name': 'The River of Fire (E2M3) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 17, + 'doom_type': 73, + 'region': "The River of Fire (E2M3) Yellow"}, + 371180: {'name': 'The River of Fire (E2M3) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 26, + 'doom_type': 2005, + 'region': "The River of Fire (E2M3) Main"}, + 371181: {'name': 'The River of Fire (E2M3) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 57, + 'doom_type': 2001, + 'region': "The River of Fire (E2M3) Main"}, + 371182: {'name': 'The River of Fire (E2M3) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 92, + 'doom_type': 53, + 'region': "The River of Fire (E2M3) Main"}, + 371183: {'name': 'The River of Fire (E2M3) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 122, + 'doom_type': 2003, + 'region': "The River of Fire (E2M3) Main"}, + 371184: {'name': 'The River of Fire (E2M3) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 128, + 'doom_type': 2004, + 'region': "The River of Fire (E2M3) Blue"}, + 371185: {'name': 'The River of Fire (E2M3) - Bag of Holding', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 136, + 'doom_type': 8, + 'region': "The River of Fire (E2M3) Blue"}, + 371186: {'name': 'The River of Fire (E2M3) - Shadowsphere', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 145, + 'doom_type': 75, + 'region': "The River of Fire (E2M3) Green"}, + 371187: {'name': 'The River of Fire (E2M3) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 146, + 'doom_type': 86, + 'region': "The River of Fire (E2M3) Main"}, + 371188: {'name': 'The River of Fire (E2M3) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 147, + 'doom_type': 84, + 'region': "The River of Fire (E2M3) Main"}, + 371189: {'name': 'The River of Fire (E2M3) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 148, + 'doom_type': 85, + 'region': "The River of Fire (E2M3) Main"}, + 371190: {'name': 'The River of Fire (E2M3) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 297, + 'doom_type': 31, + 'region': "The River of Fire (E2M3) Blue"}, + 371191: {'name': 'The River of Fire (E2M3) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 298, + 'doom_type': 36, + 'region': "The River of Fire (E2M3) Blue"}, + 371192: {'name': 'The River of Fire (E2M3) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 299, + 'doom_type': 32, + 'region': "The River of Fire (E2M3) Main"}, + 371193: {'name': 'The River of Fire (E2M3) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 300, + 'doom_type': 30, + 'region': "The River of Fire (E2M3) Yellow"}, + 371194: {'name': 'The River of Fire (E2M3) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 313, + 'doom_type': 86, + 'region': "The River of Fire (E2M3) Green"}, + 371195: {'name': 'The River of Fire (E2M3) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': 413, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Main"}, + 371196: {'name': 'The River of Fire (E2M3) - Firemace 2', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 441, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Yellow"}, + 371197: {'name': 'The River of Fire (E2M3) - Firemace 3', + 'episode': 2, + 'check_sanity': True, + 'map': 3, + 'index': 448, + 'doom_type': 2002, + 'region': "The River of Fire (E2M3) Blue"}, + 371198: {'name': 'The River of Fire (E2M3) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The River of Fire (E2M3) Blue"}, + 371199: {'name': 'The Ice Grotto (E2M4) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 18, + 'doom_type': 80, + 'region': "The Ice Grotto (E2M4) Main"}, + 371200: {'name': 'The Ice Grotto (E2M4) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 19, + 'doom_type': 79, + 'region': "The Ice Grotto (E2M4) Green"}, + 371201: {'name': 'The Ice Grotto (E2M4) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 28, + 'doom_type': 73, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371202: {'name': 'The Ice Grotto (E2M4) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 29, + 'doom_type': 2003, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371203: {'name': 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': True, + 'map': 4, + 'index': 30, + 'doom_type': 2005, + 'region': "The Ice Grotto (E2M4) Main"}, + 371204: {'name': 'The Ice Grotto (E2M4) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 31, + 'doom_type': 2001, + 'region': "The Ice Grotto (E2M4) Main"}, + 371205: {'name': 'The Ice Grotto (E2M4) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 32, + 'doom_type': 2004, + 'region': "The Ice Grotto (E2M4) Blue"}, + 371206: {'name': 'The Ice Grotto (E2M4) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 33, + 'doom_type': 53, + 'region': "The Ice Grotto (E2M4) Green"}, + 371207: {'name': 'The Ice Grotto (E2M4) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 34, + 'doom_type': 33, + 'region': "The Ice Grotto (E2M4) Green"}, + 371208: {'name': 'The Ice Grotto (E2M4) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 35, + 'doom_type': 8, + 'region': "The Ice Grotto (E2M4) Main"}, + 371209: {'name': 'The Ice Grotto (E2M4) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 36, + 'doom_type': 75, + 'region': "The Ice Grotto (E2M4) Green"}, + 371210: {'name': 'The Ice Grotto (E2M4) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 37, + 'doom_type': 36, + 'region': "The Ice Grotto (E2M4) Green"}, + 371211: {'name': 'The Ice Grotto (E2M4) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 38, + 'doom_type': 85, + 'region': "The Ice Grotto (E2M4) Main"}, + 371212: {'name': 'The Ice Grotto (E2M4) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 39, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Green"}, + 371213: {'name': 'The Ice Grotto (E2M4) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 40, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Main"}, + 371214: {'name': 'The Ice Grotto (E2M4) - Bag of Holding 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 41, + 'doom_type': 8, + 'region': "The Ice Grotto (E2M4) Green"}, + 371215: {'name': 'The Ice Grotto (E2M4) - Tome of Power 3', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 128, + 'doom_type': 86, + 'region': "The Ice Grotto (E2M4) Yellow"}, + 371216: {'name': 'The Ice Grotto (E2M4) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 283, + 'doom_type': 35, + 'region': "The Ice Grotto (E2M4) Green"}, + 371217: {'name': 'The Ice Grotto (E2M4) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 289, + 'doom_type': 32, + 'region': "The Ice Grotto (E2M4) Magenta"}, + 371218: {'name': 'The Ice Grotto (E2M4) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 291, + 'doom_type': 31, + 'region': "The Ice Grotto (E2M4) Green"}, + 371219: {'name': 'The Ice Grotto (E2M4) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 299, + 'doom_type': 30, + 'region': "The Ice Grotto (E2M4) Main"}, + 371220: {'name': 'The Ice Grotto (E2M4) - Shadowsphere 2', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': 300, + 'doom_type': 75, + 'region': "The Ice Grotto (E2M4) Main"}, + 371221: {'name': 'The Ice Grotto (E2M4) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Ice Grotto (E2M4) Blue"}, + 371222: {'name': 'The Catacombs (E2M5) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 14, + 'doom_type': 80, + 'region': "The Catacombs (E2M5) Main"}, + 371223: {'name': 'The Catacombs (E2M5) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 25, + 'doom_type': 79, + 'region': "The Catacombs (E2M5) Green"}, + 371224: {'name': 'The Catacombs (E2M5) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 27, + 'doom_type': 2004, + 'region': "The Catacombs (E2M5) Yellow"}, + 371225: {'name': 'The Catacombs (E2M5) - Phoenix Rod', + 'episode': 2, + 'check_sanity': True, + 'map': 5, + 'index': 44, + 'doom_type': 2003, + 'region': "The Catacombs (E2M5) Green"}, + 371226: {'name': 'The Catacombs (E2M5) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 107, + 'doom_type': 2001, + 'region': "The Catacombs (E2M5) Yellow"}, + 371227: {'name': 'The Catacombs (E2M5) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 108, + 'doom_type': 2005, + 'region': "The Catacombs (E2M5) Main"}, + 371228: {'name': 'The Catacombs (E2M5) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 109, + 'doom_type': 53, + 'region': "The Catacombs (E2M5) Main"}, + 371229: {'name': 'The Catacombs (E2M5) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 110, + 'doom_type': 8, + 'region': "The Catacombs (E2M5) Main"}, + 371230: {'name': 'The Catacombs (E2M5) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 112, + 'doom_type': 85, + 'region': "The Catacombs (E2M5) Yellow"}, + 371231: {'name': 'The Catacombs (E2M5) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 113, + 'doom_type': 75, + 'region': "The Catacombs (E2M5) Yellow"}, + 371232: {'name': 'The Catacombs (E2M5) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 114, + 'doom_type': 84, + 'region': "The Catacombs (E2M5) Yellow"}, + 371233: {'name': 'The Catacombs (E2M5) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 115, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371234: {'name': 'The Catacombs (E2M5) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 116, + 'doom_type': 73, + 'region': "The Catacombs (E2M5) Yellow"}, + 371235: {'name': 'The Catacombs (E2M5) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 263, + 'doom_type': 36, + 'region': "The Catacombs (E2M5) Yellow"}, + 371236: {'name': 'The Catacombs (E2M5) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 322, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371237: {'name': 'The Catacombs (E2M5) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 323, + 'doom_type': 35, + 'region': "The Catacombs (E2M5) Green"}, + 371238: {'name': 'The Catacombs (E2M5) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 324, + 'doom_type': 32, + 'region': "The Catacombs (E2M5) Yellow"}, + 371239: {'name': 'The Catacombs (E2M5) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 325, + 'doom_type': 30, + 'region': "The Catacombs (E2M5) Green"}, + 371240: {'name': 'The Catacombs (E2M5) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 326, + 'doom_type': 31, + 'region': "The Catacombs (E2M5) Green"}, + 371241: {'name': 'The Catacombs (E2M5) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 327, + 'doom_type': 33, + 'region': "The Catacombs (E2M5) Main"}, + 371242: {'name': 'The Catacombs (E2M5) - Tome of Power 3', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': 328, + 'doom_type': 86, + 'region': "The Catacombs (E2M5) Yellow"}, + 371243: {'name': 'The Catacombs (E2M5) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Catacombs (E2M5) Blue"}, + 371244: {'name': 'The Labyrinth (E2M6) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 7, + 'doom_type': 80, + 'region': "The Labyrinth (E2M6) Main"}, + 371245: {'name': 'The Labyrinth (E2M6) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 14, + 'doom_type': 79, + 'region': "The Labyrinth (E2M6) Green"}, + 371246: {'name': 'The Labyrinth (E2M6) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 15, + 'doom_type': 73, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371247: {'name': 'The Labyrinth (E2M6) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 22, + 'doom_type': 2004, + 'region': "The Labyrinth (E2M6) Green"}, + 371248: {'name': 'The Labyrinth (E2M6) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 23, + 'doom_type': 2005, + 'region': "The Labyrinth (E2M6) Main"}, + 371249: {'name': 'The Labyrinth (E2M6) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 24, + 'doom_type': 2001, + 'region': "The Labyrinth (E2M6) Main"}, + 371250: {'name': 'The Labyrinth (E2M6) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 25, + 'doom_type': 53, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371251: {'name': 'The Labyrinth (E2M6) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 26, + 'doom_type': 2003, + 'region': "The Labyrinth (E2M6) Green"}, + 371252: {'name': 'The Labyrinth (E2M6) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 27, + 'doom_type': 8, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371253: {'name': 'The Labyrinth (E2M6) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 31, + 'doom_type': 75, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371254: {'name': 'The Labyrinth (E2M6) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 32, + 'doom_type': 84, + 'region': "The Labyrinth (E2M6) Blue"}, + 371255: {'name': 'The Labyrinth (E2M6) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 33, + 'doom_type': 86, + 'region': "The Labyrinth (E2M6) Green"}, + 371256: {'name': 'The Labyrinth (E2M6) - Silver Shield', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 34, + 'doom_type': 85, + 'region': "The Labyrinth (E2M6) Main"}, + 371257: {'name': 'The Labyrinth (E2M6) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 35, + 'doom_type': 30, + 'region': "The Labyrinth (E2M6) Main"}, + 371258: {'name': 'The Labyrinth (E2M6) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 282, + 'doom_type': 35, + 'region': "The Labyrinth (E2M6) Green"}, + 371259: {'name': 'The Labyrinth (E2M6) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 283, + 'doom_type': 31, + 'region': "The Labyrinth (E2M6) Green"}, + 371260: {'name': 'The Labyrinth (E2M6) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 284, + 'doom_type': 86, + 'region': "The Labyrinth (E2M6) Green"}, + 371261: {'name': 'The Labyrinth (E2M6) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 285, + 'doom_type': 36, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371262: {'name': 'The Labyrinth (E2M6) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 336, + 'doom_type': 32, + 'region': "The Labyrinth (E2M6) Blue"}, + 371263: {'name': 'The Labyrinth (E2M6) - Phoenix Rod 2', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 422, + 'doom_type': 2003, + 'region': "The Labyrinth (E2M6) Blue"}, + 371264: {'name': 'The Labyrinth (E2M6) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 432, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Main"}, + 371265: {'name': 'The Labyrinth (E2M6) - Firemace 2', + 'episode': 2, + 'check_sanity': True, + 'map': 6, + 'index': 456, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371266: {'name': 'The Labyrinth (E2M6) - Firemace 3', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 457, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Yellow"}, + 371267: {'name': 'The Labyrinth (E2M6) - Firemace 4', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': 458, + 'doom_type': 2002, + 'region': "The Labyrinth (E2M6) Blue"}, + 371268: {'name': 'The Labyrinth (E2M6) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Labyrinth (E2M6) Blue"}, + 371269: {'name': 'The Great Hall (E2M7) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 8, + 'doom_type': 73, + 'region': "The Great Hall (E2M7) Yellow"}, + 371270: {'name': 'The Great Hall (E2M7) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 9, + 'doom_type': 80, + 'region': "The Great Hall (E2M7) Main"}, + 371271: {'name': 'The Great Hall (E2M7) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 11, + 'doom_type': 79, + 'region': "The Great Hall (E2M7) Green"}, + 371272: {'name': 'The Great Hall (E2M7) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 64, + 'doom_type': 30, + 'region': "The Great Hall (E2M7) Main"}, + 371273: {'name': 'The Great Hall (E2M7) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 76, + 'doom_type': 75, + 'region': "The Great Hall (E2M7) Main"}, + 371274: {'name': 'The Great Hall (E2M7) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 77, + 'doom_type': 84, + 'region': "The Great Hall (E2M7) Yellow"}, + 371275: {'name': 'The Great Hall (E2M7) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 78, + 'doom_type': 32, + 'region': "The Great Hall (E2M7) Blue"}, + 371276: {'name': 'The Great Hall (E2M7) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 80, + 'doom_type': 86, + 'region': "The Great Hall (E2M7) Yellow"}, + 371277: {'name': 'The Great Hall (E2M7) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 81, + 'doom_type': 36, + 'region': "The Great Hall (E2M7) Yellow"}, + 371278: {'name': 'The Great Hall (E2M7) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 82, + 'doom_type': 33, + 'region': "The Great Hall (E2M7) Main"}, + 371279: {'name': 'The Great Hall (E2M7) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 83, + 'doom_type': 8, + 'region': "The Great Hall (E2M7) Main"}, + 371280: {'name': 'The Great Hall (E2M7) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 84, + 'doom_type': 85, + 'region': "The Great Hall (E2M7) Main"}, + 371281: {'name': 'The Great Hall (E2M7) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 85, + 'doom_type': 31, + 'region': "The Great Hall (E2M7) Main"}, + 371282: {'name': 'The Great Hall (E2M7) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 86, + 'doom_type': 35, + 'region': "The Great Hall (E2M7) Yellow"}, + 371283: {'name': 'The Great Hall (E2M7) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 91, + 'doom_type': 2001, + 'region': "The Great Hall (E2M7) Main"}, + 371284: {'name': 'The Great Hall (E2M7) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 92, + 'doom_type': 2005, + 'region': "The Great Hall (E2M7) Main"}, + 371285: {'name': 'The Great Hall (E2M7) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 93, + 'doom_type': 53, + 'region': "The Great Hall (E2M7) Yellow"}, + 371286: {'name': 'The Great Hall (E2M7) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 94, + 'doom_type': 2004, + 'region': "The Great Hall (E2M7) Yellow"}, + 371287: {'name': 'The Great Hall (E2M7) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 95, + 'doom_type': 2003, + 'region': "The Great Hall (E2M7) Main"}, + 371288: {'name': 'The Great Hall (E2M7) - Tome of Power 2', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': 477, + 'doom_type': 86, + 'region': "The Great Hall (E2M7) Main"}, + 371289: {'name': 'The Great Hall (E2M7) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Great Hall (E2M7) Blue"}, + 371290: {'name': 'The Portals of Chaos (E2M8) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 9, + 'doom_type': 2001, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371291: {'name': 'The Portals of Chaos (E2M8) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 10, + 'doom_type': 53, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371292: {'name': 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 11, + 'doom_type': 2005, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371293: {'name': 'The Portals of Chaos (E2M8) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 12, + 'doom_type': 2004, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371294: {'name': 'The Portals of Chaos (E2M8) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 13, + 'doom_type': 2003, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371295: {'name': 'The Portals of Chaos (E2M8) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 14, + 'doom_type': 86, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371296: {'name': 'The Portals of Chaos (E2M8) - Bag of Holding', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 18, + 'doom_type': 8, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371297: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 40, + 'doom_type': 32, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371298: {'name': 'The Portals of Chaos (E2M8) - Shadowsphere', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 41, + 'doom_type': 75, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371299: {'name': 'The Portals of Chaos (E2M8) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 42, + 'doom_type': 85, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371300: {'name': 'The Portals of Chaos (E2M8) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 43, + 'doom_type': 31, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371301: {'name': 'The Portals of Chaos (E2M8) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 44, + 'doom_type': 36, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371302: {'name': 'The Portals of Chaos (E2M8) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 272, + 'doom_type': 84, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371303: {'name': 'The Portals of Chaos (E2M8) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': 274, + 'doom_type': 30, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371304: {'name': 'The Portals of Chaos (E2M8) - Mystic Urn 2', + 'episode': 2, + 'check_sanity': True, + 'map': 8, + 'index': 275, + 'doom_type': 32, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371305: {'name': 'The Portals of Chaos (E2M8) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "The Portals of Chaos (E2M8) Main"}, + 371306: {'name': 'The Glacier (E2M9) - Yellow key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 6, + 'doom_type': 80, + 'region': "The Glacier (E2M9) Main"}, + 371307: {'name': 'The Glacier (E2M9) - Blue key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 16, + 'doom_type': 79, + 'region': "The Glacier (E2M9) Green"}, + 371308: {'name': 'The Glacier (E2M9) - Green key', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 17, + 'doom_type': 73, + 'region': "The Glacier (E2M9) Yellow"}, + 371309: {'name': 'The Glacier (E2M9) - Phoenix Rod', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 34, + 'doom_type': 2003, + 'region': "The Glacier (E2M9) Green"}, + 371310: {'name': 'The Glacier (E2M9) - Gauntlets of the Necromancer', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 39, + 'doom_type': 2005, + 'region': "The Glacier (E2M9) Main"}, + 371311: {'name': 'The Glacier (E2M9) - Ethereal Crossbow', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 40, + 'doom_type': 2001, + 'region': "The Glacier (E2M9) Main"}, + 371312: {'name': 'The Glacier (E2M9) - Dragon Claw', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 41, + 'doom_type': 53, + 'region': "The Glacier (E2M9) Yellow"}, + 371313: {'name': 'The Glacier (E2M9) - Hellstaff', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 42, + 'doom_type': 2004, + 'region': "The Glacier (E2M9) Green"}, + 371314: {'name': 'The Glacier (E2M9) - Bag of Holding', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 43, + 'doom_type': 8, + 'region': "The Glacier (E2M9) Main"}, + 371315: {'name': 'The Glacier (E2M9) - Tome of Power', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 45, + 'doom_type': 86, + 'region': "The Glacier (E2M9) Main"}, + 371316: {'name': 'The Glacier (E2M9) - Shadowsphere', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 46, + 'doom_type': 75, + 'region': "The Glacier (E2M9) Main"}, + 371317: {'name': 'The Glacier (E2M9) - Ring of Invincibility', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 47, + 'doom_type': 84, + 'region': "The Glacier (E2M9) Green"}, + 371318: {'name': 'The Glacier (E2M9) - Silver Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 48, + 'doom_type': 85, + 'region': "The Glacier (E2M9) Main"}, + 371319: {'name': 'The Glacier (E2M9) - Enchanted Shield', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 49, + 'doom_type': 31, + 'region': "The Glacier (E2M9) Green"}, + 371320: {'name': 'The Glacier (E2M9) - Mystic Urn', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 50, + 'doom_type': 32, + 'region': "The Glacier (E2M9) Blue"}, + 371321: {'name': 'The Glacier (E2M9) - Map Scroll', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 51, + 'doom_type': 35, + 'region': "The Glacier (E2M9) Blue"}, + 371322: {'name': 'The Glacier (E2M9) - Mystic Urn 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 52, + 'doom_type': 32, + 'region': "The Glacier (E2M9) Green"}, + 371323: {'name': 'The Glacier (E2M9) - Torch', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 53, + 'doom_type': 33, + 'region': "The Glacier (E2M9) Green"}, + 371324: {'name': 'The Glacier (E2M9) - Chaos Device', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 424, + 'doom_type': 36, + 'region': "The Glacier (E2M9) Yellow"}, + 371325: {'name': 'The Glacier (E2M9) - Dragon Claw 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 456, + 'doom_type': 53, + 'region': "The Glacier (E2M9) Main"}, + 371326: {'name': 'The Glacier (E2M9) - Tome of Power 2', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 457, + 'doom_type': 86, + 'region': "The Glacier (E2M9) Main"}, + 371327: {'name': 'The Glacier (E2M9) - Torch 2', + 'episode': 2, + 'check_sanity': True, + 'map': 9, + 'index': 458, + 'doom_type': 33, + 'region': "The Glacier (E2M9) Main"}, + 371328: {'name': 'The Glacier (E2M9) - Morph Ovum', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 474, + 'doom_type': 30, + 'region': "The Glacier (E2M9) Main"}, + 371329: {'name': 'The Glacier (E2M9) - Firemace', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 479, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Main"}, + 371330: {'name': 'The Glacier (E2M9) - Firemace 2', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 501, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Yellow"}, + 371331: {'name': 'The Glacier (E2M9) - Firemace 3', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 502, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Blue"}, + 371332: {'name': 'The Glacier (E2M9) - Firemace 4', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': 503, + 'doom_type': 2002, + 'region': "The Glacier (E2M9) Main"}, + 371333: {'name': 'The Glacier (E2M9) - Exit', + 'episode': 2, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Glacier (E2M9) Blue"}, + 371334: {'name': 'The Storehouse (E3M1) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 9, + 'doom_type': 80, + 'region': "The Storehouse (E3M1) Main"}, + 371335: {'name': 'The Storehouse (E3M1) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "The Storehouse (E3M1) Yellow"}, + 371336: {'name': 'The Storehouse (E3M1) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 29, + 'doom_type': 8, + 'region': "The Storehouse (E3M1) Main"}, + 371337: {'name': 'The Storehouse (E3M1) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 38, + 'doom_type': 75, + 'region': "The Storehouse (E3M1) Main"}, + 371338: {'name': 'The Storehouse (E3M1) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 39, + 'doom_type': 84, + 'region': "The Storehouse (E3M1) Green"}, + 371339: {'name': 'The Storehouse (E3M1) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 40, + 'doom_type': 85, + 'region': "The Storehouse (E3M1) Main"}, + 371340: {'name': 'The Storehouse (E3M1) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 41, + 'doom_type': 35, + 'region': "The Storehouse (E3M1) Green"}, + 371341: {'name': 'The Storehouse (E3M1) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 42, + 'doom_type': 36, + 'region': "The Storehouse (E3M1) Main"}, + 371342: {'name': 'The Storehouse (E3M1) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 43, + 'doom_type': 86, + 'region': "The Storehouse (E3M1) Green"}, + 371343: {'name': 'The Storehouse (E3M1) - Torch', + 'episode': 3, + 'check_sanity': True, + 'map': 1, + 'index': 44, + 'doom_type': 33, + 'region': "The Storehouse (E3M1) Main"}, + 371344: {'name': 'The Storehouse (E3M1) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 45, + 'doom_type': 53, + 'region': "The Storehouse (E3M1) Main"}, + 371345: {'name': 'The Storehouse (E3M1) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 46, + 'doom_type': 2004, + 'region': "The Storehouse (E3M1) Green"}, + 371346: {'name': 'The Storehouse (E3M1) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': 47, + 'doom_type': 2005, + 'region': "The Storehouse (E3M1) Main"}, + 371347: {'name': 'The Storehouse (E3M1) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "The Storehouse (E3M1) Green"}, + 371348: {'name': 'The Cesspool (E3M2) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 4, + 'doom_type': 80, + 'region': "The Cesspool (E3M2) Main"}, + 371349: {'name': 'The Cesspool (E3M2) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 73, + 'region': "The Cesspool (E3M2) Yellow"}, + 371350: {'name': 'The Cesspool (E3M2) - Blue key', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 20, + 'doom_type': 79, + 'region': "The Cesspool (E3M2) Green"}, + 371351: {'name': 'The Cesspool (E3M2) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 144, + 'doom_type': 2001, + 'region': "The Cesspool (E3M2) Main"}, + 371352: {'name': 'The Cesspool (E3M2) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 145, + 'doom_type': 84, + 'region': "The Cesspool (E3M2) Green"}, + 371353: {'name': 'The Cesspool (E3M2) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 146, + 'doom_type': 2005, + 'region': "The Cesspool (E3M2) Green"}, + 371354: {'name': 'The Cesspool (E3M2) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 147, + 'doom_type': 53, + 'region': "The Cesspool (E3M2) Yellow"}, + 371355: {'name': 'The Cesspool (E3M2) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 148, + 'doom_type': 2003, + 'region': "The Cesspool (E3M2) Green"}, + 371356: {'name': 'The Cesspool (E3M2) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 149, + 'doom_type': 2004, + 'region': "The Cesspool (E3M2) Yellow"}, + 371357: {'name': 'The Cesspool (E3M2) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 150, + 'doom_type': 8, + 'region': "The Cesspool (E3M2) Yellow"}, + 371358: {'name': 'The Cesspool (E3M2) - Silver Shield', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 151, + 'doom_type': 85, + 'region': "The Cesspool (E3M2) Yellow"}, + 371359: {'name': 'The Cesspool (E3M2) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 152, + 'doom_type': 85, + 'region': "The Cesspool (E3M2) Main"}, + 371360: {'name': 'The Cesspool (E3M2) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 153, + 'doom_type': 30, + 'region': "The Cesspool (E3M2) Main"}, + 371361: {'name': 'The Cesspool (E3M2) - Morph Ovum 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 154, + 'doom_type': 30, + 'region': "The Cesspool (E3M2) Green"}, + 371362: {'name': 'The Cesspool (E3M2) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 164, + 'doom_type': 32, + 'region': "The Cesspool (E3M2) Main"}, + 371363: {'name': 'The Cesspool (E3M2) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 165, + 'doom_type': 75, + 'region': "The Cesspool (E3M2) Yellow"}, + 371364: {'name': 'The Cesspool (E3M2) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 166, + 'doom_type': 31, + 'region': "The Cesspool (E3M2) Green"}, + 371365: {'name': 'The Cesspool (E3M2) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 167, + 'doom_type': 35, + 'region': "The Cesspool (E3M2) Green"}, + 371366: {'name': 'The Cesspool (E3M2) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 168, + 'doom_type': 36, + 'region': "The Cesspool (E3M2) Green"}, + 371367: {'name': 'The Cesspool (E3M2) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 169, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Main"}, + 371368: {'name': 'The Cesspool (E3M2) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 170, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Green"}, + 371369: {'name': 'The Cesspool (E3M2) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 171, + 'doom_type': 86, + 'region': "The Cesspool (E3M2) Yellow"}, + 371370: {'name': 'The Cesspool (E3M2) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 172, + 'doom_type': 33, + 'region': "The Cesspool (E3M2) Main"}, + 371371: {'name': 'The Cesspool (E3M2) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 233, + 'doom_type': 8, + 'region': "The Cesspool (E3M2) Green"}, + 371372: {'name': 'The Cesspool (E3M2) - Firemace', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 555, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Green"}, + 371373: {'name': 'The Cesspool (E3M2) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 556, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Yellow"}, + 371374: {'name': 'The Cesspool (E3M2) - Firemace 3', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 557, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Blue"}, + 371375: {'name': 'The Cesspool (E3M2) - Firemace 4', + 'episode': 3, + 'check_sanity': True, + 'map': 2, + 'index': 558, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Main"}, + 371376: {'name': 'The Cesspool (E3M2) - Firemace 5', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': 559, + 'doom_type': 2002, + 'region': "The Cesspool (E3M2) Yellow"}, + 371377: {'name': 'The Cesspool (E3M2) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "The Cesspool (E3M2) Blue"}, + 371378: {'name': 'The Confluence (E3M3) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 4, + 'doom_type': 80, + 'region': "The Confluence (E3M3) Main"}, + 371379: {'name': 'The Confluence (E3M3) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 7, + 'doom_type': 73, + 'region': "The Confluence (E3M3) Yellow"}, + 371380: {'name': 'The Confluence (E3M3) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 8, + 'doom_type': 79, + 'region': "The Confluence (E3M3) Green"}, + 371381: {'name': 'The Confluence (E3M3) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 43, + 'doom_type': 2004, + 'region': "The Confluence (E3M3) Blue"}, + 371382: {'name': 'The Confluence (E3M3) - Tome of Power', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 44, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371383: {'name': 'The Confluence (E3M3) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 47, + 'doom_type': 53, + 'region': "The Confluence (E3M3) Green"}, + 371384: {'name': 'The Confluence (E3M3) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 48, + 'doom_type': 2001, + 'region': "The Confluence (E3M3) Yellow"}, + 371385: {'name': 'The Confluence (E3M3) - Hellstaff 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 49, + 'doom_type': 2004, + 'region': "The Confluence (E3M3) Blue"}, + 371386: {'name': 'The Confluence (E3M3) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 50, + 'doom_type': 2005, + 'region': "The Confluence (E3M3) Blue"}, + 371387: {'name': 'The Confluence (E3M3) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 51, + 'doom_type': 32, + 'region': "The Confluence (E3M3) Green"}, + 371388: {'name': 'The Confluence (E3M3) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 52, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Green"}, + 371389: {'name': 'The Confluence (E3M3) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 53, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371390: {'name': 'The Confluence (E3M3) - Tome of Power 4', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 54, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Blue"}, + 371391: {'name': 'The Confluence (E3M3) - Tome of Power 5', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 55, + 'doom_type': 86, + 'region': "The Confluence (E3M3) Green"}, + 371392: {'name': 'The Confluence (E3M3) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 58, + 'doom_type': 8, + 'region': "The Confluence (E3M3) Green"}, + 371393: {'name': 'The Confluence (E3M3) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 60, + 'doom_type': 30, + 'region': "The Confluence (E3M3) Green"}, + 371394: {'name': 'The Confluence (E3M3) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 72, + 'doom_type': 32, + 'region': "The Confluence (E3M3) Blue"}, + 371395: {'name': 'The Confluence (E3M3) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 73, + 'doom_type': 75, + 'region': "The Confluence (E3M3) Main"}, + 371396: {'name': 'The Confluence (E3M3) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 74, + 'doom_type': 84, + 'region': "The Confluence (E3M3) Yellow"}, + 371397: {'name': 'The Confluence (E3M3) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 75, + 'doom_type': 35, + 'region': "The Confluence (E3M3) Blue"}, + 371398: {'name': 'The Confluence (E3M3) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 76, + 'doom_type': 85, + 'region': "The Confluence (E3M3) Main"}, + 371399: {'name': 'The Confluence (E3M3) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 77, + 'doom_type': 2003, + 'region': "The Confluence (E3M3) Blue"}, + 371400: {'name': 'The Confluence (E3M3) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 78, + 'doom_type': 31, + 'region': "The Confluence (E3M3) Blue"}, + 371401: {'name': 'The Confluence (E3M3) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 79, + 'doom_type': 85, + 'region': "The Confluence (E3M3) Green"}, + 371402: {'name': 'The Confluence (E3M3) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 80, + 'doom_type': 36, + 'region': "The Confluence (E3M3) Green"}, + 371403: {'name': 'The Confluence (E3M3) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 81, + 'doom_type': 33, + 'region': "The Confluence (E3M3) Green"}, + 371404: {'name': 'The Confluence (E3M3) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 622, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Green"}, + 371405: {'name': 'The Confluence (E3M3) - Firemace 2', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 623, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Green"}, + 371406: {'name': 'The Confluence (E3M3) - Firemace 3', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 624, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Yellow"}, + 371407: {'name': 'The Confluence (E3M3) - Firemace 4', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 625, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371408: {'name': 'The Confluence (E3M3) - Firemace 5', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': 626, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371409: {'name': 'The Confluence (E3M3) - Firemace 6', + 'episode': 3, + 'check_sanity': True, + 'map': 3, + 'index': 627, + 'doom_type': 2002, + 'region': "The Confluence (E3M3) Blue"}, + 371410: {'name': 'The Confluence (E3M3) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "The Confluence (E3M3) Blue"}, + 371411: {'name': 'The Azure Fortress (E3M4) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 6, + 'doom_type': 80, + 'region': "The Azure Fortress (E3M4) Main"}, + 371412: {'name': 'The Azure Fortress (E3M4) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 21, + 'doom_type': 73, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371413: {'name': 'The Azure Fortress (E3M4) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 51, + 'doom_type': 53, + 'region': "The Azure Fortress (E3M4) Main"}, + 371414: {'name': 'The Azure Fortress (E3M4) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 52, + 'doom_type': 84, + 'region': "The Azure Fortress (E3M4) Main"}, + 371415: {'name': 'The Azure Fortress (E3M4) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 53, + 'doom_type': 2001, + 'region': "The Azure Fortress (E3M4) Main"}, + 371416: {'name': 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 54, + 'doom_type': 2005, + 'region': "The Azure Fortress (E3M4) Main"}, + 371417: {'name': 'The Azure Fortress (E3M4) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 55, + 'doom_type': 2004, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371418: {'name': 'The Azure Fortress (E3M4) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 56, + 'doom_type': 2003, + 'region': "The Azure Fortress (E3M4) Green"}, + 371419: {'name': 'The Azure Fortress (E3M4) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 58, + 'doom_type': 8, + 'region': "The Azure Fortress (E3M4) Main"}, + 371420: {'name': 'The Azure Fortress (E3M4) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 59, + 'doom_type': 8, + 'region': "The Azure Fortress (E3M4) Green"}, + 371421: {'name': 'The Azure Fortress (E3M4) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 60, + 'doom_type': 30, + 'region': "The Azure Fortress (E3M4) Main"}, + 371422: {'name': 'The Azure Fortress (E3M4) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 61, + 'doom_type': 32, + 'region': "The Azure Fortress (E3M4) Main"}, + 371423: {'name': 'The Azure Fortress (E3M4) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 62, + 'doom_type': 75, + 'region': "The Azure Fortress (E3M4) Main"}, + 371424: {'name': 'The Azure Fortress (E3M4) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 63, + 'doom_type': 85, + 'region': "The Azure Fortress (E3M4) Main"}, + 371425: {'name': 'The Azure Fortress (E3M4) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 64, + 'doom_type': 85, + 'region': "The Azure Fortress (E3M4) Green"}, + 371426: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 65, + 'doom_type': 31, + 'region': "The Azure Fortress (E3M4) Green"}, + 371427: {'name': 'The Azure Fortress (E3M4) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 66, + 'doom_type': 35, + 'region': "The Azure Fortress (E3M4) Green"}, + 371428: {'name': 'The Azure Fortress (E3M4) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 67, + 'doom_type': 36, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371429: {'name': 'The Azure Fortress (E3M4) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 68, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Green"}, + 371430: {'name': 'The Azure Fortress (E3M4) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 69, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Main"}, + 371431: {'name': 'The Azure Fortress (E3M4) - Torch 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 70, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Green"}, + 371432: {'name': 'The Azure Fortress (E3M4) - Torch 3', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 71, + 'doom_type': 33, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371433: {'name': 'The Azure Fortress (E3M4) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 72, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Main"}, + 371434: {'name': 'The Azure Fortress (E3M4) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 73, + 'doom_type': 86, + 'region': "The Azure Fortress (E3M4) Main"}, + 371435: {'name': 'The Azure Fortress (E3M4) - Enchanted Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 75, + 'doom_type': 31, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371436: {'name': 'The Azure Fortress (E3M4) - Morph Ovum 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 76, + 'doom_type': 30, + 'region': "The Azure Fortress (E3M4) Yellow"}, + 371437: {'name': 'The Azure Fortress (E3M4) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': 577, + 'doom_type': 32, + 'region': "The Azure Fortress (E3M4) Green"}, + 371438: {'name': 'The Azure Fortress (E3M4) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "The Azure Fortress (E3M4) Green"}, + 371439: {'name': 'The Ophidian Lair (E3M5) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 16, + 'doom_type': 80, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371440: {'name': 'The Ophidian Lair (E3M5) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 30, + 'doom_type': 73, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371441: {'name': 'The Ophidian Lair (E3M5) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 48, + 'doom_type': 2004, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371442: {'name': 'The Ophidian Lair (E3M5) - Phoenix Rod', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 49, + 'doom_type': 2003, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371443: {'name': 'The Ophidian Lair (E3M5) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 50, + 'doom_type': 53, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371444: {'name': 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 51, + 'doom_type': 2005, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371445: {'name': 'The Ophidian Lair (E3M5) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 52, + 'doom_type': 8, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371446: {'name': 'The Ophidian Lair (E3M5) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 53, + 'doom_type': 30, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371447: {'name': 'The Ophidian Lair (E3M5) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 62, + 'doom_type': 2001, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371448: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 63, + 'doom_type': 32, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371449: {'name': 'The Ophidian Lair (E3M5) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 64, + 'doom_type': 75, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371450: {'name': 'The Ophidian Lair (E3M5) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 65, + 'doom_type': 84, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371451: {'name': 'The Ophidian Lair (E3M5) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 66, + 'doom_type': 85, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371452: {'name': 'The Ophidian Lair (E3M5) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 67, + 'doom_type': 31, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371453: {'name': 'The Ophidian Lair (E3M5) - Silver Shield 2', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 68, + 'doom_type': 85, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371454: {'name': 'The Ophidian Lair (E3M5) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 69, + 'doom_type': 35, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371455: {'name': 'The Ophidian Lair (E3M5) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 70, + 'doom_type': 36, + 'region': "The Ophidian Lair (E3M5) Yellow"}, + 371456: {'name': 'The Ophidian Lair (E3M5) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 71, + 'doom_type': 33, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371457: {'name': 'The Ophidian Lair (E3M5) - Tome of Power', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 72, + 'doom_type': 86, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371458: {'name': 'The Ophidian Lair (E3M5) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': True, + 'map': 5, + 'index': 73, + 'doom_type': 32, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371459: {'name': 'The Ophidian Lair (E3M5) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': 74, + 'doom_type': 86, + 'region': "The Ophidian Lair (E3M5) Main"}, + 371460: {'name': 'The Ophidian Lair (E3M5) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "The Ophidian Lair (E3M5) Green"}, + 371461: {'name': 'The Halls of Fear (E3M6) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 10, + 'doom_type': 80, + 'region': "The Halls of Fear (E3M6) Main"}, + 371462: {'name': 'The Halls of Fear (E3M6) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 12, + 'doom_type': 73, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371463: {'name': 'The Halls of Fear (E3M6) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 15, + 'doom_type': 79, + 'region': "The Halls of Fear (E3M6) Green"}, + 371464: {'name': 'The Halls of Fear (E3M6) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 31, + 'doom_type': 2004, + 'region': "The Halls of Fear (E3M6) Green"}, + 371465: {'name': 'The Halls of Fear (E3M6) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 32, + 'doom_type': 2003, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371466: {'name': 'The Halls of Fear (E3M6) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 33, + 'doom_type': 2001, + 'region': "The Halls of Fear (E3M6) Main"}, + 371467: {'name': 'The Halls of Fear (E3M6) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 34, + 'doom_type': 53, + 'region': "The Halls of Fear (E3M6) Main"}, + 371468: {'name': 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 35, + 'doom_type': 2005, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371469: {'name': 'The Halls of Fear (E3M6) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 38, + 'doom_type': 36, + 'region': "The Halls of Fear (E3M6) Green"}, + 371470: {'name': 'The Halls of Fear (E3M6) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 40, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371471: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 41, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371472: {'name': 'The Halls of Fear (E3M6) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 42, + 'doom_type': 30, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371473: {'name': 'The Halls of Fear (E3M6) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 51, + 'doom_type': 32, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371474: {'name': 'The Halls of Fear (E3M6) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 52, + 'doom_type': 75, + 'region': "The Halls of Fear (E3M6) Green"}, + 371475: {'name': 'The Halls of Fear (E3M6) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 53, + 'doom_type': 84, + 'region': "The Halls of Fear (E3M6) Main"}, + 371476: {'name': 'The Halls of Fear (E3M6) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 54, + 'doom_type': 85, + 'region': "The Halls of Fear (E3M6) Yellow"}, + 371477: {'name': 'The Halls of Fear (E3M6) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 55, + 'doom_type': 31, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371478: {'name': 'The Halls of Fear (E3M6) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 56, + 'doom_type': 35, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371479: {'name': 'The Halls of Fear (E3M6) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 57, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Cyan"}, + 371480: {'name': 'The Halls of Fear (E3M6) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 58, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Green"}, + 371481: {'name': 'The Halls of Fear (E3M6) - Mystic Urn 2', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 59, + 'doom_type': 32, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371482: {'name': 'The Halls of Fear (E3M6) - Bag of Holding 3', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 363, + 'doom_type': 8, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371483: {'name': 'The Halls of Fear (E3M6) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 364, + 'doom_type': 86, + 'region': "The Halls of Fear (E3M6) Main"}, + 371484: {'name': 'The Halls of Fear (E3M6) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 468, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371485: {'name': 'The Halls of Fear (E3M6) - Hellstaff 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 472, + 'doom_type': 2004, + 'region': "The Halls of Fear (E3M6) Main"}, + 371486: {'name': 'The Halls of Fear (E3M6) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 506, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371487: {'name': 'The Halls of Fear (E3M6) - Firemace 3', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 507, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371488: {'name': 'The Halls of Fear (E3M6) - Firemace 4', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': 508, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Main"}, + 371489: {'name': 'The Halls of Fear (E3M6) - Firemace 5', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 509, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371490: {'name': 'The Halls of Fear (E3M6) - Firemace 6', + 'episode': 3, + 'check_sanity': True, + 'map': 6, + 'index': 510, + 'doom_type': 2002, + 'region': "The Halls of Fear (E3M6) Green"}, + 371491: {'name': 'The Halls of Fear (E3M6) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "The Halls of Fear (E3M6) Blue"}, + 371492: {'name': 'The Chasm (E3M7) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 5, + 'doom_type': 79, + 'region': "The Chasm (E3M7) Green"}, + 371493: {'name': 'The Chasm (E3M7) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 12, + 'doom_type': 73, + 'region': "The Chasm (E3M7) Yellow"}, + 371494: {'name': 'The Chasm (E3M7) - Yellow key', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 26, + 'doom_type': 80, + 'region': "The Chasm (E3M7) Main"}, + 371495: {'name': 'The Chasm (E3M7) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 254, + 'doom_type': 2001, + 'region': "The Chasm (E3M7) Main"}, + 371496: {'name': 'The Chasm (E3M7) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 255, + 'doom_type': 2004, + 'region': "The Chasm (E3M7) Yellow"}, + 371497: {'name': 'The Chasm (E3M7) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 256, + 'doom_type': 2005, + 'region': "The Chasm (E3M7) Green"}, + 371498: {'name': 'The Chasm (E3M7) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 257, + 'doom_type': 53, + 'region': "The Chasm (E3M7) Main"}, + 371499: {'name': 'The Chasm (E3M7) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 259, + 'doom_type': 2003, + 'region': "The Chasm (E3M7) Green"}, + 371500: {'name': 'The Chasm (E3M7) - Shadowsphere', + 'episode': 3, + 'check_sanity': True, + 'map': 7, + 'index': 260, + 'doom_type': 75, + 'region': "The Chasm (E3M7) Green"}, + 371501: {'name': 'The Chasm (E3M7) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 262, + 'doom_type': 8, + 'region': "The Chasm (E3M7) Main"}, + 371502: {'name': 'The Chasm (E3M7) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 268, + 'doom_type': 85, + 'region': "The Chasm (E3M7) Main"}, + 371503: {'name': 'The Chasm (E3M7) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 269, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Main"}, + 371504: {'name': 'The Chasm (E3M7) - Torch', + 'episode': 3, + 'check_sanity': True, + 'map': 7, + 'index': 270, + 'doom_type': 33, + 'region': "The Chasm (E3M7) Yellow"}, + 371505: {'name': 'The Chasm (E3M7) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 278, + 'doom_type': 30, + 'region': "The Chasm (E3M7) Yellow"}, + 371506: {'name': 'The Chasm (E3M7) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 282, + 'doom_type': 84, + 'region': "The Chasm (E3M7) Green"}, + 371507: {'name': 'The Chasm (E3M7) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 283, + 'doom_type': 32, + 'region': "The Chasm (E3M7) Green"}, + 371508: {'name': 'The Chasm (E3M7) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 284, + 'doom_type': 31, + 'region': "The Chasm (E3M7) Green"}, + 371509: {'name': 'The Chasm (E3M7) - Map Scroll', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 285, + 'doom_type': 35, + 'region': "The Chasm (E3M7) Green"}, + 371510: {'name': 'The Chasm (E3M7) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 286, + 'doom_type': 36, + 'region': "The Chasm (E3M7) Green"}, + 371511: {'name': 'The Chasm (E3M7) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 287, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Green"}, + 371512: {'name': 'The Chasm (E3M7) - Tome of Power 3', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 288, + 'doom_type': 86, + 'region': "The Chasm (E3M7) Green"}, + 371513: {'name': 'The Chasm (E3M7) - Torch 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 289, + 'doom_type': 33, + 'region': "The Chasm (E3M7) Green"}, + 371514: {'name': 'The Chasm (E3M7) - Shadowsphere 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 337, + 'doom_type': 75, + 'region': "The Chasm (E3M7) Main"}, + 371515: {'name': 'The Chasm (E3M7) - Bag of Holding 2', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': 660, + 'doom_type': 8, + 'region': "The Chasm (E3M7) Main"}, + 371516: {'name': 'The Chasm (E3M7) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "The Chasm (E3M7) Blue"}, + 371517: {'name': "D'Sparil'S Keep (E3M8) - Phoenix Rod", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 55, + 'doom_type': 2003, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371518: {'name': "D'Sparil'S Keep (E3M8) - Ethereal Crossbow", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 56, + 'doom_type': 2001, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371519: {'name': "D'Sparil'S Keep (E3M8) - Dragon Claw", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 57, + 'doom_type': 53, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371520: {'name': "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 58, + 'doom_type': 2005, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371521: {'name': "D'Sparil'S Keep (E3M8) - Hellstaff", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 59, + 'doom_type': 2004, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371522: {'name': "D'Sparil'S Keep (E3M8) - Bag of Holding", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 63, + 'doom_type': 8, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371523: {'name': "D'Sparil'S Keep (E3M8) - Mystic Urn", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 64, + 'doom_type': 32, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371524: {'name': "D'Sparil'S Keep (E3M8) - Ring of Invincibility", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 65, + 'doom_type': 84, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371525: {'name': "D'Sparil'S Keep (E3M8) - Shadowsphere", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 66, + 'doom_type': 75, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371526: {'name': "D'Sparil'S Keep (E3M8) - Silver Shield", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 67, + 'doom_type': 85, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371527: {'name': "D'Sparil'S Keep (E3M8) - Enchanted Shield", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 68, + 'doom_type': 31, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371528: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': 69, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371529: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 2", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 70, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371530: {'name': "D'Sparil'S Keep (E3M8) - Chaos Device", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 71, + 'doom_type': 36, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371531: {'name': "D'Sparil'S Keep (E3M8) - Tome of Power 3", + 'episode': 3, + 'check_sanity': True, + 'map': 8, + 'index': 245, + 'doom_type': 86, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371532: {'name': "D'Sparil'S Keep (E3M8) - Exit", + 'episode': 3, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "D'Sparil'S Keep (E3M8) Main"}, + 371533: {'name': 'The Aquifier (E3M9) - Blue key', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 12, + 'doom_type': 79, + 'region': "The Aquifier (E3M9) Green"}, + 371534: {'name': 'The Aquifier (E3M9) - Green key', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 13, + 'doom_type': 73, + 'region': "The Aquifier (E3M9) Yellow"}, + 371535: {'name': 'The Aquifier (E3M9) - Yellow key', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 14, + 'doom_type': 80, + 'region': "The Aquifier (E3M9) Main"}, + 371536: {'name': 'The Aquifier (E3M9) - Ethereal Crossbow', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 141, + 'doom_type': 2001, + 'region': "The Aquifier (E3M9) Main"}, + 371537: {'name': 'The Aquifier (E3M9) - Phoenix Rod', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 142, + 'doom_type': 2003, + 'region': "The Aquifier (E3M9) Yellow"}, + 371538: {'name': 'The Aquifier (E3M9) - Dragon Claw', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 143, + 'doom_type': 53, + 'region': "The Aquifier (E3M9) Green"}, + 371539: {'name': 'The Aquifier (E3M9) - Hellstaff', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 144, + 'doom_type': 2004, + 'region': "The Aquifier (E3M9) Green"}, + 371540: {'name': 'The Aquifier (E3M9) - Gauntlets of the Necromancer', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 145, + 'doom_type': 2005, + 'region': "The Aquifier (E3M9) Green"}, + 371541: {'name': 'The Aquifier (E3M9) - Ring of Invincibility', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 148, + 'doom_type': 84, + 'region': "The Aquifier (E3M9) Yellow"}, + 371542: {'name': 'The Aquifier (E3M9) - Mystic Urn', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 149, + 'doom_type': 32, + 'region': "The Aquifier (E3M9) Green"}, + 371543: {'name': 'The Aquifier (E3M9) - Silver Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 151, + 'doom_type': 85, + 'region': "The Aquifier (E3M9) Main"}, + 371544: {'name': 'The Aquifier (E3M9) - Tome of Power', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 152, + 'doom_type': 86, + 'region': "The Aquifier (E3M9) Main"}, + 371545: {'name': 'The Aquifier (E3M9) - Bag of Holding', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 153, + 'doom_type': 8, + 'region': "The Aquifier (E3M9) Yellow"}, + 371546: {'name': 'The Aquifier (E3M9) - Morph Ovum', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 154, + 'doom_type': 30, + 'region': "The Aquifier (E3M9) Green"}, + 371547: {'name': 'The Aquifier (E3M9) - Map Scroll', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 155, + 'doom_type': 35, + 'region': "The Aquifier (E3M9) Green"}, + 371548: {'name': 'The Aquifier (E3M9) - Chaos Device', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 156, + 'doom_type': 36, + 'region': "The Aquifier (E3M9) Yellow"}, + 371549: {'name': 'The Aquifier (E3M9) - Enchanted Shield', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 157, + 'doom_type': 31, + 'region': "The Aquifier (E3M9) Green"}, + 371550: {'name': 'The Aquifier (E3M9) - Tome of Power 2', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 158, + 'doom_type': 86, + 'region': "The Aquifier (E3M9) Green"}, + 371551: {'name': 'The Aquifier (E3M9) - Torch', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 159, + 'doom_type': 33, + 'region': "The Aquifier (E3M9) Main"}, + 371552: {'name': 'The Aquifier (E3M9) - Shadowsphere', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 160, + 'doom_type': 75, + 'region': "The Aquifier (E3M9) Green"}, + 371553: {'name': 'The Aquifier (E3M9) - Silver Shield 2', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 374, + 'doom_type': 85, + 'region': "The Aquifier (E3M9) Green"}, + 371554: {'name': 'The Aquifier (E3M9) - Firemace', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 478, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371555: {'name': 'The Aquifier (E3M9) - Firemace 2', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 526, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371556: {'name': 'The Aquifier (E3M9) - Firemace 3', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': 527, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Green"}, + 371557: {'name': 'The Aquifier (E3M9) - Firemace 4', + 'episode': 3, + 'check_sanity': True, + 'map': 9, + 'index': 528, + 'doom_type': 2002, + 'region': "The Aquifier (E3M9) Yellow"}, + 371558: {'name': 'The Aquifier (E3M9) - Exit', + 'episode': 3, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "The Aquifier (E3M9) Blue"}, + 371559: {'name': 'Catafalque (E4M1) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 4, + 'doom_type': 80, + 'region': "Catafalque (E4M1) Main"}, + 371560: {'name': 'Catafalque (E4M1) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 10, + 'doom_type': 73, + 'region': "Catafalque (E4M1) Yellow"}, + 371561: {'name': 'Catafalque (E4M1) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 100, + 'doom_type': 2001, + 'region': "Catafalque (E4M1) Main"}, + 371562: {'name': 'Catafalque (E4M1) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 101, + 'doom_type': 2005, + 'region': "Catafalque (E4M1) Yellow"}, + 371563: {'name': 'Catafalque (E4M1) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 102, + 'doom_type': 53, + 'region': "Catafalque (E4M1) Yellow"}, + 371564: {'name': 'Catafalque (E4M1) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 103, + 'doom_type': 2004, + 'region': "Catafalque (E4M1) Green"}, + 371565: {'name': 'Catafalque (E4M1) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 114, + 'doom_type': 75, + 'region': "Catafalque (E4M1) Yellow"}, + 371566: {'name': 'Catafalque (E4M1) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 115, + 'doom_type': 84, + 'region': "Catafalque (E4M1) Green"}, + 371567: {'name': 'Catafalque (E4M1) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 116, + 'doom_type': 85, + 'region': "Catafalque (E4M1) Main"}, + 371568: {'name': 'Catafalque (E4M1) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 117, + 'doom_type': 35, + 'region': "Catafalque (E4M1) Green"}, + 371569: {'name': 'Catafalque (E4M1) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 118, + 'doom_type': 36, + 'region': "Catafalque (E4M1) Yellow"}, + 371570: {'name': 'Catafalque (E4M1) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 119, + 'doom_type': 86, + 'region': "Catafalque (E4M1) Yellow"}, + 371571: {'name': 'Catafalque (E4M1) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 120, + 'doom_type': 86, + 'region': "Catafalque (E4M1) Main"}, + 371572: {'name': 'Catafalque (E4M1) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 121, + 'doom_type': 33, + 'region': "Catafalque (E4M1) Yellow"}, + 371573: {'name': 'Catafalque (E4M1) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 122, + 'doom_type': 8, + 'region': "Catafalque (E4M1) Main"}, + 371574: {'name': 'Catafalque (E4M1) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': 123, + 'doom_type': 30, + 'region': "Catafalque (E4M1) Main"}, + 371575: {'name': 'Catafalque (E4M1) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Catafalque (E4M1) Green"}, + 371576: {'name': 'Blockhouse (E4M2) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 18, + 'doom_type': 73, + 'region': "Blockhouse (E4M2) Yellow"}, + 371577: {'name': 'Blockhouse (E4M2) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 19, + 'doom_type': 80, + 'region': "Blockhouse (E4M2) Main"}, + 371578: {'name': 'Blockhouse (E4M2) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 25, + 'doom_type': 79, + 'region': "Blockhouse (E4M2) Green"}, + 371579: {'name': 'Blockhouse (E4M2) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 46, + 'doom_type': 2005, + 'region': "Blockhouse (E4M2) Main"}, + 371580: {'name': 'Blockhouse (E4M2) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 47, + 'doom_type': 2001, + 'region': "Blockhouse (E4M2) Main"}, + 371581: {'name': 'Blockhouse (E4M2) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 48, + 'doom_type': 53, + 'region': "Blockhouse (E4M2) Main"}, + 371582: {'name': 'Blockhouse (E4M2) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 49, + 'doom_type': 2004, + 'region': "Blockhouse (E4M2) Main"}, + 371583: {'name': 'Blockhouse (E4M2) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 50, + 'doom_type': 2003, + 'region': "Blockhouse (E4M2) Main"}, + 371584: {'name': 'Blockhouse (E4M2) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 58, + 'doom_type': 8, + 'region': "Blockhouse (E4M2) Main"}, + 371585: {'name': 'Blockhouse (E4M2) - Mystic Urn', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 67, + 'doom_type': 32, + 'region': "Blockhouse (E4M2) Main"}, + 371586: {'name': 'Blockhouse (E4M2) - Silver Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 68, + 'doom_type': 85, + 'region': "Blockhouse (E4M2) Main"}, + 371587: {'name': 'Blockhouse (E4M2) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 69, + 'doom_type': 30, + 'region': "Blockhouse (E4M2) Main"}, + 371588: {'name': 'Blockhouse (E4M2) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 2, + 'index': 70, + 'doom_type': 86, + 'region': "Blockhouse (E4M2) Main"}, + 371589: {'name': 'Blockhouse (E4M2) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 71, + 'doom_type': 36, + 'region': "Blockhouse (E4M2) Green"}, + 371590: {'name': 'Blockhouse (E4M2) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 72, + 'doom_type': 84, + 'region': "Blockhouse (E4M2) Green"}, + 371591: {'name': 'Blockhouse (E4M2) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 73, + 'doom_type': 8, + 'region': "Blockhouse (E4M2) Green"}, + 371592: {'name': 'Blockhouse (E4M2) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 74, + 'doom_type': 31, + 'region': "Blockhouse (E4M2) Yellow"}, + 371593: {'name': 'Blockhouse (E4M2) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 75, + 'doom_type': 75, + 'region': "Blockhouse (E4M2) Main"}, + 371594: {'name': 'Blockhouse (E4M2) - Ring of Invincibility 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 226, + 'doom_type': 84, + 'region': "Blockhouse (E4M2) Lake"}, + 371595: {'name': 'Blockhouse (E4M2) - Shadowsphere 2', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': 227, + 'doom_type': 75, + 'region': "Blockhouse (E4M2) Lake"}, + 371596: {'name': 'Blockhouse (E4M2) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Blockhouse (E4M2) Blue"}, + 371597: {'name': 'Ambulatory (E4M3) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 10, + 'doom_type': 80, + 'region': "Ambulatory (E4M3) Main"}, + 371598: {'name': 'Ambulatory (E4M3) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 11, + 'doom_type': 73, + 'region': "Ambulatory (E4M3) Yellow"}, + 371599: {'name': 'Ambulatory (E4M3) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 12, + 'doom_type': 79, + 'region': "Ambulatory (E4M3) Green"}, + 371600: {'name': 'Ambulatory (E4M3) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 265, + 'doom_type': 2001, + 'region': "Ambulatory (E4M3) Main"}, + 371601: {'name': 'Ambulatory (E4M3) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 266, + 'doom_type': 2005, + 'region': "Ambulatory (E4M3) Main"}, + 371602: {'name': 'Ambulatory (E4M3) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 267, + 'doom_type': 53, + 'region': "Ambulatory (E4M3) Yellow"}, + 371603: {'name': 'Ambulatory (E4M3) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 268, + 'doom_type': 2004, + 'region': "Ambulatory (E4M3) Green"}, + 371604: {'name': 'Ambulatory (E4M3) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 269, + 'doom_type': 2003, + 'region': "Ambulatory (E4M3) Blue"}, + 371605: {'name': 'Ambulatory (E4M3) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 270, + 'doom_type': 86, + 'region': "Ambulatory (E4M3) Main"}, + 371606: {'name': 'Ambulatory (E4M3) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 271, + 'doom_type': 85, + 'region': "Ambulatory (E4M3) Yellow"}, + 371607: {'name': 'Ambulatory (E4M3) - Map Scroll', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 272, + 'doom_type': 35, + 'region': "Ambulatory (E4M3) Yellow"}, + 371608: {'name': 'Ambulatory (E4M3) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 273, + 'doom_type': 8, + 'region': "Ambulatory (E4M3) Yellow"}, + 371609: {'name': 'Ambulatory (E4M3) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 274, + 'doom_type': 75, + 'region': "Ambulatory (E4M3) Yellow"}, + 371610: {'name': 'Ambulatory (E4M3) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 275, + 'doom_type': 30, + 'region': "Ambulatory (E4M3) Yellow"}, + 371611: {'name': 'Ambulatory (E4M3) - Torch', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 276, + 'doom_type': 33, + 'region': "Ambulatory (E4M3) Green"}, + 371612: {'name': 'Ambulatory (E4M3) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 277, + 'doom_type': 86, + 'region': "Ambulatory (E4M3) Green"}, + 371613: {'name': 'Ambulatory (E4M3) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 278, + 'doom_type': 31, + 'region': "Ambulatory (E4M3) Blue"}, + 371614: {'name': 'Ambulatory (E4M3) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 279, + 'doom_type': 32, + 'region': "Ambulatory (E4M3) Blue"}, + 371615: {'name': 'Ambulatory (E4M3) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 281, + 'doom_type': 84, + 'region': "Ambulatory (E4M3) Blue"}, + 371616: {'name': 'Ambulatory (E4M3) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 282, + 'doom_type': 36, + 'region': "Ambulatory (E4M3) Green"}, + 371617: {'name': 'Ambulatory (E4M3) - Ring of Invincibility 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 283, + 'doom_type': 84, + 'region': "Ambulatory (E4M3) Green"}, + 371618: {'name': 'Ambulatory (E4M3) - Morph Ovum 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 284, + 'doom_type': 30, + 'region': "Ambulatory (E4M3) Blue"}, + 371619: {'name': 'Ambulatory (E4M3) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 285, + 'doom_type': 8, + 'region': "Ambulatory (E4M3) Yellow"}, + 371620: {'name': 'Ambulatory (E4M3) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 297, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Green"}, + 371621: {'name': 'Ambulatory (E4M3) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 298, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Yellow"}, + 371622: {'name': 'Ambulatory (E4M3) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 299, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Yellow"}, + 371623: {'name': 'Ambulatory (E4M3) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': 300, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Blue"}, + 371624: {'name': 'Ambulatory (E4M3) - Firemace 5', + 'episode': 4, + 'check_sanity': True, + 'map': 3, + 'index': 301, + 'doom_type': 2002, + 'region': "Ambulatory (E4M3) Green"}, + 371625: {'name': 'Ambulatory (E4M3) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Ambulatory (E4M3) Blue"}, + 371626: {'name': 'Sepulcher (E4M4) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 27, + 'doom_type': 85, + 'region': "Sepulcher (E4M4) Main"}, + 371627: {'name': 'Sepulcher (E4M4) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 28, + 'doom_type': 2004, + 'region': "Sepulcher (E4M4) Main"}, + 371628: {'name': 'Sepulcher (E4M4) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 29, + 'doom_type': 53, + 'region': "Sepulcher (E4M4) Main"}, + 371629: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 30, + 'doom_type': 2001, + 'region': "Sepulcher (E4M4) Main"}, + 371630: {'name': 'Sepulcher (E4M4) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 31, + 'doom_type': 86, + 'region': "Sepulcher (E4M4) Main"}, + 371631: {'name': 'Sepulcher (E4M4) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 40, + 'doom_type': 75, + 'region': "Sepulcher (E4M4) Main"}, + 371632: {'name': 'Sepulcher (E4M4) - Mystic Urn', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 41, + 'doom_type': 32, + 'region': "Sepulcher (E4M4) Main"}, + 371633: {'name': 'Sepulcher (E4M4) - Chaos Device', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 50, + 'doom_type': 36, + 'region': "Sepulcher (E4M4) Main"}, + 371634: {'name': 'Sepulcher (E4M4) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 51, + 'doom_type': 31, + 'region': "Sepulcher (E4M4) Main"}, + 371635: {'name': 'Sepulcher (E4M4) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 65, + 'doom_type': 30, + 'region': "Sepulcher (E4M4) Main"}, + 371636: {'name': 'Sepulcher (E4M4) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 66, + 'doom_type': 33, + 'region': "Sepulcher (E4M4) Main"}, + 371637: {'name': 'Sepulcher (E4M4) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 67, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371638: {'name': 'Sepulcher (E4M4) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 74, + 'doom_type': 2003, + 'region': "Sepulcher (E4M4) Main"}, + 371639: {'name': 'Sepulcher (E4M4) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 137, + 'doom_type': 84, + 'region': "Sepulcher (E4M4) Main"}, + 371640: {'name': 'Sepulcher (E4M4) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 138, + 'doom_type': 8, + 'region': "Sepulcher (E4M4) Main"}, + 371641: {'name': 'Sepulcher (E4M4) - Ethereal Crossbow 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 199, + 'doom_type': 2001, + 'region': "Sepulcher (E4M4) Main"}, + 371642: {'name': 'Sepulcher (E4M4) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 235, + 'doom_type': 8, + 'region': "Sepulcher (E4M4) Main"}, + 371643: {'name': 'Sepulcher (E4M4) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 239, + 'doom_type': 86, + 'region': "Sepulcher (E4M4) Main"}, + 371644: {'name': 'Sepulcher (E4M4) - Torch 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 243, + 'doom_type': 33, + 'region': "Sepulcher (E4M4) Main"}, + 371645: {'name': 'Sepulcher (E4M4) - Silver Shield 2', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 244, + 'doom_type': 85, + 'region': "Sepulcher (E4M4) Main"}, + 371646: {'name': 'Sepulcher (E4M4) - Firemace 2', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 307, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371647: {'name': 'Sepulcher (E4M4) - Firemace 3', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 308, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371648: {'name': 'Sepulcher (E4M4) - Firemace 4', + 'episode': 4, + 'check_sanity': True, + 'map': 4, + 'index': 309, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371649: {'name': 'Sepulcher (E4M4) - Firemace 5', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 310, + 'doom_type': 2002, + 'region': "Sepulcher (E4M4) Main"}, + 371650: {'name': 'Sepulcher (E4M4) - Dragon Claw 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 325, + 'doom_type': 53, + 'region': "Sepulcher (E4M4) Main"}, + 371651: {'name': 'Sepulcher (E4M4) - Phoenix Rod 2', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': 339, + 'doom_type': 2003, + 'region': "Sepulcher (E4M4) Main"}, + 371652: {'name': 'Sepulcher (E4M4) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Sepulcher (E4M4) Main"}, + 371653: {'name': 'Great Stair (E4M5) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 3, + 'doom_type': 2001, + 'region': "Great Stair (E4M5) Main"}, + 371654: {'name': 'Great Stair (E4M5) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 27, + 'doom_type': 80, + 'region': "Great Stair (E4M5) Main"}, + 371655: {'name': 'Great Stair (E4M5) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 58, + 'doom_type': 53, + 'region': "Great Stair (E4M5) Yellow"}, + 371656: {'name': 'Great Stair (E4M5) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 64, + 'doom_type': 73, + 'region': "Great Stair (E4M5) Yellow"}, + 371657: {'name': 'Great Stair (E4M5) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 71, + 'doom_type': 79, + 'region': "Great Stair (E4M5) Green"}, + 371658: {'name': 'Great Stair (E4M5) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 78, + 'doom_type': 85, + 'region': "Great Stair (E4M5) Main"}, + 371659: {'name': 'Great Stair (E4M5) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 90, + 'doom_type': 2005, + 'region': "Great Stair (E4M5) Main"}, + 371660: {'name': 'Great Stair (E4M5) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 91, + 'doom_type': 2004, + 'region': "Great Stair (E4M5) Yellow"}, + 371661: {'name': 'Great Stair (E4M5) - Phoenix Rod', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 92, + 'doom_type': 2003, + 'region': "Great Stair (E4M5) Green"}, + 371662: {'name': 'Great Stair (E4M5) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 93, + 'doom_type': 8, + 'region': "Great Stair (E4M5) Main"}, + 371663: {'name': 'Great Stair (E4M5) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 94, + 'doom_type': 8, + 'region': "Great Stair (E4M5) Green"}, + 371664: {'name': 'Great Stair (E4M5) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 95, + 'doom_type': 30, + 'region': "Great Stair (E4M5) Main"}, + 371665: {'name': 'Great Stair (E4M5) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 110, + 'doom_type': 32, + 'region': "Great Stair (E4M5) Yellow"}, + 371666: {'name': 'Great Stair (E4M5) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 111, + 'doom_type': 75, + 'region': "Great Stair (E4M5) Yellow"}, + 371667: {'name': 'Great Stair (E4M5) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 112, + 'doom_type': 84, + 'region': "Great Stair (E4M5) Main"}, + 371668: {'name': 'Great Stair (E4M5) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 113, + 'doom_type': 31, + 'region': "Great Stair (E4M5) Green"}, + 371669: {'name': 'Great Stair (E4M5) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 114, + 'doom_type': 35, + 'region': "Great Stair (E4M5) Green"}, + 371670: {'name': 'Great Stair (E4M5) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 115, + 'doom_type': 36, + 'region': "Great Stair (E4M5) Main"}, + 371671: {'name': 'Great Stair (E4M5) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 116, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Main"}, + 371672: {'name': 'Great Stair (E4M5) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 117, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Yellow"}, + 371673: {'name': 'Great Stair (E4M5) - Torch', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 118, + 'doom_type': 33, + 'region': "Great Stair (E4M5) Main"}, + 371674: {'name': 'Great Stair (E4M5) - Firemace', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 123, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Main"}, + 371675: {'name': 'Great Stair (E4M5) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 124, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Main"}, + 371676: {'name': 'Great Stair (E4M5) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 125, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Yellow"}, + 371677: {'name': 'Great Stair (E4M5) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 126, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Blue"}, + 371678: {'name': 'Great Stair (E4M5) - Firemace 5', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 127, + 'doom_type': 2002, + 'region': "Great Stair (E4M5) Yellow"}, + 371679: {'name': 'Great Stair (E4M5) - Mystic Urn 2', + 'episode': 4, + 'check_sanity': True, + 'map': 5, + 'index': 507, + 'doom_type': 32, + 'region': "Great Stair (E4M5) Green"}, + 371680: {'name': 'Great Stair (E4M5) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': 508, + 'doom_type': 86, + 'region': "Great Stair (E4M5) Green"}, + 371681: {'name': 'Great Stair (E4M5) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Great Stair (E4M5) Blue"}, + 371682: {'name': 'Halls of the Apostate (E4M6) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 17, + 'doom_type': 73, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371683: {'name': 'Halls of the Apostate (E4M6) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 18, + 'doom_type': 79, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371684: {'name': 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 59, + 'doom_type': 2005, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371685: {'name': 'Halls of the Apostate (E4M6) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 60, + 'doom_type': 2001, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371686: {'name': 'Halls of the Apostate (E4M6) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 61, + 'doom_type': 53, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371687: {'name': 'Halls of the Apostate (E4M6) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 62, + 'doom_type': 2004, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371688: {'name': 'Halls of the Apostate (E4M6) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 63, + 'doom_type': 2003, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371689: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 68, + 'doom_type': 8, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371690: {'name': 'Halls of the Apostate (E4M6) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 79, + 'doom_type': 30, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371691: {'name': 'Halls of the Apostate (E4M6) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 80, + 'doom_type': 32, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371692: {'name': 'Halls of the Apostate (E4M6) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 81, + 'doom_type': 75, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371693: {'name': 'Halls of the Apostate (E4M6) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 82, + 'doom_type': 85, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371694: {'name': 'Halls of the Apostate (E4M6) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 83, + 'doom_type': 85, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371695: {'name': 'Halls of the Apostate (E4M6) - Enchanted Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 84, + 'doom_type': 31, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371696: {'name': 'Halls of the Apostate (E4M6) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 85, + 'doom_type': 35, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371697: {'name': 'Halls of the Apostate (E4M6) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 86, + 'doom_type': 36, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371698: {'name': 'Halls of the Apostate (E4M6) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 6, + 'index': 87, + 'doom_type': 86, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371699: {'name': 'Halls of the Apostate (E4M6) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 88, + 'doom_type': 86, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371700: {'name': 'Halls of the Apostate (E4M6) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 89, + 'doom_type': 8, + 'region': "Halls of the Apostate (E4M6) Green"}, + 371701: {'name': 'Halls of the Apostate (E4M6) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 108, + 'doom_type': 84, + 'region': "Halls of the Apostate (E4M6) Yellow"}, + 371702: {'name': 'Halls of the Apostate (E4M6) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': 420, + 'doom_type': 80, + 'region': "Halls of the Apostate (E4M6) Main"}, + 371703: {'name': 'Halls of the Apostate (E4M6) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Halls of the Apostate (E4M6) Blue"}, + 371704: {'name': 'Ramparts of Perdition (E4M7) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 28, + 'doom_type': 80, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371705: {'name': 'Ramparts of Perdition (E4M7) - Green key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 33, + 'doom_type': 73, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371706: {'name': 'Ramparts of Perdition (E4M7) - Blue key', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 36, + 'doom_type': 79, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371707: {'name': 'Ramparts of Perdition (E4M7) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 39, + 'doom_type': 84, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371708: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 40, + 'doom_type': 32, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371709: {'name': 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 124, + 'doom_type': 2005, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371710: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 125, + 'doom_type': 2001, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371711: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 126, + 'doom_type': 53, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371712: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 127, + 'doom_type': 2003, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371713: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 128, + 'doom_type': 2004, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371714: {'name': 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 129, + 'doom_type': 2001, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371715: {'name': 'Ramparts of Perdition (E4M7) - Dragon Claw 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 130, + 'doom_type': 53, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371716: {'name': 'Ramparts of Perdition (E4M7) - Phoenix Rod 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 131, + 'doom_type': 2003, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371717: {'name': 'Ramparts of Perdition (E4M7) - Hellstaff 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 132, + 'doom_type': 2004, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371718: {'name': 'Ramparts of Perdition (E4M7) - Firemace', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 133, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371719: {'name': 'Ramparts of Perdition (E4M7) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 134, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371720: {'name': 'Ramparts of Perdition (E4M7) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 135, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371721: {'name': 'Ramparts of Perdition (E4M7) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 136, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371722: {'name': 'Ramparts of Perdition (E4M7) - Firemace 5', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 137, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371723: {'name': 'Ramparts of Perdition (E4M7) - Firemace 6', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 138, + 'doom_type': 2002, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371724: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 140, + 'doom_type': 8, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371725: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 141, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371726: {'name': 'Ramparts of Perdition (E4M7) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 142, + 'doom_type': 8, + 'region': "Ramparts of Perdition (E4M7) Green"}, + 371727: {'name': 'Ramparts of Perdition (E4M7) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 143, + 'doom_type': 30, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371728: {'name': 'Ramparts of Perdition (E4M7) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 153, + 'doom_type': 75, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371729: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 154, + 'doom_type': 85, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371730: {'name': 'Ramparts of Perdition (E4M7) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 155, + 'doom_type': 85, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371731: {'name': 'Ramparts of Perdition (E4M7) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 156, + 'doom_type': 31, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371732: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 157, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371733: {'name': 'Ramparts of Perdition (E4M7) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 158, + 'doom_type': 86, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371734: {'name': 'Ramparts of Perdition (E4M7) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 159, + 'doom_type': 33, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371735: {'name': 'Ramparts of Perdition (E4M7) - Torch 2', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 160, + 'doom_type': 33, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371736: {'name': 'Ramparts of Perdition (E4M7) - Mystic Urn 2', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 161, + 'doom_type': 32, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371737: {'name': 'Ramparts of Perdition (E4M7) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': 162, + 'doom_type': 36, + 'region': "Ramparts of Perdition (E4M7) Yellow"}, + 371738: {'name': 'Ramparts of Perdition (E4M7) - Map Scroll', + 'episode': 4, + 'check_sanity': True, + 'map': 7, + 'index': 163, + 'doom_type': 35, + 'region': "Ramparts of Perdition (E4M7) Main"}, + 371739: {'name': 'Ramparts of Perdition (E4M7) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Ramparts of Perdition (E4M7) Blue"}, + 371740: {'name': 'Shattered Bridge (E4M8) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 5, + 'doom_type': 80, + 'region': "Shattered Bridge (E4M8) Main"}, + 371741: {'name': 'Shattered Bridge (E4M8) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 58, + 'doom_type': 53, + 'region': "Shattered Bridge (E4M8) Main"}, + 371742: {'name': 'Shattered Bridge (E4M8) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 79, + 'doom_type': 2003, + 'region': "Shattered Bridge (E4M8) Boss"}, + 371743: {'name': 'Shattered Bridge (E4M8) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 80, + 'doom_type': 2001, + 'region': "Shattered Bridge (E4M8) Main"}, + 371744: {'name': 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 81, + 'doom_type': 2005, + 'region': "Shattered Bridge (E4M8) Main"}, + 371745: {'name': 'Shattered Bridge (E4M8) - Hellstaff', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 82, + 'doom_type': 2004, + 'region': "Shattered Bridge (E4M8) Main"}, + 371746: {'name': 'Shattered Bridge (E4M8) - Bag of Holding', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 96, + 'doom_type': 8, + 'region': "Shattered Bridge (E4M8) Main"}, + 371747: {'name': 'Shattered Bridge (E4M8) - Morph Ovum', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 97, + 'doom_type': 30, + 'region': "Shattered Bridge (E4M8) Main"}, + 371748: {'name': 'Shattered Bridge (E4M8) - Silver Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 98, + 'doom_type': 85, + 'region': "Shattered Bridge (E4M8) Main"}, + 371749: {'name': 'Shattered Bridge (E4M8) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 108, + 'doom_type': 8, + 'region': "Shattered Bridge (E4M8) Main"}, + 371750: {'name': 'Shattered Bridge (E4M8) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 109, + 'doom_type': 32, + 'region': "Shattered Bridge (E4M8) Main"}, + 371751: {'name': 'Shattered Bridge (E4M8) - Shadowsphere', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 110, + 'doom_type': 75, + 'region': "Shattered Bridge (E4M8) Main"}, + 371752: {'name': 'Shattered Bridge (E4M8) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 111, + 'doom_type': 84, + 'region': "Shattered Bridge (E4M8) Main"}, + 371753: {'name': 'Shattered Bridge (E4M8) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 112, + 'doom_type': 36, + 'region': "Shattered Bridge (E4M8) Main"}, + 371754: {'name': 'Shattered Bridge (E4M8) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 8, + 'index': 113, + 'doom_type': 86, + 'region': "Shattered Bridge (E4M8) Main"}, + 371755: {'name': 'Shattered Bridge (E4M8) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 114, + 'doom_type': 33, + 'region': "Shattered Bridge (E4M8) Main"}, + 371756: {'name': 'Shattered Bridge (E4M8) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 115, + 'doom_type': 86, + 'region': "Shattered Bridge (E4M8) Main"}, + 371757: {'name': 'Shattered Bridge (E4M8) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': 118, + 'doom_type': 31, + 'region': "Shattered Bridge (E4M8) Main"}, + 371758: {'name': 'Shattered Bridge (E4M8) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Shattered Bridge (E4M8) Boss"}, + 371759: {'name': 'Mausoleum (E4M9) - Yellow key', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 50, + 'doom_type': 80, + 'region': "Mausoleum (E4M9) Main"}, + 371760: {'name': 'Mausoleum (E4M9) - Gauntlets of the Necromancer', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 59, + 'doom_type': 2005, + 'region': "Mausoleum (E4M9) Main"}, + 371761: {'name': 'Mausoleum (E4M9) - Ethereal Crossbow', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 60, + 'doom_type': 2001, + 'region': "Mausoleum (E4M9) Main"}, + 371762: {'name': 'Mausoleum (E4M9) - Dragon Claw', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 61, + 'doom_type': 53, + 'region': "Mausoleum (E4M9) Main"}, + 371763: {'name': 'Mausoleum (E4M9) - Hellstaff', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 62, + 'doom_type': 2004, + 'region': "Mausoleum (E4M9) Main"}, + 371764: {'name': 'Mausoleum (E4M9) - Phoenix Rod', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 63, + 'doom_type': 2003, + 'region': "Mausoleum (E4M9) Main"}, + 371765: {'name': 'Mausoleum (E4M9) - Firemace', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 64, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371766: {'name': 'Mausoleum (E4M9) - Firemace 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 65, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371767: {'name': 'Mausoleum (E4M9) - Firemace 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 66, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371768: {'name': 'Mausoleum (E4M9) - Firemace 4', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 67, + 'doom_type': 2002, + 'region': "Mausoleum (E4M9) Main"}, + 371769: {'name': 'Mausoleum (E4M9) - Bag of Holding', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 68, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371770: {'name': 'Mausoleum (E4M9) - Bag of Holding 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 69, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371771: {'name': 'Mausoleum (E4M9) - Bag of Holding 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 70, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Main"}, + 371772: {'name': 'Mausoleum (E4M9) - Bag of Holding 4', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 71, + 'doom_type': 8, + 'region': "Mausoleum (E4M9) Yellow"}, + 371773: {'name': 'Mausoleum (E4M9) - Morph Ovum', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 79, + 'doom_type': 30, + 'region': "Mausoleum (E4M9) Main"}, + 371774: {'name': 'Mausoleum (E4M9) - Mystic Urn', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 81, + 'doom_type': 32, + 'region': "Mausoleum (E4M9) Main"}, + 371775: {'name': 'Mausoleum (E4M9) - Shadowsphere', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 82, + 'doom_type': 75, + 'region': "Mausoleum (E4M9) Main"}, + 371776: {'name': 'Mausoleum (E4M9) - Ring of Invincibility', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 83, + 'doom_type': 84, + 'region': "Mausoleum (E4M9) Main"}, + 371777: {'name': 'Mausoleum (E4M9) - Silver Shield', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 84, + 'doom_type': 85, + 'region': "Mausoleum (E4M9) Main"}, + 371778: {'name': 'Mausoleum (E4M9) - Silver Shield 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 85, + 'doom_type': 85, + 'region': "Mausoleum (E4M9) Main"}, + 371779: {'name': 'Mausoleum (E4M9) - Enchanted Shield', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 86, + 'doom_type': 31, + 'region': "Mausoleum (E4M9) Yellow"}, + 371780: {'name': 'Mausoleum (E4M9) - Map Scroll', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 87, + 'doom_type': 35, + 'region': "Mausoleum (E4M9) Yellow"}, + 371781: {'name': 'Mausoleum (E4M9) - Chaos Device', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 88, + 'doom_type': 36, + 'region': "Mausoleum (E4M9) Main"}, + 371782: {'name': 'Mausoleum (E4M9) - Torch', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 89, + 'doom_type': 33, + 'region': "Mausoleum (E4M9) Main"}, + 371783: {'name': 'Mausoleum (E4M9) - Torch 2', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 90, + 'doom_type': 33, + 'region': "Mausoleum (E4M9) Main"}, + 371784: {'name': 'Mausoleum (E4M9) - Tome of Power', + 'episode': 4, + 'check_sanity': True, + 'map': 9, + 'index': 91, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371785: {'name': 'Mausoleum (E4M9) - Tome of Power 2', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 93, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371786: {'name': 'Mausoleum (E4M9) - Tome of Power 3', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': 94, + 'doom_type': 86, + 'region': "Mausoleum (E4M9) Main"}, + 371787: {'name': 'Mausoleum (E4M9) - Exit', + 'episode': 4, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Mausoleum (E4M9) Yellow"}, + 371788: {'name': 'Ochre Cliffs (E5M1) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 4, + 'doom_type': 80, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371789: {'name': 'Ochre Cliffs (E5M1) - Blue key', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 7, + 'doom_type': 79, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371790: {'name': 'Ochre Cliffs (E5M1) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 9, + 'doom_type': 73, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371791: {'name': 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 92, + 'doom_type': 2005, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371792: {'name': 'Ochre Cliffs (E5M1) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 93, + 'doom_type': 2001, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371793: {'name': 'Ochre Cliffs (E5M1) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 94, + 'doom_type': 53, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371794: {'name': 'Ochre Cliffs (E5M1) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 95, + 'doom_type': 2004, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371795: {'name': 'Ochre Cliffs (E5M1) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 96, + 'doom_type': 2003, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371796: {'name': 'Ochre Cliffs (E5M1) - Firemace', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 97, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371797: {'name': 'Ochre Cliffs (E5M1) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 98, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371798: {'name': 'Ochre Cliffs (E5M1) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 99, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371799: {'name': 'Ochre Cliffs (E5M1) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 100, + 'doom_type': 2002, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371800: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 101, + 'doom_type': 8, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371801: {'name': 'Ochre Cliffs (E5M1) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 102, + 'doom_type': 30, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371802: {'name': 'Ochre Cliffs (E5M1) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 112, + 'doom_type': 32, + 'region': "Ochre Cliffs (E5M1) Green"}, + 371803: {'name': 'Ochre Cliffs (E5M1) - Shadowsphere', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 113, + 'doom_type': 75, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371804: {'name': 'Ochre Cliffs (E5M1) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 114, + 'doom_type': 84, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371805: {'name': 'Ochre Cliffs (E5M1) - Silver Shield', + 'episode': 5, + 'check_sanity': True, + 'map': 1, + 'index': 115, + 'doom_type': 85, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371806: {'name': 'Ochre Cliffs (E5M1) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 116, + 'doom_type': 31, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371807: {'name': 'Ochre Cliffs (E5M1) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 117, + 'doom_type': 35, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371808: {'name': 'Ochre Cliffs (E5M1) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 118, + 'doom_type': 36, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371809: {'name': 'Ochre Cliffs (E5M1) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 119, + 'doom_type': 33, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371810: {'name': 'Ochre Cliffs (E5M1) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 120, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Main"}, + 371811: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 121, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371812: {'name': 'Ochre Cliffs (E5M1) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 122, + 'doom_type': 86, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371813: {'name': 'Ochre Cliffs (E5M1) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': 129, + 'doom_type': 8, + 'region': "Ochre Cliffs (E5M1) Yellow"}, + 371814: {'name': 'Ochre Cliffs (E5M1) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 1, + 'index': -1, + 'doom_type': -1, + 'region': "Ochre Cliffs (E5M1) Blue"}, + 371815: {'name': 'Rapids (E5M2) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 2, + 'doom_type': 73, + 'region': "Rapids (E5M2) Yellow"}, + 371816: {'name': 'Rapids (E5M2) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 3, + 'doom_type': 80, + 'region': "Rapids (E5M2) Main"}, + 371817: {'name': 'Rapids (E5M2) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 34, + 'doom_type': 2001, + 'region': "Rapids (E5M2) Main"}, + 371818: {'name': 'Rapids (E5M2) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 35, + 'doom_type': 2005, + 'region': "Rapids (E5M2) Main"}, + 371819: {'name': 'Rapids (E5M2) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 36, + 'doom_type': 53, + 'region': "Rapids (E5M2) Yellow"}, + 371820: {'name': 'Rapids (E5M2) - Hellstaff', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 37, + 'doom_type': 2004, + 'region': "Rapids (E5M2) Yellow"}, + 371821: {'name': 'Rapids (E5M2) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 38, + 'doom_type': 2003, + 'region': "Rapids (E5M2) Green"}, + 371822: {'name': 'Rapids (E5M2) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 39, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371823: {'name': 'Rapids (E5M2) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 40, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371824: {'name': 'Rapids (E5M2) - Bag of Holding 3', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 41, + 'doom_type': 8, + 'region': "Rapids (E5M2) Yellow"}, + 371825: {'name': 'Rapids (E5M2) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 42, + 'doom_type': 30, + 'region': "Rapids (E5M2) Yellow"}, + 371826: {'name': 'Rapids (E5M2) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 50, + 'doom_type': 32, + 'region': "Rapids (E5M2) Green"}, + 371827: {'name': 'Rapids (E5M2) - Shadowsphere', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 51, + 'doom_type': 75, + 'region': "Rapids (E5M2) Yellow"}, + 371828: {'name': 'Rapids (E5M2) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 52, + 'doom_type': 84, + 'region': "Rapids (E5M2) Green"}, + 371829: {'name': 'Rapids (E5M2) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 53, + 'doom_type': 85, + 'region': "Rapids (E5M2) Main"}, + 371830: {'name': 'Rapids (E5M2) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 54, + 'doom_type': 31, + 'region': "Rapids (E5M2) Yellow"}, + 371831: {'name': 'Rapids (E5M2) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 55, + 'doom_type': 35, + 'region': "Rapids (E5M2) Yellow"}, + 371832: {'name': 'Rapids (E5M2) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 56, + 'doom_type': 86, + 'region': "Rapids (E5M2) Yellow"}, + 371833: {'name': 'Rapids (E5M2) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 57, + 'doom_type': 36, + 'region': "Rapids (E5M2) Green"}, + 371834: {'name': 'Rapids (E5M2) - Tome of Power 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 58, + 'doom_type': 86, + 'region': "Rapids (E5M2) Yellow"}, + 371835: {'name': 'Rapids (E5M2) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 59, + 'doom_type': 33, + 'region': "Rapids (E5M2) Main"}, + 371836: {'name': 'Rapids (E5M2) - Enchanted Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 66, + 'doom_type': 31, + 'region': "Rapids (E5M2) Main"}, + 371837: {'name': 'Rapids (E5M2) - Hellstaff 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 67, + 'doom_type': 2004, + 'region': "Rapids (E5M2) Main"}, + 371838: {'name': 'Rapids (E5M2) - Phoenix Rod 2', + 'episode': 5, + 'check_sanity': True, + 'map': 2, + 'index': 68, + 'doom_type': 2003, + 'region': "Rapids (E5M2) Main"}, + 371839: {'name': 'Rapids (E5M2) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 71, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Main"}, + 371840: {'name': 'Rapids (E5M2) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 72, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371841: {'name': 'Rapids (E5M2) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 73, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371842: {'name': 'Rapids (E5M2) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 74, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Yellow"}, + 371843: {'name': 'Rapids (E5M2) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': 75, + 'doom_type': 2002, + 'region': "Rapids (E5M2) Green"}, + 371844: {'name': 'Rapids (E5M2) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 2, + 'index': -1, + 'doom_type': -1, + 'region': "Rapids (E5M2) Green"}, + 371845: {'name': 'Quay (E5M3) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 12, + 'doom_type': 73, + 'region': "Quay (E5M3) Yellow"}, + 371846: {'name': 'Quay (E5M3) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 13, + 'doom_type': 79, + 'region': "Quay (E5M3) Green"}, + 371847: {'name': 'Quay (E5M3) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 15, + 'doom_type': 80, + 'region': "Quay (E5M3) Main"}, + 371848: {'name': 'Quay (E5M3) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 212, + 'doom_type': 2001, + 'region': "Quay (E5M3) Main"}, + 371849: {'name': 'Quay (E5M3) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 213, + 'doom_type': 2005, + 'region': "Quay (E5M3) Main"}, + 371850: {'name': 'Quay (E5M3) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 214, + 'doom_type': 53, + 'region': "Quay (E5M3) Yellow"}, + 371851: {'name': 'Quay (E5M3) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 215, + 'doom_type': 2004, + 'region': "Quay (E5M3) Green"}, + 371852: {'name': 'Quay (E5M3) - Phoenix Rod', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 216, + 'doom_type': 2003, + 'region': "Quay (E5M3) Blue"}, + 371853: {'name': 'Quay (E5M3) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 217, + 'doom_type': 8, + 'region': "Quay (E5M3) Main"}, + 371854: {'name': 'Quay (E5M3) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 218, + 'doom_type': 30, + 'region': "Quay (E5M3) Blue"}, + 371855: {'name': 'Quay (E5M3) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 229, + 'doom_type': 32, + 'region': "Quay (E5M3) Green"}, + 371856: {'name': 'Quay (E5M3) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 230, + 'doom_type': 31, + 'region': "Quay (E5M3) Green"}, + 371857: {'name': 'Quay (E5M3) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 231, + 'doom_type': 84, + 'region': "Quay (E5M3) Main"}, + 371858: {'name': 'Quay (E5M3) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 232, + 'doom_type': 75, + 'region': "Quay (E5M3) Main"}, + 371859: {'name': 'Quay (E5M3) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 233, + 'doom_type': 85, + 'region': "Quay (E5M3) Main"}, + 371860: {'name': 'Quay (E5M3) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 234, + 'doom_type': 85, + 'region': "Quay (E5M3) Blue"}, + 371861: {'name': 'Quay (E5M3) - Map Scroll', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 235, + 'doom_type': 35, + 'region': "Quay (E5M3) Blue"}, + 371862: {'name': 'Quay (E5M3) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 236, + 'doom_type': 36, + 'region': "Quay (E5M3) Blue"}, + 371863: {'name': 'Quay (E5M3) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 237, + 'doom_type': 86, + 'region': "Quay (E5M3) Main"}, + 371864: {'name': 'Quay (E5M3) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 238, + 'doom_type': 86, + 'region': "Quay (E5M3) Green"}, + 371865: {'name': 'Quay (E5M3) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 239, + 'doom_type': 86, + 'region': "Quay (E5M3) Blue"}, + 371866: {'name': 'Quay (E5M3) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 240, + 'doom_type': 33, + 'region': "Quay (E5M3) Green"}, + 371867: {'name': 'Quay (E5M3) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 242, + 'doom_type': 2002, + 'region': "Quay (E5M3) Blue"}, + 371868: {'name': 'Quay (E5M3) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 243, + 'doom_type': 2002, + 'region': "Quay (E5M3) Main"}, + 371869: {'name': 'Quay (E5M3) - Firemace 3', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 244, + 'doom_type': 2002, + 'region': "Quay (E5M3) Yellow"}, + 371870: {'name': 'Quay (E5M3) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 245, + 'doom_type': 2002, + 'region': "Quay (E5M3) Yellow"}, + 371871: {'name': 'Quay (E5M3) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 246, + 'doom_type': 2002, + 'region': "Quay (E5M3) Green"}, + 371872: {'name': 'Quay (E5M3) - Firemace 6', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': 247, + 'doom_type': 2002, + 'region': "Quay (E5M3) Blue"}, + 371873: {'name': 'Quay (E5M3) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': True, + 'map': 3, + 'index': 252, + 'doom_type': 8, + 'region': "Quay (E5M3) Yellow"}, + 371874: {'name': 'Quay (E5M3) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 3, + 'index': -1, + 'doom_type': -1, + 'region': "Quay (E5M3) Blue"}, + 371875: {'name': 'Courtyard (E5M4) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 3, + 'doom_type': 79, + 'region': "Courtyard (E5M4) Main"}, + 371876: {'name': 'Courtyard (E5M4) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 16, + 'doom_type': 80, + 'region': "Courtyard (E5M4) Main"}, + 371877: {'name': 'Courtyard (E5M4) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 21, + 'doom_type': 73, + 'region': "Courtyard (E5M4) Kakis"}, + 371878: {'name': 'Courtyard (E5M4) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 84, + 'doom_type': 2005, + 'region': "Courtyard (E5M4) Main"}, + 371879: {'name': 'Courtyard (E5M4) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 85, + 'doom_type': 2001, + 'region': "Courtyard (E5M4) Main"}, + 371880: {'name': 'Courtyard (E5M4) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 86, + 'doom_type': 53, + 'region': "Courtyard (E5M4) Main"}, + 371881: {'name': 'Courtyard (E5M4) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 87, + 'doom_type': 2004, + 'region': "Courtyard (E5M4) Kakis"}, + 371882: {'name': 'Courtyard (E5M4) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 88, + 'doom_type': 2003, + 'region': "Courtyard (E5M4) Main"}, + 371883: {'name': 'Courtyard (E5M4) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 89, + 'doom_type': 30, + 'region': "Courtyard (E5M4) Main"}, + 371884: {'name': 'Courtyard (E5M4) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 90, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Main"}, + 371885: {'name': 'Courtyard (E5M4) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 91, + 'doom_type': 85, + 'region': "Courtyard (E5M4) Main"}, + 371886: {'name': 'Courtyard (E5M4) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 103, + 'doom_type': 32, + 'region': "Courtyard (E5M4) Main"}, + 371887: {'name': 'Courtyard (E5M4) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 104, + 'doom_type': 84, + 'region': "Courtyard (E5M4) Kakis"}, + 371888: {'name': 'Courtyard (E5M4) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 105, + 'doom_type': 75, + 'region': "Courtyard (E5M4) Main"}, + 371889: {'name': 'Courtyard (E5M4) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 106, + 'doom_type': 31, + 'region': "Courtyard (E5M4) Blue"}, + 371890: {'name': 'Courtyard (E5M4) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 107, + 'doom_type': 35, + 'region': "Courtyard (E5M4) Kakis"}, + 371891: {'name': 'Courtyard (E5M4) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 108, + 'doom_type': 36, + 'region': "Courtyard (E5M4) Main"}, + 371892: {'name': 'Courtyard (E5M4) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 109, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Main"}, + 371893: {'name': 'Courtyard (E5M4) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 110, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Blue"}, + 371894: {'name': 'Courtyard (E5M4) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 111, + 'doom_type': 86, + 'region': "Courtyard (E5M4) Kakis"}, + 371895: {'name': 'Courtyard (E5M4) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 112, + 'doom_type': 33, + 'region': "Courtyard (E5M4) Main"}, + 371896: {'name': 'Courtyard (E5M4) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 213, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Blue"}, + 371897: {'name': 'Courtyard (E5M4) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 219, + 'doom_type': 85, + 'region': "Courtyard (E5M4) Kakis"}, + 371898: {'name': 'Courtyard (E5M4) - Bag of Holding 3', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': 272, + 'doom_type': 8, + 'region': "Courtyard (E5M4) Main"}, + 371899: {'name': 'Courtyard (E5M4) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 4, + 'index': -1, + 'doom_type': -1, + 'region': "Courtyard (E5M4) Blue"}, + 371900: {'name': 'Hydratyr (E5M5) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 3, + 'doom_type': 80, + 'region': "Hydratyr (E5M5) Main"}, + 371901: {'name': 'Hydratyr (E5M5) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 5, + 'doom_type': 73, + 'region': "Hydratyr (E5M5) Yellow"}, + 371902: {'name': 'Hydratyr (E5M5) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 11, + 'doom_type': 79, + 'region': "Hydratyr (E5M5) Green"}, + 371903: {'name': 'Hydratyr (E5M5) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 238, + 'doom_type': 2001, + 'region': "Hydratyr (E5M5) Main"}, + 371904: {'name': 'Hydratyr (E5M5) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 239, + 'doom_type': 2005, + 'region': "Hydratyr (E5M5) Yellow"}, + 371905: {'name': 'Hydratyr (E5M5) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 240, + 'doom_type': 2004, + 'region': "Hydratyr (E5M5) Yellow"}, + 371906: {'name': 'Hydratyr (E5M5) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 241, + 'doom_type': 53, + 'region': "Hydratyr (E5M5) Yellow"}, + 371907: {'name': 'Hydratyr (E5M5) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 242, + 'doom_type': 2003, + 'region': "Hydratyr (E5M5) Green"}, + 371908: {'name': 'Hydratyr (E5M5) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 243, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371909: {'name': 'Hydratyr (E5M5) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 244, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371910: {'name': 'Hydratyr (E5M5) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 245, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371911: {'name': 'Hydratyr (E5M5) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 246, + 'doom_type': 2002, + 'region': "Hydratyr (E5M5) Green"}, + 371912: {'name': 'Hydratyr (E5M5) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 248, + 'doom_type': 8, + 'region': "Hydratyr (E5M5) Main"}, + 371913: {'name': 'Hydratyr (E5M5) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 259, + 'doom_type': 30, + 'region': "Hydratyr (E5M5) Green"}, + 371914: {'name': 'Hydratyr (E5M5) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 260, + 'doom_type': 8, + 'region': "Hydratyr (E5M5) Green"}, + 371915: {'name': 'Hydratyr (E5M5) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 261, + 'doom_type': 32, + 'region': "Hydratyr (E5M5) Blue"}, + 371916: {'name': 'Hydratyr (E5M5) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 262, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Main"}, + 371917: {'name': 'Hydratyr (E5M5) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 263, + 'doom_type': 75, + 'region': "Hydratyr (E5M5) Main"}, + 371918: {'name': 'Hydratyr (E5M5) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 264, + 'doom_type': 84, + 'region': "Hydratyr (E5M5) Yellow"}, + 371919: {'name': 'Hydratyr (E5M5) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 265, + 'doom_type': 36, + 'region': "Hydratyr (E5M5) Yellow"}, + 371920: {'name': 'Hydratyr (E5M5) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 266, + 'doom_type': 35, + 'region': "Hydratyr (E5M5) Yellow"}, + 371921: {'name': 'Hydratyr (E5M5) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 267, + 'doom_type': 31, + 'region': "Hydratyr (E5M5) Green"}, + 371922: {'name': 'Hydratyr (E5M5) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 268, + 'doom_type': 33, + 'region': "Hydratyr (E5M5) Main"}, + 371923: {'name': 'Hydratyr (E5M5) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 269, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Blue"}, + 371924: {'name': 'Hydratyr (E5M5) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 270, + 'doom_type': 85, + 'region': "Hydratyr (E5M5) Blue"}, + 371925: {'name': 'Hydratyr (E5M5) - Silver Shield 2', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 271, + 'doom_type': 85, + 'region': "Hydratyr (E5M5) Main"}, + 371926: {'name': 'Hydratyr (E5M5) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': 272, + 'doom_type': 86, + 'region': "Hydratyr (E5M5) Yellow"}, + 371927: {'name': 'Hydratyr (E5M5) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 5, + 'index': -1, + 'doom_type': -1, + 'region': "Hydratyr (E5M5) Blue"}, + 371928: {'name': 'Colonnade (E5M6) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 8, + 'doom_type': 80, + 'region': "Colonnade (E5M6) Main"}, + 371929: {'name': 'Colonnade (E5M6) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 9, + 'doom_type': 73, + 'region': "Colonnade (E5M6) Yellow"}, + 371930: {'name': 'Colonnade (E5M6) - Blue key', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 10, + 'doom_type': 79, + 'region': "Colonnade (E5M6) Green"}, + 371931: {'name': 'Colonnade (E5M6) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 91, + 'doom_type': 53, + 'region': "Colonnade (E5M6) Main"}, + 371932: {'name': 'Colonnade (E5M6) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 92, + 'doom_type': 2004, + 'region': "Colonnade (E5M6) Yellow"}, + 371933: {'name': 'Colonnade (E5M6) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 93, + 'doom_type': 2003, + 'region': "Colonnade (E5M6) Green"}, + 371934: {'name': 'Colonnade (E5M6) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 94, + 'doom_type': 2005, + 'region': "Colonnade (E5M6) Yellow"}, + 371935: {'name': 'Colonnade (E5M6) - Firemace', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 95, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371936: {'name': 'Colonnade (E5M6) - Firemace 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 96, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371937: {'name': 'Colonnade (E5M6) - Firemace 3', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 97, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Yellow"}, + 371938: {'name': 'Colonnade (E5M6) - Firemace 4', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 98, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Main"}, + 371939: {'name': 'Colonnade (E5M6) - Firemace 5', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 99, + 'doom_type': 2002, + 'region': "Colonnade (E5M6) Main"}, + 371940: {'name': 'Colonnade (E5M6) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 100, + 'doom_type': 31, + 'region': "Colonnade (E5M6) Yellow"}, + 371941: {'name': 'Colonnade (E5M6) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 101, + 'doom_type': 86, + 'region': "Colonnade (E5M6) Yellow"}, + 371942: {'name': 'Colonnade (E5M6) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 102, + 'doom_type': 85, + 'region': "Colonnade (E5M6) Main"}, + 371943: {'name': 'Colonnade (E5M6) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 103, + 'doom_type': 30, + 'region': "Colonnade (E5M6) Main"}, + 371944: {'name': 'Colonnade (E5M6) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 104, + 'doom_type': 36, + 'region': "Colonnade (E5M6) Main"}, + 371945: {'name': 'Colonnade (E5M6) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 105, + 'doom_type': 8, + 'region': "Colonnade (E5M6) Main"}, + 371946: {'name': 'Colonnade (E5M6) - Bag of Holding 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 106, + 'doom_type': 8, + 'region': "Colonnade (E5M6) Green"}, + 371947: {'name': 'Colonnade (E5M6) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 121, + 'doom_type': 32, + 'region': "Colonnade (E5M6) Yellow"}, + 371948: {'name': 'Colonnade (E5M6) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 122, + 'doom_type': 75, + 'region': "Colonnade (E5M6) Yellow"}, + 371949: {'name': 'Colonnade (E5M6) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 123, + 'doom_type': 84, + 'region': "Colonnade (E5M6) Main"}, + 371950: {'name': 'Colonnade (E5M6) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 124, + 'doom_type': 33, + 'region': "Colonnade (E5M6) Yellow"}, + 371951: {'name': 'Colonnade (E5M6) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 125, + 'doom_type': 35, + 'region': "Colonnade (E5M6) Yellow"}, + 371952: {'name': 'Colonnade (E5M6) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 126, + 'doom_type': 86, + 'region': "Colonnade (E5M6) Yellow"}, + 371953: {'name': 'Colonnade (E5M6) - Mystic Urn 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 127, + 'doom_type': 32, + 'region': "Colonnade (E5M6) Blue"}, + 371954: {'name': 'Colonnade (E5M6) - Ring of Invincibility 2', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 128, + 'doom_type': 84, + 'region': "Colonnade (E5M6) Blue"}, + 371955: {'name': 'Colonnade (E5M6) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': 348, + 'doom_type': 2001, + 'region': "Colonnade (E5M6) Main"}, + 371956: {'name': 'Colonnade (E5M6) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 6, + 'index': -1, + 'doom_type': -1, + 'region': "Colonnade (E5M6) Blue"}, + 371957: {'name': 'Foetid Manse (E5M7) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 7, + 'doom_type': 31, + 'region': "Foetid Manse (E5M7) Blue"}, + 371958: {'name': 'Foetid Manse (E5M7) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 8, + 'doom_type': 32, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371959: {'name': 'Foetid Manse (E5M7) - Morph Ovum', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 9, + 'doom_type': 30, + 'region': "Foetid Manse (E5M7) Green"}, + 371960: {'name': 'Foetid Manse (E5M7) - Green key', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 12, + 'doom_type': 73, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371961: {'name': 'Foetid Manse (E5M7) - Yellow key', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 15, + 'doom_type': 80, + 'region': "Foetid Manse (E5M7) Main"}, + 371962: {'name': 'Foetid Manse (E5M7) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 218, + 'doom_type': 2001, + 'region': "Foetid Manse (E5M7) Main"}, + 371963: {'name': 'Foetid Manse (E5M7) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 219, + 'doom_type': 2005, + 'region': "Foetid Manse (E5M7) Main"}, + 371964: {'name': 'Foetid Manse (E5M7) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 220, + 'doom_type': 53, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371965: {'name': 'Foetid Manse (E5M7) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 221, + 'doom_type': 2004, + 'region': "Foetid Manse (E5M7) Green"}, + 371966: {'name': 'Foetid Manse (E5M7) - Phoenix Rod', + 'episode': 5, + 'check_sanity': True, + 'map': 7, + 'index': 222, + 'doom_type': 2003, + 'region': "Foetid Manse (E5M7) Green"}, + 371967: {'name': 'Foetid Manse (E5M7) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 223, + 'doom_type': 75, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371968: {'name': 'Foetid Manse (E5M7) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 224, + 'doom_type': 84, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371969: {'name': 'Foetid Manse (E5M7) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 225, + 'doom_type': 85, + 'region': "Foetid Manse (E5M7) Green"}, + 371970: {'name': 'Foetid Manse (E5M7) - Map Scroll', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 234, + 'doom_type': 35, + 'region': "Foetid Manse (E5M7) Green"}, + 371971: {'name': 'Foetid Manse (E5M7) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 235, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371972: {'name': 'Foetid Manse (E5M7) - Tome of Power 2', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 236, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Green"}, + 371973: {'name': 'Foetid Manse (E5M7) - Tome of Power 3', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 237, + 'doom_type': 86, + 'region': "Foetid Manse (E5M7) Green"}, + 371974: {'name': 'Foetid Manse (E5M7) - Torch', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 238, + 'doom_type': 33, + 'region': "Foetid Manse (E5M7) Yellow"}, + 371975: {'name': 'Foetid Manse (E5M7) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 239, + 'doom_type': 36, + 'region': "Foetid Manse (E5M7) Green"}, + 371976: {'name': 'Foetid Manse (E5M7) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': 240, + 'doom_type': 8, + 'region': "Foetid Manse (E5M7) Green"}, + 371977: {'name': 'Foetid Manse (E5M7) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 7, + 'index': -1, + 'doom_type': -1, + 'region': "Foetid Manse (E5M7) Blue"}, + 371978: {'name': 'Field of Judgement (E5M8) - Hellstaff', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 18, + 'doom_type': 2004, + 'region': "Field of Judgement (E5M8) Main"}, + 371979: {'name': 'Field of Judgement (E5M8) - Phoenix Rod', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 19, + 'doom_type': 2003, + 'region': "Field of Judgement (E5M8) Main"}, + 371980: {'name': 'Field of Judgement (E5M8) - Ethereal Crossbow', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 20, + 'doom_type': 2001, + 'region': "Field of Judgement (E5M8) Main"}, + 371981: {'name': 'Field of Judgement (E5M8) - Dragon Claw', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 21, + 'doom_type': 53, + 'region': "Field of Judgement (E5M8) Main"}, + 371982: {'name': 'Field of Judgement (E5M8) - Gauntlets of the Necromancer', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 22, + 'doom_type': 2005, + 'region': "Field of Judgement (E5M8) Main"}, + 371983: {'name': 'Field of Judgement (E5M8) - Mystic Urn', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 23, + 'doom_type': 32, + 'region': "Field of Judgement (E5M8) Main"}, + 371984: {'name': 'Field of Judgement (E5M8) - Shadowsphere', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 24, + 'doom_type': 75, + 'region': "Field of Judgement (E5M8) Main"}, + 371985: {'name': 'Field of Judgement (E5M8) - Enchanted Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 25, + 'doom_type': 31, + 'region': "Field of Judgement (E5M8) Main"}, + 371986: {'name': 'Field of Judgement (E5M8) - Ring of Invincibility', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 26, + 'doom_type': 84, + 'region': "Field of Judgement (E5M8) Main"}, + 371987: {'name': 'Field of Judgement (E5M8) - Tome of Power', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 27, + 'doom_type': 86, + 'region': "Field of Judgement (E5M8) Main"}, + 371988: {'name': 'Field of Judgement (E5M8) - Chaos Device', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 28, + 'doom_type': 36, + 'region': "Field of Judgement (E5M8) Main"}, + 371989: {'name': 'Field of Judgement (E5M8) - Silver Shield', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 29, + 'doom_type': 85, + 'region': "Field of Judgement (E5M8) Main"}, + 371990: {'name': 'Field of Judgement (E5M8) - Bag of Holding', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': 62, + 'doom_type': 8, + 'region': "Field of Judgement (E5M8) Main"}, + 371991: {'name': 'Field of Judgement (E5M8) - Exit', + 'episode': 5, + 'check_sanity': False, + 'map': 8, + 'index': -1, + 'doom_type': -1, + 'region': "Field of Judgement (E5M8) Main"}, + 371992: {'name': "Skein of D'Sparil (E5M9) - Blue key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 0, + 'doom_type': 79, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 371993: {'name': "Skein of D'Sparil (E5M9) - Green key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 1, + 'doom_type': 73, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 371994: {'name': "Skein of D'Sparil (E5M9) - Yellow key", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 13, + 'doom_type': 80, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371995: {'name': "Skein of D'Sparil (E5M9) - Ethereal Crossbow", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 21, + 'doom_type': 2001, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371996: {'name': "Skein of D'Sparil (E5M9) - Dragon Claw", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 44, + 'doom_type': 53, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371997: {'name': "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 45, + 'doom_type': 2005, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 371998: {'name': "Skein of D'Sparil (E5M9) - Hellstaff", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 46, + 'doom_type': 2004, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 371999: {'name': "Skein of D'Sparil (E5M9) - Phoenix Rod", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 47, + 'doom_type': 2003, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372000: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 48, + 'doom_type': 8, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372001: {'name': "Skein of D'Sparil (E5M9) - Silver Shield", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 51, + 'doom_type': 85, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372002: {'name': "Skein of D'Sparil (E5M9) - Tome of Power", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 52, + 'doom_type': 86, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372003: {'name': "Skein of D'Sparil (E5M9) - Torch", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 53, + 'doom_type': 33, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372004: {'name': "Skein of D'Sparil (E5M9) - Morph Ovum", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 54, + 'doom_type': 30, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372005: {'name': "Skein of D'Sparil (E5M9) - Shadowsphere", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 64, + 'doom_type': 75, + 'region': "Skein of D'Sparil (E5M9) Yellow"}, + 372006: {'name': "Skein of D'Sparil (E5M9) - Chaos Device", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 65, + 'doom_type': 36, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372007: {'name': "Skein of D'Sparil (E5M9) - Ring of Invincibility", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 66, + 'doom_type': 84, + 'region': "Skein of D'Sparil (E5M9) Main"}, + 372008: {'name': "Skein of D'Sparil (E5M9) - Enchanted Shield", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 67, + 'doom_type': 31, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372009: {'name': "Skein of D'Sparil (E5M9) - Mystic Urn", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 68, + 'doom_type': 32, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372010: {'name': "Skein of D'Sparil (E5M9) - Tome of Power 2", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 69, + 'doom_type': 86, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372011: {'name': "Skein of D'Sparil (E5M9) - Map Scroll", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 70, + 'doom_type': 35, + 'region': "Skein of D'Sparil (E5M9) Green"}, + 372012: {'name': "Skein of D'Sparil (E5M9) - Bag of Holding 2", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': 243, + 'doom_type': 8, + 'region': "Skein of D'Sparil (E5M9) Blue"}, + 372013: {'name': "Skein of D'Sparil (E5M9) - Exit", + 'episode': 5, + 'check_sanity': False, + 'map': 9, + 'index': -1, + 'doom_type': -1, + 'region': "Skein of D'Sparil (E5M9) Blue"}, +} + + +location_name_groups: Dict[str, Set[str]] = { + 'Ambulatory (E4M3)': { + 'Ambulatory (E4M3) - Bag of Holding', + 'Ambulatory (E4M3) - Bag of Holding 2', + 'Ambulatory (E4M3) - Blue key', + 'Ambulatory (E4M3) - Chaos Device', + 'Ambulatory (E4M3) - Dragon Claw', + 'Ambulatory (E4M3) - Enchanted Shield', + 'Ambulatory (E4M3) - Ethereal Crossbow', + 'Ambulatory (E4M3) - Exit', + 'Ambulatory (E4M3) - Firemace', + 'Ambulatory (E4M3) - Firemace 2', + 'Ambulatory (E4M3) - Firemace 3', + 'Ambulatory (E4M3) - Firemace 4', + 'Ambulatory (E4M3) - Firemace 5', + 'Ambulatory (E4M3) - Gauntlets of the Necromancer', + 'Ambulatory (E4M3) - Green key', + 'Ambulatory (E4M3) - Hellstaff', + 'Ambulatory (E4M3) - Map Scroll', + 'Ambulatory (E4M3) - Morph Ovum', + 'Ambulatory (E4M3) - Morph Ovum 2', + 'Ambulatory (E4M3) - Mystic Urn', + 'Ambulatory (E4M3) - Phoenix Rod', + 'Ambulatory (E4M3) - Ring of Invincibility', + 'Ambulatory (E4M3) - Ring of Invincibility 2', + 'Ambulatory (E4M3) - Shadowsphere', + 'Ambulatory (E4M3) - Silver Shield', + 'Ambulatory (E4M3) - Tome of Power', + 'Ambulatory (E4M3) - Tome of Power 2', + 'Ambulatory (E4M3) - Torch', + 'Ambulatory (E4M3) - Yellow key', + }, + 'Blockhouse (E4M2)': { + 'Blockhouse (E4M2) - Bag of Holding', + 'Blockhouse (E4M2) - Bag of Holding 2', + 'Blockhouse (E4M2) - Blue key', + 'Blockhouse (E4M2) - Chaos Device', + 'Blockhouse (E4M2) - Dragon Claw', + 'Blockhouse (E4M2) - Enchanted Shield', + 'Blockhouse (E4M2) - Ethereal Crossbow', + 'Blockhouse (E4M2) - Exit', + 'Blockhouse (E4M2) - Gauntlets of the Necromancer', + 'Blockhouse (E4M2) - Green key', + 'Blockhouse (E4M2) - Hellstaff', + 'Blockhouse (E4M2) - Morph Ovum', + 'Blockhouse (E4M2) - Mystic Urn', + 'Blockhouse (E4M2) - Phoenix Rod', + 'Blockhouse (E4M2) - Ring of Invincibility', + 'Blockhouse (E4M2) - Ring of Invincibility 2', + 'Blockhouse (E4M2) - Shadowsphere', + 'Blockhouse (E4M2) - Shadowsphere 2', + 'Blockhouse (E4M2) - Silver Shield', + 'Blockhouse (E4M2) - Tome of Power', + 'Blockhouse (E4M2) - Yellow key', + }, + 'Catafalque (E4M1)': { + 'Catafalque (E4M1) - Bag of Holding', + 'Catafalque (E4M1) - Chaos Device', + 'Catafalque (E4M1) - Dragon Claw', + 'Catafalque (E4M1) - Ethereal Crossbow', + 'Catafalque (E4M1) - Exit', + 'Catafalque (E4M1) - Gauntlets of the Necromancer', + 'Catafalque (E4M1) - Green key', + 'Catafalque (E4M1) - Hellstaff', + 'Catafalque (E4M1) - Map Scroll', + 'Catafalque (E4M1) - Morph Ovum', + 'Catafalque (E4M1) - Ring of Invincibility', + 'Catafalque (E4M1) - Shadowsphere', + 'Catafalque (E4M1) - Silver Shield', + 'Catafalque (E4M1) - Tome of Power', + 'Catafalque (E4M1) - Tome of Power 2', + 'Catafalque (E4M1) - Torch', + 'Catafalque (E4M1) - Yellow key', + }, + 'Colonnade (E5M6)': { + 'Colonnade (E5M6) - Bag of Holding', + 'Colonnade (E5M6) - Bag of Holding 2', + 'Colonnade (E5M6) - Blue key', + 'Colonnade (E5M6) - Chaos Device', + 'Colonnade (E5M6) - Dragon Claw', + 'Colonnade (E5M6) - Enchanted Shield', + 'Colonnade (E5M6) - Ethereal Crossbow', + 'Colonnade (E5M6) - Exit', + 'Colonnade (E5M6) - Firemace', + 'Colonnade (E5M6) - Firemace 2', + 'Colonnade (E5M6) - Firemace 3', + 'Colonnade (E5M6) - Firemace 4', + 'Colonnade (E5M6) - Firemace 5', + 'Colonnade (E5M6) - Gauntlets of the Necromancer', + 'Colonnade (E5M6) - Green key', + 'Colonnade (E5M6) - Hellstaff', + 'Colonnade (E5M6) - Map Scroll', + 'Colonnade (E5M6) - Morph Ovum', + 'Colonnade (E5M6) - Mystic Urn', + 'Colonnade (E5M6) - Mystic Urn 2', + 'Colonnade (E5M6) - Phoenix Rod', + 'Colonnade (E5M6) - Ring of Invincibility', + 'Colonnade (E5M6) - Ring of Invincibility 2', + 'Colonnade (E5M6) - Shadowsphere', + 'Colonnade (E5M6) - Silver Shield', + 'Colonnade (E5M6) - Tome of Power', + 'Colonnade (E5M6) - Tome of Power 2', + 'Colonnade (E5M6) - Torch', + 'Colonnade (E5M6) - Yellow key', + }, + 'Courtyard (E5M4)': { + 'Courtyard (E5M4) - Bag of Holding', + 'Courtyard (E5M4) - Bag of Holding 2', + 'Courtyard (E5M4) - Bag of Holding 3', + 'Courtyard (E5M4) - Blue key', + 'Courtyard (E5M4) - Chaos Device', + 'Courtyard (E5M4) - Dragon Claw', + 'Courtyard (E5M4) - Enchanted Shield', + 'Courtyard (E5M4) - Ethereal Crossbow', + 'Courtyard (E5M4) - Exit', + 'Courtyard (E5M4) - Gauntlets of the Necromancer', + 'Courtyard (E5M4) - Green key', + 'Courtyard (E5M4) - Hellstaff', + 'Courtyard (E5M4) - Map Scroll', + 'Courtyard (E5M4) - Morph Ovum', + 'Courtyard (E5M4) - Mystic Urn', + 'Courtyard (E5M4) - Phoenix Rod', + 'Courtyard (E5M4) - Ring of Invincibility', + 'Courtyard (E5M4) - Shadowsphere', + 'Courtyard (E5M4) - Silver Shield', + 'Courtyard (E5M4) - Silver Shield 2', + 'Courtyard (E5M4) - Tome of Power', + 'Courtyard (E5M4) - Tome of Power 2', + 'Courtyard (E5M4) - Tome of Power 3', + 'Courtyard (E5M4) - Torch', + 'Courtyard (E5M4) - Yellow key', + }, + "D'Sparil'S Keep (E3M8)": { + "D'Sparil'S Keep (E3M8) - Bag of Holding", + "D'Sparil'S Keep (E3M8) - Chaos Device", + "D'Sparil'S Keep (E3M8) - Dragon Claw", + "D'Sparil'S Keep (E3M8) - Enchanted Shield", + "D'Sparil'S Keep (E3M8) - Ethereal Crossbow", + "D'Sparil'S Keep (E3M8) - Exit", + "D'Sparil'S Keep (E3M8) - Gauntlets of the Necromancer", + "D'Sparil'S Keep (E3M8) - Hellstaff", + "D'Sparil'S Keep (E3M8) - Mystic Urn", + "D'Sparil'S Keep (E3M8) - Phoenix Rod", + "D'Sparil'S Keep (E3M8) - Ring of Invincibility", + "D'Sparil'S Keep (E3M8) - Shadowsphere", + "D'Sparil'S Keep (E3M8) - Silver Shield", + "D'Sparil'S Keep (E3M8) - Tome of Power", + "D'Sparil'S Keep (E3M8) - Tome of Power 2", + "D'Sparil'S Keep (E3M8) - Tome of Power 3", + }, + 'Field of Judgement (E5M8)': { + 'Field of Judgement (E5M8) - Bag of Holding', + 'Field of Judgement (E5M8) - Chaos Device', + 'Field of Judgement (E5M8) - Dragon Claw', + 'Field of Judgement (E5M8) - Enchanted Shield', + 'Field of Judgement (E5M8) - Ethereal Crossbow', + 'Field of Judgement (E5M8) - Exit', + 'Field of Judgement (E5M8) - Gauntlets of the Necromancer', + 'Field of Judgement (E5M8) - Hellstaff', + 'Field of Judgement (E5M8) - Mystic Urn', + 'Field of Judgement (E5M8) - Phoenix Rod', + 'Field of Judgement (E5M8) - Ring of Invincibility', + 'Field of Judgement (E5M8) - Shadowsphere', + 'Field of Judgement (E5M8) - Silver Shield', + 'Field of Judgement (E5M8) - Tome of Power', + }, + 'Foetid Manse (E5M7)': { + 'Foetid Manse (E5M7) - Bag of Holding', + 'Foetid Manse (E5M7) - Chaos Device', + 'Foetid Manse (E5M7) - Dragon Claw', + 'Foetid Manse (E5M7) - Enchanted Shield', + 'Foetid Manse (E5M7) - Ethereal Crossbow', + 'Foetid Manse (E5M7) - Exit', + 'Foetid Manse (E5M7) - Gauntlets of the Necromancer', + 'Foetid Manse (E5M7) - Green key', + 'Foetid Manse (E5M7) - Hellstaff', + 'Foetid Manse (E5M7) - Map Scroll', + 'Foetid Manse (E5M7) - Morph Ovum', + 'Foetid Manse (E5M7) - Mystic Urn', + 'Foetid Manse (E5M7) - Phoenix Rod', + 'Foetid Manse (E5M7) - Ring of Invincibility', + 'Foetid Manse (E5M7) - Shadowsphere', + 'Foetid Manse (E5M7) - Silver Shield', + 'Foetid Manse (E5M7) - Tome of Power', + 'Foetid Manse (E5M7) - Tome of Power 2', + 'Foetid Manse (E5M7) - Tome of Power 3', + 'Foetid Manse (E5M7) - Torch', + 'Foetid Manse (E5M7) - Yellow key', + }, + 'Great Stair (E4M5)': { + 'Great Stair (E4M5) - Bag of Holding', + 'Great Stair (E4M5) - Bag of Holding 2', + 'Great Stair (E4M5) - Blue key', + 'Great Stair (E4M5) - Chaos Device', + 'Great Stair (E4M5) - Dragon Claw', + 'Great Stair (E4M5) - Enchanted Shield', + 'Great Stair (E4M5) - Ethereal Crossbow', + 'Great Stair (E4M5) - Exit', + 'Great Stair (E4M5) - Firemace', + 'Great Stair (E4M5) - Firemace 2', + 'Great Stair (E4M5) - Firemace 3', + 'Great Stair (E4M5) - Firemace 4', + 'Great Stair (E4M5) - Firemace 5', + 'Great Stair (E4M5) - Gauntlets of the Necromancer', + 'Great Stair (E4M5) - Green key', + 'Great Stair (E4M5) - Hellstaff', + 'Great Stair (E4M5) - Map Scroll', + 'Great Stair (E4M5) - Morph Ovum', + 'Great Stair (E4M5) - Mystic Urn', + 'Great Stair (E4M5) - Mystic Urn 2', + 'Great Stair (E4M5) - Phoenix Rod', + 'Great Stair (E4M5) - Ring of Invincibility', + 'Great Stair (E4M5) - Shadowsphere', + 'Great Stair (E4M5) - Silver Shield', + 'Great Stair (E4M5) - Tome of Power', + 'Great Stair (E4M5) - Tome of Power 2', + 'Great Stair (E4M5) - Tome of Power 3', + 'Great Stair (E4M5) - Torch', + 'Great Stair (E4M5) - Yellow key', + }, + 'Halls of the Apostate (E4M6)': { + 'Halls of the Apostate (E4M6) - Bag of Holding', + 'Halls of the Apostate (E4M6) - Bag of Holding 2', + 'Halls of the Apostate (E4M6) - Blue key', + 'Halls of the Apostate (E4M6) - Chaos Device', + 'Halls of the Apostate (E4M6) - Dragon Claw', + 'Halls of the Apostate (E4M6) - Enchanted Shield', + 'Halls of the Apostate (E4M6) - Ethereal Crossbow', + 'Halls of the Apostate (E4M6) - Exit', + 'Halls of the Apostate (E4M6) - Gauntlets of the Necromancer', + 'Halls of the Apostate (E4M6) - Green key', + 'Halls of the Apostate (E4M6) - Hellstaff', + 'Halls of the Apostate (E4M6) - Map Scroll', + 'Halls of the Apostate (E4M6) - Morph Ovum', + 'Halls of the Apostate (E4M6) - Mystic Urn', + 'Halls of the Apostate (E4M6) - Phoenix Rod', + 'Halls of the Apostate (E4M6) - Ring of Invincibility', + 'Halls of the Apostate (E4M6) - Shadowsphere', + 'Halls of the Apostate (E4M6) - Silver Shield', + 'Halls of the Apostate (E4M6) - Silver Shield 2', + 'Halls of the Apostate (E4M6) - Tome of Power', + 'Halls of the Apostate (E4M6) - Tome of Power 2', + 'Halls of the Apostate (E4M6) - Yellow key', + }, + "Hell's Maw (E1M8)": { + "Hell's Maw (E1M8) - Bag of Holding", + "Hell's Maw (E1M8) - Bag of Holding 2", + "Hell's Maw (E1M8) - Dragon Claw", + "Hell's Maw (E1M8) - Ethereal Crossbow", + "Hell's Maw (E1M8) - Exit", + "Hell's Maw (E1M8) - Gauntlets of the Necromancer", + "Hell's Maw (E1M8) - Morph Ovum", + "Hell's Maw (E1M8) - Ring of Invincibility", + "Hell's Maw (E1M8) - Ring of Invincibility 2", + "Hell's Maw (E1M8) - Ring of Invincibility 3", + "Hell's Maw (E1M8) - Shadowsphere", + "Hell's Maw (E1M8) - Silver Shield", + "Hell's Maw (E1M8) - Tome of Power", + "Hell's Maw (E1M8) - Tome of Power 2", + }, + 'Hydratyr (E5M5)': { + 'Hydratyr (E5M5) - Bag of Holding', + 'Hydratyr (E5M5) - Bag of Holding 2', + 'Hydratyr (E5M5) - Blue key', + 'Hydratyr (E5M5) - Chaos Device', + 'Hydratyr (E5M5) - Dragon Claw', + 'Hydratyr (E5M5) - Enchanted Shield', + 'Hydratyr (E5M5) - Ethereal Crossbow', + 'Hydratyr (E5M5) - Exit', + 'Hydratyr (E5M5) - Firemace', + 'Hydratyr (E5M5) - Firemace 2', + 'Hydratyr (E5M5) - Firemace 3', + 'Hydratyr (E5M5) - Firemace 4', + 'Hydratyr (E5M5) - Gauntlets of the Necromancer', + 'Hydratyr (E5M5) - Green key', + 'Hydratyr (E5M5) - Hellstaff', + 'Hydratyr (E5M5) - Map Scroll', + 'Hydratyr (E5M5) - Morph Ovum', + 'Hydratyr (E5M5) - Mystic Urn', + 'Hydratyr (E5M5) - Phoenix Rod', + 'Hydratyr (E5M5) - Ring of Invincibility', + 'Hydratyr (E5M5) - Shadowsphere', + 'Hydratyr (E5M5) - Silver Shield', + 'Hydratyr (E5M5) - Silver Shield 2', + 'Hydratyr (E5M5) - Tome of Power', + 'Hydratyr (E5M5) - Tome of Power 2', + 'Hydratyr (E5M5) - Tome of Power 3', + 'Hydratyr (E5M5) - Torch', + 'Hydratyr (E5M5) - Yellow key', + }, + 'Mausoleum (E4M9)': { + 'Mausoleum (E4M9) - Bag of Holding', + 'Mausoleum (E4M9) - Bag of Holding 2', + 'Mausoleum (E4M9) - Bag of Holding 3', + 'Mausoleum (E4M9) - Bag of Holding 4', + 'Mausoleum (E4M9) - Chaos Device', + 'Mausoleum (E4M9) - Dragon Claw', + 'Mausoleum (E4M9) - Enchanted Shield', + 'Mausoleum (E4M9) - Ethereal Crossbow', + 'Mausoleum (E4M9) - Exit', + 'Mausoleum (E4M9) - Firemace', + 'Mausoleum (E4M9) - Firemace 2', + 'Mausoleum (E4M9) - Firemace 3', + 'Mausoleum (E4M9) - Firemace 4', + 'Mausoleum (E4M9) - Gauntlets of the Necromancer', + 'Mausoleum (E4M9) - Hellstaff', + 'Mausoleum (E4M9) - Map Scroll', + 'Mausoleum (E4M9) - Morph Ovum', + 'Mausoleum (E4M9) - Mystic Urn', + 'Mausoleum (E4M9) - Phoenix Rod', + 'Mausoleum (E4M9) - Ring of Invincibility', + 'Mausoleum (E4M9) - Shadowsphere', + 'Mausoleum (E4M9) - Silver Shield', + 'Mausoleum (E4M9) - Silver Shield 2', + 'Mausoleum (E4M9) - Tome of Power', + 'Mausoleum (E4M9) - Tome of Power 2', + 'Mausoleum (E4M9) - Tome of Power 3', + 'Mausoleum (E4M9) - Torch', + 'Mausoleum (E4M9) - Torch 2', + 'Mausoleum (E4M9) - Yellow key', + }, + 'Ochre Cliffs (E5M1)': { + 'Ochre Cliffs (E5M1) - Bag of Holding', + 'Ochre Cliffs (E5M1) - Bag of Holding 2', + 'Ochre Cliffs (E5M1) - Blue key', + 'Ochre Cliffs (E5M1) - Chaos Device', + 'Ochre Cliffs (E5M1) - Dragon Claw', + 'Ochre Cliffs (E5M1) - Enchanted Shield', + 'Ochre Cliffs (E5M1) - Ethereal Crossbow', + 'Ochre Cliffs (E5M1) - Exit', + 'Ochre Cliffs (E5M1) - Firemace', + 'Ochre Cliffs (E5M1) - Firemace 2', + 'Ochre Cliffs (E5M1) - Firemace 3', + 'Ochre Cliffs (E5M1) - Firemace 4', + 'Ochre Cliffs (E5M1) - Gauntlets of the Necromancer', + 'Ochre Cliffs (E5M1) - Green key', + 'Ochre Cliffs (E5M1) - Hellstaff', + 'Ochre Cliffs (E5M1) - Map Scroll', + 'Ochre Cliffs (E5M1) - Morph Ovum', + 'Ochre Cliffs (E5M1) - Mystic Urn', + 'Ochre Cliffs (E5M1) - Phoenix Rod', + 'Ochre Cliffs (E5M1) - Ring of Invincibility', + 'Ochre Cliffs (E5M1) - Shadowsphere', + 'Ochre Cliffs (E5M1) - Silver Shield', + 'Ochre Cliffs (E5M1) - Tome of Power', + 'Ochre Cliffs (E5M1) - Tome of Power 2', + 'Ochre Cliffs (E5M1) - Tome of Power 3', + 'Ochre Cliffs (E5M1) - Torch', + 'Ochre Cliffs (E5M1) - Yellow key', + }, + 'Quay (E5M3)': { + 'Quay (E5M3) - Bag of Holding', + 'Quay (E5M3) - Bag of Holding 2', + 'Quay (E5M3) - Blue key', + 'Quay (E5M3) - Chaos Device', + 'Quay (E5M3) - Dragon Claw', + 'Quay (E5M3) - Enchanted Shield', + 'Quay (E5M3) - Ethereal Crossbow', + 'Quay (E5M3) - Exit', + 'Quay (E5M3) - Firemace', + 'Quay (E5M3) - Firemace 2', + 'Quay (E5M3) - Firemace 3', + 'Quay (E5M3) - Firemace 4', + 'Quay (E5M3) - Firemace 5', + 'Quay (E5M3) - Firemace 6', + 'Quay (E5M3) - Gauntlets of the Necromancer', + 'Quay (E5M3) - Green key', + 'Quay (E5M3) - Hellstaff', + 'Quay (E5M3) - Map Scroll', + 'Quay (E5M3) - Morph Ovum', + 'Quay (E5M3) - Mystic Urn', + 'Quay (E5M3) - Phoenix Rod', + 'Quay (E5M3) - Ring of Invincibility', + 'Quay (E5M3) - Shadowsphere', + 'Quay (E5M3) - Silver Shield', + 'Quay (E5M3) - Silver Shield 2', + 'Quay (E5M3) - Tome of Power', + 'Quay (E5M3) - Tome of Power 2', + 'Quay (E5M3) - Tome of Power 3', + 'Quay (E5M3) - Torch', + 'Quay (E5M3) - Yellow key', + }, + 'Ramparts of Perdition (E4M7)': { + 'Ramparts of Perdition (E4M7) - Bag of Holding', + 'Ramparts of Perdition (E4M7) - Bag of Holding 2', + 'Ramparts of Perdition (E4M7) - Blue key', + 'Ramparts of Perdition (E4M7) - Chaos Device', + 'Ramparts of Perdition (E4M7) - Dragon Claw', + 'Ramparts of Perdition (E4M7) - Dragon Claw 2', + 'Ramparts of Perdition (E4M7) - Enchanted Shield', + 'Ramparts of Perdition (E4M7) - Ethereal Crossbow', + 'Ramparts of Perdition (E4M7) - Ethereal Crossbow 2', + 'Ramparts of Perdition (E4M7) - Exit', + 'Ramparts of Perdition (E4M7) - Firemace', + 'Ramparts of Perdition (E4M7) - Firemace 2', + 'Ramparts of Perdition (E4M7) - Firemace 3', + 'Ramparts of Perdition (E4M7) - Firemace 4', + 'Ramparts of Perdition (E4M7) - Firemace 5', + 'Ramparts of Perdition (E4M7) - Firemace 6', + 'Ramparts of Perdition (E4M7) - Gauntlets of the Necromancer', + 'Ramparts of Perdition (E4M7) - Green key', + 'Ramparts of Perdition (E4M7) - Hellstaff', + 'Ramparts of Perdition (E4M7) - Hellstaff 2', + 'Ramparts of Perdition (E4M7) - Map Scroll', + 'Ramparts of Perdition (E4M7) - Morph Ovum', + 'Ramparts of Perdition (E4M7) - Mystic Urn', + 'Ramparts of Perdition (E4M7) - Mystic Urn 2', + 'Ramparts of Perdition (E4M7) - Phoenix Rod', + 'Ramparts of Perdition (E4M7) - Phoenix Rod 2', + 'Ramparts of Perdition (E4M7) - Ring of Invincibility', + 'Ramparts of Perdition (E4M7) - Shadowsphere', + 'Ramparts of Perdition (E4M7) - Silver Shield', + 'Ramparts of Perdition (E4M7) - Silver Shield 2', + 'Ramparts of Perdition (E4M7) - Tome of Power', + 'Ramparts of Perdition (E4M7) - Tome of Power 2', + 'Ramparts of Perdition (E4M7) - Tome of Power 3', + 'Ramparts of Perdition (E4M7) - Torch', + 'Ramparts of Perdition (E4M7) - Torch 2', + 'Ramparts of Perdition (E4M7) - Yellow key', + }, + 'Rapids (E5M2)': { + 'Rapids (E5M2) - Bag of Holding', + 'Rapids (E5M2) - Bag of Holding 2', + 'Rapids (E5M2) - Bag of Holding 3', + 'Rapids (E5M2) - Chaos Device', + 'Rapids (E5M2) - Dragon Claw', + 'Rapids (E5M2) - Enchanted Shield', + 'Rapids (E5M2) - Enchanted Shield 2', + 'Rapids (E5M2) - Ethereal Crossbow', + 'Rapids (E5M2) - Exit', + 'Rapids (E5M2) - Firemace', + 'Rapids (E5M2) - Firemace 2', + 'Rapids (E5M2) - Firemace 3', + 'Rapids (E5M2) - Firemace 4', + 'Rapids (E5M2) - Firemace 5', + 'Rapids (E5M2) - Gauntlets of the Necromancer', + 'Rapids (E5M2) - Green key', + 'Rapids (E5M2) - Hellstaff', + 'Rapids (E5M2) - Hellstaff 2', + 'Rapids (E5M2) - Map Scroll', + 'Rapids (E5M2) - Morph Ovum', + 'Rapids (E5M2) - Mystic Urn', + 'Rapids (E5M2) - Phoenix Rod', + 'Rapids (E5M2) - Phoenix Rod 2', + 'Rapids (E5M2) - Ring of Invincibility', + 'Rapids (E5M2) - Shadowsphere', + 'Rapids (E5M2) - Silver Shield', + 'Rapids (E5M2) - Tome of Power', + 'Rapids (E5M2) - Tome of Power 2', + 'Rapids (E5M2) - Torch', + 'Rapids (E5M2) - Yellow key', + }, + 'Sepulcher (E4M4)': { + 'Sepulcher (E4M4) - Bag of Holding', + 'Sepulcher (E4M4) - Bag of Holding 2', + 'Sepulcher (E4M4) - Chaos Device', + 'Sepulcher (E4M4) - Dragon Claw', + 'Sepulcher (E4M4) - Dragon Claw 2', + 'Sepulcher (E4M4) - Enchanted Shield', + 'Sepulcher (E4M4) - Ethereal Crossbow', + 'Sepulcher (E4M4) - Ethereal Crossbow 2', + 'Sepulcher (E4M4) - Exit', + 'Sepulcher (E4M4) - Firemace', + 'Sepulcher (E4M4) - Firemace 2', + 'Sepulcher (E4M4) - Firemace 3', + 'Sepulcher (E4M4) - Firemace 4', + 'Sepulcher (E4M4) - Firemace 5', + 'Sepulcher (E4M4) - Hellstaff', + 'Sepulcher (E4M4) - Morph Ovum', + 'Sepulcher (E4M4) - Mystic Urn', + 'Sepulcher (E4M4) - Phoenix Rod', + 'Sepulcher (E4M4) - Phoenix Rod 2', + 'Sepulcher (E4M4) - Ring of Invincibility', + 'Sepulcher (E4M4) - Shadowsphere', + 'Sepulcher (E4M4) - Silver Shield', + 'Sepulcher (E4M4) - Silver Shield 2', + 'Sepulcher (E4M4) - Tome of Power', + 'Sepulcher (E4M4) - Tome of Power 2', + 'Sepulcher (E4M4) - Torch', + 'Sepulcher (E4M4) - Torch 2', + }, + 'Shattered Bridge (E4M8)': { + 'Shattered Bridge (E4M8) - Bag of Holding', + 'Shattered Bridge (E4M8) - Bag of Holding 2', + 'Shattered Bridge (E4M8) - Chaos Device', + 'Shattered Bridge (E4M8) - Dragon Claw', + 'Shattered Bridge (E4M8) - Enchanted Shield', + 'Shattered Bridge (E4M8) - Ethereal Crossbow', + 'Shattered Bridge (E4M8) - Exit', + 'Shattered Bridge (E4M8) - Gauntlets of the Necromancer', + 'Shattered Bridge (E4M8) - Hellstaff', + 'Shattered Bridge (E4M8) - Morph Ovum', + 'Shattered Bridge (E4M8) - Mystic Urn', + 'Shattered Bridge (E4M8) - Phoenix Rod', + 'Shattered Bridge (E4M8) - Ring of Invincibility', + 'Shattered Bridge (E4M8) - Shadowsphere', + 'Shattered Bridge (E4M8) - Silver Shield', + 'Shattered Bridge (E4M8) - Tome of Power', + 'Shattered Bridge (E4M8) - Tome of Power 2', + 'Shattered Bridge (E4M8) - Torch', + 'Shattered Bridge (E4M8) - Yellow key', + }, + "Skein of D'Sparil (E5M9)": { + "Skein of D'Sparil (E5M9) - Bag of Holding", + "Skein of D'Sparil (E5M9) - Bag of Holding 2", + "Skein of D'Sparil (E5M9) - Blue key", + "Skein of D'Sparil (E5M9) - Chaos Device", + "Skein of D'Sparil (E5M9) - Dragon Claw", + "Skein of D'Sparil (E5M9) - Enchanted Shield", + "Skein of D'Sparil (E5M9) - Ethereal Crossbow", + "Skein of D'Sparil (E5M9) - Exit", + "Skein of D'Sparil (E5M9) - Gauntlets of the Necromancer", + "Skein of D'Sparil (E5M9) - Green key", + "Skein of D'Sparil (E5M9) - Hellstaff", + "Skein of D'Sparil (E5M9) - Map Scroll", + "Skein of D'Sparil (E5M9) - Morph Ovum", + "Skein of D'Sparil (E5M9) - Mystic Urn", + "Skein of D'Sparil (E5M9) - Phoenix Rod", + "Skein of D'Sparil (E5M9) - Ring of Invincibility", + "Skein of D'Sparil (E5M9) - Shadowsphere", + "Skein of D'Sparil (E5M9) - Silver Shield", + "Skein of D'Sparil (E5M9) - Tome of Power", + "Skein of D'Sparil (E5M9) - Tome of Power 2", + "Skein of D'Sparil (E5M9) - Torch", + "Skein of D'Sparil (E5M9) - Yellow key", + }, + 'The Aquifier (E3M9)': { + 'The Aquifier (E3M9) - Bag of Holding', + 'The Aquifier (E3M9) - Blue key', + 'The Aquifier (E3M9) - Chaos Device', + 'The Aquifier (E3M9) - Dragon Claw', + 'The Aquifier (E3M9) - Enchanted Shield', + 'The Aquifier (E3M9) - Ethereal Crossbow', + 'The Aquifier (E3M9) - Exit', + 'The Aquifier (E3M9) - Firemace', + 'The Aquifier (E3M9) - Firemace 2', + 'The Aquifier (E3M9) - Firemace 3', + 'The Aquifier (E3M9) - Firemace 4', + 'The Aquifier (E3M9) - Gauntlets of the Necromancer', + 'The Aquifier (E3M9) - Green key', + 'The Aquifier (E3M9) - Hellstaff', + 'The Aquifier (E3M9) - Map Scroll', + 'The Aquifier (E3M9) - Morph Ovum', + 'The Aquifier (E3M9) - Mystic Urn', + 'The Aquifier (E3M9) - Phoenix Rod', + 'The Aquifier (E3M9) - Ring of Invincibility', + 'The Aquifier (E3M9) - Shadowsphere', + 'The Aquifier (E3M9) - Silver Shield', + 'The Aquifier (E3M9) - Silver Shield 2', + 'The Aquifier (E3M9) - Tome of Power', + 'The Aquifier (E3M9) - Tome of Power 2', + 'The Aquifier (E3M9) - Torch', + 'The Aquifier (E3M9) - Yellow key', + }, + 'The Azure Fortress (E3M4)': { + 'The Azure Fortress (E3M4) - Bag of Holding', + 'The Azure Fortress (E3M4) - Bag of Holding 2', + 'The Azure Fortress (E3M4) - Chaos Device', + 'The Azure Fortress (E3M4) - Dragon Claw', + 'The Azure Fortress (E3M4) - Enchanted Shield', + 'The Azure Fortress (E3M4) - Enchanted Shield 2', + 'The Azure Fortress (E3M4) - Ethereal Crossbow', + 'The Azure Fortress (E3M4) - Exit', + 'The Azure Fortress (E3M4) - Gauntlets of the Necromancer', + 'The Azure Fortress (E3M4) - Green key', + 'The Azure Fortress (E3M4) - Hellstaff', + 'The Azure Fortress (E3M4) - Map Scroll', + 'The Azure Fortress (E3M4) - Morph Ovum', + 'The Azure Fortress (E3M4) - Morph Ovum 2', + 'The Azure Fortress (E3M4) - Mystic Urn', + 'The Azure Fortress (E3M4) - Mystic Urn 2', + 'The Azure Fortress (E3M4) - Phoenix Rod', + 'The Azure Fortress (E3M4) - Ring of Invincibility', + 'The Azure Fortress (E3M4) - Shadowsphere', + 'The Azure Fortress (E3M4) - Silver Shield', + 'The Azure Fortress (E3M4) - Silver Shield 2', + 'The Azure Fortress (E3M4) - Tome of Power', + 'The Azure Fortress (E3M4) - Tome of Power 2', + 'The Azure Fortress (E3M4) - Tome of Power 3', + 'The Azure Fortress (E3M4) - Torch', + 'The Azure Fortress (E3M4) - Torch 2', + 'The Azure Fortress (E3M4) - Torch 3', + 'The Azure Fortress (E3M4) - Yellow key', + }, + 'The Catacombs (E2M5)': { + 'The Catacombs (E2M5) - Bag of Holding', + 'The Catacombs (E2M5) - Blue key', + 'The Catacombs (E2M5) - Chaos Device', + 'The Catacombs (E2M5) - Dragon Claw', + 'The Catacombs (E2M5) - Enchanted Shield', + 'The Catacombs (E2M5) - Ethereal Crossbow', + 'The Catacombs (E2M5) - Exit', + 'The Catacombs (E2M5) - Gauntlets of the Necromancer', + 'The Catacombs (E2M5) - Green key', + 'The Catacombs (E2M5) - Hellstaff', + 'The Catacombs (E2M5) - Map Scroll', + 'The Catacombs (E2M5) - Morph Ovum', + 'The Catacombs (E2M5) - Mystic Urn', + 'The Catacombs (E2M5) - Phoenix Rod', + 'The Catacombs (E2M5) - Ring of Invincibility', + 'The Catacombs (E2M5) - Shadowsphere', + 'The Catacombs (E2M5) - Silver Shield', + 'The Catacombs (E2M5) - Tome of Power', + 'The Catacombs (E2M5) - Tome of Power 2', + 'The Catacombs (E2M5) - Tome of Power 3', + 'The Catacombs (E2M5) - Torch', + 'The Catacombs (E2M5) - Yellow key', + }, + 'The Cathedral (E1M6)': { + 'The Cathedral (E1M6) - Bag of Holding', + 'The Cathedral (E1M6) - Bag of Holding 2', + 'The Cathedral (E1M6) - Bag of Holding 3', + 'The Cathedral (E1M6) - Dragon Claw', + 'The Cathedral (E1M6) - Ethereal Crossbow', + 'The Cathedral (E1M6) - Exit', + 'The Cathedral (E1M6) - Gauntlets of the Necromancer', + 'The Cathedral (E1M6) - Green key', + 'The Cathedral (E1M6) - Map Scroll', + 'The Cathedral (E1M6) - Morph Ovum', + 'The Cathedral (E1M6) - Ring of Invincibility', + 'The Cathedral (E1M6) - Ring of Invincibility 2', + 'The Cathedral (E1M6) - Shadowsphere', + 'The Cathedral (E1M6) - Silver Shield', + 'The Cathedral (E1M6) - Silver Shield 2', + 'The Cathedral (E1M6) - Silver Shield 3', + 'The Cathedral (E1M6) - Tome of Power', + 'The Cathedral (E1M6) - Tome of Power 2', + 'The Cathedral (E1M6) - Tome of Power 3', + 'The Cathedral (E1M6) - Tome of Power 4', + 'The Cathedral (E1M6) - Torch', + 'The Cathedral (E1M6) - Yellow key', + }, + 'The Cesspool (E3M2)': { + 'The Cesspool (E3M2) - Bag of Holding', + 'The Cesspool (E3M2) - Bag of Holding 2', + 'The Cesspool (E3M2) - Blue key', + 'The Cesspool (E3M2) - Chaos Device', + 'The Cesspool (E3M2) - Dragon Claw', + 'The Cesspool (E3M2) - Enchanted Shield', + 'The Cesspool (E3M2) - Ethereal Crossbow', + 'The Cesspool (E3M2) - Exit', + 'The Cesspool (E3M2) - Firemace', + 'The Cesspool (E3M2) - Firemace 2', + 'The Cesspool (E3M2) - Firemace 3', + 'The Cesspool (E3M2) - Firemace 4', + 'The Cesspool (E3M2) - Firemace 5', + 'The Cesspool (E3M2) - Gauntlets of the Necromancer', + 'The Cesspool (E3M2) - Green key', + 'The Cesspool (E3M2) - Hellstaff', + 'The Cesspool (E3M2) - Map Scroll', + 'The Cesspool (E3M2) - Morph Ovum', + 'The Cesspool (E3M2) - Morph Ovum 2', + 'The Cesspool (E3M2) - Mystic Urn', + 'The Cesspool (E3M2) - Phoenix Rod', + 'The Cesspool (E3M2) - Ring of Invincibility', + 'The Cesspool (E3M2) - Shadowsphere', + 'The Cesspool (E3M2) - Silver Shield', + 'The Cesspool (E3M2) - Silver Shield 2', + 'The Cesspool (E3M2) - Tome of Power', + 'The Cesspool (E3M2) - Tome of Power 2', + 'The Cesspool (E3M2) - Tome of Power 3', + 'The Cesspool (E3M2) - Torch', + 'The Cesspool (E3M2) - Yellow key', + }, + 'The Chasm (E3M7)': { + 'The Chasm (E3M7) - Bag of Holding', + 'The Chasm (E3M7) - Bag of Holding 2', + 'The Chasm (E3M7) - Blue key', + 'The Chasm (E3M7) - Chaos Device', + 'The Chasm (E3M7) - Dragon Claw', + 'The Chasm (E3M7) - Enchanted Shield', + 'The Chasm (E3M7) - Ethereal Crossbow', + 'The Chasm (E3M7) - Exit', + 'The Chasm (E3M7) - Gauntlets of the Necromancer', + 'The Chasm (E3M7) - Green key', + 'The Chasm (E3M7) - Hellstaff', + 'The Chasm (E3M7) - Map Scroll', + 'The Chasm (E3M7) - Morph Ovum', + 'The Chasm (E3M7) - Mystic Urn', + 'The Chasm (E3M7) - Phoenix Rod', + 'The Chasm (E3M7) - Ring of Invincibility', + 'The Chasm (E3M7) - Shadowsphere', + 'The Chasm (E3M7) - Shadowsphere 2', + 'The Chasm (E3M7) - Silver Shield', + 'The Chasm (E3M7) - Tome of Power', + 'The Chasm (E3M7) - Tome of Power 2', + 'The Chasm (E3M7) - Tome of Power 3', + 'The Chasm (E3M7) - Torch', + 'The Chasm (E3M7) - Torch 2', + 'The Chasm (E3M7) - Yellow key', + }, + 'The Citadel (E1M5)': { + 'The Citadel (E1M5) - Bag of Holding', + 'The Citadel (E1M5) - Blue key', + 'The Citadel (E1M5) - Dragon Claw', + 'The Citadel (E1M5) - Ethereal Crossbow', + 'The Citadel (E1M5) - Exit', + 'The Citadel (E1M5) - Gauntlets of the Necromancer', + 'The Citadel (E1M5) - Green key', + 'The Citadel (E1M5) - Map Scroll', + 'The Citadel (E1M5) - Morph Ovum', + 'The Citadel (E1M5) - Ring of Invincibility', + 'The Citadel (E1M5) - Shadowsphere', + 'The Citadel (E1M5) - Silver Shield', + 'The Citadel (E1M5) - Silver Shield 2', + 'The Citadel (E1M5) - Tome of Power', + 'The Citadel (E1M5) - Tome of Power 2', + 'The Citadel (E1M5) - Tome of Power 3', + 'The Citadel (E1M5) - Tome of Power 4', + 'The Citadel (E1M5) - Tome of Power 5', + 'The Citadel (E1M5) - Torch', + 'The Citadel (E1M5) - Torch 2', + 'The Citadel (E1M5) - Yellow key', + }, + 'The Confluence (E3M3)': { + 'The Confluence (E3M3) - Bag of Holding', + 'The Confluence (E3M3) - Blue key', + 'The Confluence (E3M3) - Chaos Device', + 'The Confluence (E3M3) - Dragon Claw', + 'The Confluence (E3M3) - Enchanted Shield', + 'The Confluence (E3M3) - Ethereal Crossbow', + 'The Confluence (E3M3) - Exit', + 'The Confluence (E3M3) - Firemace', + 'The Confluence (E3M3) - Firemace 2', + 'The Confluence (E3M3) - Firemace 3', + 'The Confluence (E3M3) - Firemace 4', + 'The Confluence (E3M3) - Firemace 5', + 'The Confluence (E3M3) - Firemace 6', + 'The Confluence (E3M3) - Gauntlets of the Necromancer', + 'The Confluence (E3M3) - Green key', + 'The Confluence (E3M3) - Hellstaff', + 'The Confluence (E3M3) - Hellstaff 2', + 'The Confluence (E3M3) - Map Scroll', + 'The Confluence (E3M3) - Morph Ovum', + 'The Confluence (E3M3) - Mystic Urn', + 'The Confluence (E3M3) - Mystic Urn 2', + 'The Confluence (E3M3) - Phoenix Rod', + 'The Confluence (E3M3) - Ring of Invincibility', + 'The Confluence (E3M3) - Shadowsphere', + 'The Confluence (E3M3) - Silver Shield', + 'The Confluence (E3M3) - Silver Shield 2', + 'The Confluence (E3M3) - Tome of Power', + 'The Confluence (E3M3) - Tome of Power 2', + 'The Confluence (E3M3) - Tome of Power 3', + 'The Confluence (E3M3) - Tome of Power 4', + 'The Confluence (E3M3) - Tome of Power 5', + 'The Confluence (E3M3) - Torch', + 'The Confluence (E3M3) - Yellow key', + }, + 'The Crater (E2M1)': { + 'The Crater (E2M1) - Bag of Holding', + 'The Crater (E2M1) - Dragon Claw', + 'The Crater (E2M1) - Ethereal Crossbow', + 'The Crater (E2M1) - Exit', + 'The Crater (E2M1) - Green key', + 'The Crater (E2M1) - Hellstaff', + 'The Crater (E2M1) - Mystic Urn', + 'The Crater (E2M1) - Shadowsphere', + 'The Crater (E2M1) - Silver Shield', + 'The Crater (E2M1) - Tome of Power', + 'The Crater (E2M1) - Torch', + 'The Crater (E2M1) - Yellow key', + }, + 'The Crypts (E1M7)': { + 'The Crypts (E1M7) - Bag of Holding', + 'The Crypts (E1M7) - Blue key', + 'The Crypts (E1M7) - Dragon Claw', + 'The Crypts (E1M7) - Ethereal Crossbow', + 'The Crypts (E1M7) - Exit', + 'The Crypts (E1M7) - Gauntlets of the Necromancer', + 'The Crypts (E1M7) - Green key', + 'The Crypts (E1M7) - Map Scroll', + 'The Crypts (E1M7) - Morph Ovum', + 'The Crypts (E1M7) - Ring of Invincibility', + 'The Crypts (E1M7) - Shadowsphere', + 'The Crypts (E1M7) - Silver Shield', + 'The Crypts (E1M7) - Silver Shield 2', + 'The Crypts (E1M7) - Tome of Power', + 'The Crypts (E1M7) - Tome of Power 2', + 'The Crypts (E1M7) - Torch', + 'The Crypts (E1M7) - Torch 2', + 'The Crypts (E1M7) - Yellow key', + }, + 'The Docks (E1M1)': { + 'The Docks (E1M1) - Bag of Holding', + 'The Docks (E1M1) - Ethereal Crossbow', + 'The Docks (E1M1) - Exit', + 'The Docks (E1M1) - Gauntlets of the Necromancer', + 'The Docks (E1M1) - Silver Shield', + 'The Docks (E1M1) - Tome of Power', + 'The Docks (E1M1) - Yellow key', + }, + 'The Dungeons (E1M2)': { + 'The Dungeons (E1M2) - Bag of Holding', + 'The Dungeons (E1M2) - Blue key', + 'The Dungeons (E1M2) - Dragon Claw', + 'The Dungeons (E1M2) - Ethereal Crossbow', + 'The Dungeons (E1M2) - Exit', + 'The Dungeons (E1M2) - Gauntlets of the Necromancer', + 'The Dungeons (E1M2) - Green key', + 'The Dungeons (E1M2) - Map Scroll', + 'The Dungeons (E1M2) - Ring of Invincibility', + 'The Dungeons (E1M2) - Shadowsphere', + 'The Dungeons (E1M2) - Silver Shield', + 'The Dungeons (E1M2) - Silver Shield 2', + 'The Dungeons (E1M2) - Tome of Power', + 'The Dungeons (E1M2) - Tome of Power 2', + 'The Dungeons (E1M2) - Torch', + 'The Dungeons (E1M2) - Yellow key', + }, + 'The Gatehouse (E1M3)': { + 'The Gatehouse (E1M3) - Bag of Holding', + 'The Gatehouse (E1M3) - Dragon Claw', + 'The Gatehouse (E1M3) - Ethereal Crossbow', + 'The Gatehouse (E1M3) - Exit', + 'The Gatehouse (E1M3) - Gauntlets of the Necromancer', + 'The Gatehouse (E1M3) - Green key', + 'The Gatehouse (E1M3) - Morph Ovum', + 'The Gatehouse (E1M3) - Ring of Invincibility', + 'The Gatehouse (E1M3) - Shadowsphere', + 'The Gatehouse (E1M3) - Silver Shield', + 'The Gatehouse (E1M3) - Tome of Power', + 'The Gatehouse (E1M3) - Tome of Power 2', + 'The Gatehouse (E1M3) - Tome of Power 3', + 'The Gatehouse (E1M3) - Torch', + 'The Gatehouse (E1M3) - Yellow key', + }, + 'The Glacier (E2M9)': { + 'The Glacier (E2M9) - Bag of Holding', + 'The Glacier (E2M9) - Blue key', + 'The Glacier (E2M9) - Chaos Device', + 'The Glacier (E2M9) - Dragon Claw', + 'The Glacier (E2M9) - Dragon Claw 2', + 'The Glacier (E2M9) - Enchanted Shield', + 'The Glacier (E2M9) - Ethereal Crossbow', + 'The Glacier (E2M9) - Exit', + 'The Glacier (E2M9) - Firemace', + 'The Glacier (E2M9) - Firemace 2', + 'The Glacier (E2M9) - Firemace 3', + 'The Glacier (E2M9) - Firemace 4', + 'The Glacier (E2M9) - Gauntlets of the Necromancer', + 'The Glacier (E2M9) - Green key', + 'The Glacier (E2M9) - Hellstaff', + 'The Glacier (E2M9) - Map Scroll', + 'The Glacier (E2M9) - Morph Ovum', + 'The Glacier (E2M9) - Mystic Urn', + 'The Glacier (E2M9) - Mystic Urn 2', + 'The Glacier (E2M9) - Phoenix Rod', + 'The Glacier (E2M9) - Ring of Invincibility', + 'The Glacier (E2M9) - Shadowsphere', + 'The Glacier (E2M9) - Silver Shield', + 'The Glacier (E2M9) - Tome of Power', + 'The Glacier (E2M9) - Tome of Power 2', + 'The Glacier (E2M9) - Torch', + 'The Glacier (E2M9) - Torch 2', + 'The Glacier (E2M9) - Yellow key', + }, + 'The Graveyard (E1M9)': { + 'The Graveyard (E1M9) - Bag of Holding', + 'The Graveyard (E1M9) - Blue key', + 'The Graveyard (E1M9) - Dragon Claw', + 'The Graveyard (E1M9) - Dragon Claw 2', + 'The Graveyard (E1M9) - Ethereal Crossbow', + 'The Graveyard (E1M9) - Exit', + 'The Graveyard (E1M9) - Green key', + 'The Graveyard (E1M9) - Map Scroll', + 'The Graveyard (E1M9) - Morph Ovum', + 'The Graveyard (E1M9) - Ring of Invincibility', + 'The Graveyard (E1M9) - Shadowsphere', + 'The Graveyard (E1M9) - Silver Shield', + 'The Graveyard (E1M9) - Tome of Power', + 'The Graveyard (E1M9) - Tome of Power 2', + 'The Graveyard (E1M9) - Torch', + 'The Graveyard (E1M9) - Yellow key', + }, + 'The Great Hall (E2M7)': { + 'The Great Hall (E2M7) - Bag of Holding', + 'The Great Hall (E2M7) - Blue key', + 'The Great Hall (E2M7) - Chaos Device', + 'The Great Hall (E2M7) - Dragon Claw', + 'The Great Hall (E2M7) - Enchanted Shield', + 'The Great Hall (E2M7) - Ethereal Crossbow', + 'The Great Hall (E2M7) - Exit', + 'The Great Hall (E2M7) - Gauntlets of the Necromancer', + 'The Great Hall (E2M7) - Green key', + 'The Great Hall (E2M7) - Hellstaff', + 'The Great Hall (E2M7) - Map Scroll', + 'The Great Hall (E2M7) - Morph Ovum', + 'The Great Hall (E2M7) - Mystic Urn', + 'The Great Hall (E2M7) - Phoenix Rod', + 'The Great Hall (E2M7) - Ring of Invincibility', + 'The Great Hall (E2M7) - Shadowsphere', + 'The Great Hall (E2M7) - Silver Shield', + 'The Great Hall (E2M7) - Tome of Power', + 'The Great Hall (E2M7) - Tome of Power 2', + 'The Great Hall (E2M7) - Torch', + 'The Great Hall (E2M7) - Yellow key', + }, + 'The Guard Tower (E1M4)': { + 'The Guard Tower (E1M4) - Bag of Holding', + 'The Guard Tower (E1M4) - Dragon Claw', + 'The Guard Tower (E1M4) - Ethereal Crossbow', + 'The Guard Tower (E1M4) - Exit', + 'The Guard Tower (E1M4) - Gauntlets of the Necromancer', + 'The Guard Tower (E1M4) - Green key', + 'The Guard Tower (E1M4) - Map Scroll', + 'The Guard Tower (E1M4) - Morph Ovum', + 'The Guard Tower (E1M4) - Shadowsphere', + 'The Guard Tower (E1M4) - Silver Shield', + 'The Guard Tower (E1M4) - Tome of Power', + 'The Guard Tower (E1M4) - Tome of Power 2', + 'The Guard Tower (E1M4) - Tome of Power 3', + 'The Guard Tower (E1M4) - Torch', + 'The Guard Tower (E1M4) - Yellow key', + }, + 'The Halls of Fear (E3M6)': { + 'The Halls of Fear (E3M6) - Bag of Holding', + 'The Halls of Fear (E3M6) - Bag of Holding 2', + 'The Halls of Fear (E3M6) - Bag of Holding 3', + 'The Halls of Fear (E3M6) - Blue key', + 'The Halls of Fear (E3M6) - Chaos Device', + 'The Halls of Fear (E3M6) - Dragon Claw', + 'The Halls of Fear (E3M6) - Enchanted Shield', + 'The Halls of Fear (E3M6) - Ethereal Crossbow', + 'The Halls of Fear (E3M6) - Exit', + 'The Halls of Fear (E3M6) - Firemace', + 'The Halls of Fear (E3M6) - Firemace 2', + 'The Halls of Fear (E3M6) - Firemace 3', + 'The Halls of Fear (E3M6) - Firemace 4', + 'The Halls of Fear (E3M6) - Firemace 5', + 'The Halls of Fear (E3M6) - Firemace 6', + 'The Halls of Fear (E3M6) - Gauntlets of the Necromancer', + 'The Halls of Fear (E3M6) - Green key', + 'The Halls of Fear (E3M6) - Hellstaff', + 'The Halls of Fear (E3M6) - Hellstaff 2', + 'The Halls of Fear (E3M6) - Map Scroll', + 'The Halls of Fear (E3M6) - Morph Ovum', + 'The Halls of Fear (E3M6) - Mystic Urn', + 'The Halls of Fear (E3M6) - Mystic Urn 2', + 'The Halls of Fear (E3M6) - Phoenix Rod', + 'The Halls of Fear (E3M6) - Ring of Invincibility', + 'The Halls of Fear (E3M6) - Shadowsphere', + 'The Halls of Fear (E3M6) - Silver Shield', + 'The Halls of Fear (E3M6) - Tome of Power', + 'The Halls of Fear (E3M6) - Tome of Power 2', + 'The Halls of Fear (E3M6) - Tome of Power 3', + 'The Halls of Fear (E3M6) - Yellow key', + }, + 'The Ice Grotto (E2M4)': { + 'The Ice Grotto (E2M4) - Bag of Holding', + 'The Ice Grotto (E2M4) - Bag of Holding 2', + 'The Ice Grotto (E2M4) - Blue key', + 'The Ice Grotto (E2M4) - Chaos Device', + 'The Ice Grotto (E2M4) - Dragon Claw', + 'The Ice Grotto (E2M4) - Enchanted Shield', + 'The Ice Grotto (E2M4) - Ethereal Crossbow', + 'The Ice Grotto (E2M4) - Exit', + 'The Ice Grotto (E2M4) - Gauntlets of the Necromancer', + 'The Ice Grotto (E2M4) - Green key', + 'The Ice Grotto (E2M4) - Hellstaff', + 'The Ice Grotto (E2M4) - Map Scroll', + 'The Ice Grotto (E2M4) - Morph Ovum', + 'The Ice Grotto (E2M4) - Mystic Urn', + 'The Ice Grotto (E2M4) - Phoenix Rod', + 'The Ice Grotto (E2M4) - Shadowsphere', + 'The Ice Grotto (E2M4) - Shadowsphere 2', + 'The Ice Grotto (E2M4) - Silver Shield', + 'The Ice Grotto (E2M4) - Tome of Power', + 'The Ice Grotto (E2M4) - Tome of Power 2', + 'The Ice Grotto (E2M4) - Tome of Power 3', + 'The Ice Grotto (E2M4) - Torch', + 'The Ice Grotto (E2M4) - Yellow key', + }, + 'The Labyrinth (E2M6)': { + 'The Labyrinth (E2M6) - Bag of Holding', + 'The Labyrinth (E2M6) - Blue key', + 'The Labyrinth (E2M6) - Chaos Device', + 'The Labyrinth (E2M6) - Dragon Claw', + 'The Labyrinth (E2M6) - Enchanted Shield', + 'The Labyrinth (E2M6) - Ethereal Crossbow', + 'The Labyrinth (E2M6) - Exit', + 'The Labyrinth (E2M6) - Firemace', + 'The Labyrinth (E2M6) - Firemace 2', + 'The Labyrinth (E2M6) - Firemace 3', + 'The Labyrinth (E2M6) - Firemace 4', + 'The Labyrinth (E2M6) - Gauntlets of the Necromancer', + 'The Labyrinth (E2M6) - Green key', + 'The Labyrinth (E2M6) - Hellstaff', + 'The Labyrinth (E2M6) - Map Scroll', + 'The Labyrinth (E2M6) - Morph Ovum', + 'The Labyrinth (E2M6) - Mystic Urn', + 'The Labyrinth (E2M6) - Phoenix Rod', + 'The Labyrinth (E2M6) - Phoenix Rod 2', + 'The Labyrinth (E2M6) - Ring of Invincibility', + 'The Labyrinth (E2M6) - Shadowsphere', + 'The Labyrinth (E2M6) - Silver Shield', + 'The Labyrinth (E2M6) - Tome of Power', + 'The Labyrinth (E2M6) - Tome of Power 2', + 'The Labyrinth (E2M6) - Yellow key', + }, + 'The Lava Pits (E2M2)': { + 'The Lava Pits (E2M2) - Bag of Holding', + 'The Lava Pits (E2M2) - Bag of Holding 2', + 'The Lava Pits (E2M2) - Chaos Device', + 'The Lava Pits (E2M2) - Dragon Claw', + 'The Lava Pits (E2M2) - Enchanted Shield', + 'The Lava Pits (E2M2) - Ethereal Crossbow', + 'The Lava Pits (E2M2) - Exit', + 'The Lava Pits (E2M2) - Gauntlets of the Necromancer', + 'The Lava Pits (E2M2) - Green key', + 'The Lava Pits (E2M2) - Hellstaff', + 'The Lava Pits (E2M2) - Map Scroll', + 'The Lava Pits (E2M2) - Morph Ovum', + 'The Lava Pits (E2M2) - Mystic Urn', + 'The Lava Pits (E2M2) - Ring of Invincibility', + 'The Lava Pits (E2M2) - Shadowsphere', + 'The Lava Pits (E2M2) - Silver Shield', + 'The Lava Pits (E2M2) - Silver Shield 2', + 'The Lava Pits (E2M2) - Tome of Power', + 'The Lava Pits (E2M2) - Tome of Power 2', + 'The Lava Pits (E2M2) - Tome of Power 3', + 'The Lava Pits (E2M2) - Yellow key', + }, + 'The Ophidian Lair (E3M5)': { + 'The Ophidian Lair (E3M5) - Bag of Holding', + 'The Ophidian Lair (E3M5) - Chaos Device', + 'The Ophidian Lair (E3M5) - Dragon Claw', + 'The Ophidian Lair (E3M5) - Enchanted Shield', + 'The Ophidian Lair (E3M5) - Ethereal Crossbow', + 'The Ophidian Lair (E3M5) - Exit', + 'The Ophidian Lair (E3M5) - Gauntlets of the Necromancer', + 'The Ophidian Lair (E3M5) - Green key', + 'The Ophidian Lair (E3M5) - Hellstaff', + 'The Ophidian Lair (E3M5) - Map Scroll', + 'The Ophidian Lair (E3M5) - Morph Ovum', + 'The Ophidian Lair (E3M5) - Mystic Urn', + 'The Ophidian Lair (E3M5) - Mystic Urn 2', + 'The Ophidian Lair (E3M5) - Phoenix Rod', + 'The Ophidian Lair (E3M5) - Ring of Invincibility', + 'The Ophidian Lair (E3M5) - Shadowsphere', + 'The Ophidian Lair (E3M5) - Silver Shield', + 'The Ophidian Lair (E3M5) - Silver Shield 2', + 'The Ophidian Lair (E3M5) - Tome of Power', + 'The Ophidian Lair (E3M5) - Tome of Power 2', + 'The Ophidian Lair (E3M5) - Torch', + 'The Ophidian Lair (E3M5) - Yellow key', + }, + 'The Portals of Chaos (E2M8)': { + 'The Portals of Chaos (E2M8) - Bag of Holding', + 'The Portals of Chaos (E2M8) - Chaos Device', + 'The Portals of Chaos (E2M8) - Dragon Claw', + 'The Portals of Chaos (E2M8) - Enchanted Shield', + 'The Portals of Chaos (E2M8) - Ethereal Crossbow', + 'The Portals of Chaos (E2M8) - Exit', + 'The Portals of Chaos (E2M8) - Gauntlets of the Necromancer', + 'The Portals of Chaos (E2M8) - Hellstaff', + 'The Portals of Chaos (E2M8) - Morph Ovum', + 'The Portals of Chaos (E2M8) - Mystic Urn', + 'The Portals of Chaos (E2M8) - Mystic Urn 2', + 'The Portals of Chaos (E2M8) - Phoenix Rod', + 'The Portals of Chaos (E2M8) - Ring of Invincibility', + 'The Portals of Chaos (E2M8) - Shadowsphere', + 'The Portals of Chaos (E2M8) - Silver Shield', + 'The Portals of Chaos (E2M8) - Tome of Power', + }, + 'The River of Fire (E2M3)': { + 'The River of Fire (E2M3) - Bag of Holding', + 'The River of Fire (E2M3) - Blue key', + 'The River of Fire (E2M3) - Chaos Device', + 'The River of Fire (E2M3) - Dragon Claw', + 'The River of Fire (E2M3) - Enchanted Shield', + 'The River of Fire (E2M3) - Ethereal Crossbow', + 'The River of Fire (E2M3) - Exit', + 'The River of Fire (E2M3) - Firemace', + 'The River of Fire (E2M3) - Firemace 2', + 'The River of Fire (E2M3) - Firemace 3', + 'The River of Fire (E2M3) - Gauntlets of the Necromancer', + 'The River of Fire (E2M3) - Green key', + 'The River of Fire (E2M3) - Hellstaff', + 'The River of Fire (E2M3) - Morph Ovum', + 'The River of Fire (E2M3) - Mystic Urn', + 'The River of Fire (E2M3) - Phoenix Rod', + 'The River of Fire (E2M3) - Ring of Invincibility', + 'The River of Fire (E2M3) - Shadowsphere', + 'The River of Fire (E2M3) - Silver Shield', + 'The River of Fire (E2M3) - Tome of Power', + 'The River of Fire (E2M3) - Tome of Power 2', + 'The River of Fire (E2M3) - Yellow key', + }, + 'The Storehouse (E3M1)': { + 'The Storehouse (E3M1) - Bag of Holding', + 'The Storehouse (E3M1) - Chaos Device', + 'The Storehouse (E3M1) - Dragon Claw', + 'The Storehouse (E3M1) - Exit', + 'The Storehouse (E3M1) - Gauntlets of the Necromancer', + 'The Storehouse (E3M1) - Green key', + 'The Storehouse (E3M1) - Hellstaff', + 'The Storehouse (E3M1) - Map Scroll', + 'The Storehouse (E3M1) - Ring of Invincibility', + 'The Storehouse (E3M1) - Shadowsphere', + 'The Storehouse (E3M1) - Silver Shield', + 'The Storehouse (E3M1) - Tome of Power', + 'The Storehouse (E3M1) - Torch', + 'The Storehouse (E3M1) - Yellow key', + }, +} + + +death_logic_locations = [ + "Ramparts of Perdition (E4M7) - Ring of Invincibility", + "Ramparts of Perdition (E4M7) - Ethereal Crossbow 2", +] diff --git a/worlds/heretic/Maps.py b/worlds/heretic/Maps.py new file mode 100644 index 0000000000..716de29041 --- /dev/null +++ b/worlds/heretic/Maps.py @@ -0,0 +1,52 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List + + +map_names: List[str] = [ + 'The Docks (E1M1)', + 'The Dungeons (E1M2)', + 'The Gatehouse (E1M3)', + 'The Guard Tower (E1M4)', + 'The Citadel (E1M5)', + 'The Cathedral (E1M6)', + 'The Crypts (E1M7)', + "Hell's Maw (E1M8)", + 'The Graveyard (E1M9)', + 'The Crater (E2M1)', + 'The Lava Pits (E2M2)', + 'The River of Fire (E2M3)', + 'The Ice Grotto (E2M4)', + 'The Catacombs (E2M5)', + 'The Labyrinth (E2M6)', + 'The Great Hall (E2M7)', + 'The Portals of Chaos (E2M8)', + 'The Glacier (E2M9)', + 'The Storehouse (E3M1)', + 'The Cesspool (E3M2)', + 'The Confluence (E3M3)', + 'The Azure Fortress (E3M4)', + 'The Ophidian Lair (E3M5)', + 'The Halls of Fear (E3M6)', + 'The Chasm (E3M7)', + "D'Sparil'S Keep (E3M8)", + 'The Aquifier (E3M9)', + 'Catafalque (E4M1)', + 'Blockhouse (E4M2)', + 'Ambulatory (E4M3)', + 'Sepulcher (E4M4)', + 'Great Stair (E4M5)', + 'Halls of the Apostate (E4M6)', + 'Ramparts of Perdition (E4M7)', + 'Shattered Bridge (E4M8)', + 'Mausoleum (E4M9)', + 'Ochre Cliffs (E5M1)', + 'Rapids (E5M2)', + 'Quay (E5M3)', + 'Courtyard (E5M4)', + 'Hydratyr (E5M5)', + 'Colonnade (E5M6)', + 'Foetid Manse (E5M7)', + 'Field of Judgement (E5M8)', + "Skein of D'Sparil (E5M9)", +] diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py new file mode 100644 index 0000000000..34255f39eb --- /dev/null +++ b/worlds/heretic/Options.py @@ -0,0 +1,167 @@ +import typing + +from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool + + +class Goal(Choice): + """ + Choose the main goal. + complete_all_levels: All levels of the selected episodes + complete_boss_levels: Boss levels (E#M8) of selected episodes + """ + display_name = "Goal" + option_complete_all_levels = 0 + option_complete_boss_levels = 1 + default = 0 + + +class Difficulty(Choice): + """ + Choose the difficulty option. Those match DOOM's difficulty options. + baby (I'm too young to die.) double ammos, half damage, less monsters or strength. + easy (Hey, not too rough.) less monsters or strength. + medium (Hurt me plenty.) Default. + hard (Ultra-Violence.) More monsters or strength. + nightmare (Nightmare!) Monsters attack more rapidly and respawn. + + wet nurse (hou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. + easy (Yellowbellies-r-us) - Fewer monsters and more items than medium. + medium (Bringest them oneth) - Completely balanced, this is the standard difficulty level. + hard (Thou art a smite-meister) - More monsters and fewer items than medium. + black plague (Black plague possesses thee) - Same as hard, but monsters and their projectiles move much faster. Cheating is also disabled. + """ + display_name = "Difficulty" + option_wet_nurse = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_black_plague = 4 + default = 2 + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + vanilla: No randomization + shuffle: Monsters are shuffled within the level + random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + option_random_chaotic = 3 + default = 1 + + +class RandomPickups(Choice): + """ + Choose how pickups are randomized. + vanilla: No randomization + shuffle: Pickups are shuffled within the level + random_balanced: Pickups are completely randomized, but balanced based on existing ratio in the level. (Small pickups vs Big) + """ + display_name = "Random Pickups" + option_vanilla = 0 + option_shuffle = 1 + option_random_balanced = 2 + default = 1 + + +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class AllowDeathLogic(Toggle): + """Some locations require a timed puzzle that can only be tried once. + After which, if the player failed to get it, the location cannot be checked anymore. + By default, no progression items are placed here. There is a way, hovewer, to still get them: + Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" + display_name = "Allow Death Logic" + + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Heretic" + + +class StartWithMapScrolls(Toggle): + """Give the player all Map Scroll items from the start.""" + display_name = "Start With Map Scrolls" + + +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_message="Reset level on death" + + +class CheckSanity(Toggle): + """Include redundant checks. This increase total check count for the game. + i.e.: In a room, there might be 3 checks close to each other. By default, two of them will be remove. + This was done to lower the total count check for Heretic, as it is quite high compared to other games. + Check Sanity restores original checks.""" + display_name = "Check Sanity" + + +class Episode1(DefaultOnToggle): + """City of the Damned. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 1" + + +class Episode2(DefaultOnToggle): + """Hell's Maw. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 2" + + +class Episode3(DefaultOnToggle): + """The Dome of D'Sparil. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 3" + + +class Episode4(Toggle): + """The Ossuary. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 4" + + +class Episode5(Toggle): + """The Stagnant Demesne. + If none of the episodes are chosen, Episode 1 will be chosen by default.""" + display_name = "Episode 5" + + +options: typing.Dict[str, AssembleOptions] = { + "start_inventory_from_pool": StartInventoryPool, + "goal": Goal, + "difficulty": Difficulty, + "random_monsters": RandomMonsters, + "random_pickups": RandomPickups, + "random_music": RandomMusic, + "allow_death_logic": AllowDeathLogic, + "pro": Pro, + "check_sanity": CheckSanity, + "start_with_map_scrolls": StartWithMapScrolls, + "reset_level_on_death": ResetLevelOnDeath, + "death_link": DeathLink, + "episode1": Episode1, + "episode2": Episode2, + "episode3": Episode3, + "episode4": Episode4, + "episode5": Episode5 +} diff --git a/worlds/heretic/Regions.py b/worlds/heretic/Regions.py new file mode 100644 index 0000000000..a30f0120a0 --- /dev/null +++ b/worlds/heretic/Regions.py @@ -0,0 +1,894 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import List +from BaseClasses import TypedDict + +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): + name: str + connects_to_hub: bool + episode: int + connections: List[ConnectionDict] + + +regions:List[RegionDict] = [ + # The Docks (E1M1) + {"name":"The Docks (E1M1) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Docks (E1M1) Yellow","pro":False}]}, + {"name":"The Docks (E1M1) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Docks (E1M1) Main","pro":False}, + {"target":"The Docks (E1M1) Sea","pro":False}]}, + {"name":"The Docks (E1M1) Sea", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Docks (E1M1) Main","pro":False}]}, + + # The Dungeons (E1M2) + {"name":"The Dungeons (E1M2) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Yellow","pro":False}, + {"target":"The Dungeons (E1M2) Green","pro":False}]}, + {"name":"The Dungeons (E1M2) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Dungeons (E1M2) Yellow","pro":False}]}, + {"name":"The Dungeons (E1M2) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Main","pro":False}, + {"target":"The Dungeons (E1M2) Blue","pro":False}]}, + {"name":"The Dungeons (E1M2) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Dungeons (E1M2) Main","pro":False}, + {"target":"The Dungeons (E1M2) Yellow","pro":False}]}, + + # The Gatehouse (E1M3) + {"name":"The Gatehouse (E1M3) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Gatehouse (E1M3) Yellow","pro":False}, + {"target":"The Gatehouse (E1M3) Sea","pro":False}, + {"target":"The Gatehouse (E1M3) Green","pro":False}]}, + {"name":"The Gatehouse (E1M3) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + {"name":"The Gatehouse (E1M3) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + {"name":"The Gatehouse (E1M3) Sea", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Gatehouse (E1M3) Main","pro":False}]}, + + # The Guard Tower (E1M4) + {"name":"The Guard Tower (E1M4) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]}, + {"name":"The Guard Tower (E1M4) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Guard Tower (E1M4) Green","pro":False}, + {"target":"The Guard Tower (E1M4) Main","pro":False}]}, + {"name":"The Guard Tower (E1M4) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Guard Tower (E1M4) Yellow","pro":False}]}, + + # The Citadel (E1M5) + {"name":"The Citadel (E1M5) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Yellow","pro":False}]}, + {"name":"The Citadel (E1M5) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Green","pro":False}]}, + {"name":"The Citadel (E1M5) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Citadel (E1M5) Main","pro":False}, + {"target":"The Citadel (E1M5) Well","pro":False}, + {"target":"The Citadel (E1M5) Green","pro":False}]}, + {"name":"The Citadel (E1M5) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Citadel (E1M5) Main","pro":False}, + {"target":"The Citadel (E1M5) Well","pro":False}, + {"target":"The Citadel (E1M5) Blue","pro":False}]}, + {"name":"The Citadel (E1M5) Well", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Citadel (E1M5) Main","pro":False}]}, + + # The Cathedral (E1M6) + {"name":"The Cathedral (E1M6) Main", + "connects_to_hub":True, + "episode":1, + "connections":[{"target":"The Cathedral (E1M6) Yellow","pro":False}]}, + {"name":"The Cathedral (E1M6) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Cathedral (E1M6) Green","pro":False}, + {"target":"The Cathedral (E1M6) Main","pro":False}, + {"target":"The Cathedral (E1M6) Main Fly","pro":False}]}, + {"name":"The Cathedral (E1M6) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Cathedral (E1M6) Yellow","pro":False}, + {"target":"The Cathedral (E1M6) Main Fly","pro":False}]}, + {"name":"The Cathedral (E1M6) Main Fly", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Cathedral (E1M6) Main","pro":False}]}, + + # The Crypts (E1M7) + {"name":"The Crypts (E1M7) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Green","pro":False}]}, + {"name":"The Crypts (E1M7) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Main","pro":False}]}, + {"name":"The Crypts (E1M7) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Main","pro":False}, + {"target":"The Crypts (E1M7) Green","pro":False}, + {"target":"The Crypts (E1M7) Blue","pro":False}]}, + {"name":"The Crypts (E1M7) Green", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"The Crypts (E1M7) Yellow","pro":False}, + {"target":"The Crypts (E1M7) Main","pro":False}]}, + + # Hell's Maw (E1M8) + {"name":"Hell's Maw (E1M8) Main", + "connects_to_hub":True, + "episode":1, + "connections":[]}, + + # The Graveyard (E1M9) + {"name":"The Graveyard (E1M9) Main", + "connects_to_hub":True, + "episode":1, + "connections":[ + {"target":"The Graveyard (E1M9) Yellow","pro":False}, + {"target":"The Graveyard (E1M9) Green","pro":False}, + {"target":"The Graveyard (E1M9) Blue","pro":False}]}, + {"name":"The Graveyard (E1M9) Blue", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + {"name":"The Graveyard (E1M9) Yellow", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + {"name":"The Graveyard (E1M9) Green", + "connects_to_hub":False, + "episode":1, + "connections":[{"target":"The Graveyard (E1M9) Main","pro":False}]}, + + # The Crater (E2M1) + {"name":"The Crater (E2M1) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]}, + {"name":"The Crater (E2M1) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Crater (E2M1) Main","pro":False}, + {"target":"The Crater (E2M1) Green","pro":False}]}, + {"name":"The Crater (E2M1) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Crater (E2M1) Yellow","pro":False}]}, + + # The Lava Pits (E2M2) + {"name":"The Lava Pits (E2M2) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Lava Pits (E2M2) Yellow","pro":False}]}, + {"name":"The Lava Pits (E2M2) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Lava Pits (E2M2) Green","pro":False}, + {"target":"The Lava Pits (E2M2) Main","pro":False}]}, + {"name":"The Lava Pits (E2M2) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Lava Pits (E2M2) Main","pro":False}, + {"target":"The Lava Pits (E2M2) Yellow","pro":False}]}, + + # The River of Fire (E2M3) + {"name":"The River of Fire (E2M3) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The River of Fire (E2M3) Yellow","pro":False}, + {"target":"The River of Fire (E2M3) Blue","pro":False}, + {"target":"The River of Fire (E2M3) Green","pro":False}]}, + {"name":"The River of Fire (E2M3) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + {"name":"The River of Fire (E2M3) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + {"name":"The River of Fire (E2M3) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The River of Fire (E2M3) Main","pro":False}]}, + + # The Ice Grotto (E2M4) + {"name":"The Ice Grotto (E2M4) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Green","pro":False}, + {"target":"The Ice Grotto (E2M4) Yellow","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Ice Grotto (E2M4) Green","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Main","pro":False}, + {"target":"The Ice Grotto (E2M4) Magenta","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Ice Grotto (E2M4) Main","pro":False}, + {"target":"The Ice Grotto (E2M4) Blue","pro":False}]}, + {"name":"The Ice Grotto (E2M4) Magenta", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Ice Grotto (E2M4) Yellow","pro":False}]}, + + # The Catacombs (E2M5) + {"name":"The Catacombs (E2M5) Main", + "connects_to_hub":True, + "episode":2, + "connections":[{"target":"The Catacombs (E2M5) Yellow","pro":False}]}, + {"name":"The Catacombs (E2M5) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Catacombs (E2M5) Green","pro":False}]}, + {"name":"The Catacombs (E2M5) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Catacombs (E2M5) Green","pro":False}, + {"target":"The Catacombs (E2M5) Main","pro":False}]}, + {"name":"The Catacombs (E2M5) Green", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Catacombs (E2M5) Blue","pro":False}, + {"target":"The Catacombs (E2M5) Yellow","pro":False}, + {"target":"The Catacombs (E2M5) Main","pro":False}]}, + + # The Labyrinth (E2M6) + {"name":"The Labyrinth (E2M6) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Labyrinth (E2M6) Blue","pro":False}, + {"target":"The Labyrinth (E2M6) Yellow","pro":False}, + {"target":"The Labyrinth (E2M6) Green","pro":False}]}, + {"name":"The Labyrinth (E2M6) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + {"name":"The Labyrinth (E2M6) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + {"name":"The Labyrinth (E2M6) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Labyrinth (E2M6) Main","pro":False}]}, + + # The Great Hall (E2M7) + {"name":"The Great Hall (E2M7) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Great Hall (E2M7) Yellow","pro":False}, + {"target":"The Great Hall (E2M7) Green","pro":False}]}, + {"name":"The Great Hall (E2M7) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Great Hall (E2M7) Yellow","pro":False}]}, + {"name":"The Great Hall (E2M7) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[ + {"target":"The Great Hall (E2M7) Blue","pro":False}, + {"target":"The Great Hall (E2M7) Main","pro":False}]}, + {"name":"The Great Hall (E2M7) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Great Hall (E2M7) Main","pro":False}]}, + + # The Portals of Chaos (E2M8) + {"name":"The Portals of Chaos (E2M8) Main", + "connects_to_hub":True, + "episode":2, + "connections":[]}, + + # The Glacier (E2M9) + {"name":"The Glacier (E2M9) Main", + "connects_to_hub":True, + "episode":2, + "connections":[ + {"target":"The Glacier (E2M9) Yellow","pro":False}, + {"target":"The Glacier (E2M9) Blue","pro":False}, + {"target":"The Glacier (E2M9) Green","pro":False}]}, + {"name":"The Glacier (E2M9) Blue", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + {"name":"The Glacier (E2M9) Yellow", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + {"name":"The Glacier (E2M9) Green", + "connects_to_hub":False, + "episode":2, + "connections":[{"target":"The Glacier (E2M9) Main","pro":False}]}, + + # The Storehouse (E3M1) + {"name":"The Storehouse (E3M1) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Storehouse (E3M1) Yellow","pro":False}, + {"target":"The Storehouse (E3M1) Green","pro":False}]}, + {"name":"The Storehouse (E3M1) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]}, + {"name":"The Storehouse (E3M1) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Storehouse (E3M1) Main","pro":False}]}, + + # The Cesspool (E3M2) + {"name":"The Cesspool (E3M2) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Cesspool (E3M2) Yellow","pro":False}]}, + {"name":"The Cesspool (E3M2) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Cesspool (E3M2) Green","pro":False}]}, + {"name":"The Cesspool (E3M2) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Cesspool (E3M2) Main","pro":False}, + {"target":"The Cesspool (E3M2) Green","pro":False}]}, + {"name":"The Cesspool (E3M2) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Cesspool (E3M2) Blue","pro":False}, + {"target":"The Cesspool (E3M2) Main","pro":False}, + {"target":"The Cesspool (E3M2) Yellow","pro":False}]}, + + # The Confluence (E3M3) + {"name":"The Confluence (E3M3) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Confluence (E3M3) Green","pro":False}, + {"target":"The Confluence (E3M3) Yellow","pro":False}]}, + {"name":"The Confluence (E3M3) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Confluence (E3M3) Green","pro":False}]}, + {"name":"The Confluence (E3M3) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Confluence (E3M3) Main","pro":False}]}, + {"name":"The Confluence (E3M3) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Confluence (E3M3) Main","pro":False}, + {"target":"The Confluence (E3M3) Blue","pro":False}, + {"target":"The Confluence (E3M3) Yellow","pro":False}]}, + + # The Azure Fortress (E3M4) + {"name":"The Azure Fortress (E3M4) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Azure Fortress (E3M4) Green","pro":False}, + {"target":"The Azure Fortress (E3M4) Yellow","pro":False}]}, + {"name":"The Azure Fortress (E3M4) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]}, + {"name":"The Azure Fortress (E3M4) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Azure Fortress (E3M4) Main","pro":False}]}, + + # The Ophidian Lair (E3M5) + {"name":"The Ophidian Lair (E3M5) Main", + "connects_to_hub":True, + "episode":3, + "connections":[ + {"target":"The Ophidian Lair (E3M5) Yellow","pro":False}, + {"target":"The Ophidian Lair (E3M5) Green","pro":False}]}, + {"name":"The Ophidian Lair (E3M5) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]}, + {"name":"The Ophidian Lair (E3M5) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Ophidian Lair (E3M5) Main","pro":False}]}, + + # The Halls of Fear (E3M6) + {"name":"The Halls of Fear (E3M6) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Halls of Fear (E3M6) Yellow","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Blue","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}, + {"target":"The Halls of Fear (E3M6) Green","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}, + {"target":"The Halls of Fear (E3M6) Cyan","pro":False}]}, + {"name":"The Halls of Fear (E3M6) Cyan", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Halls of Fear (E3M6) Yellow","pro":False}, + {"target":"The Halls of Fear (E3M6) Main","pro":False}]}, + + # The Chasm (E3M7) + {"name":"The Chasm (E3M7) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]}, + {"name":"The Chasm (E3M7) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[]}, + {"name":"The Chasm (E3M7) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Chasm (E3M7) Main","pro":False}, + {"target":"The Chasm (E3M7) Green","pro":False}, + {"target":"The Chasm (E3M7) Blue","pro":False}]}, + {"name":"The Chasm (E3M7) Green", + "connects_to_hub":False, + "episode":3, + "connections":[{"target":"The Chasm (E3M7) Yellow","pro":False}]}, + + # D'Sparil'S Keep (E3M8) + {"name":"D'Sparil'S Keep (E3M8) Main", + "connects_to_hub":True, + "episode":3, + "connections":[]}, + + # The Aquifier (E3M9) + {"name":"The Aquifier (E3M9) Main", + "connects_to_hub":True, + "episode":3, + "connections":[{"target":"The Aquifier (E3M9) Yellow","pro":False}]}, + {"name":"The Aquifier (E3M9) Blue", + "connects_to_hub":False, + "episode":3, + "connections":[]}, + {"name":"The Aquifier (E3M9) Yellow", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Aquifier (E3M9) Green","pro":False}, + {"target":"The Aquifier (E3M9) Main","pro":False}]}, + {"name":"The Aquifier (E3M9) Green", + "connects_to_hub":False, + "episode":3, + "connections":[ + {"target":"The Aquifier (E3M9) Yellow","pro":False}, + {"target":"The Aquifier (E3M9) Main","pro":False}, + {"target":"The Aquifier (E3M9) Blue","pro":False}]}, + + # Catafalque (E4M1) + {"name":"Catafalque (E4M1) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Catafalque (E4M1) Yellow","pro":False}]}, + {"name":"Catafalque (E4M1) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Catafalque (E4M1) Green","pro":False}, + {"target":"Catafalque (E4M1) Main","pro":False}]}, + {"name":"Catafalque (E4M1) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Catafalque (E4M1) Main","pro":False}]}, + + # Blockhouse (E4M2) + {"name":"Blockhouse (E4M2) Main", + "connects_to_hub":True, + "episode":4, + "connections":[ + {"target":"Blockhouse (E4M2) Yellow","pro":False}, + {"target":"Blockhouse (E4M2) Green","pro":False}, + {"target":"Blockhouse (E4M2) Blue","pro":False}]}, + {"name":"Blockhouse (E4M2) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Blockhouse (E4M2) Main","pro":False}, + {"target":"Blockhouse (E4M2) Balcony","pro":False}, + {"target":"Blockhouse (E4M2) Lake","pro":False}]}, + {"name":"Blockhouse (E4M2) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]}, + {"name":"Blockhouse (E4M2) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Main","pro":False}]}, + {"name":"Blockhouse (E4M2) Lake", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Blockhouse (E4M2) Balcony","pro":False}]}, + {"name":"Blockhouse (E4M2) Balcony", + "connects_to_hub":False, + "episode":4, + "connections":[]}, + + # Ambulatory (E4M3) + {"name":"Ambulatory (E4M3) Main", + "connects_to_hub":True, + "episode":4, + "connections":[ + {"target":"Ambulatory (E4M3) Blue","pro":False}, + {"target":"Ambulatory (E4M3) Yellow","pro":False}, + {"target":"Ambulatory (E4M3) Green","pro":False}]}, + {"name":"Ambulatory (E4M3) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Ambulatory (E4M3) Yellow","pro":False}, + {"target":"Ambulatory (E4M3) Green","pro":False}]}, + {"name":"Ambulatory (E4M3) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]}, + {"name":"Ambulatory (E4M3) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ambulatory (E4M3) Main","pro":False}]}, + + # Sepulcher (E4M4) + {"name":"Sepulcher (E4M4) Main", + "connects_to_hub":True, + "episode":4, + "connections":[]}, + + # Great Stair (E4M5) + {"name":"Great Stair (E4M5) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Great Stair (E4M5) Yellow","pro":False}]}, + {"name":"Great Stair (E4M5) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Great Stair (E4M5) Green","pro":False}]}, + {"name":"Great Stair (E4M5) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Great Stair (E4M5) Main","pro":False}, + {"target":"Great Stair (E4M5) Green","pro":False}]}, + {"name":"Great Stair (E4M5) Green", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Great Stair (E4M5) Blue","pro":False}, + {"target":"Great Stair (E4M5) Yellow","pro":False}]}, + + # Halls of the Apostate (E4M6) + {"name":"Halls of the Apostate (E4M6) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Halls of the Apostate (E4M6) Yellow","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Halls of the Apostate (E4M6) Green","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Halls of the Apostate (E4M6) Main","pro":False}, + {"target":"Halls of the Apostate (E4M6) Green","pro":False}]}, + {"name":"Halls of the Apostate (E4M6) Green", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Halls of the Apostate (E4M6) Yellow","pro":False}, + {"target":"Halls of the Apostate (E4M6) Blue","pro":False}]}, + + # Ramparts of Perdition (E4M7) + {"name":"Ramparts of Perdition (E4M7) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Blue", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Ramparts of Perdition (E4M7) Main","pro":False}, + {"target":"Ramparts of Perdition (E4M7) Green","pro":False}, + {"target":"Ramparts of Perdition (E4M7) Blue","pro":False}]}, + {"name":"Ramparts of Perdition (E4M7) Green", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Ramparts of Perdition (E4M7) Yellow","pro":False}]}, + + # Shattered Bridge (E4M8) + {"name":"Shattered Bridge (E4M8) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Shattered Bridge (E4M8) Yellow","pro":False}]}, + {"name":"Shattered Bridge (E4M8) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Shattered Bridge (E4M8) Main","pro":False}, + {"target":"Shattered Bridge (E4M8) Boss","pro":False}]}, + {"name":"Shattered Bridge (E4M8) Boss", + "connects_to_hub":False, + "episode":4, + "connections":[]}, + + # Mausoleum (E4M9) + {"name":"Mausoleum (E4M9) Main", + "connects_to_hub":True, + "episode":4, + "connections":[{"target":"Mausoleum (E4M9) Yellow","pro":False}]}, + {"name":"Mausoleum (E4M9) Yellow", + "connects_to_hub":False, + "episode":4, + "connections":[{"target":"Mausoleum (E4M9) Main","pro":False}]}, + + # Ochre Cliffs (E5M1) + {"name":"Ochre Cliffs (E5M1) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Ochre Cliffs (E5M1) Main","pro":False}, + {"target":"Ochre Cliffs (E5M1) Green","pro":False}, + {"target":"Ochre Cliffs (E5M1) Blue","pro":False}]}, + {"name":"Ochre Cliffs (E5M1) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Ochre Cliffs (E5M1) Yellow","pro":False}]}, + + # Rapids (E5M2) + {"name":"Rapids (E5M2) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Rapids (E5M2) Yellow","pro":False}]}, + {"name":"Rapids (E5M2) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Rapids (E5M2) Main","pro":False}, + {"target":"Rapids (E5M2) Green","pro":False}]}, + {"name":"Rapids (E5M2) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Rapids (E5M2) Yellow","pro":False}, + {"target":"Rapids (E5M2) Main","pro":False}]}, + + # Quay (E5M3) + {"name":"Quay (E5M3) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Yellow","pro":False}, + {"target":"Quay (E5M3) Green","pro":False}, + {"target":"Quay (E5M3) Blue","pro":False}]}, + {"name":"Quay (E5M3) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Green","pro":False}, + {"target":"Quay (E5M3) Main","pro":False}]}, + {"name":"Quay (E5M3) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Quay (E5M3) Main","pro":False}]}, + {"name":"Quay (E5M3) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Quay (E5M3) Main","pro":False}, + {"target":"Quay (E5M3) Blue","pro":False}]}, + + # Courtyard (E5M4) + {"name":"Courtyard (E5M4) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Courtyard (E5M4) Kakis","pro":False}, + {"target":"Courtyard (E5M4) Blue","pro":False}]}, + {"name":"Courtyard (E5M4) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]}, + {"name":"Courtyard (E5M4) Kakis", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Courtyard (E5M4) Main","pro":False}]}, + + # Hydratyr (E5M5) + {"name":"Hydratyr (E5M5) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Hydratyr (E5M5) Yellow","pro":False}]}, + {"name":"Hydratyr (E5M5) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Hydratyr (E5M5) Green","pro":False}]}, + {"name":"Hydratyr (E5M5) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Hydratyr (E5M5) Main","pro":False}, + {"target":"Hydratyr (E5M5) Green","pro":False}]}, + {"name":"Hydratyr (E5M5) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Hydratyr (E5M5) Main","pro":False}, + {"target":"Hydratyr (E5M5) Yellow","pro":False}, + {"target":"Hydratyr (E5M5) Blue","pro":False}]}, + + # Colonnade (E5M6) + {"name":"Colonnade (E5M6) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Colonnade (E5M6) Yellow","pro":False}, + {"target":"Colonnade (E5M6) Blue","pro":False}]}, + {"name":"Colonnade (E5M6) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Colonnade (E5M6) Main","pro":False}]}, + {"name":"Colonnade (E5M6) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Colonnade (E5M6) Main","pro":False}, + {"target":"Colonnade (E5M6) Green","pro":False}]}, + {"name":"Colonnade (E5M6) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Colonnade (E5M6) Yellow","pro":False}]}, + + # Foetid Manse (E5M7) + {"name":"Foetid Manse (E5M7) Main", + "connects_to_hub":True, + "episode":5, + "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]}, + {"name":"Foetid Manse (E5M7) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Foetid Manse (E5M7) Yellow","pro":False}]}, + {"name":"Foetid Manse (E5M7) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Foetid Manse (E5M7) Main","pro":False}, + {"target":"Foetid Manse (E5M7) Green","pro":False}, + {"target":"Foetid Manse (E5M7) Blue","pro":False}]}, + {"name":"Foetid Manse (E5M7) Green", + "connects_to_hub":False, + "episode":5, + "connections":[ + {"target":"Foetid Manse (E5M7) Yellow","pro":False}, + {"target":"Foetid Manse (E5M7) Main","pro":False}]}, + + # Field of Judgement (E5M8) + {"name":"Field of Judgement (E5M8) Main", + "connects_to_hub":True, + "episode":5, + "connections":[]}, + + # Skein of D'Sparil (E5M9) + {"name":"Skein of D'Sparil (E5M9) Main", + "connects_to_hub":True, + "episode":5, + "connections":[ + {"target":"Skein of D'Sparil (E5M9) Blue","pro":False}, + {"target":"Skein of D'Sparil (E5M9) Yellow","pro":False}, + {"target":"Skein of D'Sparil (E5M9) Green","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Blue", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Yellow", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, + {"name":"Skein of D'Sparil (E5M9) Green", + "connects_to_hub":False, + "episode":5, + "connections":[{"target":"Skein of D'Sparil (E5M9) Main","pro":False}]}, +] diff --git a/worlds/heretic/Rules.py b/worlds/heretic/Rules.py new file mode 100644 index 0000000000..7ef15d7920 --- /dev/null +++ b/worlds/heretic/Rules.py @@ -0,0 +1,736 @@ +# This file is auto generated. More info: https://github.com/Daivuk/apdoom + +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import HereticWorld + + +def set_episode1_rules(player, world, pro): + # The Docks (E1M1) + set_rule(world.get_entrance("Hub -> The Docks (E1M1) Main", player), lambda state: + state.has("The Docks (E1M1)", player, 1)) + set_rule(world.get_entrance("The Docks (E1M1) Main -> The Docks (E1M1) Yellow", player), lambda state: + state.has("The Docks (E1M1) - Yellow key", player, 1)) + + # The Dungeons (E1M2) + set_rule(world.get_entrance("Hub -> The Dungeons (E1M2) Main", player), lambda state: + (state.has("The Dungeons (E1M2)", player, 1)) and + (state.has("Dragon Claw", player, 1) or + state.has("Ethereal Crossbow", player, 1))) + set_rule(world.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Yellow", player), lambda state: + state.has("The Dungeons (E1M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Main -> The Dungeons (E1M2) Green", player), lambda state: + state.has("The Dungeons (E1M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Blue -> The Dungeons (E1M2) Yellow", player), lambda state: + state.has("The Dungeons (E1M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Dungeons (E1M2) Yellow -> The Dungeons (E1M2) Blue", player), lambda state: + state.has("The Dungeons (E1M2) - Blue key", player, 1)) + + # The Gatehouse (E1M3) + set_rule(world.get_entrance("Hub -> The Gatehouse (E1M3) Main", player), lambda state: + (state.has("The Gatehouse (E1M3)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Yellow", player), lambda state: + state.has("The Gatehouse (E1M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Sea", player), lambda state: + state.has("The Gatehouse (E1M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Main -> The Gatehouse (E1M3) Green", player), lambda state: + state.has("The Gatehouse (E1M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Gatehouse (E1M3) Green -> The Gatehouse (E1M3) Main", player), lambda state: + state.has("The Gatehouse (E1M3) - Green key", player, 1)) + + # The Guard Tower (E1M4) + set_rule(world.get_entrance("Hub -> The Guard Tower (E1M4) Main", player), lambda state: + (state.has("The Guard Tower (E1M4)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Guard Tower (E1M4) Main -> The Guard Tower (E1M4) Yellow", player), lambda state: + state.has("The Guard Tower (E1M4) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Guard Tower (E1M4) Yellow -> The Guard Tower (E1M4) Green", player), lambda state: + state.has("The Guard Tower (E1M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Guard Tower (E1M4) Green -> The Guard Tower (E1M4) Yellow", player), lambda state: + state.has("The Guard Tower (E1M4) - Green key", player, 1)) + + # The Citadel (E1M5) + set_rule(world.get_entrance("Hub -> The Citadel (E1M5) Main", player), lambda state: + (state.has("The Citadel (E1M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Dragon Claw", player, 1) or + state.has("Gauntlets of the Necromancer", player, 1))) + set_rule(world.get_entrance("The Citadel (E1M5) Main -> The Citadel (E1M5) Yellow", player), lambda state: + state.has("The Citadel (E1M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Blue -> The Citadel (E1M5) Green", player), lambda state: + state.has("The Citadel (E1M5) - Blue key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Yellow -> The Citadel (E1M5) Green", player), lambda state: + state.has("The Citadel (E1M5) - Green key", player, 1)) + set_rule(world.get_entrance("The Citadel (E1M5) Green -> The Citadel (E1M5) Blue", player), lambda state: + state.has("The Citadel (E1M5) - Blue key", player, 1)) + + # The Cathedral (E1M6) + set_rule(world.get_entrance("Hub -> The Cathedral (E1M6) Main", player), lambda state: + (state.has("The Cathedral (E1M6)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Cathedral (E1M6) Main -> The Cathedral (E1M6) Yellow", player), lambda state: + state.has("The Cathedral (E1M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Cathedral (E1M6) Yellow -> The Cathedral (E1M6) Green", player), lambda state: + state.has("The Cathedral (E1M6) - Green key", player, 1)) + + # The Crypts (E1M7) + set_rule(world.get_entrance("Hub -> The Crypts (E1M7) Main", player), lambda state: + (state.has("The Crypts (E1M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Yellow", player), lambda state: + state.has("The Crypts (E1M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Main -> The Crypts (E1M7) Green", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Green", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Yellow -> The Crypts (E1M7) Blue", player), lambda state: + state.has("The Crypts (E1M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Crypts (E1M7) Green -> The Crypts (E1M7) Main", player), lambda state: + state.has("The Crypts (E1M7) - Green key", player, 1)) + + # Hell's Maw (E1M8) + set_rule(world.get_entrance("Hub -> Hell's Maw (E1M8) Main", player), lambda state: + state.has("Hell's Maw (E1M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + + # The Graveyard (E1M9) + set_rule(world.get_entrance("Hub -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Yellow", player), lambda state: + state.has("The Graveyard (E1M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Green", player), lambda state: + state.has("The Graveyard (E1M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Main -> The Graveyard (E1M9) Blue", player), lambda state: + state.has("The Graveyard (E1M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Yellow -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Graveyard (E1M9) Green -> The Graveyard (E1M9) Main", player), lambda state: + state.has("The Graveyard (E1M9) - Green key", player, 1)) + + +def set_episode2_rules(player, world, pro): + # The Crater (E2M1) + set_rule(world.get_entrance("Hub -> The Crater (E2M1) Main", player), lambda state: + state.has("The Crater (E2M1)", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Main -> The Crater (E2M1) Yellow", player), lambda state: + state.has("The Crater (E2M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Yellow -> The Crater (E2M1) Green", player), lambda state: + state.has("The Crater (E2M1) - Green key", player, 1)) + set_rule(world.get_entrance("The Crater (E2M1) Green -> The Crater (E2M1) Yellow", player), lambda state: + state.has("The Crater (E2M1) - Green key", player, 1)) + + # The Lava Pits (E2M2) + set_rule(world.get_entrance("Hub -> The Lava Pits (E2M2) Main", player), lambda state: + (state.has("The Lava Pits (E2M2)", player, 1)) and + (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1))) + set_rule(world.get_entrance("The Lava Pits (E2M2) Main -> The Lava Pits (E2M2) Yellow", player), lambda state: + state.has("The Lava Pits (E2M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Green", player), lambda state: + state.has("The Lava Pits (E2M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Yellow -> The Lava Pits (E2M2) Main", player), lambda state: + state.has("The Lava Pits (E2M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Lava Pits (E2M2) Green -> The Lava Pits (E2M2) Yellow", player), lambda state: + state.has("The Lava Pits (E2M2) - Green key", player, 1)) + + # The River of Fire (E2M3) + set_rule(world.get_entrance("Hub -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3)", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Yellow", player), lambda state: + state.has("The River of Fire (E2M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Blue", player), lambda state: + state.has("The River of Fire (E2M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Main -> The River of Fire (E2M3) Green", player), lambda state: + state.has("The River of Fire (E2M3) - Green key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Blue -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Yellow -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The River of Fire (E2M3) Green -> The River of Fire (E2M3) Main", player), lambda state: + state.has("The River of Fire (E2M3) - Green key", player, 1)) + + # The Ice Grotto (E2M4) + set_rule(world.get_entrance("Hub -> The Ice Grotto (E2M4) Main", player), lambda state: + (state.has("The Ice Grotto (E2M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Hellstaff", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Green", player), lambda state: + state.has("The Ice Grotto (E2M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Main -> The Ice Grotto (E2M4) Yellow", player), lambda state: + state.has("The Ice Grotto (E2M4) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Blue -> The Ice Grotto (E2M4) Green", player), lambda state: + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Yellow -> The Ice Grotto (E2M4) Magenta", player), lambda state: + state.has("The Ice Grotto (E2M4) - Green key", player, 1) and + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + set_rule(world.get_entrance("The Ice Grotto (E2M4) Green -> The Ice Grotto (E2M4) Blue", player), lambda state: + state.has("The Ice Grotto (E2M4) - Blue key", player, 1)) + + # The Catacombs (E2M5) + set_rule(world.get_entrance("Hub -> The Catacombs (E2M5) Main", player), lambda state: + (state.has("The Catacombs (E2M5)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Catacombs (E2M5) Main -> The Catacombs (E2M5) Yellow", player), lambda state: + state.has("The Catacombs (E2M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Blue -> The Catacombs (E2M5) Green", player), lambda state: + state.has("The Catacombs (E2M5) - Blue key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Yellow -> The Catacombs (E2M5) Green", player), lambda state: + state.has("The Catacombs (E2M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Catacombs (E2M5) Green -> The Catacombs (E2M5) Blue", player), lambda state: + state.has("The Catacombs (E2M5) - Blue key", player, 1)) + + # The Labyrinth (E2M6) + set_rule(world.get_entrance("Hub -> The Labyrinth (E2M6) Main", player), lambda state: + (state.has("The Labyrinth (E2M6)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Blue", player), lambda state: + state.has("The Labyrinth (E2M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Yellow", player), lambda state: + state.has("The Labyrinth (E2M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Main -> The Labyrinth (E2M6) Green", player), lambda state: + state.has("The Labyrinth (E2M6) - Green key", player, 1)) + set_rule(world.get_entrance("The Labyrinth (E2M6) Blue -> The Labyrinth (E2M6) Main", player), lambda state: + state.has("The Labyrinth (E2M6) - Blue key", player, 1)) + + # The Great Hall (E2M7) + set_rule(world.get_entrance("Hub -> The Great Hall (E2M7) Main", player), lambda state: + (state.has("The Great Hall (E2M7)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Yellow", player), lambda state: + state.has("The Great Hall (E2M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Main -> The Great Hall (E2M7) Green", player), lambda state: + state.has("The Great Hall (E2M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Blue -> The Great Hall (E2M7) Yellow", player), lambda state: + state.has("The Great Hall (E2M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Blue", player), lambda state: + state.has("The Great Hall (E2M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Great Hall (E2M7) Yellow -> The Great Hall (E2M7) Main", player), lambda state: + state.has("The Great Hall (E2M7) - Yellow key", player, 1)) + + # The Portals of Chaos (E2M8) + set_rule(world.get_entrance("Hub -> The Portals of Chaos (E2M8) Main", player), lambda state: + state.has("The Portals of Chaos (E2M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + + # The Glacier (E2M9) + set_rule(world.get_entrance("Hub -> The Glacier (E2M9) Main", player), lambda state: + (state.has("The Glacier (E2M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Yellow", player), lambda state: + state.has("The Glacier (E2M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Blue", player), lambda state: + state.has("The Glacier (E2M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Main -> The Glacier (E2M9) Green", player), lambda state: + state.has("The Glacier (E2M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Blue -> The Glacier (E2M9) Main", player), lambda state: + state.has("The Glacier (E2M9) - Blue key", player, 1)) + set_rule(world.get_entrance("The Glacier (E2M9) Yellow -> The Glacier (E2M9) Main", player), lambda state: + state.has("The Glacier (E2M9) - Yellow key", player, 1)) + + +def set_episode3_rules(player, world, pro): + # The Storehouse (E3M1) + set_rule(world.get_entrance("Hub -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1)", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Yellow", player), lambda state: + state.has("The Storehouse (E3M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Main -> The Storehouse (E3M1) Green", player), lambda state: + state.has("The Storehouse (E3M1) - Green key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Yellow -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Storehouse (E3M1) Green -> The Storehouse (E3M1) Main", player), lambda state: + state.has("The Storehouse (E3M1) - Green key", player, 1)) + + # The Cesspool (E3M2) + set_rule(world.get_entrance("Hub -> The Cesspool (E3M2) Main", player), lambda state: + state.has("The Cesspool (E3M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Main -> The Cesspool (E3M2) Yellow", player), lambda state: + state.has("The Cesspool (E3M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Blue -> The Cesspool (E3M2) Green", player), lambda state: + state.has("The Cesspool (E3M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Yellow -> The Cesspool (E3M2) Green", player), lambda state: + state.has("The Cesspool (E3M2) - Green key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Blue", player), lambda state: + state.has("The Cesspool (E3M2) - Blue key", player, 1)) + set_rule(world.get_entrance("The Cesspool (E3M2) Green -> The Cesspool (E3M2) Yellow", player), lambda state: + state.has("The Cesspool (E3M2) - Green key", player, 1)) + + # The Confluence (E3M3) + set_rule(world.get_entrance("Hub -> The Confluence (E3M3) Main", player), lambda state: + (state.has("The Confluence (E3M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Green", player), lambda state: + state.has("The Confluence (E3M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Main -> The Confluence (E3M3) Yellow", player), lambda state: + state.has("The Confluence (E3M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Blue -> The Confluence (E3M3) Green", player), lambda state: + state.has("The Confluence (E3M3) - Blue key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Main", player), lambda state: + state.has("The Confluence (E3M3) - Green key", player, 1)) + set_rule(world.get_entrance("The Confluence (E3M3) Green -> The Confluence (E3M3) Blue", player), lambda state: + state.has("The Confluence (E3M3) - Blue key", player, 1)) + + # The Azure Fortress (E3M4) + set_rule(world.get_entrance("Hub -> The Azure Fortress (E3M4) Main", player), lambda state: + (state.has("The Azure Fortress (E3M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Firemace", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Gauntlets of the Necromancer", player, 1))) + set_rule(world.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Green", player), lambda state: + state.has("The Azure Fortress (E3M4) - Green key", player, 1)) + set_rule(world.get_entrance("The Azure Fortress (E3M4) Main -> The Azure Fortress (E3M4) Yellow", player), lambda state: + state.has("The Azure Fortress (E3M4) - Yellow key", player, 1)) + + # The Ophidian Lair (E3M5) + set_rule(world.get_entrance("Hub -> The Ophidian Lair (E3M5) Main", player), lambda state: + (state.has("The Ophidian Lair (E3M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Yellow", player), lambda state: + state.has("The Ophidian Lair (E3M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Ophidian Lair (E3M5) Main -> The Ophidian Lair (E3M5) Green", player), lambda state: + state.has("The Ophidian Lair (E3M5) - Green key", player, 1)) + + # The Halls of Fear (E3M6) + set_rule(world.get_entrance("Hub -> The Halls of Fear (E3M6) Main", player), lambda state: + (state.has("The Halls of Fear (E3M6)", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Main -> The Halls of Fear (E3M6) Yellow", player), lambda state: + state.has("The Halls of Fear (E3M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Blue -> The Halls of Fear (E3M6) Yellow", player), lambda state: + state.has("The Halls of Fear (E3M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Blue", player), lambda state: + state.has("The Halls of Fear (E3M6) - Blue key", player, 1)) + set_rule(world.get_entrance("The Halls of Fear (E3M6) Yellow -> The Halls of Fear (E3M6) Green", player), lambda state: + state.has("The Halls of Fear (E3M6) - Green key", player, 1)) + + # The Chasm (E3M7) + set_rule(world.get_entrance("Hub -> The Chasm (E3M7) Main", player), lambda state: + (state.has("The Chasm (E3M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) and + (state.has("Gauntlets of the Necromancer", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("The Chasm (E3M7) Main -> The Chasm (E3M7) Yellow", player), lambda state: + state.has("The Chasm (E3M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Main", player), lambda state: + state.has("The Chasm (E3M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Green", player), lambda state: + state.has("The Chasm (E3M7) - Green key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Yellow -> The Chasm (E3M7) Blue", player), lambda state: + state.has("The Chasm (E3M7) - Blue key", player, 1)) + set_rule(world.get_entrance("The Chasm (E3M7) Green -> The Chasm (E3M7) Yellow", player), lambda state: + state.has("The Chasm (E3M7) - Green key", player, 1)) + + # D'Sparil'S Keep (E3M8) + set_rule(world.get_entrance("Hub -> D'Sparil'S Keep (E3M8) Main", player), lambda state: + state.has("D'Sparil'S Keep (E3M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + + # The Aquifier (E3M9) + set_rule(world.get_entrance("Hub -> The Aquifier (E3M9) Main", player), lambda state: + state.has("The Aquifier (E3M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Main -> The Aquifier (E3M9) Yellow", player), lambda state: + state.has("The Aquifier (E3M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Green", player), lambda state: + state.has("The Aquifier (E3M9) - Green key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Yellow -> The Aquifier (E3M9) Main", player), lambda state: + state.has("The Aquifier (E3M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("The Aquifier (E3M9) Green -> The Aquifier (E3M9) Yellow", player), lambda state: + state.has("The Aquifier (E3M9) - Green key", player, 1)) + + +def set_episode4_rules(player, world, pro): + # Catafalque (E4M1) + set_rule(world.get_entrance("Hub -> Catafalque (E4M1) Main", player), lambda state: + state.has("Catafalque (E4M1)", player, 1)) + set_rule(world.get_entrance("Catafalque (E4M1) Main -> Catafalque (E4M1) Yellow", player), lambda state: + state.has("Catafalque (E4M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Catafalque (E4M1) Yellow -> Catafalque (E4M1) Green", player), lambda state: + (state.has("Catafalque (E4M1) - Green key", player, 1)) and (state.has("Ethereal Crossbow", player, 1) or + state.has("Dragon Claw", player, 1) or + state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + + # Blockhouse (E4M2) + set_rule(world.get_entrance("Hub -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Yellow", player), lambda state: + state.has("Blockhouse (E4M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Green", player), lambda state: + state.has("Blockhouse (E4M2) - Green key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Main -> Blockhouse (E4M2) Blue", player), lambda state: + state.has("Blockhouse (E4M2) - Blue key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Green -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2) - Green key", player, 1)) + set_rule(world.get_entrance("Blockhouse (E4M2) Blue -> Blockhouse (E4M2) Main", player), lambda state: + state.has("Blockhouse (E4M2) - Blue key", player, 1)) + + # Ambulatory (E4M3) + set_rule(world.get_entrance("Hub -> Ambulatory (E4M3) Main", player), lambda state: + (state.has("Ambulatory (E4M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Blue", player), lambda state: + state.has("Ambulatory (E4M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Yellow", player), lambda state: + state.has("Ambulatory (E4M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ambulatory (E4M3) Main -> Ambulatory (E4M3) Green", player), lambda state: + state.has("Ambulatory (E4M3) - Green key", player, 1)) + + # Sepulcher (E4M4) + set_rule(world.get_entrance("Hub -> Sepulcher (E4M4) Main", player), lambda state: + (state.has("Sepulcher (E4M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + + # Great Stair (E4M5) + set_rule(world.get_entrance("Hub -> Great Stair (E4M5) Main", player), lambda state: + (state.has("Great Stair (E4M5)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Hellstaff", player, 1) or + state.has("Phoenix Rod", player, 1))) + set_rule(world.get_entrance("Great Stair (E4M5) Main -> Great Stair (E4M5) Yellow", player), lambda state: + state.has("Great Stair (E4M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Blue -> Great Stair (E4M5) Green", player), lambda state: + state.has("Great Stair (E4M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Yellow -> Great Stair (E4M5) Green", player), lambda state: + state.has("Great Stair (E4M5) - Green key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Blue", player), lambda state: + state.has("Great Stair (E4M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Great Stair (E4M5) Green -> Great Stair (E4M5) Yellow", player), lambda state: + state.has("Great Stair (E4M5) - Green key", player, 1)) + + # Halls of the Apostate (E4M6) + set_rule(world.get_entrance("Hub -> Halls of the Apostate (E4M6) Main", player), lambda state: + (state.has("Halls of the Apostate (E4M6)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Main -> Halls of the Apostate (E4M6) Yellow", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Blue -> Halls of the Apostate (E4M6) Green", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Yellow -> Halls of the Apostate (E4M6) Green", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Green key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Yellow", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Green key", player, 1)) + set_rule(world.get_entrance("Halls of the Apostate (E4M6) Green -> Halls of the Apostate (E4M6) Blue", player), lambda state: + state.has("Halls of the Apostate (E4M6) - Blue key", player, 1)) + + # Ramparts of Perdition (E4M7) + set_rule(world.get_entrance("Hub -> Ramparts of Perdition (E4M7) Main", player), lambda state: + (state.has("Ramparts of Perdition (E4M7)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Main -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Blue -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Main", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Green", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Green key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Yellow -> Ramparts of Perdition (E4M7) Blue", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Blue key", player, 1)) + set_rule(world.get_entrance("Ramparts of Perdition (E4M7) Green -> Ramparts of Perdition (E4M7) Yellow", player), lambda state: + state.has("Ramparts of Perdition (E4M7) - Green key", player, 1)) + + # Shattered Bridge (E4M8) + set_rule(world.get_entrance("Hub -> Shattered Bridge (E4M8) Main", player), lambda state: + state.has("Shattered Bridge (E4M8)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1)) + set_rule(world.get_entrance("Shattered Bridge (E4M8) Main -> Shattered Bridge (E4M8) Yellow", player), lambda state: + state.has("Shattered Bridge (E4M8) - Yellow key", player, 1)) + set_rule(world.get_entrance("Shattered Bridge (E4M8) Yellow -> Shattered Bridge (E4M8) Main", player), lambda state: + state.has("Shattered Bridge (E4M8) - Yellow key", player, 1)) + + # Mausoleum (E4M9) + set_rule(world.get_entrance("Hub -> Mausoleum (E4M9) Main", player), lambda state: + (state.has("Mausoleum (E4M9)", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Mausoleum (E4M9) Main -> Mausoleum (E4M9) Yellow", player), lambda state: + state.has("Mausoleum (E4M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Mausoleum (E4M9) Yellow -> Mausoleum (E4M9) Main", player), lambda state: + state.has("Mausoleum (E4M9) - Yellow key", player, 1)) + + +def set_episode5_rules(player, world, pro): + # Ochre Cliffs (E5M1) + set_rule(world.get_entrance("Hub -> Ochre Cliffs (E5M1) Main", player), lambda state: + state.has("Ochre Cliffs (E5M1)", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Main -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Blue -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Blue key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Main", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Yellow key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Green", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Green key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Yellow -> Ochre Cliffs (E5M1) Blue", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Blue key", player, 1)) + set_rule(world.get_entrance("Ochre Cliffs (E5M1) Green -> Ochre Cliffs (E5M1) Yellow", player), lambda state: + state.has("Ochre Cliffs (E5M1) - Green key", player, 1)) + + # Rapids (E5M2) + set_rule(world.get_entrance("Hub -> Rapids (E5M2) Main", player), lambda state: + state.has("Rapids (E5M2)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Main -> Rapids (E5M2) Yellow", player), lambda state: + state.has("Rapids (E5M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Main", player), lambda state: + state.has("Rapids (E5M2) - Yellow key", player, 1)) + set_rule(world.get_entrance("Rapids (E5M2) Yellow -> Rapids (E5M2) Green", player), lambda state: + state.has("Rapids (E5M2) - Green key", player, 1)) + + # Quay (E5M3) + set_rule(world.get_entrance("Hub -> Quay (E5M3) Main", player), lambda state: + (state.has("Quay (E5M3)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1) or + state.has("Firemace", player, 1))) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Yellow", player), lambda state: + state.has("Quay (E5M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Green", player), lambda state: + state.has("Quay (E5M3) - Green key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Main -> Quay (E5M3) Blue", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Blue -> Quay (E5M3) Green", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Yellow -> Quay (E5M3) Main", player), lambda state: + state.has("Quay (E5M3) - Yellow key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Main", player), lambda state: + state.has("Quay (E5M3) - Green key", player, 1)) + set_rule(world.get_entrance("Quay (E5M3) Green -> Quay (E5M3) Blue", player), lambda state: + state.has("Quay (E5M3) - Blue key", player, 1)) + + # Courtyard (E5M4) + set_rule(world.get_entrance("Hub -> Courtyard (E5M4) Main", player), lambda state: + (state.has("Courtyard (E5M4)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Firemace", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Kakis", player), lambda state: + state.has("Courtyard (E5M4) - Yellow key", player, 1) or + state.has("Courtyard (E5M4) - Green key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Main -> Courtyard (E5M4) Blue", player), lambda state: + state.has("Courtyard (E5M4) - Blue key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Blue -> Courtyard (E5M4) Main", player), lambda state: + state.has("Courtyard (E5M4) - Blue key", player, 1)) + set_rule(world.get_entrance("Courtyard (E5M4) Kakis -> Courtyard (E5M4) Main", player), lambda state: + state.has("Courtyard (E5M4) - Yellow key", player, 1) or + state.has("Courtyard (E5M4) - Green key", player, 1)) + + # Hydratyr (E5M5) + set_rule(world.get_entrance("Hub -> Hydratyr (E5M5) Main", player), lambda state: + (state.has("Hydratyr (E5M5)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Hydratyr (E5M5) Main -> Hydratyr (E5M5) Yellow", player), lambda state: + state.has("Hydratyr (E5M5) - Yellow key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Blue -> Hydratyr (E5M5) Green", player), lambda state: + state.has("Hydratyr (E5M5) - Blue key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Yellow -> Hydratyr (E5M5) Green", player), lambda state: + state.has("Hydratyr (E5M5) - Green key", player, 1)) + set_rule(world.get_entrance("Hydratyr (E5M5) Green -> Hydratyr (E5M5) Blue", player), lambda state: + state.has("Hydratyr (E5M5) - Blue key", player, 1)) + + # Colonnade (E5M6) + set_rule(world.get_entrance("Hub -> Colonnade (E5M6) Main", player), lambda state: + (state.has("Colonnade (E5M6)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Yellow", player), lambda state: + state.has("Colonnade (E5M6) - Yellow key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Main -> Colonnade (E5M6) Blue", player), lambda state: + state.has("Colonnade (E5M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Blue -> Colonnade (E5M6) Main", player), lambda state: + state.has("Colonnade (E5M6) - Blue key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Yellow -> Colonnade (E5M6) Green", player), lambda state: + state.has("Colonnade (E5M6) - Green key", player, 1)) + set_rule(world.get_entrance("Colonnade (E5M6) Green -> Colonnade (E5M6) Yellow", player), lambda state: + state.has("Colonnade (E5M6) - Green key", player, 1)) + + # Foetid Manse (E5M7) + set_rule(world.get_entrance("Hub -> Foetid Manse (E5M7) Main", player), lambda state: + (state.has("Foetid Manse (E5M7)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Firemace", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1)) and + (state.has("Phoenix Rod", player, 1) or + state.has("Hellstaff", player, 1))) + set_rule(world.get_entrance("Foetid Manse (E5M7) Main -> Foetid Manse (E5M7) Yellow", player), lambda state: + state.has("Foetid Manse (E5M7) - Yellow key", player, 1)) + set_rule(world.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Green", player), lambda state: + state.has("Foetid Manse (E5M7) - Green key", player, 1)) + set_rule(world.get_entrance("Foetid Manse (E5M7) Yellow -> Foetid Manse (E5M7) Blue", player), lambda state: + state.has("Foetid Manse (E5M7) - Blue key", player, 1)) + + # Field of Judgement (E5M8) + set_rule(world.get_entrance("Hub -> Field of Judgement (E5M8) Main", player), lambda state: + state.has("Field of Judgement (E5M8)", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Firemace", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Bag of Holding", player, 1)) + + # Skein of D'Sparil (E5M9) + set_rule(world.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9)", player, 1) and + state.has("Bag of Holding", player, 1) and + state.has("Hellstaff", player, 1) and + state.has("Phoenix Rod", player, 1) and + state.has("Dragon Claw", player, 1) and + state.has("Ethereal Crossbow", player, 1) and + state.has("Gauntlets of the Necromancer", player, 1) and + state.has("Firemace", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Blue", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Blue key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Yellow", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Main -> Skein of D'Sparil (E5M9) Green", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Green key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Yellow -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Yellow key", player, 1)) + set_rule(world.get_entrance("Skein of D'Sparil (E5M9) Green -> Skein of D'Sparil (E5M9) Main", player), lambda state: + state.has("Skein of D'Sparil (E5M9) - Green key", player, 1)) + + +def set_rules(heretic_world: "HereticWorld", included_episodes, pro): + player = heretic_world.player + world = heretic_world.multiworld + + if included_episodes[0]: + set_episode1_rules(player, world, pro) + if included_episodes[1]: + set_episode2_rules(player, world, pro) + if included_episodes[2]: + set_episode3_rules(player, world, pro) + if included_episodes[3]: + set_episode4_rules(player, world, pro) + if included_episodes[4]: + set_episode5_rules(player, world, pro) diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py new file mode 100644 index 0000000000..b0b2bfce8f --- /dev/null +++ b/worlds/heretic/__init__.py @@ -0,0 +1,287 @@ +import functools +import logging +from typing import Any, Dict, List, Set + +from BaseClasses import Entrance, CollectionState, Item, ItemClassification, Location, MultiWorld, Region, Tutorial +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Maps, Options, Regions, Rules + +logger = logging.getLogger("Heretic") + +HERETIC_TYPE_LEVEL_COMPLETE = -2 +HERETIC_TYPE_MAP_SCROLL = 35 + + +class HereticLocation(Location): + game: str = "Heretic" + + +class HereticItem(Item): + game: str = "Heretic" + + +class HereticWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Heretic randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class HereticWorld(World): + """ + Heretic is a dark fantasy first-person shooter video game released in December 1994. It was developed by Raven Software. + """ + option_definitions = Options.options + game = "Heretic" + web = HereticWeb() + data_version = 3 + required_client_version = (0, 3, 9) + + item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} + item_name_groups = Items.item_name_groups + + location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_groups = Locations.location_name_groups + + starting_level_for_episode: List[str] = [ + "The Docks (E1M1)", + "The Crater (E2M1)", + "The Storehouse (E3M1)", + "Catafalque (E4M1)", + "Ochre Cliffs (E5M1)" + ] + + boss_level_for_espidoes: List[str] = [ + "Hell's Maw (E1M8)", + "The Portals of Chaos (E2M8)", + "D'Sparil'S Keep (E3M8)", + "Shattered Bridge (E4M8)", + "Field of Judgement (E5M8)" + ] + + # Item ratio that scales depending on episode count. These are the ratio for 1 episode. + items_ratio: Dict[str, float] = { + "Timebomb of the Ancients": 16, + "Tome of Power": 16, + "Silver Shield": 10, + "Enchanted Shield": 5, + "Morph Ovum": 3, + "Mystic Urn": 2, + "Chaos Device": 1, + "Ring of Invincibility": 1, + "Shadowsphere": 1 + } + + def __init__(self, world: MultiWorld, player: int): + self.included_episodes = [1, 1, 1, 0, 0] + self.location_count = 0 + + super().__init__(world, player) + + def get_episode_count(self): + return functools.reduce(lambda count, episode: count + episode, self.included_episodes) + + def generate_early(self): + # Cache which episodes are included + for i in range(5): + self.included_episodes[i] = getattr(self.multiworld, f"episode{i + 1}")[self.player].value + + # If no episodes selected, select Episode 1 + if self.get_episode_count() == 0: + self.included_episodes[0] = 1 + + def create_regions(self): + pro = getattr(self.multiworld, "pro")[self.player].value + check_sanity = getattr(self.multiworld, "check_sanity")[self.player].value + + # Main regions + menu_region = Region("Menu", self.player, self.multiworld) + hub_region = Region("Hub", self.player, self.multiworld) + self.multiworld.regions += [menu_region, hub_region] + menu_region.add_exits(["Hub"]) + + # Create regions and locations + main_regions = [] + connections = [] + for region_dict in Regions.regions: + if not self.included_episodes[region_dict["episode"] - 1]: + continue + + region_name = region_dict["name"] + if region_dict["connects_to_hub"]: + main_regions.append(region_name) + + region = Region(region_name, self.player, self.multiworld) + region.add_locations({ + loc["name"]: loc_id + for loc_id, loc in Locations.location_table.items() + if loc["region"] == region_name and (not loc["check_sanity"] or check_sanity) + }, HereticLocation) + + self.multiworld.regions.append(region) + + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) + + # Connect main regions to Hub + hub_region.add_exits(main_regions) + + # Do the other connections between regions (They are not all both ways) + for connection in connections: + source = connection[0] + target = self.multiworld.get_region(connection[1], self.player) + + entrance = Entrance(self.player, f"{source.name} -> {target.name}", source) + source.exits.append(entrance) + entrance.connect(target) + + # Sum locations for items creation + self.location_count = len(self.multiworld.get_locations(self.player)) + + def completion_rule(self, state: CollectionState): + goal_levels = Maps.map_names + if getattr(self.multiworld, "goal")[self.player].value: + goal_levels = self.boss_level_for_espidoes + + for map_name in goal_levels: + if map_name + " - Exit" not in self.location_name_to_id: + continue + + # Exit location names are in form: The Docks (E1M1) - Exit + loc = Locations.location_table[self.location_name_to_id[map_name + " - Exit"]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + # Map complete item names are in form: The Docks (E1M1) - Complete + if not state.has(map_name + " - Complete", self.player, 1): + return False + + return True + + def set_rules(self): + pro = getattr(self.multiworld, "pro")[self.player].value + allow_death_logic = getattr(self.multiworld, "allow_death_logic")[self.player].value + + Rules.set_rules(self, self.included_episodes, pro) + self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) + + # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed + # platform) Unless the user allows for it. + if not allow_death_logic: + for death_logic_location in Locations.death_logic_locations: + self.multiworld.exclude_locations[self.player].value.add(death_logic_location) + + def create_item(self, name: str) -> HereticItem: + item_id: int = self.item_name_to_id[name] + return HereticItem(name, Items.item_table[item_id]["classification"], item_id, self.player) + + def create_items(self): + itempool: List[HereticItem] = [] + start_with_map_scrolls: bool = getattr(self.multiworld, "start_with_map_scrolls")[self.player].value + + # Items + for item_id, item in Items.item_table.items(): + if item["doom_type"] == HERETIC_TYPE_LEVEL_COMPLETE: + continue # We'll fill it manually later + + if item["doom_type"] == HERETIC_TYPE_MAP_SCROLL and start_with_map_scrolls: + continue # We'll fill it manually, and we will put fillers in place + + if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: + continue + + count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 + itempool += [self.create_item(item["name"]) for _ in range(count)] + + # Place end level items in locked locations + for map_name in Maps.map_names: + loc_name = map_name + " - Exit" + item_name = map_name + " - Complete" + + if loc_name not in self.location_name_to_id: + continue + + if item_name not in self.item_name_to_id: + continue + + loc = Locations.location_table[self.location_name_to_id[loc_name]] + if not self.included_episodes[loc["episode"] - 1]: + continue + + self.multiworld.get_location(loc_name, self.player).place_locked_item(self.create_item(item_name)) + self.location_count -= 1 + + # Give starting levels right away + for i in range(len(self.included_episodes)): + if self.included_episodes[i]: + self.multiworld.push_precollected(self.create_item(self.starting_level_for_episode[i])) + + # Give Computer area maps if option selected + if getattr(self.multiworld, "start_with_map_scrolls")[self.player].value: + for item_id, item_dict in Items.item_table.items(): + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == HERETIC_TYPE_MAP_SCROLL and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) + + # Fill the rest starting with powerups, then fillers + self.create_ratioed_items("Chaos Device", itempool) + self.create_ratioed_items("Morph Ovum", itempool) + self.create_ratioed_items("Mystic Urn", itempool) + self.create_ratioed_items("Ring of Invincibility", itempool) + self.create_ratioed_items("Shadowsphere", itempool) + self.create_ratioed_items("Timebomb of the Ancients", itempool) + self.create_ratioed_items("Tome of Power", itempool) + self.create_ratioed_items("Silver Shield", itempool) + self.create_ratioed_items("Enchanted Shield", itempool) + + while len(itempool) < self.location_count: + itempool.append(self.create_item(self.get_filler_item_name())) + + # add itempool to multiworld + self.multiworld.itempool += itempool + + def get_filler_item_name(self): + return self.multiworld.random.choice([ + "Quartz Flask", + "Crystal Geode", + "Energy Orb", + "Greater Runes", + "Inferno Orb", + "Pile of Mace Spheres", + "Quiver of Ethereal Arrows" + ]) + + def create_ratioed_items(self, item_name: str, itempool: List[HereticItem]): + remaining_loc = self.location_count - len(itempool) + if remaining_loc <= 0: + return + + episode_count = self.get_episode_count() + count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count)) + if count == 0: + logger.warning("Warning, no " + item_name + " will be placed.") + return + + for i in range(count): + itempool.append(self.create_item(item_name)) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "check_sanity") + + # Make sure we send proper episode settings + slot_data["episode1"] = self.included_episodes[0] + slot_data["episode2"] = self.included_episodes[1] + slot_data["episode3"] = self.included_episodes[2] + slot_data["episode4"] = self.included_episodes[3] + slot_data["episode5"] = self.included_episodes[4] + + return slot_data diff --git a/worlds/heretic/docs/en_Heretic.md b/worlds/heretic/docs/en_Heretic.md new file mode 100644 index 0000000000..97d371de2c --- /dev/null +++ b/worlds/heretic/docs/en_Heretic.md @@ -0,0 +1,23 @@ +# Heretic + +## Where is the settings page? + +The [player settings page](../player-settings) contains the options needed to configure your game session. + +## What does randomization do to this game? + +Weapons, keys, and level unlocks have been randomized. Monsters and Pickups are also randomized. Typically, you will end up playing different levels out of order to find your keys and level unlocks and eventually complete your game. + +Maps can be selected on a level select screen. You can exit a level at any time by visiting the hub station at the beginning of each level. The state of each level is saved and restored upon re-entering the level. + +## What is the goal? + +The goal is to complete every level in the episodes you have chosen to play. + +## What is a "check" in The Heretic? + +Weapons, keys, and powerups have been replaced with Archipelago checks. Some have been selectively removed because Heretic contains a lot of collectibles. Usually when many bunch together, one was kept. The switch at the end of each level is also a check. + +## What "items" can you unlock in Heretic? + +Keys and level unlocks are your main progression items. Weapon unlocks and some upgrades are your useful items. Powerups, ammo, healing, and armor are filler items. diff --git a/worlds/heretic/docs/setup_en.md b/worlds/heretic/docs/setup_en.md new file mode 100644 index 0000000000..e01d616e8f --- /dev/null +++ b/worlds/heretic/docs/setup_en.md @@ -0,0 +1,51 @@ +# Heretic Randomizer Setup + +## Required Software + +- [Heretic (e.g. Steam version)](https://store.steampowered.com/app/2390/Heretic_Shadow_of_the_Serpent_Riders/) +- [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) (Same download for DOOM 1993, DOOM II and Heretic) + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing APDoom +1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. +2. Copy HERETIC.WAD from your steam install into the extracted folder. + You can find the folder in steam by finding the game in your library, + right clicking it and choosing *Manage→Browse Local Files*. + +## Joining a MultiWorld Game + +1. Launch apdoom-launcher.exe +2. Choose Heretic in the dropdown +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch Game" +5. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +APDOOM has in-game messages, +but they disappear quickly and there's no reasonable way to check your message history in-game. + +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from Heretic can be difficult because names are rather long and contain special characters. For example: +``` +!hint The River of Fire (E2M3) - Green key +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint e2m3 green +``` +For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `green`. + +## Auto-Tracking + +APDOOM has a functional map tracker integrated into the level select screen. +It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, +and how many of the checks you have completed in each level. From dd47790c318edb9befdcff0e58de9a310f4298dd Mon Sep 17 00:00:00 2001 From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:38:18 -0500 Subject: [PATCH 228/327] DS3: Added 'Early Banner' Setting (#2199) Co-authored-by: Zach Parks --- worlds/dark_souls_3/Options.py | 11 +++++++++++ worlds/dark_souls_3/__init__.py | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index d613e47334..df0bb953b8 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -171,6 +171,16 @@ class MaxLevelsIn10WeaponPoolOption(Range): default = 10 +class EarlySmallLothricBanner(Choice): + """This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or + into an early sphere across all worlds.""" + display_name = "Early Small Lothric Banner" + option_off = 0 + option_early_global = 1 + option_early_local = 2 + default = option_off + + class LateBasinOfVowsOption(Toggle): """This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early, @@ -215,6 +225,7 @@ dark_souls_options: typing.Dict[str, Option] = { "max_levels_in_5": MaxLevelsIn5WeaponPoolOption, "min_levels_in_10": MinLevelsIn10WeaponPoolOption, "max_levels_in_10": MaxLevelsIn10WeaponPoolOption, + "early_banner": EarlySmallLothricBanner, "late_basin_of_vows": LateBasinOfVowsOption, "late_dlc": LateDLCOption, "no_spell_requirements": NoSpellRequirementsOption, diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b9879f70f3..7ee6c2a641 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -9,7 +9,7 @@ from worlds.generic.Rules import set_rule, add_rule, add_item_rule from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary -from .Options import RandomizeWeaponLevelOption, PoolTypeOption, dark_souls_options +from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options class DarkSouls3Web(WebWorld): @@ -86,6 +86,10 @@ class DarkSouls3World(World): self.enabled_location_categories.add(DS3LocationCategory.NPC) if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: self.enabled_location_categories.add(DS3LocationCategory.KEY) + if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global: + self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1 + elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local: + self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1 if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: self.enabled_location_categories.add(DS3LocationCategory.BOSS) if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: From 59ed2602bd48c7d861b0230562a85c239110f4f6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 25 Nov 2023 15:42:03 +0100 Subject: [PATCH 229/327] Pokemon: delete old files (#2501) --- inno_setup.iss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inno_setup.iss b/inno_setup.iss index 4744fa2b72..10d699ad70 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -81,6 +81,8 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" +Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" Type: filesandordirs; Name: "{app}\SNI\lua*" Type: filesandordirs; Name: "{app}\EnemizerCLI*" From c138918400447ff9b8425e4344adb319f56e766f Mon Sep 17 00:00:00 2001 From: David St-Louis Date: Sat, 25 Nov 2023 09:43:14 -0500 Subject: [PATCH 230/327] DOOM 1993: Added various new options (#2067) --- worlds/doom_1993/Items.py | 1 + worlds/doom_1993/Locations.py | 2 +- worlds/doom_1993/Options.py | 62 +++++- worlds/doom_1993/Regions.py | 351 ++++++++++++++++-------------- worlds/doom_1993/Rules.py | 21 +- worlds/doom_1993/__init__.py | 39 +++- worlds/doom_1993/docs/setup_en.md | 26 ++- 7 files changed, 316 insertions(+), 186 deletions(-) diff --git a/worlds/doom_1993/Items.py b/worlds/doom_1993/Items.py index fe5576c4df..3c5124d4d5 100644 --- a/worlds/doom_1993/Items.py +++ b/worlds/doom_1993/Items.py @@ -1165,6 +1165,7 @@ item_table: Dict[int, ItemDict] = { item_name_groups: Dict[str, Set[str]] = { 'Ammos': {'Box of bullets', 'Box of rockets', 'Box of shotgun shells', 'Energy cell pack', }, + 'Computer area maps': {'Against Thee Wickedly (E4M6) - Computer area map', 'And Hell Followed (E4M7) - Computer area map', 'Central Processing (E1M6) - Computer area map', 'Command Center (E2M5) - Computer area map', 'Command Control (E1M4) - Computer area map', 'Computer Station (E1M7) - Computer area map', 'Containment Area (E2M2) - Computer area map', 'Deimos Anomaly (E2M1) - Computer area map', 'Deimos Lab (E2M4) - Computer area map', 'Dis (E3M8) - Computer area map', 'Fear (E4M9) - Computer area map', 'Fortress of Mystery (E2M9) - Computer area map', 'Halls of the Damned (E2M6) - Computer area map', 'Hangar (E1M1) - Computer area map', 'Hell Beneath (E4M1) - Computer area map', 'Hell Keep (E3M1) - Computer area map', 'House of Pain (E3M4) - Computer area map', 'Limbo (E3M7) - Computer area map', 'Military Base (E1M9) - Computer area map', 'Mt. Erebus (E3M6) - Computer area map', 'Nuclear Plant (E1M2) - Computer area map', 'Pandemonium (E3M3) - Computer area map', 'Perfect Hatred (E4M2) - Computer area map', 'Phobos Anomaly (E1M8) - Computer area map', 'Phobos Lab (E1M5) - Computer area map', 'Refinery (E2M3) - Computer area map', 'Sever the Wicked (E4M3) - Computer area map', 'Slough of Despair (E3M2) - Computer area map', 'Spawning Vats (E2M7) - Computer area map', 'They Will Repent (E4M5) - Computer area map', 'Tower of Babel (E2M8) - Computer area map', 'Toxin Refinery (E1M3) - Computer area map', 'Unholy Cathedral (E3M5) - Computer area map', 'Unruly Evil (E4M4) - Computer area map', 'Unto the Cruel (E4M8) - Computer area map', 'Warrens (E3M9) - Computer area map', }, 'Keys': {'Against Thee Wickedly (E4M6) - Blue skull key', 'Against Thee Wickedly (E4M6) - Red skull key', 'Against Thee Wickedly (E4M6) - Yellow skull key', 'And Hell Followed (E4M7) - Blue skull key', 'And Hell Followed (E4M7) - Red skull key', 'And Hell Followed (E4M7) - Yellow skull key', 'Central Processing (E1M6) - Blue keycard', 'Central Processing (E1M6) - Red keycard', 'Central Processing (E1M6) - Yellow keycard', 'Command Control (E1M4) - Blue keycard', 'Command Control (E1M4) - Yellow keycard', 'Computer Station (E1M7) - Blue keycard', 'Computer Station (E1M7) - Red keycard', 'Computer Station (E1M7) - Yellow keycard', 'Containment Area (E2M2) - Blue keycard', 'Containment Area (E2M2) - Red keycard', 'Containment Area (E2M2) - Yellow keycard', 'Deimos Anomaly (E2M1) - Blue keycard', 'Deimos Anomaly (E2M1) - Red keycard', 'Deimos Lab (E2M4) - Blue keycard', 'Deimos Lab (E2M4) - Yellow keycard', 'Fear (E4M9) - Yellow skull key', 'Fortress of Mystery (E2M9) - Blue skull key', 'Fortress of Mystery (E2M9) - Red skull key', 'Fortress of Mystery (E2M9) - Yellow skull key', 'Halls of the Damned (E2M6) - Blue skull key', 'Halls of the Damned (E2M6) - Red skull key', 'Halls of the Damned (E2M6) - Yellow skull key', 'Hell Beneath (E4M1) - Blue skull key', 'Hell Beneath (E4M1) - Red skull key', 'House of Pain (E3M4) - Blue skull key', 'House of Pain (E3M4) - Red skull key', 'House of Pain (E3M4) - Yellow skull key', 'Limbo (E3M7) - Blue skull key', 'Limbo (E3M7) - Red skull key', 'Limbo (E3M7) - Yellow skull key', 'Military Base (E1M9) - Blue keycard', 'Military Base (E1M9) - Red keycard', 'Military Base (E1M9) - Yellow keycard', 'Mt. Erebus (E3M6) - Blue skull key', 'Nuclear Plant (E1M2) - Red keycard', 'Pandemonium (E3M3) - Blue skull key', 'Perfect Hatred (E4M2) - Blue skull key', 'Perfect Hatred (E4M2) - Yellow skull key', 'Phobos Lab (E1M5) - Blue keycard', 'Phobos Lab (E1M5) - Yellow keycard', 'Refinery (E2M3) - Blue keycard', 'Sever the Wicked (E4M3) - Blue skull key', 'Sever the Wicked (E4M3) - Red skull key', 'Slough of Despair (E3M2) - Blue skull key', 'Spawning Vats (E2M7) - Blue keycard', 'Spawning Vats (E2M7) - Red keycard', 'Spawning Vats (E2M7) - Yellow keycard', 'They Will Repent (E4M5) - Blue skull key', 'They Will Repent (E4M5) - Red skull key', 'They Will Repent (E4M5) - Yellow skull key', 'Toxin Refinery (E1M3) - Blue keycard', 'Toxin Refinery (E1M3) - Yellow keycard', 'Unholy Cathedral (E3M5) - Blue skull key', 'Unholy Cathedral (E3M5) - Yellow skull key', 'Unruly Evil (E4M4) - Red skull key', 'Unto the Cruel (E4M8) - Red skull key', 'Unto the Cruel (E4M8) - Yellow skull key', 'Warrens (E3M9) - Blue skull key', 'Warrens (E3M9) - Red skull key', }, 'Levels': {'Against Thee Wickedly (E4M6)', 'And Hell Followed (E4M7)', 'Central Processing (E1M6)', 'Command Center (E2M5)', 'Command Control (E1M4)', 'Computer Station (E1M7)', 'Containment Area (E2M2)', 'Deimos Anomaly (E2M1)', 'Deimos Lab (E2M4)', 'Dis (E3M8)', 'Fear (E4M9)', 'Fortress of Mystery (E2M9)', 'Halls of the Damned (E2M6)', 'Hangar (E1M1)', 'Hell Beneath (E4M1)', 'Hell Keep (E3M1)', 'House of Pain (E3M4)', 'Limbo (E3M7)', 'Military Base (E1M9)', 'Mt. Erebus (E3M6)', 'Nuclear Plant (E1M2)', 'Pandemonium (E3M3)', 'Perfect Hatred (E4M2)', 'Phobos Anomaly (E1M8)', 'Phobos Lab (E1M5)', 'Refinery (E2M3)', 'Sever the Wicked (E4M3)', 'Slough of Despair (E3M2)', 'Spawning Vats (E2M7)', 'They Will Repent (E4M5)', 'Tower of Babel (E2M8)', 'Toxin Refinery (E1M3)', 'Unholy Cathedral (E3M5)', 'Unruly Evil (E4M4)', 'Unto the Cruel (E4M8)', 'Warrens (E3M9)', }, 'Powerups': {'Armor', 'Berserk', 'Invulnerability', 'Mega Armor', 'Partial invisibility', 'Supercharge', }, diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 778efb4661..2cbb9b9d15 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -1968,7 +1968,7 @@ location_table: Dict[int, LocationDict] = { 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Containment Area (E2M2) Red"}, + 'region': "Containment Area (E2M2) Red Exit"}, 351326: {'name': 'Deimos Anomaly (E2M1) - Exit', 'episode': 2, 'map': 1, diff --git a/worlds/doom_1993/Options.py b/worlds/doom_1993/Options.py index 72bb7c3aea..59f7bcef49 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -1,6 +1,18 @@ import typing -from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle +from Options import AssembleOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool + + +class Goal(Choice): + """ + Choose the main goal. + complete_all_levels: All levels of the selected episodes + complete_boss_levels: Boss levels (E#M8) of selected episodes + """ + display_name = "Goal" + option_complete_all_levels = 0 + option_complete_boss_levels = 1 + default = 0 class Difficulty(Choice): @@ -27,11 +39,13 @@ class RandomMonsters(Choice): vanilla: No randomization shuffle: Monsters are shuffled within the level random_balanced: Monsters are completely randomized, but balanced based on existing ratio in the level. (Small monsters vs medium vs big) + random_chaotic: Monsters are completely randomized, but balanced based on existing ratio in the entire game. """ display_name = "Random Monsters" option_vanilla = 0 option_shuffle = 1 option_random_balanced = 2 + option_random_chaotic = 3 default = 1 @@ -49,6 +63,34 @@ class RandomPickups(Choice): default = 1 +class RandomMusic(Choice): + """ + Level musics will be randomized. + vanilla: No randomization + shuffle_selected: Selected episodes' levels will be shuffled + shuffle_game: All the music will be shuffled + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffle_selected = 1 + option_shuffle_game = 2 + default = 0 + + +class FlipLevels(Choice): + """ + Flip levels on one axis. + vanilla: No flipping + flipped: All levels are flipped + randomly_flipped: Random levels are flipped + """ + display_name = "Flip Levels" + option_vanilla = 0 + option_flipped = 1 + option_randomly_flipped = 2 + default = 0 + + class AllowDeathLogic(Toggle): """Some locations require a timed puzzle that can only be tried once. After which, if the player failed to get it, the location cannot be checked anymore. @@ -56,12 +98,24 @@ class AllowDeathLogic(Toggle): Get killed in the current map. The map will reset, you can now attempt the puzzle again.""" display_name = "Allow Death Logic" + +class Pro(Toggle): + """Include difficult tricks into rules. Mostly employed by speed runners. + i.e.: Leaps across to a locked area, trigger a switch behind a window at the right angle, etc.""" + display_name = "Pro Doom" + class StartWithComputerAreaMaps(Toggle): """Give the player all Computer Area Map items from the start.""" display_name = "Start With Computer Area Maps" +class ResetLevelOnDeath(DefaultOnToggle): + """When dying, levels are reset and monsters respawned. But inventory and checks are kept. + Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" + display_name="Reset Level on Death" + + class Episode1(DefaultOnToggle): """Knee-Deep in the Dead. If none of the episodes are chosen, Episode 1 will be chosen by default.""" @@ -87,12 +141,18 @@ class Episode4(Toggle): options: typing.Dict[str, AssembleOptions] = { + "start_inventory_from_pool": StartInventoryPool, + "goal": Goal, "difficulty": Difficulty, "random_monsters": RandomMonsters, "random_pickups": RandomPickups, + "random_music": RandomMusic, + "flip_levels": FlipLevels, "allow_death_logic": AllowDeathLogic, + "pro": Pro, "start_with_computer_area_maps": StartWithComputerAreaMaps, "death_link": DeathLink, + "reset_level_on_death": ResetLevelOnDeath, "episode1": Episode1, "episode2": Episode2, "episode3": Episode3, diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index 602c29f5bd..f013bdceaf 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -3,11 +3,15 @@ from typing import List from BaseClasses import TypedDict -class RegionDict(TypedDict, total=False): +class ConnectionDict(TypedDict, total=False): + target: str + pro: bool + +class RegionDict(TypedDict, total=False): name: str connects_to_hub: bool episode: int - connections: List[str] + connections: List[ConnectionDict] regions:List[RegionDict] = [ @@ -21,121 +25,131 @@ regions:List[RegionDict] = [ {"name":"Nuclear Plant (E1M2) Main", "connects_to_hub":True, "episode":1, - "connections":["Nuclear Plant (E1M2) Red"]}, + "connections":[{"target":"Nuclear Plant (E1M2) Red","pro":False}]}, {"name":"Nuclear Plant (E1M2) Red", "connects_to_hub":False, "episode":1, - "connections":["Nuclear Plant (E1M2) Main"]}, + "connections":[{"target":"Nuclear Plant (E1M2) Main","pro":False}]}, # Toxin Refinery (E1M3) {"name":"Toxin Refinery (E1M3) Main", "connects_to_hub":True, "episode":1, - "connections":["Toxin Refinery (E1M3) Blue"]}, + "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]}, {"name":"Toxin Refinery (E1M3) Blue", "connects_to_hub":False, "episode":1, "connections":[ - "Toxin Refinery (E1M3) Yellow", - "Toxin Refinery (E1M3) Main"]}, + {"target":"Toxin Refinery (E1M3) Yellow","pro":False}, + {"target":"Toxin Refinery (E1M3) Main","pro":False}]}, {"name":"Toxin Refinery (E1M3) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Toxin Refinery (E1M3) Blue"]}, + "connections":[{"target":"Toxin Refinery (E1M3) Blue","pro":False}]}, # Command Control (E1M4) {"name":"Command Control (E1M4) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Command Control (E1M4) Blue", - "Command Control (E1M4) Yellow"]}, + {"target":"Command Control (E1M4) Blue","pro":False}, + {"target":"Command Control (E1M4) Yellow","pro":False}, + {"target":"Command Control (E1M4) Ledge","pro":True}]}, {"name":"Command Control (E1M4) Blue", "connects_to_hub":False, "episode":1, - "connections":["Command Control (E1M4) Main"]}, + "connections":[ + {"target":"Command Control (E1M4) Ledge","pro":False}, + {"target":"Command Control (E1M4) Main","pro":False}]}, {"name":"Command Control (E1M4) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Command Control (E1M4) Main"]}, + "connections":[{"target":"Command Control (E1M4) Main","pro":False}]}, + {"name":"Command Control (E1M4) Ledge", + "connects_to_hub":False, + "episode":1, + "connections":[ + {"target":"Command Control (E1M4) Main","pro":False}, + {"target":"Command Control (E1M4) Blue","pro":False}, + {"target":"Command Control (E1M4) Yellow","pro":False}]}, # Phobos Lab (E1M5) {"name":"Phobos Lab (E1M5) Main", "connects_to_hub":True, "episode":1, - "connections":["Phobos Lab (E1M5) Yellow"]}, + "connections":[{"target":"Phobos Lab (E1M5) Yellow","pro":False}]}, {"name":"Phobos Lab (E1M5) Yellow", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Main", - "Phobos Lab (E1M5) Blue", - "Phobos Lab (E1M5) Green"]}, + {"target":"Phobos Lab (E1M5) Main","pro":False}, + {"target":"Phobos Lab (E1M5) Blue","pro":False}, + {"target":"Phobos Lab (E1M5) Green","pro":False}]}, {"name":"Phobos Lab (E1M5) Blue", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Green", - "Phobos Lab (E1M5) Yellow"]}, + {"target":"Phobos Lab (E1M5) Green","pro":False}, + {"target":"Phobos Lab (E1M5) Yellow","pro":False}]}, {"name":"Phobos Lab (E1M5) Green", "connects_to_hub":False, "episode":1, "connections":[ - "Phobos Lab (E1M5) Main", - "Phobos Lab (E1M5) Blue"]}, + {"target":"Phobos Lab (E1M5) Main","pro":False}, + {"target":"Phobos Lab (E1M5) Blue","pro":False}]}, # Central Processing (E1M6) {"name":"Central Processing (E1M6) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Central Processing (E1M6) Yellow", - "Central Processing (E1M6) Red", - "Central Processing (E1M6) Blue", - "Central Processing (E1M6) Nukage"]}, + {"target":"Central Processing (E1M6) Yellow","pro":False}, + {"target":"Central Processing (E1M6) Red","pro":False}, + {"target":"Central Processing (E1M6) Blue","pro":False}, + {"target":"Central Processing (E1M6) Nukage","pro":False}]}, {"name":"Central Processing (E1M6) Red", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Blue", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Main"]}, + "connections":[{"target":"Central Processing (E1M6) Main","pro":False}]}, {"name":"Central Processing (E1M6) Nukage", "connects_to_hub":False, "episode":1, - "connections":["Central Processing (E1M6) Yellow"]}, + "connections":[{"target":"Central Processing (E1M6) Yellow","pro":False}]}, # Computer Station (E1M7) {"name":"Computer Station (E1M7) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Computer Station (E1M7) Red", - "Computer Station (E1M7) Yellow"]}, + {"target":"Computer Station (E1M7) Red","pro":False}, + {"target":"Computer Station (E1M7) Yellow","pro":False}]}, {"name":"Computer Station (E1M7) Blue", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Yellow"]}, + "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]}, {"name":"Computer Station (E1M7) Red", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Main"]}, + "connections":[{"target":"Computer Station (E1M7) Main","pro":False}]}, {"name":"Computer Station (E1M7) Yellow", "connects_to_hub":False, "episode":1, "connections":[ - "Computer Station (E1M7) Blue", - "Computer Station (E1M7) Courtyard", - "Computer Station (E1M7) Main"]}, + {"target":"Computer Station (E1M7) Blue","pro":False}, + {"target":"Computer Station (E1M7) Courtyard","pro":False}, + {"target":"Computer Station (E1M7) Main","pro":False}]}, {"name":"Computer Station (E1M7) Courtyard", "connects_to_hub":False, "episode":1, - "connections":["Computer Station (E1M7) Yellow"]}, + "connections":[{"target":"Computer Station (E1M7) Yellow","pro":False}]}, # Phobos Anomaly (E1M8) {"name":"Phobos Anomaly (E1M8) Main", @@ -145,91 +159,98 @@ regions:List[RegionDict] = [ {"name":"Phobos Anomaly (E1M8) Start", "connects_to_hub":True, "episode":1, - "connections":["Phobos Anomaly (E1M8) Main"]}, + "connections":[{"target":"Phobos Anomaly (E1M8) Main","pro":False}]}, # Military Base (E1M9) {"name":"Military Base (E1M9) Main", "connects_to_hub":True, "episode":1, "connections":[ - "Military Base (E1M9) Blue", - "Military Base (E1M9) Yellow", - "Military Base (E1M9) Red"]}, + {"target":"Military Base (E1M9) Blue","pro":False}, + {"target":"Military Base (E1M9) Yellow","pro":False}, + {"target":"Military Base (E1M9) Red","pro":False}]}, {"name":"Military Base (E1M9) Blue", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, {"name":"Military Base (E1M9) Red", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, {"name":"Military Base (E1M9) Yellow", "connects_to_hub":False, "episode":1, - "connections":["Military Base (E1M9) Main"]}, + "connections":[{"target":"Military Base (E1M9) Main","pro":False}]}, # Deimos Anomaly (E2M1) {"name":"Deimos Anomaly (E2M1) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Deimos Anomaly (E2M1) Red", - "Deimos Anomaly (E2M1) Blue"]}, + {"target":"Deimos Anomaly (E2M1) Red","pro":False}, + {"target":"Deimos Anomaly (E2M1) Blue","pro":False}]}, {"name":"Deimos Anomaly (E2M1) Blue", "connects_to_hub":False, "episode":2, - "connections":["Deimos Anomaly (E2M1) Main"]}, + "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]}, {"name":"Deimos Anomaly (E2M1) Red", "connects_to_hub":False, "episode":2, - "connections":["Deimos Anomaly (E2M1) Main"]}, + "connections":[{"target":"Deimos Anomaly (E2M1) Main","pro":False}]}, # Containment Area (E2M2) {"name":"Containment Area (E2M2) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Containment Area (E2M2) Yellow", - "Containment Area (E2M2) Blue", - "Containment Area (E2M2) Red"]}, + {"target":"Containment Area (E2M2) Yellow","pro":False}, + {"target":"Containment Area (E2M2) Blue","pro":False}, + {"target":"Containment Area (E2M2) Red","pro":False}, + {"target":"Containment Area (E2M2) Red Exit","pro":True}]}, {"name":"Containment Area (E2M2) Blue", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]}, {"name":"Containment Area (E2M2) Red", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[ + {"target":"Containment Area (E2M2) Main","pro":False}, + {"target":"Containment Area (E2M2) Red Exit","pro":False}]}, {"name":"Containment Area (E2M2) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Containment Area (E2M2) Main"]}, + "connections":[{"target":"Containment Area (E2M2) Main","pro":False}]}, + {"name":"Containment Area (E2M2) Red Exit", + "connects_to_hub":False, + "episode":2, + "connections":[]}, # Refinery (E2M3) {"name":"Refinery (E2M3) Main", "connects_to_hub":True, "episode":2, - "connections":["Refinery (E2M3) Blue"]}, + "connections":[{"target":"Refinery (E2M3) Blue","pro":False}]}, {"name":"Refinery (E2M3) Blue", "connects_to_hub":False, "episode":2, - "connections":["Refinery (E2M3) Main"]}, + "connections":[{"target":"Refinery (E2M3) Main","pro":False}]}, # Deimos Lab (E2M4) {"name":"Deimos Lab (E2M4) Main", "connects_to_hub":True, "episode":2, - "connections":["Deimos Lab (E2M4) Blue"]}, + "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]}, {"name":"Deimos Lab (E2M4) Blue", "connects_to_hub":False, "episode":2, "connections":[ - "Deimos Lab (E2M4) Main", - "Deimos Lab (E2M4) Yellow"]}, + {"target":"Deimos Lab (E2M4) Main","pro":False}, + {"target":"Deimos Lab (E2M4) Yellow","pro":False}]}, {"name":"Deimos Lab (E2M4) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Deimos Lab (E2M4) Blue"]}, + "connections":[{"target":"Deimos Lab (E2M4) Blue","pro":False}]}, # Command Center (E2M5) {"name":"Command Center (E2M5) Main", @@ -242,47 +263,54 @@ regions:List[RegionDict] = [ "connects_to_hub":True, "episode":2, "connections":[ - "Halls of the Damned (E2M6) Blue Yellow Red", - "Halls of the Damned (E2M6) Yellow", - "Halls of the Damned (E2M6) One way Yellow"]}, + {"target":"Halls of the Damned (E2M6) Blue Yellow Red","pro":False}, + {"target":"Halls of the Damned (E2M6) Yellow","pro":False}, + {"target":"Halls of the Damned (E2M6) One way Yellow","pro":False}]}, {"name":"Halls of the Damned (E2M6) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, {"name":"Halls of the Damned (E2M6) Blue Yellow Red", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, {"name":"Halls of the Damned (E2M6) One way Yellow", "connects_to_hub":False, "episode":2, - "connections":["Halls of the Damned (E2M6) Main"]}, + "connections":[{"target":"Halls of the Damned (E2M6) Main","pro":False}]}, # Spawning Vats (E2M7) {"name":"Spawning Vats (E2M7) Main", "connects_to_hub":True, "episode":2, "connections":[ - "Spawning Vats (E2M7) Blue", - "Spawning Vats (E2M7) Entrance Secret", - "Spawning Vats (E2M7) Red", - "Spawning Vats (E2M7) Yellow"]}, + {"target":"Spawning Vats (E2M7) Blue","pro":False}, + {"target":"Spawning Vats (E2M7) Entrance Secret","pro":False}, + {"target":"Spawning Vats (E2M7) Red","pro":False}, + {"target":"Spawning Vats (E2M7) Yellow","pro":False}, + {"target":"Spawning Vats (E2M7) Red Exit","pro":True}]}, {"name":"Spawning Vats (E2M7) Blue", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, {"name":"Spawning Vats (E2M7) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, {"name":"Spawning Vats (E2M7) Red", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[ + {"target":"Spawning Vats (E2M7) Main","pro":False}, + {"target":"Spawning Vats (E2M7) Red Exit","pro":False}]}, {"name":"Spawning Vats (E2M7) Entrance Secret", "connects_to_hub":False, "episode":2, - "connections":["Spawning Vats (E2M7) Main"]}, + "connections":[{"target":"Spawning Vats (E2M7) Main","pro":False}]}, + {"name":"Spawning Vats (E2M7) Red Exit", + "connects_to_hub":False, + "episode":2, + "connections":[]}, # Tower of Babel (E2M8) {"name":"Tower of Babel (E2M8) Main", @@ -295,134 +323,134 @@ regions:List[RegionDict] = [ "connects_to_hub":True, "episode":2, "connections":[ - "Fortress of Mystery (E2M9) Blue", - "Fortress of Mystery (E2M9) Red", - "Fortress of Mystery (E2M9) Yellow"]}, + {"target":"Fortress of Mystery (E2M9) Blue","pro":False}, + {"target":"Fortress of Mystery (E2M9) Red","pro":False}, + {"target":"Fortress of Mystery (E2M9) Yellow","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Blue", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Red", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, {"name":"Fortress of Mystery (E2M9) Yellow", "connects_to_hub":False, "episode":2, - "connections":["Fortress of Mystery (E2M9) Main"]}, + "connections":[{"target":"Fortress of Mystery (E2M9) Main","pro":False}]}, # Hell Keep (E3M1) {"name":"Hell Keep (E3M1) Main", "connects_to_hub":True, "episode":3, - "connections":["Hell Keep (E3M1) Narrow"]}, + "connections":[{"target":"Hell Keep (E3M1) Narrow","pro":False}]}, {"name":"Hell Keep (E3M1) Narrow", "connects_to_hub":False, "episode":3, - "connections":["Hell Keep (E3M1) Main"]}, + "connections":[{"target":"Hell Keep (E3M1) Main","pro":False}]}, # Slough of Despair (E3M2) {"name":"Slough of Despair (E3M2) Main", "connects_to_hub":True, "episode":3, - "connections":["Slough of Despair (E3M2) Blue"]}, + "connections":[{"target":"Slough of Despair (E3M2) Blue","pro":False}]}, {"name":"Slough of Despair (E3M2) Blue", "connects_to_hub":False, "episode":3, - "connections":["Slough of Despair (E3M2) Main"]}, + "connections":[{"target":"Slough of Despair (E3M2) Main","pro":False}]}, # Pandemonium (E3M3) {"name":"Pandemonium (E3M3) Main", "connects_to_hub":True, "episode":3, - "connections":["Pandemonium (E3M3) Blue"]}, + "connections":[{"target":"Pandemonium (E3M3) Blue","pro":False}]}, {"name":"Pandemonium (E3M3) Blue", "connects_to_hub":False, "episode":3, - "connections":["Pandemonium (E3M3) Main"]}, + "connections":[{"target":"Pandemonium (E3M3) Main","pro":False}]}, # House of Pain (E3M4) {"name":"House of Pain (E3M4) Main", "connects_to_hub":True, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, {"name":"House of Pain (E3M4) Blue", "connects_to_hub":False, "episode":3, "connections":[ - "House of Pain (E3M4) Main", - "House of Pain (E3M4) Yellow", - "House of Pain (E3M4) Red"]}, + {"target":"House of Pain (E3M4) Main","pro":False}, + {"target":"House of Pain (E3M4) Yellow","pro":False}, + {"target":"House of Pain (E3M4) Red","pro":False}]}, {"name":"House of Pain (E3M4) Red", "connects_to_hub":False, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, {"name":"House of Pain (E3M4) Yellow", "connects_to_hub":False, "episode":3, - "connections":["House of Pain (E3M4) Blue"]}, + "connections":[{"target":"House of Pain (E3M4) Blue","pro":False}]}, # Unholy Cathedral (E3M5) {"name":"Unholy Cathedral (E3M5) Main", "connects_to_hub":True, "episode":3, "connections":[ - "Unholy Cathedral (E3M5) Yellow", - "Unholy Cathedral (E3M5) Blue"]}, + {"target":"Unholy Cathedral (E3M5) Yellow","pro":False}, + {"target":"Unholy Cathedral (E3M5) Blue","pro":False}]}, {"name":"Unholy Cathedral (E3M5) Blue", "connects_to_hub":False, "episode":3, - "connections":["Unholy Cathedral (E3M5) Main"]}, + "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]}, {"name":"Unholy Cathedral (E3M5) Yellow", "connects_to_hub":False, "episode":3, - "connections":["Unholy Cathedral (E3M5) Main"]}, + "connections":[{"target":"Unholy Cathedral (E3M5) Main","pro":False}]}, # Mt. Erebus (E3M6) {"name":"Mt. Erebus (E3M6) Main", "connects_to_hub":True, "episode":3, - "connections":["Mt. Erebus (E3M6) Blue"]}, + "connections":[{"target":"Mt. Erebus (E3M6) Blue","pro":False}]}, {"name":"Mt. Erebus (E3M6) Blue", "connects_to_hub":False, "episode":3, - "connections":["Mt. Erebus (E3M6) Main"]}, + "connections":[{"target":"Mt. Erebus (E3M6) Main","pro":False}]}, # Limbo (E3M7) {"name":"Limbo (E3M7) Main", "connects_to_hub":True, "episode":3, "connections":[ - "Limbo (E3M7) Red", - "Limbo (E3M7) Blue", - "Limbo (E3M7) Pink"]}, + {"target":"Limbo (E3M7) Red","pro":False}, + {"target":"Limbo (E3M7) Blue","pro":False}, + {"target":"Limbo (E3M7) Pink","pro":False}]}, {"name":"Limbo (E3M7) Blue", "connects_to_hub":False, "episode":3, - "connections":["Limbo (E3M7) Main"]}, + "connections":[{"target":"Limbo (E3M7) Main","pro":False}]}, {"name":"Limbo (E3M7) Red", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Main", - "Limbo (E3M7) Yellow", - "Limbo (E3M7) Green"]}, + {"target":"Limbo (E3M7) Main","pro":False}, + {"target":"Limbo (E3M7) Yellow","pro":False}, + {"target":"Limbo (E3M7) Green","pro":False}]}, {"name":"Limbo (E3M7) Yellow", "connects_to_hub":False, "episode":3, - "connections":["Limbo (E3M7) Red"]}, + "connections":[{"target":"Limbo (E3M7) Red","pro":False}]}, {"name":"Limbo (E3M7) Pink", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Green", - "Limbo (E3M7) Main"]}, + {"target":"Limbo (E3M7) Green","pro":False}, + {"target":"Limbo (E3M7) Main","pro":False}]}, {"name":"Limbo (E3M7) Green", "connects_to_hub":False, "episode":3, "connections":[ - "Limbo (E3M7) Pink", - "Limbo (E3M7) Red"]}, + {"target":"Limbo (E3M7) Pink","pro":False}, + {"target":"Limbo (E3M7) Red","pro":False}]}, # Dis (E3M8) {"name":"Dis (E3M8) Main", @@ -435,8 +463,8 @@ regions:List[RegionDict] = [ "connects_to_hub":True, "episode":3, "connections":[ - "Warrens (E3M9) Blue", - "Warrens (E3M9) Blue trigger"]}, + {"target":"Warrens (E3M9) Blue","pro":False}, + {"target":"Warrens (E3M9) Blue trigger","pro":False}]}, {"name":"Warrens (E3M9) Red", "connects_to_hub":False, "episode":3, @@ -445,8 +473,8 @@ regions:List[RegionDict] = [ "connects_to_hub":False, "episode":3, "connections":[ - "Warrens (E3M9) Main", - "Warrens (E3M9) Red"]}, + {"target":"Warrens (E3M9) Main","pro":False}, + {"target":"Warrens (E3M9) Red","pro":False}]}, {"name":"Warrens (E3M9) Blue trigger", "connects_to_hub":False, "episode":3, @@ -457,36 +485,36 @@ regions:List[RegionDict] = [ "connects_to_hub":True, "episode":4, "connections":[ - "Hell Beneath (E4M1) Red", - "Hell Beneath (E4M1) Blue"]}, + {"target":"Hell Beneath (E4M1) Red","pro":False}, + {"target":"Hell Beneath (E4M1) Blue","pro":False}]}, {"name":"Hell Beneath (E4M1) Red", "connects_to_hub":False, "episode":4, - "connections":["Hell Beneath (E4M1) Main"]}, + "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]}, {"name":"Hell Beneath (E4M1) Blue", "connects_to_hub":False, "episode":4, - "connections":["Hell Beneath (E4M1) Main"]}, + "connections":[{"target":"Hell Beneath (E4M1) Main","pro":False}]}, # Perfect Hatred (E4M2) {"name":"Perfect Hatred (E4M2) Main", "connects_to_hub":True, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Blue", - "Perfect Hatred (E4M2) Yellow"]}, + {"target":"Perfect Hatred (E4M2) Blue","pro":False}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Main", - "Perfect Hatred (E4M2) Cave"]}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}, + {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, "connections":[ - "Perfect Hatred (E4M2) Main", - "Perfect Hatred (E4M2) Cave"]}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}, + {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, @@ -496,132 +524,135 @@ regions:List[RegionDict] = [ {"name":"Sever the Wicked (E4M3) Main", "connects_to_hub":True, "episode":4, - "connections":["Sever the Wicked (E4M3) Red"]}, + "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]}, {"name":"Sever the Wicked (E4M3) Red", "connects_to_hub":False, "episode":4, "connections":[ - "Sever the Wicked (E4M3) Blue", - "Sever the Wicked (E4M3) Main"]}, + {"target":"Sever the Wicked (E4M3) Blue","pro":False}, + {"target":"Sever the Wicked (E4M3) Main","pro":False}]}, {"name":"Sever the Wicked (E4M3) Blue", "connects_to_hub":False, "episode":4, - "connections":["Sever the Wicked (E4M3) Red"]}, + "connections":[{"target":"Sever the Wicked (E4M3) Red","pro":False}]}, # Unruly Evil (E4M4) {"name":"Unruly Evil (E4M4) Main", "connects_to_hub":True, "episode":4, - "connections":["Unruly Evil (E4M4) Red"]}, + "connections":[{"target":"Unruly Evil (E4M4) Red","pro":False}]}, {"name":"Unruly Evil (E4M4) Red", "connects_to_hub":False, "episode":4, - "connections":["Unruly Evil (E4M4) Main"]}, + "connections":[{"target":"Unruly Evil (E4M4) Main","pro":False}]}, # They Will Repent (E4M5) {"name":"They Will Repent (E4M5) Main", "connects_to_hub":True, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Yellow", "connects_to_hub":False, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Blue", "connects_to_hub":False, "episode":4, - "connections":["They Will Repent (E4M5) Red"]}, + "connections":[{"target":"They Will Repent (E4M5) Red","pro":False}]}, {"name":"They Will Repent (E4M5) Red", "connects_to_hub":False, "episode":4, "connections":[ - "They Will Repent (E4M5) Main", - "They Will Repent (E4M5) Yellow", - "They Will Repent (E4M5) Blue"]}, + {"target":"They Will Repent (E4M5) Main","pro":False}, + {"target":"They Will Repent (E4M5) Yellow","pro":False}, + {"target":"They Will Repent (E4M5) Blue","pro":False}]}, # Against Thee Wickedly (E4M6) {"name":"Against Thee Wickedly (E4M6) Main", "connects_to_hub":True, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Blue"]}, + "connections":[ + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Pink","pro":True}]}, {"name":"Against Thee Wickedly (E4M6) Red", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Blue", - "Against Thee Wickedly (E4M6) Pink", - "Against Thee Wickedly (E4M6) Main"]}, + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Pink","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Main","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Magenta","pro":True}]}, {"name":"Against Thee Wickedly (E4M6) Blue", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Main", - "Against Thee Wickedly (E4M6) Yellow", - "Against Thee Wickedly (E4M6) Red"]}, + {"target":"Against Thee Wickedly (E4M6) Main","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Yellow","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Red","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Magenta", "connects_to_hub":False, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Main"]}, + "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Yellow", "connects_to_hub":False, "episode":4, "connections":[ - "Against Thee Wickedly (E4M6) Blue", - "Against Thee Wickedly (E4M6) Magenta"]}, + {"target":"Against Thee Wickedly (E4M6) Blue","pro":False}, + {"target":"Against Thee Wickedly (E4M6) Magenta","pro":False}]}, {"name":"Against Thee Wickedly (E4M6) Pink", "connects_to_hub":False, "episode":4, - "connections":["Against Thee Wickedly (E4M6) Main"]}, + "connections":[{"target":"Against Thee Wickedly (E4M6) Main","pro":False}]}, # And Hell Followed (E4M7) {"name":"And Hell Followed (E4M7) Main", "connects_to_hub":True, "episode":4, "connections":[ - "And Hell Followed (E4M7) Blue", - "And Hell Followed (E4M7) Red", - "And Hell Followed (E4M7) Yellow"]}, + {"target":"And Hell Followed (E4M7) Blue","pro":False}, + {"target":"And Hell Followed (E4M7) Red","pro":False}, + {"target":"And Hell Followed (E4M7) Yellow","pro":False}]}, {"name":"And Hell Followed (E4M7) Red", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, {"name":"And Hell Followed (E4M7) Blue", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, {"name":"And Hell Followed (E4M7) Yellow", "connects_to_hub":False, "episode":4, - "connections":["And Hell Followed (E4M7) Main"]}, + "connections":[{"target":"And Hell Followed (E4M7) Main","pro":False}]}, # Unto the Cruel (E4M8) {"name":"Unto the Cruel (E4M8) Main", "connects_to_hub":True, "episode":4, "connections":[ - "Unto the Cruel (E4M8) Red", - "Unto the Cruel (E4M8) Yellow", - "Unto the Cruel (E4M8) Orange"]}, + {"target":"Unto the Cruel (E4M8) Red","pro":False}, + {"target":"Unto the Cruel (E4M8) Yellow","pro":False}, + {"target":"Unto the Cruel (E4M8) Orange","pro":False}]}, {"name":"Unto the Cruel (E4M8) Yellow", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, {"name":"Unto the Cruel (E4M8) Red", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, {"name":"Unto the Cruel (E4M8) Orange", "connects_to_hub":False, "episode":4, - "connections":["Unto the Cruel (E4M8) Main"]}, + "connections":[{"target":"Unto the Cruel (E4M8) Main","pro":False}]}, # Fear (E4M9) {"name":"Fear (E4M9) Main", "connects_to_hub":True, "episode":4, - "connections":["Fear (E4M9) Yellow"]}, + "connections":[{"target":"Fear (E4M9) Yellow","pro":False}]}, {"name":"Fear (E4M9) Yellow", "connects_to_hub":False, "episode":4, - "connections":["Fear (E4M9) Main"]}, + "connections":[{"target":"Fear (E4M9) Main","pro":False}]}, ] diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 6e13a8af34..d5abc367a1 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -7,7 +7,7 @@ if TYPE_CHECKING: from . import DOOM1993World -def set_episode1_rules(player, world): +def set_episode1_rules(player, world, pro): # Hangar (E1M1) set_rule(world.get_entrance("Hub -> Hangar (E1M1) Main", player), lambda state: state.has("Hangar (E1M1)", player, 1)) @@ -130,7 +130,7 @@ def set_episode1_rules(player, world): state.has("Military Base (E1M9) - Yellow keycard", player, 1)) -def set_episode2_rules(player, world): +def set_episode2_rules(player, world, pro): # Deimos Anomaly (E2M1) set_rule(world.get_entrance("Hub -> Deimos Anomaly (E2M1) Main", player), lambda state: state.has("Deimos Anomaly (E2M1)", player, 1)) @@ -226,6 +226,9 @@ def set_episode2_rules(player, world): state.has("Spawning Vats (E2M7) - Red keycard", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Yellow", player), lambda state: state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1)) + if pro: + set_rule(world.get_entrance("Spawning Vats (E2M7) Main -> Spawning Vats (E2M7) Red Exit", player), lambda state: + state.has("Rocket launcher", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Yellow -> Spawning Vats (E2M7) Main", player), lambda state: state.has("Spawning Vats (E2M7) - Yellow keycard", player, 1)) set_rule(world.get_entrance("Spawning Vats (E2M7) Red -> Spawning Vats (E2M7) Main", player), lambda state: @@ -260,7 +263,7 @@ def set_episode2_rules(player, world): state.has("Fortress of Mystery (E2M9) - Yellow skull key", player, 1)) -def set_episode3_rules(player, world): +def set_episode3_rules(player, world, pro): # Hell Keep (E3M1) set_rule(world.get_entrance("Hub -> Hell Keep (E3M1) Main", player), lambda state: state.has("Hell Keep (E3M1)", player, 1)) @@ -385,7 +388,7 @@ def set_episode3_rules(player, world): state.has("Warrens (E3M9) - Red skull key", player, 1)) -def set_episode4_rules(player, world): +def set_episode4_rules(player, world, pro): # Hell Beneath (E4M1) set_rule(world.get_entrance("Hub -> Hell Beneath (E4M1) Main", player), lambda state: state.has("Hell Beneath (E4M1)", player, 1)) @@ -520,15 +523,15 @@ def set_episode4_rules(player, world): state.has("Fear (E4M9) - Yellow skull key", player, 1)) -def set_rules(doom_1993_world: "DOOM1993World", included_episodes): +def set_rules(doom_1993_world: "DOOM1993World", included_episodes, pro): player = doom_1993_world.player world = doom_1993_world.multiworld if included_episodes[0]: - set_episode1_rules(player, world) + set_episode1_rules(player, world, pro) if included_episodes[1]: - set_episode2_rules(player, world) + set_episode2_rules(player, world, pro) if included_episodes[2]: - set_episode3_rules(player, world) + set_episode3_rules(player, world, pro) if included_episodes[3]: - set_episode4_rules(player, world) + set_episode4_rules(player, world, pro) diff --git a/worlds/doom_1993/__init__.py b/worlds/doom_1993/__init__.py index 83a8652af1..e420b34b4f 100644 --- a/worlds/doom_1993/__init__.py +++ b/worlds/doom_1993/__init__.py @@ -56,6 +56,13 @@ class DOOM1993World(World): "Hell Beneath (E4M1)" ] + boss_level_for_espidoes: List[str] = [ + "Phobos Anomaly (E1M8)", + "Tower of Babel (E2M8)", + "Dis (E3M8)", + "Unto the Cruel (E4M8)" + ] + # Item ratio that scales depending on episode count. These are the ratio for 3 episode. items_ratio: Dict[str, float] = { "Armor": 41, @@ -90,6 +97,8 @@ class DOOM1993World(World): self.included_episodes[0] = 1 def create_regions(self): + pro = getattr(self.multiworld, "pro")[self.player].value + # Main regions menu_region = Region("Menu", self.player, self.multiworld) hub_region = Region("Hub", self.player, self.multiworld) @@ -116,8 +125,11 @@ class DOOM1993World(World): self.multiworld.regions.append(region) - for connection in region_dict["connections"]: - connections.append((region, connection)) + for connection_dict in region_dict["connections"]: + # Check if it's a pro-only connection + if connection_dict["pro"] and not pro: + continue + connections.append((region, connection_dict["target"])) # Connect main regions to Hub hub_region.add_exits(main_regions) @@ -135,7 +147,11 @@ class DOOM1993World(World): self.location_count = len(self.multiworld.get_locations(self.player)) def completion_rule(self, state: CollectionState): - for map_name in Maps.map_names: + goal_levels = Maps.map_names + if getattr(self.multiworld, "goal")[self.player].value: + goal_levels = self.boss_level_for_espidoes + + for map_name in goal_levels: if map_name + " - Exit" not in self.location_name_to_id: continue @@ -151,12 +167,15 @@ class DOOM1993World(World): return True def set_rules(self): - Rules.set_rules(self, self.included_episodes) + pro = getattr(self.multiworld, "pro")[self.player].value + allow_death_logic = getattr(self.multiworld, "allow_death_logic")[self.player].value + + Rules.set_rules(self, self.included_episodes, pro) self.multiworld.completion_condition[self.player] = lambda state: self.completion_rule(state) # Forbid progression items to locations that can be missed and can't be picked up. (e.g. One-time timed # platform) Unless the user allows for it. - if not getattr(self.multiworld, "allow_death_logic")[self.player].value: + if not allow_death_logic: for death_logic_location in Locations.death_logic_locations: self.multiworld.exclude_locations[self.player].value.add(death_logic_location) @@ -165,7 +184,6 @@ class DOOM1993World(World): return DOOM1993Item(name, Items.item_table[item_id]["classification"], item_id, self.player) def create_items(self): - is_only_first_episode: bool = self.get_episode_count() == 1 and self.included_episodes[0] itempool: List[DOOM1993Item] = [] start_with_computer_area_maps: bool = getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value @@ -180,9 +198,6 @@ class DOOM1993World(World): if item["episode"] != -1 and not self.included_episodes[item["episode"] - 1]: continue - if item["name"] in {"BFG9000", "Plasma Gun"} and is_only_first_episode: - continue # Don't include those guns if only first episode - count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1 itempool += [self.create_item(item["name"]) for _ in range(count)] @@ -212,8 +227,10 @@ class DOOM1993World(World): # Give Computer area maps if option selected if getattr(self.multiworld, "start_with_computer_area_maps")[self.player].value: for item_id, item_dict in Items.item_table.items(): - if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP: - self.multiworld.push_precollected(self.create_item(item_dict["name"])) + item_episode = item_dict["episode"] + if item_episode > 0: + if item_dict["doom_type"] == DOOM_TYPE_COMPUTER_AREA_MAP and self.included_episodes[item_episode - 1]: + self.multiworld.push_precollected(self.create_item(item_dict["name"])) # Fill the rest starting with powerups, then fillers self.create_ratioed_items("Armor", itempool) diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index cfd97f623a..1e546d359c 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -8,6 +8,8 @@ ## Optional Software - [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) +- [PopTracker](https://github.com/black-sliver/PopTracker/) + - [OZone's APDoom tracker pack](https://github.com/Ozone31/doom-ap-tracker/releases) ## Installing AP Doom 1. Download [APDOOM.zip](https://github.com/Daivuk/apdoom/releases) and extract it. @@ -17,10 +19,11 @@ ## Joining a MultiWorld Game -1. Launch APDoomLauncher.exe -2. Enter the Archipelago server address, slot name, and password (if you have one) -3. Press "Launch DOOM" -4. Enjoy! +1. Launch apdoom-launcher.exe +2. Select `Ultimate DOOM` from the drop-down +3. Enter the Archipelago server address, slot name, and password (if you have one) +4. Press "Launch DOOM" +5. Enjoy! To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. @@ -31,8 +34,23 @@ We recommend having Archipelago's Text Client open on the side to keep track of APDOOM has in-game messages, but they disappear quickly and there's no reasonable way to check your message history in-game. +### Hinting + +To hint from in-game, use the chat (Default key: 'T'). Hinting from DOOM can be difficult because names are rather long and contain special characters. For example: +``` +!hint Toxin Refinery (E1M3) - Computer area map +``` +The game has a hint helper implemented, where you can simply type this: +``` +!hint e1m3 map +``` +For this to work, include the map short name (`E1M1`), followed by one of the keywords: `map`, `blue`, `yellow`, `red`. + ## Auto-Tracking APDOOM has a functional map tracker integrated into the level select screen. It tells you which levels you have unlocked, which keys you have for each level, which levels have been completed, and how many of the checks you have completed in each level. + +For better tracking, try OZone's poptracker package: https://github.com/Ozone31/doom-ap-tracker/releases . +Requires [PopTracker](https://github.com/black-sliver/PopTracker/). From 2ccf11f3d78c49011b3e69878910cc46ef7e5309 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:46:00 -0500 Subject: [PATCH 231/327] KH2: Version 2 (#2009) Co-authored-by: Aaron Wagener Co-authored-by: Joe Prochaska --- KH2Client.py | 892 +--------- setup.py | 1 - worlds/LauncherComponents.py | 2 - worlds/kh2/Client.py | 881 ++++++++++ worlds/kh2/Items.py | 1352 ++++++--------- worlds/kh2/Locations.py | 2458 +++++++++++---------------- worlds/kh2/Logic.py | 642 +++++++ worlds/kh2/Names/ItemName.py | 137 +- worlds/kh2/Names/LocationName.py | 235 ++- worlds/kh2/Names/RegionName.py | 210 ++- worlds/kh2/OpenKH.py | 250 ++- worlds/kh2/Options.py | 234 ++- worlds/kh2/Regions.py | 1215 ++++++------- worlds/kh2/Rules.py | 1233 +++++++++++++- worlds/kh2/WorldLocations.py | 171 +- worlds/kh2/__init__.py | 615 ++++--- worlds/kh2/logic.py | 312 ---- worlds/kh2/mod_template/mod.yml | 38 - worlds/kh2/test/TestGoal.py | 30 - worlds/kh2/test/TestSlotData.py | 21 - worlds/kh2/test/__init__.py | 2 +- worlds/kh2/test/test_fight_logic.py | 19 + worlds/kh2/test/test_form_logic.py | 214 +++ worlds/kh2/test/test_goal.py | 59 + 24 files changed, 6341 insertions(+), 4882 deletions(-) create mode 100644 worlds/kh2/Client.py create mode 100644 worlds/kh2/Logic.py delete mode 100644 worlds/kh2/logic.py delete mode 100644 worlds/kh2/mod_template/mod.yml delete mode 100644 worlds/kh2/test/TestGoal.py delete mode 100644 worlds/kh2/test/TestSlotData.py create mode 100644 worlds/kh2/test/test_fight_logic.py create mode 100644 worlds/kh2/test/test_form_logic.py create mode 100644 worlds/kh2/test/test_goal.py diff --git a/KH2Client.py b/KH2Client.py index 1134932dc2..69e4adf8bf 100644 --- a/KH2Client.py +++ b/KH2Client.py @@ -1,894 +1,8 @@ -import os -import asyncio import ModuleUpdate -import json import Utils -from pymem import pymem -from worlds.kh2.Items import exclusionItem_table, CheckDupingItems -from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table - -from worlds.kh2.WorldLocations import * - -from worlds import network_data_package - -if __name__ == "__main__": - Utils.init_logging("KH2Client", exception_logger="Client") - -from NetUtils import ClientStatus -from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ - CommonContext, server_loop - +from worlds.kh2.Client import launch ModuleUpdate.update() -kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"] - - -# class KH2CommandProcessor(ClientCommandProcessor): - - -class KH2Context(CommonContext): - # command_processor: int = KH2CommandProcessor - game = "Kingdom Hearts 2" - items_handling = 0b101 # Indicates you get items sent from other worlds. - - def __init__(self, server_address, password): - super(KH2Context, self).__init__(server_address, password) - self.kh2LocalItems = None - self.ability = None - self.growthlevel = None - self.KH2_sync_task = None - self.syncing = False - self.kh2connected = False - self.serverconneced = False - self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} - self.location_name_to_data = {name: data for name, data, in all_locations.items()} - self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in - item_dictionary_table.items() if data.code} - self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in - all_locations.items() if data.code} - self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} - - self.location_table = {} - self.collectible_table = {} - self.collectible_override_flags_address = 0 - self.collectible_offsets = {} - self.sending = [] - # list used to keep track of locations+items player has. Used for disoneccting - self.kh2seedsave = None - self.slotDataProgressionNames = {} - self.kh2seedname = None - self.kh2slotdata = None - self.itemamount = {} - # sora equipped, valor equipped, master equipped, final equipped - self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4) - if "localappdata" in os.environ: - self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") - self.amountOfPieces = 0 - # hooked object - self.kh2 = None - self.ItemIsSafe = False - self.game_connected = False - self.finalxemnas = False - self.worldid = { - # 1: {}, # world of darkness (story cutscenes) - 2: TT_Checks, - # 3: {}, # destiny island doesn't have checks to ima put tt checks here - 4: HB_Checks, - 5: BC_Checks, - 6: Oc_Checks, - 7: AG_Checks, - 8: LoD_Checks, - 9: HundredAcreChecks, - 10: PL_Checks, - 11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc - 12: DC_Checks, - 13: TR_Checks, - 14: HT_Checks, - 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb - 16: PR_Checks, - 17: SP_Checks, - 18: TWTNW_Checks, - # 255: {}, # starting screen - } - # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room - self.sveroom = 0x2A09C00 + 0x41 - # 0 not in battle 1 in yellow battle 2 red battle #short - self.inBattle = 0x2A0EAC4 + 0x40 - self.onDeath = 0xAB9078 - # PC Address anchors - self.Now = 0x0714DB8 - self.Save = 0x09A70B0 - self.Sys3 = 0x2A59DF0 - self.Bt10 = 0x2A74880 - self.BtlEnd = 0x2A0D3E0 - self.Slot1 = 0x2A20C98 - - self.chest_set = set(exclusion_table["Chests"]) - - self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) - self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) - self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) - - self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) - - self.equipment_categories = CheckDupingItems["Equipment"] - self.armor_set = set(self.equipment_categories["Armor"]) - self.accessories_set = set(self.equipment_categories["Accessories"]) - self.all_equipment = self.armor_set.union(self.accessories_set) - - self.Equipment_Anchor_Dict = { - "Armor": [0x2504, 0x2506, 0x2508, 0x250A], - "Accessories": [0x2514, 0x2516, 0x2518, 0x251A]} - - self.AbilityQuantityDict = {} - self.ability_categories = CheckDupingItems["Abilities"] - - self.sora_ability_set = set(self.ability_categories["Sora"]) - self.donald_ability_set = set(self.ability_categories["Donald"]) - self.goofy_ability_set = set(self.ability_categories["Goofy"]) - - self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) - - self.boost_set = set(CheckDupingItems["Boosts"]) - self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) - self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} - # Growth:[level 1,level 4,slot] - self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA], - "Quick Run": [0x62, 0x65, 0x25DC], - "Dodge Roll": [0x234, 0x237, 0x25DE], - "Aerial Dodge": [0x066, 0x069, 0x25E0], - "Glide": [0x6A, 0x6D, 0x25E2]} - self.boost_to_anchor_dict = { - "Power Boost": 0x24F9, - "Magic Boost": 0x24FA, - "Defense Boost": 0x24FB, - "AP Boost": 0x24F8} - - self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]] - self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} - - self.bitmask_item_code = [ - 0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007 - , 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C - , 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023 - , 0x13002A, 0x13002B, 0x13002C, 0x13002D] - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(KH2Context, self).server_auth(password_requested) - await self.get_username() - await self.send_connect() - - async def connection_closed(self): - self.kh2connected = False - self.serverconneced = False - if self.kh2seedname is not None and self.auth is not None: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).connection_closed() - - async def disconnect(self, allow_autoreconnect: bool = False): - self.kh2connected = False - self.serverconneced = False - if self.kh2seedname not in {None} and self.auth not in {None}: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).disconnect() - - @property - def endpoints(self): - if self.server: - return [self.server] - else: - return [] - - async def shutdown(self): - if self.kh2seedname not in {None} and self.auth not in {None}: - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'w') as f: - f.write(json.dumps(self.kh2seedsave, indent=4)) - await super(KH2Context, self).shutdown() - - def on_package(self, cmd: str, args: dict): - if cmd in {"RoomInfo"}: - self.kh2seedname = args['seed_name'] - if not os.path.exists(self.game_communication_path): - os.makedirs(self.game_communication_path) - if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): - self.kh2seedsave = {"itemIndex": -1, - # back of soras invo is 0x25E2. Growth should be moved there - # Character: [back of invo, front of invo] - "SoraInvo": [0x25D8, 0x2546], - "DonaldInvo": [0x26F4, 0x2658], - "GoofyInvo": [0x280A, 0x276C], - "AmountInvo": { - "ServerItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, - "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }, - "LocalItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }}, - # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked - "LocationsChecked": [], - "Levels": { - "SoraLevel": 0, - "ValorLevel": 0, - "WisdomLevel": 0, - "LimitLevel": 0, - "MasterLevel": 0, - "FinalLevel": 0, - }, - "SoldEquipment": [], - "SoldBoosts": {"Power Boost": 0, - "Magic Boost": 0, - "Defense Boost": 0, - "AP Boost": 0} - } - with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), - 'wt') as f: - pass - self.locations_checked = set() - elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): - with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f: - self.kh2seedsave = json.load(f) - self.locations_checked = set(self.kh2seedsave["LocationsChecked"]) - self.serverconneced = True - - if cmd in {"Connected"}: - self.kh2slotdata = args['slot_data'] - self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} - try: - self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - logger.info("You are now auto-tracking") - self.kh2connected = True - except Exception as e: - logger.info("Line 247") - if self.kh2connected: - logger.info("Connection Lost") - self.kh2connected = False - logger.info(e) - - if cmd in {"ReceivedItems"}: - start_index = args["index"] - if start_index == 0: - # resetting everything that were sent from the server - self.kh2seedsave["SoraInvo"][0] = 0x25D8 - self.kh2seedsave["DonaldInvo"][0] = 0x26F4 - self.kh2seedsave["GoofyInvo"][0] = 0x280A - self.kh2seedsave["itemIndex"] = - 1 - self.kh2seedsave["AmountInvo"]["ServerItems"] = { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, - "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - } - if start_index > self.kh2seedsave["itemIndex"]: - self.kh2seedsave["itemIndex"] = start_index - for item in args['items']: - asyncio.create_task(self.give_item(item.item)) - - if cmd in {"RoomUpdate"}: - if "checked_locations" in args: - new_locations = set(args["checked_locations"]) - # TODO: make this take locations from other players on the same slot so proper coop happens - # items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if - # location_id in self.kh2LocalItems.keys()] - self.checked_locations |= new_locations - - async def checkWorldLocations(self): - try: - currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big") - if currentworldint in self.worldid: - curworldid = self.worldid[currentworldint] - for location, data in curworldid.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and (int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex) > 0: - self.sending = self.sending + [(int(locationId))] - except Exception as e: - logger.info("Line 285") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def checkLevels(self): - try: - for location, data in SoraLevels.items(): - currentLevel = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big") - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and currentLevel >= data.bitIndex: - if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel: - self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel - self.sending = self.sending + [(int(locationId))] - formDict = { - 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], - 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]} - for i in range(5): - for location, data in formDict[i][1].items(): - formlevel = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked \ - and formlevel >= data.bitIndex: - if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]: - self.kh2seedsave["Levels"][formDict[i][0]] = formlevel - self.sending = self.sending + [(int(locationId))] - except Exception as e: - logger.info("Line 312") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def checkSlots(self): - try: - for location, data in weaponSlots.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") > 0: - self.sending = self.sending + [(int(locationId))] - - for location, data in formSlots.items(): - locationId = kh2_loc_name_to_id[location] - if locationId not in self.locations_checked: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex > 0: - # self.locations_checked - self.sending = self.sending + [(int(locationId))] - - except Exception as e: - if self.kh2connected: - logger.info("Line 333") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def verifyChests(self): - try: - for location in self.locations_checked: - locationName = self.lookup_id_to_Location[location] - if locationName in self.chest_set: - if locationName in self.location_name_to_worlddata.keys(): - locationData = self.location_name_to_worlddata[locationName] - if int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), - "big") & 0x1 << locationData.bitIndex == 0: - roomData = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, - 1), "big") - self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, - (roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1) - - except Exception as e: - if self.kh2connected: - logger.info("Line 350") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - async def verifyLevel(self): - for leveltype, anchor in {"SoraLevel": 0x24FF, - "ValorLevel": 0x32F6, - "WisdomLevel": 0x332E, - "LimitLevel": 0x3366, - "MasterLevel": 0x339E, - "FinalLevel": 0x33D6}.items(): - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \ - self.kh2seedsave["Levels"][leveltype]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor, - (self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1) - - async def give_item(self, item, ItemType="ServerItems"): - try: - itemname = self.lookup_id_to_item[item] - itemcode = self.item_name_to_data[itemname] - if itemcode.ability: - abilityInvoType = 0 - TwilightZone = 2 - if ItemType == "LocalItems": - abilityInvoType = 1 - TwilightZone = -2 - if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: - self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1 - return - - if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = [] - # appending the slot that the ability should be in - - if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \ - self.AbilityQuantityDict[itemname]: - if itemname in self.sora_ability_set: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["SoraInvo"][abilityInvoType]) - self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone - elif itemname in self.donald_ability_set: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["DonaldInvo"][abilityInvoType]) - self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone - else: - self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append( - self.kh2seedsave["GoofyInvo"][abilityInvoType]) - self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone - - elif itemcode.code in self.bitmask_item_code: - - if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]: - self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname) - - elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: - - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]: - self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1 - elif itemname in self.all_equipment: - - self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname) - - elif itemname in self.all_weapons: - if itemname in self.keyblade_set: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname) - elif itemname in self.staff_set: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname) - else: - self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname) - - elif itemname in self.boost_set: - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]: - self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1 - - elif itemname in self.stat_increase_set: - - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]: - self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1 - - else: - if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]: - self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1 - else: - self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1 - - except Exception as e: - if self.kh2connected: - logger.info("Line 398") - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - def run_gui(self): - """Import kivy UI system and start running it as self.ui_task.""" - from kvui import GameManager - - class KH2Manager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago KH2 Client" - - self.ui = KH2Manager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - async def IsInShop(self, sellable, master_boost): - # journal = 0x741230 shop = 0x741320 - # if journal=-1 and shop = 5 then in shop - # if journam !=-1 and shop = 10 then journal - journal = self.kh2.read_short(self.kh2.base_address + 0x741230) - shop = self.kh2.read_short(self.kh2.base_address + 0x741320) - if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - # print("your in the shop") - sellable_dict = {} - for itemName in sellable: - itemdata = self.item_name_to_data[itemName] - amount = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") - sellable_dict[itemName] = amount - while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - journal = self.kh2.read_short(self.kh2.base_address + 0x741230) - shop = self.kh2.read_short(self.kh2.base_address + 0x741320) - await asyncio.sleep(0.5) - for item, amount in sellable_dict.items(): - itemdata = self.item_name_to_data[item] - afterShop = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big") - if afterShop < amount: - if item in master_boost: - self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop) - else: - self.kh2seedsave["SoldEquipment"].append(item) - - async def verifyItems(self): - try: - local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys()) - server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys()) - master_amount = local_amount | server_amount - - local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys()) - server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys()) - master_ability = local_ability | server_ability - - local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"]) - server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"]) - master_bitmask = local_bitmask | server_bitmask - - local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"]) - local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"]) - local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"]) - - server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"]) - server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"]) - server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"]) - - master_keyblade = local_keyblade | server_keyblade - master_staff = local_staff | server_staff - master_shield = local_shield | server_shield - - local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"]) - server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"]) - master_equipment = local_equipment | server_equipment - - local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys()) - server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys()) - master_magic = local_magic | server_magic - - local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys()) - server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys()) - master_stat = local_stat | server_stat - - local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys()) - server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys()) - master_boost = local_boost | server_boost - - master_sell = master_equipment | master_staff | master_shield | master_boost - await asyncio.create_task(self.IsInShop(master_sell, master_boost)) - for itemName in master_amount: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_amount: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName] - if itemName in server_amount: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName] - - if itemName == "Torn Page": - # Torn Pages are handled differently because they can be consumed. - # Will check the progression in 100 acre and - the amount of visits - # amountofitems-amount of visits done - for location, data in tornPageLocks.items(): - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), - "big") & 0x1 << data.bitIndex > 0: - amountOfItems -= 1 - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems and amountOfItems >= 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_keyblade: - itemData = self.item_name_to_data[itemName] - # if the inventory slot for that keyblade is less than the amount they should have - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1), - "big") != 13: - # Checking form anchors for the keyblade - if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \ - or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (0).to_bytes(1, 'big'), 1) - else: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - for itemName in master_staff: - itemData = self.item_name_to_data[itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \ - and itemName not in self.kh2seedsave["SoldEquipment"]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_shield: - itemData = self.item_name_to_data[itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1 \ - and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \ - and itemName not in self.kh2seedsave["SoldEquipment"]: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_ability: - itemData = self.item_name_to_data[itemName] - ability_slot = [] - if itemName in local_ability: - ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName] - if itemName in server_ability: - ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName] - for slot in ability_slot: - current = self.kh2.read_short(self.kh2.base_address + self.Save + slot) - ability = current & 0x0FFF - if ability | 0x8000 != (0x8000 + itemData.memaddr): - if current - 0x8000 > 0: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, (0x8000 + itemData.memaddr)) - else: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr) - # removes the duped ability if client gave faster than the game. - for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}: - if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and \ - self.kh2seedsave[charInvo][1] + 2 < self.kh2seedsave[charInvo][0]: - self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0) - # remove the dummy level 1 growths if they are in these invo slots. - for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: - current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot) - ability = current & 0x0FFF - if 0x05E <= ability <= 0x06D: - self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0) - - for itemName in self.master_growth: - growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \ - + self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName] - if growthLevel > 0: - slot = self.growth_values_dict[itemName][2] - min_growth = self.growth_values_dict[itemName][0] - max_growth = self.growth_values_dict[itemName][1] - if growthLevel > 4: - growthLevel = 4 - current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot) - ability = current_growth_level & 0x0FFF - # if the player should be getting a growth ability - if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: - # if it should be level one of that growth - if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: - self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth) - # if it is already in the inventory - elif ability | 0x8000 < (0x8000 + max_growth): - self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1) - - for itemName in master_bitmask: - itemData = self.item_name_to_data[itemName] - itemMemory = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") - if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") & 0x1 << itemData.bitmask) == 0: - # when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game. - if itemName in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}: - self.kh2.write_bytes(self.kh2.base_address + self.Save + 0x3410, - (0).to_bytes(1, 'big'), 1) - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1) - - for itemName in master_equipment: - itemData = self.item_name_to_data[itemName] - isThere = False - if itemName in self.accessories_set: - Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] - else: - Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] - # Checking form anchors for the equipment - for slot in Equipment_Anchor_List: - if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id: - isThere = True - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (0).to_bytes(1, 'big'), 1) - break - if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != 1: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (1).to_bytes(1, 'big'), 1) - - for itemName in master_magic: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_magic: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName] - if itemName in server_magic: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName] - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems \ - and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_stat: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_stat: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName] - if itemName in server_stat: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName] - - # 0x130293 is Crit_1's location id for touching the computer - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") != amountOfItems \ - and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1), - "big") >= 5 and int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x23DF, 1), - "big") > 0: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - amountOfItems.to_bytes(1, 'big'), 1) - - for itemName in master_boost: - itemData = self.item_name_to_data[itemName] - amountOfItems = 0 - if itemName in local_boost: - amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName] - if itemName in server_boost: - amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName] - amountOfBoostsInInvo = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), - "big") - amountOfUsedBoosts = int.from_bytes( - self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1), - "big") - # Ap Boots start at +50 for some reason - if itemName == "AP Boost": - amountOfUsedBoosts -= 50 - totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts) - if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][ - itemName] and amountOfBoostsInInvo < 255: - self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, - (amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1) - - except Exception as e: - logger.info("Line 573") - if self.kh2connected: - logger.info("Connection Lost.") - self.kh2connected = False - logger.info(e) - - -def finishedGame(ctx: KH2Context, message): - if ctx.kh2slotdata['FinalXemnas'] == 1: - if 0x1301ED in message[0]["locations"]: - ctx.finalxemnas = True - # three proofs - if ctx.kh2slotdata['Goal'] == 0: - if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \ - and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \ - and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0: - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - elif ctx.kh2slotdata['Goal'] == 1: - if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \ - ctx.kh2slotdata['LuckyEmblemsRequired']: - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - elif ctx.kh2slotdata['Goal'] == 2: - for boss in ctx.kh2slotdata["hitlist"]: - if boss in message[0]["locations"]: - ctx.amountOfPieces += 1 - if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]: - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1) - ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1) - if ctx.kh2slotdata['FinalXemnas'] == 1: - if ctx.finalxemnas: - return True - else: - return False - else: - return True - else: - return False - - -async def kh2_watcher(ctx: KH2Context): - while not ctx.exit_event.is_set(): - try: - if ctx.kh2connected and ctx.serverconneced: - ctx.sending = [] - await asyncio.create_task(ctx.checkWorldLocations()) - await asyncio.create_task(ctx.checkLevels()) - await asyncio.create_task(ctx.checkSlots()) - await asyncio.create_task(ctx.verifyChests()) - await asyncio.create_task(ctx.verifyItems()) - await asyncio.create_task(ctx.verifyLevel()) - message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message): - await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True - location_ids = [] - location_ids = [location for location in message[0]["locations"] if location not in location_ids] - for location in location_ids: - if location not in ctx.locations_checked: - ctx.locations_checked.add(location) - ctx.kh2seedsave["LocationsChecked"].append(location) - if location in ctx.kh2LocalItems: - item = ctx.kh2slotdata["LocalItems"][str(location)] - await asyncio.create_task(ctx.give_item(item, "LocalItems")) - await ctx.send_msgs(message) - elif not ctx.kh2connected and ctx.serverconneced: - logger.info("Game is not open. Disconnecting from Server.") - await ctx.disconnect() - except Exception as e: - logger.info("Line 661") - if ctx.kh2connected: - logger.info("Connection Lost.") - ctx.kh2connected = False - logger.info(e) - await asyncio.sleep(0.5) - - if __name__ == '__main__': - async def main(args): - ctx = KH2Context(args.connect, args.password) - ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - progression_watcher = asyncio.create_task( - kh2_watcher(ctx), name="KH2ProgressionWatcher") - - await ctx.exit_event.wait() - ctx.server_address = None - - await progression_watcher - - await ctx.shutdown() - - - import colorama - - parser = get_base_parser(description="KH2 Client, for text interfacing.") - - args, rest = parser.parse_known_args() - colorama.init() - asyncio.run(main(args)) - colorama.deinit() + Utils.init_logging("KH2Client", exception_logger="Client") + launch() diff --git a/setup.py b/setup.py index 0d2da0bb18..c864a8cc9d 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ non_apworlds: set = { "Clique", "DLCQuest", "Final Fantasy", - "Kingdom Hearts 2", "Lufia II Ancient Cave", "Meritous", "Ocarina of Time", diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 31739bb246..03c89b75ff 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -112,8 +112,6 @@ components: List[Component] = [ # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), - # Kingdom Hearts 2 - Component('KH2 Client', "KH2Client"), #MegaMan Battle Network 3 Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py new file mode 100644 index 0000000000..be85dc6907 --- /dev/null +++ b/worlds/kh2/Client.py @@ -0,0 +1,881 @@ +import ModuleUpdate + +ModuleUpdate.update() + +import os +import asyncio +import json +from pymem import pymem +from . import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, SupportAbility_Table, ActionAbility_Table, all_weapon_slot +from .Names import ItemName +from .WorldLocations import * + +from NetUtils import ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, CommonContext, server_loop + + +class KH2Context(CommonContext): + # command_processor: int = KH2CommandProcessor + game = "Kingdom Hearts 2" + items_handling = 0b111 # Indicates you get items sent from other worlds. + + def __init__(self, server_address, password): + super(KH2Context, self).__init__(server_address, password) + self.goofy_ability_to_slot = dict() + self.donald_ability_to_slot = dict() + self.all_weapon_location_id = None + self.sora_ability_to_slot = dict() + self.kh2_seed_save = None + self.kh2_local_items = None + self.growthlevel = None + self.kh2connected = False + self.serverconneced = False + self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} + self.location_name_to_data = {name: data for name, data, in all_locations.items()} + self.kh2_loc_name_to_id = None + self.kh2_item_name_to_id = None + self.lookup_id_to_item = None + self.lookup_id_to_location = None + self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in + dic.items()} + self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} + + self.sending = [] + # list used to keep track of locations+items player has. Used for disoneccting + self.kh2_seed_save_cache = { + "itemIndex": -1, + # back of soras invo is 0x25E2. Growth should be moved there + # Character: [back of invo, front of invo] + "SoraInvo": [0x25D8, 0x2546], + "DonaldInvo": [0x26F4, 0x2658], + "GoofyInvo": [0x2808, 0x276C], + "AmountInvo": { + "Ability": {}, + "Amount": { + "Bounty": 0, + }, + "Growth": { + "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0 + }, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": { + "Fire Element": 0, + "Blizzard Element": 0, + "Thunder Element": 0, + "Cure Element": 0, + "Magnet Element": 0, + "Reflect Element": 0 + }, + "StatIncrease": { + ItemName.MaxHPUp: 0, + ItemName.MaxMPUp: 0, + ItemName.DriveGaugeUp: 0, + ItemName.ArmorSlotUp: 0, + ItemName.AccessorySlotUp: 0, + ItemName.ItemSlotUp: 0, + }, + }, + } + self.front_of_inventory = { + "Sora": 0x2546, + "Donald": 0x2658, + "Goofy": 0x276C, + } + self.kh2seedname = None + self.kh2slotdata = None + self.itemamount = {} + if "localappdata" in os.environ: + self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP") + self.hitlist_bounties = 0 + # hooked object + self.kh2 = None + self.final_xemnas = False + self.worldid_to_locations = { + # 1: {}, # world of darkness (story cutscenes) + 2: TT_Checks, + # 3: {}, # destiny island doesn't have checks + 4: HB_Checks, + 5: BC_Checks, + 6: Oc_Checks, + 7: AG_Checks, + 8: LoD_Checks, + 9: HundredAcreChecks, + 10: PL_Checks, + 11: Atlantica_Checks, + 12: DC_Checks, + 13: TR_Checks, + 14: HT_Checks, + 15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb + 16: PR_Checks, + 17: SP_Checks, + 18: TWTNW_Checks, + # 255: {}, # starting screen + } + # 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room + # self.sveroom = 0x2A09C00 + 0x41 + # 0 not in battle 1 in yellow battle 2 red battle #short + # self.inBattle = 0x2A0EAC4 + 0x40 + # self.onDeath = 0xAB9078 + # PC Address anchors + self.Now = 0x0714DB8 + self.Save = 0x09A70B0 + # self.Sys3 = 0x2A59DF0 + # self.Bt10 = 0x2A74880 + # self.BtlEnd = 0x2A0D3E0 + self.Slot1 = 0x2A20C98 + + self.chest_set = set(exclusion_table["Chests"]) + self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) + self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"]) + self.shield_set = set(CheckDupingItems["Weapons"]["Shields"]) + + self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set) + + self.equipment_categories = CheckDupingItems["Equipment"] + self.armor_set = set(self.equipment_categories["Armor"]) + self.accessories_set = set(self.equipment_categories["Accessories"]) + self.all_equipment = self.armor_set.union(self.accessories_set) + + self.Equipment_Anchor_Dict = { + "Armor": [0x2504, 0x2506, 0x2508, 0x250A], + "Accessories": [0x2514, 0x2516, 0x2518, 0x251A] + } + + self.AbilityQuantityDict = {} + self.ability_categories = CheckDupingItems["Abilities"] + + self.sora_ability_set = set(self.ability_categories["Sora"]) + self.donald_ability_set = set(self.ability_categories["Donald"]) + self.goofy_ability_set = set(self.ability_categories["Goofy"]) + + self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set) + + self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) + self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} + + # Growth:[level 1,level 4,slot] + self.growth_values_dict = { + "High Jump": [0x05E, 0x061, 0x25DA], + "Quick Run": [0x62, 0x65, 0x25DC], + "Dodge Roll": [0x234, 0x237, 0x25DE], + "Aerial Dodge": [0x66, 0x069, 0x25E0], + "Glide": [0x6A, 0x6D, 0x25E2] + } + + self.ability_code_list = None + self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(KH2Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname is not None and self.auth is not None: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).connection_closed() + + async def disconnect(self, allow_autoreconnect: bool = False): + self.kh2connected = False + self.serverconneced = False + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).disconnect() + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + if self.kh2seedname not in {None} and self.auth not in {None}: + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'w') as f: + f.write(json.dumps(self.kh2_seed_save, indent=4)) + await super(KH2Context, self).shutdown() + + def kh2_read_short(self, address): + return self.kh2.read_short(self.kh2.base_address + address) + + def kh2_write_short(self, address, value): + return self.kh2.write_short(self.kh2.base_address + address, value) + + def kh2_write_byte(self, address, value): + return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1) + + def kh2_read_byte(self, address): + return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + + def on_package(self, cmd: str, args: dict): + if cmd in {"RoomInfo"}: + self.kh2seedname = args['seed_name'] + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"): + self.kh2_seed_save = { + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + "SummonLevel": 0, + }, + "SoldEquipment": [], + } + with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"), + 'wt') as f: + pass + # self.locations_checked = set() + elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"): + with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f: + self.kh2_seed_save = json.load(f) + if self.kh2_seed_save is None: + self.kh2_seed_save = { + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + "SummonLevel": 0, + }, + "SoldEquipment": [], + } + # self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"]) + # self.serverconneced = True + + if cmd in {"Connected"}: + asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}])) + self.kh2slotdata = args['slot_data'] + # self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} + self.locations_checked = set(args["checked_locations"]) + + if cmd in {"ReceivedItems"}: + # 0x2546 + # 0x2658 + # 0x276A + start_index = args["index"] + if start_index == 0: + self.kh2_seed_save_cache = { + "itemIndex": -1, + # back of soras invo is 0x25E2. Growth should be moved there + # Character: [back of invo, front of invo] + "SoraInvo": [0x25D8, 0x2546], + "DonaldInvo": [0x26F4, 0x2658], + "GoofyInvo": [0x2808, 0x276C], + "AmountInvo": { + "Ability": {}, + "Amount": { + "Bounty": 0, + }, + "Growth": { + "High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0 + }, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": { + "Fire Element": 0, + "Blizzard Element": 0, + "Thunder Element": 0, + "Cure Element": 0, + "Magnet Element": 0, + "Reflect Element": 0 + }, + "StatIncrease": { + ItemName.MaxHPUp: 0, + ItemName.MaxMPUp: 0, + ItemName.DriveGaugeUp: 0, + ItemName.ArmorSlotUp: 0, + ItemName.AccessorySlotUp: 0, + ItemName.ItemSlotUp: 0, + }, + }, + } + if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced: + self.kh2_seed_save_cache["itemIndex"] = start_index + for item in args['items']: + asyncio.create_task(self.give_item(item.item, item.location)) + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + new_locations = set(args["checked_locations"]) + self.locations_checked |= new_locations + + if cmd in {"DataPackage"}: + self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"] + self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()} + self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"] + self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()} + self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]] + + if "keyblade_abilities" in self.kh2slotdata.keys(): + sora_ability_dict = self.kh2slotdata["KeybladeAbilities"] + # sora ability to slot + # itemid:[slots that are available for that item] + for k, v in sora_ability_dict.items(): + if v >= 1: + if k not in self.sora_ability_to_slot.keys(): + self.sora_ability_to_slot[k] = [] + for _ in range(sora_ability_dict[k]): + self.sora_ability_to_slot[k].append(self.kh2_seed_save_cache["SoraInvo"][0]) + self.kh2_seed_save_cache["SoraInvo"][0] -= 2 + donald_ability_dict = self.kh2slotdata["StaffAbilities"] + for k, v in donald_ability_dict.items(): + if v >= 1: + if k not in self.donald_ability_to_slot.keys(): + self.donald_ability_to_slot[k] = [] + for _ in range(donald_ability_dict[k]): + self.donald_ability_to_slot[k].append(self.kh2_seed_save_cache["DonaldInvo"][0]) + self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 + goofy_ability_dict = self.kh2slotdata["ShieldAbilities"] + for k, v in goofy_ability_dict.items(): + if v >= 1: + if k not in self.goofy_ability_to_slot.keys(): + self.goofy_ability_to_slot[k] = [] + for _ in range(goofy_ability_dict[k]): + self.goofy_ability_to_slot[k].append(self.kh2_seed_save_cache["GoofyInvo"][0]) + self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + + all_weapon_location_id = [] + for weapon_location in all_weapon_slot: + all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location]) + self.all_weapon_location_id = set(all_weapon_location_id) + try: + self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + logger.info("You are now auto-tracking") + self.kh2connected = True + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info("Game is not open.") + self.serverconneced = True + asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}])) + + async def checkWorldLocations(self): + try: + currentworldint = self.kh2_read_byte(self.Now) + await self.send_msgs([{ + "cmd": "Set", "key": "Slot: " + str(self.slot) + " :CurrentWorld", + "default": 0, "want_reply": True, "operations": [{ + "operation": "replace", + "value": currentworldint + }] + }]) + if currentworldint in self.worldid_to_locations: + curworldid = self.worldid_to_locations[currentworldint] + for location, data in curworldid.items(): + if location in self.kh2_loc_name_to_id.keys(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 425") + + async def checkLevels(self): + try: + for location, data in SoraLevels.items(): + currentLevel = self.kh2_read_byte(self.Save + 0x24FF) + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and currentLevel >= data.bitIndex: + if self.kh2_seed_save["Levels"]["SoraLevel"] < currentLevel: + self.kh2_seed_save["Levels"]["SoraLevel"] = currentLevel + self.sending = self.sending + [(int(locationId))] + formDict = { + 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], + 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels], 5: ["SummonLevel", SummonLevels] + } + # TODO: remove formDict[i][0] in self.kh2_seed_save_cache["Levels"].keys() after 4.3 + for i in range(6): + for location, data in formDict[i][1].items(): + formlevel = self.kh2_read_byte(self.Save + data.addrObtained) + if location in self.kh2_loc_name_to_id.keys(): + # if current form level is above other form level + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ + and formlevel >= data.bitIndex: + if formlevel > self.kh2_seed_save["Levels"][formDict[i][0]]: + self.kh2_seed_save["Levels"][formDict[i][0]] = formlevel + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 456") + + async def checkSlots(self): + try: + for location, data in weaponSlots.items(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked: + if self.kh2_read_byte(self.Save + data.addrObtained) > 0: + self.sending = self.sending + [(int(locationId))] + + for location, data in formSlots.items(): + locationId = self.kh2_loc_name_to_id[location] + if locationId not in self.locations_checked and self.kh2_read_byte(self.Save + 0x06B2) == 0: + if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + self.sending = self.sending + [(int(locationId))] + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 475") + + async def verifyChests(self): + try: + for location in self.locations_checked: + locationName = self.lookup_id_to_location[location] + if locationName in self.chest_set: + if locationName in self.location_name_to_worlddata.keys(): + locationData = self.location_name_to_worlddata[locationName] + if self.kh2_read_byte(self.Save + locationData.addrObtained) & 0x1 << locationData.bitIndex == 0: + roomData = self.kh2_read_byte(self.Save + locationData.addrObtained) + self.kh2_write_byte(self.Save + locationData.addrObtained, roomData | 0x01 << locationData.bitIndex) + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 491") + + async def verifyLevel(self): + for leveltype, anchor in { + "SoraLevel": 0x24FF, + "ValorLevel": 0x32F6, + "WisdomLevel": 0x332E, + "LimitLevel": 0x3366, + "MasterLevel": 0x339E, + "FinalLevel": 0x33D6 + }.items(): + if self.kh2_read_byte(self.Save + anchor) < self.kh2_seed_save["Levels"][leveltype]: + self.kh2_write_byte(self.Save + anchor, self.kh2_seed_save["Levels"][leveltype]) + + async def give_item(self, item, location): + try: + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + itemname = self.lookup_id_to_item[item] + itemdata = self.item_name_to_data[itemname] + # itemcode = self.kh2_item_name_to_id[itemname] + if itemdata.ability: + if location in self.all_weapon_location_id: + return + if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: + self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1 + return + + if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] + # appending the slot that the ability should be in + # for non beta. remove after 4.3 + if "PoptrackerVersion" in self.kh2slotdata: + if self.kh2slotdata["PoptrackerVersionCheck"] < 4.3: + if (itemname in self.sora_ability_set + and len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < self.item_name_to_data[itemname].quantity) \ + and self.kh2_seed_save_cache["SoraInvo"][1] > 0x254C: + ability_slot = self.kh2_seed_save_cache["SoraInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["SoraInvo"][1] -= 2 + elif itemname in self.donald_ability_set: + ability_slot = self.kh2_seed_save_cache["DonaldInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["DonaldInvo"][1] -= 2 + else: + ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + + elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ + self.AbilityQuantityDict[itemname]: + if itemname in self.sora_ability_set: + ability_slot = self.kh2_seed_save_cache["SoraInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["SoraInvo"][0] -= 2 + elif itemname in self.donald_ability_set: + ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 + elif itemname in self.goofy_ability_set: + ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] + self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) + self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: + # if memaddr is in a bitmask location in memory + if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: + self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname) + + elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: + # if memaddr is in magic addresses + self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1 + + elif itemname in self.all_equipment: + self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname) + + elif itemname in self.all_weapons: + if itemname in self.keyblade_set: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname) + elif itemname in self.staff_set: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"].append(itemname) + else: + self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname) + + elif itemname in self.stat_increase_set: + self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1 + else: + if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]: + self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1 + else: + self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] = 1 + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 582") + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager + + class KH2Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago KH2 Client" + + self.ui = KH2Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + async def IsInShop(self, sellable): + # journal = 0x741230 shop = 0x741320 + # if journal=-1 and shop = 5 then in shop + # if journal !=-1 and shop = 10 then journal + + journal = self.kh2_read_short(0x741230) + shop = self.kh2_read_short(0x741320) + if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + # print("your in the shop") + sellable_dict = {} + for itemName in sellable: + itemdata = self.item_name_to_data[itemName] + amount = self.kh2_read_byte(self.Save + itemdata.memaddr) + sellable_dict[itemName] = amount + while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): + journal = self.kh2_read_short(0x741230) + shop = self.kh2_read_short(0x741320) + await asyncio.sleep(0.5) + for item, amount in sellable_dict.items(): + itemdata = self.item_name_to_data[item] + afterShop = self.kh2_read_byte(self.Save + itemdata.memaddr) + if afterShop < amount: + self.kh2_seed_save["SoldEquipment"].append(item) + + async def verifyItems(self): + try: + master_amount = set(self.kh2_seed_save_cache["AmountInvo"]["Amount"].keys()) + + master_ability = set(self.kh2_seed_save_cache["AmountInvo"]["Ability"].keys()) + + master_bitmask = set(self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]) + + master_keyblade = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"]) + master_staff = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Donald"]) + master_shield = set(self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"]) + + master_equipment = set(self.kh2_seed_save_cache["AmountInvo"]["Equipment"]) + + master_magic = set(self.kh2_seed_save_cache["AmountInvo"]["Magic"].keys()) + + master_stat = set(self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"].keys()) + + master_sell = master_equipment | master_staff | master_shield + + await asyncio.create_task(self.IsInShop(master_sell)) + + for item_name in master_amount: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Amount"][item_name] + + if item_name == "Torn Page": + # Torn Pages are handled differently because they can be consumed. + # Will check the progression in 100 acre and - the amount of visits + # amountofitems-amount of visits done + for location, data in tornPageLocks.items(): + if self.kh2_read_byte(self.Save + data.addrObtained) & 0x1 << data.bitIndex > 0: + amount_of_items -= 1 + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and amount_of_items >= 0: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + + for item_name in master_keyblade: + item_data = self.item_name_to_data[item_name] + # if the inventory slot for that keyblade is less than the amount they should have, + # and they are not in stt + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 and self.kh2_read_byte(self.Save + 0x1CFF) != 13: + # Checking form anchors for the keyblade to remove extra keyblades + if self.kh2_read_short(self.Save + 0x24F0) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x32F4) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x339C) == item_data.kh2id \ + or self.kh2_read_short(self.Save + 0x33D4) == item_data.kh2id: + self.kh2_write_byte(self.Save + item_data.memaddr, 0) + else: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_staff: + item_data = self.item_name_to_data[item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \ + and self.kh2_read_short(self.Save + 0x2604) != item_data.kh2id \ + and item_name not in self.kh2_seed_save["SoldEquipment"]: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_shield: + item_data = self.item_name_to_data[item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1 \ + and self.kh2_read_short(self.Save + 0x2718) != item_data.kh2id \ + and item_name not in self.kh2_seed_save["SoldEquipment"]: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_ability: + item_data = self.item_name_to_data[item_name] + ability_slot = [] + ability_slot += self.kh2_seed_save_cache["AmountInvo"]["Ability"][item_name] + for slot in ability_slot: + current = self.kh2_read_short(self.Save + slot) + ability = current & 0x0FFF + if ability | 0x8000 != (0x8000 + item_data.memaddr): + if current - 0x8000 > 0: + self.kh2_write_short(self.Save + slot, 0x8000 + item_data.memaddr) + else: + self.kh2_write_short(self.Save + slot, item_data.memaddr) + # removes the duped ability if client gave faster than the game. + + for charInvo in {"Sora", "Donald", "Goofy"}: + if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: + print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") + self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + + # remove the dummy level 1 growths if they are in these invo slots. + for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: + current = self.kh2_read_short(self.Save + inventorySlot) + ability = current & 0x0FFF + if 0x05E <= ability <= 0x06D: + self.kh2_write_short(self.Save + inventorySlot, 0) + + for item_name in self.master_growth: + growthLevel = self.kh2_seed_save_cache["AmountInvo"]["Growth"][item_name] + if growthLevel > 0: + slot = self.growth_values_dict[item_name][2] + min_growth = self.growth_values_dict[item_name][0] + max_growth = self.growth_values_dict[item_name][1] + if growthLevel > 4: + growthLevel = 4 + current_growth_level = self.kh2_read_short(self.Save + slot) + ability = current_growth_level & 0x0FFF + + # if the player should be getting a growth ability + if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel: + # if it should be level one of that growth + if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth: + self.kh2_write_short(self.Save + slot, min_growth) + # if it is already in the inventory + elif ability | 0x8000 < (0x8000 + max_growth): + self.kh2_write_short(self.Save + slot, current_growth_level + 1) + + for item_name in master_bitmask: + item_data = self.item_name_to_data[item_name] + itemMemory = self.kh2_read_byte(self.Save + item_data.memaddr) + if self.kh2_read_byte(self.Save + item_data.memaddr) & 0x1 << item_data.bitmask == 0: + # when getting a form anti points should be reset to 0 but bit-shift doesn't trigger the game. + if item_name in {"Valor Form", "Wisdom Form", "Limit Form", "Master Form", "Final Form"}: + self.kh2_write_byte(self.Save + 0x3410, 0) + self.kh2_write_byte(self.Save + item_data.memaddr, itemMemory | 0x01 << item_data.bitmask) + + for item_name in master_equipment: + item_data = self.item_name_to_data[item_name] + is_there = False + if item_name in self.accessories_set: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"] + else: + Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"] + # Checking form anchors for the equipment + for slot in Equipment_Anchor_List: + if self.kh2_read_short(self.Save + slot) == item_data.kh2id: + is_there = True + if self.kh2_read_byte(self.Save + item_data.memaddr) != 0: + self.kh2_write_byte(self.Save + item_data.memaddr, 0) + break + if not is_there and item_name not in self.kh2_seed_save["SoldEquipment"]: + if self.kh2_read_byte(self.Save + item_data.memaddr) != 1: + self.kh2_write_byte(self.Save + item_data.memaddr, 1) + + for item_name in master_magic: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + + for item_name in master_stat: + item_data = self.item_name_to_data[item_name] + amount_of_items = 0 + amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + + # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) + if "PoptrackerVersionCheck" in self.kh2slotdata: + if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 + self.kh2_write_byte(self.Save + 0x3607, 1) + + except Exception as e: + if self.kh2connected: + self.kh2connected = False + logger.info(e) + logger.info("line 840") + + +def finishedGame(ctx: KH2Context, message): + if ctx.kh2slotdata['FinalXemnas'] == 1: + if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked: + ctx.final_xemnas = True + # three proofs + if ctx.kh2slotdata['Goal'] == 0: + if ctx.kh2_read_byte(ctx.Save + 0x36B2) > 0 \ + and ctx.kh2_read_byte(ctx.Save + 0x36B3) > 0 \ + and ctx.kh2_read_byte(ctx.Save + 0x36B4) > 0: + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata['Goal'] == 1: + if ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata['Goal'] == 2: + # for backwards compat + if "hitlist" in ctx.kh2slotdata: + for boss in ctx.kh2slotdata["hitlist"]: + if boss in message[0]["locations"]: + ctx.hitlist_bounties += 1 + if ctx.hitlist_bounties >= ctx.kh2slotdata["BountyRequired"] or ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"]: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + elif ctx.kh2slotdata["Goal"] == 3: + if ctx.kh2_seed_save_cache["AmountInvo"]["Amount"]["Bounty"] >= ctx.kh2slotdata["BountyRequired"] and \ + ctx.kh2_read_byte(ctx.Save + 0x3641) >= ctx.kh2slotdata['LuckyEmblemsRequired']: + if ctx.kh2_read_byte(ctx.Save + 0x36B3) < 1: + ctx.kh2_write_byte(ctx.Save + 0x36B2, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B3, 1) + ctx.kh2_write_byte(ctx.Save + 0x36B4, 1) + logger.info("The Final Door is now Open") + if ctx.kh2slotdata['FinalXemnas'] == 1: + if ctx.final_xemnas: + return True + return False + return True + return False + + +async def kh2_watcher(ctx: KH2Context): + while not ctx.exit_event.is_set(): + try: + if ctx.kh2connected and ctx.serverconneced: + ctx.sending = [] + await asyncio.create_task(ctx.checkWorldLocations()) + await asyncio.create_task(ctx.checkLevels()) + await asyncio.create_task(ctx.checkSlots()) + await asyncio.create_task(ctx.verifyChests()) + await asyncio.create_task(ctx.verifyItems()) + await asyncio.create_task(ctx.verifyLevel()) + message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] + if finishedGame(ctx, message): + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + await ctx.send_msgs(message) + elif not ctx.kh2connected and ctx.serverconneced: + logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") + ctx.kh2 = None + while not ctx.kh2connected and ctx.serverconneced: + await asyncio.sleep(15) + ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + if ctx.kh2 is not None: + logger.info("You are now auto-tracking") + ctx.kh2connected = True + except Exception as e: + if ctx.kh2connected: + ctx.kh2connected = False + logger.info(e) + logger.info("line 940") + await asyncio.sleep(0.5) + + +def launch(): + async def main(args): + ctx = KH2Context(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + kh2_watcher(ctx), name="KH2ProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + import colorama + + parser = get_base_parser(description="KH2 Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/worlds/kh2/Items.py b/worlds/kh2/Items.py index aa0e326c3d..3e656b418b 100644 --- a/worlds/kh2/Items.py +++ b/worlds/kh2/Items.py @@ -9,7 +9,6 @@ class KH2Item(Item): class ItemData(typing.NamedTuple): - code: typing.Optional[int] quantity: int = 0 kh2id: int = 0 # Save+ mem addr @@ -20,336 +19,421 @@ class ItemData(typing.NamedTuple): ability: bool = False +# 0x130000 Reports_Table = { - ItemName.SecretAnsemsReport1: ItemData(0x130000, 1, 226, 0x36C4, 6), - ItemName.SecretAnsemsReport2: ItemData(0x130001, 1, 227, 0x36C4, 7), - ItemName.SecretAnsemsReport3: ItemData(0x130002, 1, 228, 0x36C5, 0), - ItemName.SecretAnsemsReport4: ItemData(0x130003, 1, 229, 0x36C5, 1), - ItemName.SecretAnsemsReport5: ItemData(0x130004, 1, 230, 0x36C5, 2), - ItemName.SecretAnsemsReport6: ItemData(0x130005, 1, 231, 0x36C5, 3), - ItemName.SecretAnsemsReport7: ItemData(0x130006, 1, 232, 0x36C5, 4), - ItemName.SecretAnsemsReport8: ItemData(0x130007, 1, 233, 0x36C5, 5), - ItemName.SecretAnsemsReport9: ItemData(0x130008, 1, 234, 0x36C5, 6), - ItemName.SecretAnsemsReport10: ItemData(0x130009, 1, 235, 0x36C5, 7), - ItemName.SecretAnsemsReport11: ItemData(0x13000A, 1, 236, 0x36C6, 0), - ItemName.SecretAnsemsReport12: ItemData(0x13000B, 1, 237, 0x36C6, 1), - ItemName.SecretAnsemsReport13: ItemData(0x13000C, 1, 238, 0x36C6, 2), + ItemName.SecretAnsemsReport1: ItemData(1, 226, 0x36C4, 6), + ItemName.SecretAnsemsReport2: ItemData(1, 227, 0x36C4, 7), + ItemName.SecretAnsemsReport3: ItemData(1, 228, 0x36C5, 0), + ItemName.SecretAnsemsReport4: ItemData(1, 229, 0x36C5, 1), + ItemName.SecretAnsemsReport5: ItemData(1, 230, 0x36C5, 2), + ItemName.SecretAnsemsReport6: ItemData(1, 231, 0x36C5, 3), + ItemName.SecretAnsemsReport7: ItemData(1, 232, 0x36C5, 4), + ItemName.SecretAnsemsReport8: ItemData(1, 233, 0x36C5, 5), + ItemName.SecretAnsemsReport9: ItemData(1, 234, 0x36C5, 6), + ItemName.SecretAnsemsReport10: ItemData(1, 235, 0x36C5, 7), + ItemName.SecretAnsemsReport11: ItemData(1, 236, 0x36C6, 0), + ItemName.SecretAnsemsReport12: ItemData(1, 237, 0x36C6, 1), + ItemName.SecretAnsemsReport13: ItemData(1, 238, 0x36C6, 2), } Progression_Table = { - ItemName.ProofofConnection: ItemData(0x13000D, 1, 593, 0x36B2), - ItemName.ProofofNonexistence: ItemData(0x13000E, 1, 594, 0x36B3), - ItemName.ProofofPeace: ItemData(0x13000F, 1, 595, 0x36B4), - ItemName.PromiseCharm: ItemData(0x130010, 1, 524, 0x3694), - ItemName.NamineSketches: ItemData(0x130011, 1, 368, 0x3642), - ItemName.CastleKey: ItemData(0x130012, 2, 460, 0x365D), # dummy 13 - ItemName.BattlefieldsofWar: ItemData(0x130013, 2, 54, 0x35AE), - ItemName.SwordoftheAncestor: ItemData(0x130014, 2, 55, 0x35AF), - ItemName.BeastsClaw: ItemData(0x130015, 2, 59, 0x35B3), - ItemName.BoneFist: ItemData(0x130016, 2, 60, 0x35B4), - ItemName.ProudFang: ItemData(0x130017, 2, 61, 0x35B5), - ItemName.SkillandCrossbones: ItemData(0x130018, 2, 62, 0x35B6), - ItemName.Scimitar: ItemData(0x130019, 2, 72, 0x35C0), - ItemName.MembershipCard: ItemData(0x13001A, 2, 369, 0x3643), - ItemName.IceCream: ItemData(0x13001B, 3, 375, 0x3649), + ItemName.ProofofConnection: ItemData(1, 593, 0x36B2), + ItemName.ProofofNonexistence: ItemData(1, 594, 0x36B3), + ItemName.ProofofPeace: ItemData(1, 595, 0x36B4), + ItemName.PromiseCharm: ItemData(1, 524, 0x3694), + ItemName.NamineSketches: ItemData(1, 368, 0x3642), + ItemName.CastleKey: ItemData(2, 460, 0x365D), # dummy 13 + ItemName.BattlefieldsofWar: ItemData(2, 54, 0x35AE), + ItemName.SwordoftheAncestor: ItemData(2, 55, 0x35AF), + ItemName.BeastsClaw: ItemData(2, 59, 0x35B3), + ItemName.BoneFist: ItemData(2, 60, 0x35B4), + ItemName.ProudFang: ItemData(2, 61, 0x35B5), + ItemName.SkillandCrossbones: ItemData(2, 62, 0x35B6), + ItemName.Scimitar: ItemData(2, 72, 0x35C0), + ItemName.MembershipCard: ItemData(2, 369, 0x3643), + ItemName.IceCream: ItemData(3, 375, 0x3649), # Changed to 3 instead of one poster, picture and ice cream respectively - ItemName.WaytotheDawn: ItemData(0x13001C, 1, 73, 0x35C1), + ItemName.WaytotheDawn: ItemData(2, 73, 0x35C1), # currently first visit locking doesn't work for twtnw.When goa is updated should be 2 - ItemName.IdentityDisk: ItemData(0x13001D, 2, 74, 0x35C2), - ItemName.TornPages: ItemData(0x13001E, 5, 32, 0x3598), + ItemName.IdentityDisk: ItemData(2, 74, 0x35C2), + ItemName.TornPages: ItemData(5, 32, 0x3598), } Forms_Table = { - ItemName.ValorForm: ItemData(0x13001F, 1, 26, 0x36C0, 1), - ItemName.WisdomForm: ItemData(0x130020, 1, 27, 0x36C0, 2), - ItemName.LimitForm: ItemData(0x130021, 1, 563, 0x36CA, 3), - ItemName.MasterForm: ItemData(0x130022, 1, 31, 0x36C0, 6), - ItemName.FinalForm: ItemData(0x130023, 1, 29, 0x36C0, 4), + ItemName.ValorForm: ItemData(1, 26, 0x36C0, 1), + ItemName.WisdomForm: ItemData(1, 27, 0x36C0, 2), + ItemName.LimitForm: ItemData(1, 563, 0x36CA, 3), + ItemName.MasterForm: ItemData(1, 31, 0x36C0, 6), + ItemName.FinalForm: ItemData(1, 29, 0x36C0, 4), + ItemName.AntiForm: ItemData(1, 30, 0x36C0, 5) } Magic_Table = { - ItemName.FireElement: ItemData(0x130024, 3, 21, 0x3594), - ItemName.BlizzardElement: ItemData(0x130025, 3, 22, 0x3595), - ItemName.ThunderElement: ItemData(0x130026, 3, 23, 0x3596), - ItemName.CureElement: ItemData(0x130027, 3, 24, 0x3597), - ItemName.MagnetElement: ItemData(0x130028, 3, 87, 0x35CF), - ItemName.ReflectElement: ItemData(0x130029, 3, 88, 0x35D0), - ItemName.Genie: ItemData(0x13002A, 1, 159, 0x36C4, 4), - ItemName.PeterPan: ItemData(0x13002B, 1, 160, 0x36C4, 5), - ItemName.Stitch: ItemData(0x13002C, 1, 25, 0x36C0, 0), - ItemName.ChickenLittle: ItemData(0x13002D, 1, 383, 0x36C0, 3), + ItemName.FireElement: ItemData(3, 21, 0x3594), + ItemName.BlizzardElement: ItemData(3, 22, 0x3595), + ItemName.ThunderElement: ItemData(3, 23, 0x3596), + ItemName.CureElement: ItemData(3, 24, 0x3597), + ItemName.MagnetElement: ItemData(3, 87, 0x35CF), + ItemName.ReflectElement: ItemData(3, 88, 0x35D0), +} +Summon_Table = { + ItemName.Genie: ItemData(1, 159, 0x36C4, 4), + ItemName.PeterPan: ItemData(1, 160, 0x36C4, 5), + ItemName.Stitch: ItemData(1, 25, 0x36C0, 0), + ItemName.ChickenLittle: ItemData(1, 383, 0x36C0, 3), } - Movement_Table = { - ItemName.HighJump: ItemData(0x13002E, 4, 94, 0x05E, 0, True), - ItemName.QuickRun: ItemData(0x13002F, 4, 98, 0x062, 0, True), - ItemName.DodgeRoll: ItemData(0x130030, 4, 564, 0x234, 0, True), - ItemName.AerialDodge: ItemData(0x130031, 4, 102, 0x066, 0, True), - ItemName.Glide: ItemData(0x130032, 4, 106, 0x06A, 0, True), + ItemName.HighJump: ItemData(4, 94, 0x05E, ability=True), + ItemName.QuickRun: ItemData(4, 98, 0x062, ability=True), + ItemName.DodgeRoll: ItemData(4, 564, 0x234, ability=True), + ItemName.AerialDodge: ItemData(4, 102, 0x066, ability=True), + ItemName.Glide: ItemData(4, 106, 0x06A, ability=True), } Keyblade_Table = { - ItemName.Oathkeeper: ItemData(0x130033, 1, 42, 0x35A2), - ItemName.Oblivion: ItemData(0x130034, 1, 43, 0x35A3), - ItemName.StarSeeker: ItemData(0x130035, 1, 480, 0x367B), - ItemName.HiddenDragon: ItemData(0x130036, 1, 481, 0x367C), - ItemName.HerosCrest: ItemData(0x130037, 1, 484, 0x367F), - ItemName.Monochrome: ItemData(0x130038, 1, 485, 0x3680), - ItemName.FollowtheWind: ItemData(0x130039, 1, 486, 0x3681), - ItemName.CircleofLife: ItemData(0x13003A, 1, 487, 0x3682), - ItemName.PhotonDebugger: ItemData(0x13003B, 1, 488, 0x3683), - ItemName.GullWing: ItemData(0x13003C, 1, 489, 0x3684), - ItemName.RumblingRose: ItemData(0x13003D, 1, 490, 0x3685), - ItemName.GuardianSoul: ItemData(0x13003E, 1, 491, 0x3686), - ItemName.WishingLamp: ItemData(0x13003F, 1, 492, 0x3687), - ItemName.DecisivePumpkin: ItemData(0x130040, 1, 493, 0x3688), - ItemName.SleepingLion: ItemData(0x130041, 1, 494, 0x3689), - ItemName.SweetMemories: ItemData(0x130042, 1, 495, 0x368A), - ItemName.MysteriousAbyss: ItemData(0x130043, 1, 496, 0x368B), - ItemName.TwoBecomeOne: ItemData(0x130044, 1, 543, 0x3698), - ItemName.FatalCrest: ItemData(0x130045, 1, 497, 0x368C), - ItemName.BondofFlame: ItemData(0x130046, 1, 498, 0x368D), - ItemName.Fenrir: ItemData(0x130047, 1, 499, 0x368E), - ItemName.UltimaWeapon: ItemData(0x130048, 1, 500, 0x368F), - ItemName.WinnersProof: ItemData(0x130049, 1, 544, 0x3699), - ItemName.Pureblood: ItemData(0x13004A, 1, 71, 0x35BF), + ItemName.Oathkeeper: ItemData(1, 42, 0x35A2), + ItemName.Oblivion: ItemData(1, 43, 0x35A3), + ItemName.StarSeeker: ItemData(1, 480, 0x367B), + ItemName.HiddenDragon: ItemData(1, 481, 0x367C), + ItemName.HerosCrest: ItemData(1, 484, 0x367F), + ItemName.Monochrome: ItemData(1, 485, 0x3680), + ItemName.FollowtheWind: ItemData(1, 486, 0x3681), + ItemName.CircleofLife: ItemData(1, 487, 0x3682), + ItemName.PhotonDebugger: ItemData(1, 488, 0x3683), + ItemName.GullWing: ItemData(1, 489, 0x3684), + ItemName.RumblingRose: ItemData(1, 490, 0x3685), + ItemName.GuardianSoul: ItemData(1, 491, 0x3686), + ItemName.WishingLamp: ItemData(1, 492, 0x3687), + ItemName.DecisivePumpkin: ItemData(1, 493, 0x3688), + ItemName.SleepingLion: ItemData(1, 494, 0x3689), + ItemName.SweetMemories: ItemData(1, 495, 0x368A), + ItemName.MysteriousAbyss: ItemData(1, 496, 0x368B), + ItemName.TwoBecomeOne: ItemData(1, 543, 0x3698), + ItemName.FatalCrest: ItemData(1, 497, 0x368C), + ItemName.BondofFlame: ItemData(1, 498, 0x368D), + ItemName.Fenrir: ItemData(1, 499, 0x368E), + ItemName.UltimaWeapon: ItemData(1, 500, 0x368F), + ItemName.WinnersProof: ItemData(1, 544, 0x3699), + ItemName.Pureblood: ItemData(1, 71, 0x35BF), } Staffs_Table = { - ItemName.Centurion2: ItemData(0x13004B, 1, 546, 0x369B), - ItemName.MeteorStaff: ItemData(0x13004C, 1, 150, 0x35F1), - ItemName.NobodyLance: ItemData(0x13004D, 1, 155, 0x35F6), - ItemName.PreciousMushroom: ItemData(0x13004E, 1, 549, 0x369E), - ItemName.PreciousMushroom2: ItemData(0x13004F, 1, 550, 0x369F), - ItemName.PremiumMushroom: ItemData(0x130050, 1, 551, 0x36A0), - ItemName.RisingDragon: ItemData(0x130051, 1, 154, 0x35F5), - ItemName.SaveTheQueen2: ItemData(0x130052, 1, 503, 0x3692), - ItemName.ShamansRelic: ItemData(0x130053, 1, 156, 0x35F7), + ItemName.Centurion2: ItemData(1, 546, 0x369B), + ItemName.MeteorStaff: ItemData(1, 150, 0x35F1), + ItemName.NobodyLance: ItemData(1, 155, 0x35F6), + ItemName.PreciousMushroom: ItemData(1, 549, 0x369E), + ItemName.PreciousMushroom2: ItemData(1, 550, 0x369F), + ItemName.PremiumMushroom: ItemData(1, 551, 0x36A0), + ItemName.RisingDragon: ItemData(1, 154, 0x35F5), + ItemName.SaveTheQueen2: ItemData(1, 503, 0x3692), + ItemName.ShamansRelic: ItemData(1, 156, 0x35F7), } Shields_Table = { - ItemName.AkashicRecord: ItemData(0x130054, 1, 146, 0x35ED), - ItemName.FrozenPride2: ItemData(0x130055, 1, 553, 0x36A2), - ItemName.GenjiShield: ItemData(0x130056, 1, 145, 0x35EC), - ItemName.MajesticMushroom: ItemData(0x130057, 1, 556, 0x36A5), - ItemName.MajesticMushroom2: ItemData(0x130058, 1, 557, 0x36A6), - ItemName.NobodyGuard: ItemData(0x130059, 1, 147, 0x35EE), - ItemName.OgreShield: ItemData(0x13005A, 1, 141, 0x35E8), - ItemName.SaveTheKing2: ItemData(0x13005B, 1, 504, 0x3693), - ItemName.UltimateMushroom: ItemData(0x13005C, 1, 558, 0x36A7), + ItemName.AkashicRecord: ItemData(1, 146, 0x35ED), + ItemName.FrozenPride2: ItemData(1, 553, 0x36A2), + ItemName.GenjiShield: ItemData(1, 145, 0x35EC), + ItemName.MajesticMushroom: ItemData(1, 556, 0x36A5), + ItemName.MajesticMushroom2: ItemData(1, 557, 0x36A6), + ItemName.NobodyGuard: ItemData(1, 147, 0x35EE), + ItemName.OgreShield: ItemData(1, 141, 0x35E8), + ItemName.SaveTheKing2: ItemData(1, 504, 0x3693), + ItemName.UltimateMushroom: ItemData(1, 558, 0x36A7), } Accessory_Table = { - ItemName.AbilityRing: ItemData(0x13005D, 1, 8, 0x3587), - ItemName.EngineersRing: ItemData(0x13005E, 1, 9, 0x3588), - ItemName.TechniciansRing: ItemData(0x13005F, 1, 10, 0x3589), - ItemName.SkillRing: ItemData(0x130060, 1, 38, 0x359F), - ItemName.SkillfulRing: ItemData(0x130061, 1, 39, 0x35A0), - ItemName.ExpertsRing: ItemData(0x130062, 1, 11, 0x358A), - ItemName.MastersRing: ItemData(0x130063, 1, 34, 0x359B), - ItemName.CosmicRing: ItemData(0x130064, 1, 52, 0x35AD), - ItemName.ExecutivesRing: ItemData(0x130065, 1, 599, 0x36B5), - ItemName.SardonyxRing: ItemData(0x130066, 1, 12, 0x358B), - ItemName.TourmalineRing: ItemData(0x130067, 1, 13, 0x358C), - ItemName.AquamarineRing: ItemData(0x130068, 1, 14, 0x358D), - ItemName.GarnetRing: ItemData(0x130069, 1, 15, 0x358E), - ItemName.DiamondRing: ItemData(0x13006A, 1, 16, 0x358F), - ItemName.SilverRing: ItemData(0x13006B, 1, 17, 0x3590), - ItemName.GoldRing: ItemData(0x13006C, 1, 18, 0x3591), - ItemName.PlatinumRing: ItemData(0x13006D, 1, 19, 0x3592), - ItemName.MythrilRing: ItemData(0x13006E, 1, 20, 0x3593), - ItemName.OrichalcumRing: ItemData(0x13006F, 1, 28, 0x359A), - ItemName.SoldierEarring: ItemData(0x130070, 1, 40, 0x35A6), - ItemName.FencerEarring: ItemData(0x130071, 1, 46, 0x35A7), - ItemName.MageEarring: ItemData(0x130072, 1, 47, 0x35A8), - ItemName.SlayerEarring: ItemData(0x130073, 1, 48, 0x35AC), - ItemName.Medal: ItemData(0x130074, 1, 53, 0x35B0), - ItemName.MoonAmulet: ItemData(0x130075, 1, 35, 0x359C), - ItemName.StarCharm: ItemData(0x130076, 1, 36, 0x359E), - ItemName.CosmicArts: ItemData(0x130077, 1, 56, 0x35B1), - ItemName.ShadowArchive: ItemData(0x130078, 1, 57, 0x35B2), - ItemName.ShadowArchive2: ItemData(0x130079, 1, 58, 0x35B7), - ItemName.FullBloom: ItemData(0x13007A, 1, 64, 0x35B9), - ItemName.FullBloom2: ItemData(0x13007B, 1, 66, 0x35BB), - ItemName.DrawRing: ItemData(0x13007C, 1, 65, 0x35BA), - ItemName.LuckyRing: ItemData(0x13007D, 1, 63, 0x35B8), + ItemName.AbilityRing: ItemData(1, 8, 0x3587), + ItemName.EngineersRing: ItemData(1, 9, 0x3588), + ItemName.TechniciansRing: ItemData(1, 10, 0x3589), + ItemName.SkillRing: ItemData(1, 38, 0x359F), + ItemName.SkillfulRing: ItemData(1, 39, 0x35A0), + ItemName.ExpertsRing: ItemData(1, 11, 0x358A), + ItemName.MastersRing: ItemData(1, 34, 0x359B), + ItemName.CosmicRing: ItemData(1, 52, 0x35AD), + ItemName.ExecutivesRing: ItemData(1, 599, 0x36B5), + ItemName.SardonyxRing: ItemData(1, 12, 0x358B), + ItemName.TourmalineRing: ItemData(1, 13, 0x358C), + ItemName.AquamarineRing: ItemData(1, 14, 0x358D), + ItemName.GarnetRing: ItemData(1, 15, 0x358E), + ItemName.DiamondRing: ItemData(1, 16, 0x358F), + ItemName.SilverRing: ItemData(1, 17, 0x3590), + ItemName.GoldRing: ItemData(1, 18, 0x3591), + ItemName.PlatinumRing: ItemData(1, 19, 0x3592), + ItemName.MythrilRing: ItemData(1, 20, 0x3593), + ItemName.OrichalcumRing: ItemData(1, 28, 0x359A), + ItemName.SoldierEarring: ItemData(1, 40, 0x35A6), + ItemName.FencerEarring: ItemData(1, 46, 0x35A7), + ItemName.MageEarring: ItemData(1, 47, 0x35A8), + ItemName.SlayerEarring: ItemData(1, 48, 0x35AC), + ItemName.Medal: ItemData(1, 53, 0x35B0), + ItemName.MoonAmulet: ItemData(1, 35, 0x359C), + ItemName.StarCharm: ItemData(1, 36, 0x359E), + ItemName.CosmicArts: ItemData(1, 56, 0x35B1), + ItemName.ShadowArchive: ItemData(1, 57, 0x35B2), + ItemName.ShadowArchive2: ItemData(1, 58, 0x35B7), + ItemName.FullBloom: ItemData(1, 64, 0x35B9), + ItemName.FullBloom2: ItemData(1, 66, 0x35BB), + ItemName.DrawRing: ItemData(1, 65, 0x35BA), + ItemName.LuckyRing: ItemData(1, 63, 0x35B8), } Armor_Table = { - ItemName.ElvenBandana: ItemData(0x13007E, 1, 67, 0x35BC), - ItemName.DivineBandana: ItemData(0x13007F, 1, 68, 0x35BD), - ItemName.ProtectBelt: ItemData(0x130080, 1, 78, 0x35C7), - ItemName.GaiaBelt: ItemData(0x130081, 1, 79, 0x35CA), - ItemName.PowerBand: ItemData(0x130082, 1, 69, 0x35BE), - ItemName.BusterBand: ItemData(0x130083, 1, 70, 0x35C6), - ItemName.CosmicBelt: ItemData(0x130084, 1, 111, 0x35D1), - ItemName.FireBangle: ItemData(0x130085, 1, 173, 0x35D7), - ItemName.FiraBangle: ItemData(0x130086, 1, 174, 0x35D8), - ItemName.FiragaBangle: ItemData(0x130087, 1, 197, 0x35D9), - ItemName.FiragunBangle: ItemData(0x130088, 1, 284, 0x35DA), - ItemName.BlizzardArmlet: ItemData(0x130089, 1, 286, 0x35DC), - ItemName.BlizzaraArmlet: ItemData(0x13008A, 1, 287, 0x35DD), - ItemName.BlizzagaArmlet: ItemData(0x13008B, 1, 288, 0x35DE), - ItemName.BlizzagunArmlet: ItemData(0x13008C, 1, 289, 0x35DF), - ItemName.ThunderTrinket: ItemData(0x13008D, 1, 291, 0x35E2), - ItemName.ThundaraTrinket: ItemData(0x13008E, 1, 292, 0x35E3), - ItemName.ThundagaTrinket: ItemData(0x13008F, 1, 293, 0x35E4), - ItemName.ThundagunTrinket: ItemData(0x130090, 1, 294, 0x35E5), - ItemName.ShockCharm: ItemData(0x130091, 1, 132, 0x35D2), - ItemName.ShockCharm2: ItemData(0x130092, 1, 133, 0x35D3), - ItemName.ShadowAnklet: ItemData(0x130093, 1, 296, 0x35F9), - ItemName.DarkAnklet: ItemData(0x130094, 1, 297, 0x35FB), - ItemName.MidnightAnklet: ItemData(0x130095, 1, 298, 0x35FC), - ItemName.ChaosAnklet: ItemData(0x130096, 1, 299, 0x35FD), - ItemName.ChampionBelt: ItemData(0x130097, 1, 305, 0x3603), - ItemName.AbasChain: ItemData(0x130098, 1, 301, 0x35FF), - ItemName.AegisChain: ItemData(0x130099, 1, 302, 0x3600), - ItemName.Acrisius: ItemData(0x13009A, 1, 303, 0x3601), - ItemName.Acrisius2: ItemData(0x13009B, 1, 307, 0x3605), - ItemName.CosmicChain: ItemData(0x13009C, 1, 308, 0x3606), - ItemName.PetiteRibbon: ItemData(0x13009D, 1, 306, 0x3604), - ItemName.Ribbon: ItemData(0x13009E, 1, 304, 0x3602), - ItemName.GrandRibbon: ItemData(0x13009F, 1, 157, 0x35D4), + ItemName.ElvenBandana: ItemData(1, 67, 0x35BC), + ItemName.DivineBandana: ItemData(1, 68, 0x35BD), + ItemName.ProtectBelt: ItemData(1, 78, 0x35C7), + ItemName.GaiaBelt: ItemData(1, 79, 0x35CA), + ItemName.PowerBand: ItemData(1, 69, 0x35BE), + ItemName.BusterBand: ItemData(1, 70, 0x35C6), + ItemName.CosmicBelt: ItemData(1, 111, 0x35D1), + ItemName.FireBangle: ItemData(1, 173, 0x35D7), + ItemName.FiraBangle: ItemData(1, 174, 0x35D8), + ItemName.FiragaBangle: ItemData(1, 197, 0x35D9), + ItemName.FiragunBangle: ItemData(1, 284, 0x35DA), + ItemName.BlizzardArmlet: ItemData(1, 286, 0x35DC), + ItemName.BlizzaraArmlet: ItemData(1, 287, 0x35DD), + ItemName.BlizzagaArmlet: ItemData(1, 288, 0x35DE), + ItemName.BlizzagunArmlet: ItemData(1, 289, 0x35DF), + ItemName.ThunderTrinket: ItemData(1, 291, 0x35E2), + ItemName.ThundaraTrinket: ItemData(1, 292, 0x35E3), + ItemName.ThundagaTrinket: ItemData(1, 293, 0x35E4), + ItemName.ThundagunTrinket: ItemData(1, 294, 0x35E5), + ItemName.ShockCharm: ItemData(1, 132, 0x35D2), + ItemName.ShockCharm2: ItemData(1, 133, 0x35D3), + ItemName.ShadowAnklet: ItemData(1, 296, 0x35F9), + ItemName.DarkAnklet: ItemData(1, 297, 0x35FB), + ItemName.MidnightAnklet: ItemData(1, 298, 0x35FC), + ItemName.ChaosAnklet: ItemData(1, 299, 0x35FD), + ItemName.ChampionBelt: ItemData(1, 305, 0x3603), + ItemName.AbasChain: ItemData(1, 301, 0x35FF), + ItemName.AegisChain: ItemData(1, 302, 0x3600), + ItemName.Acrisius: ItemData(1, 303, 0x3601), + ItemName.Acrisius2: ItemData(1, 307, 0x3605), + ItemName.CosmicChain: ItemData(1, 308, 0x3606), + ItemName.PetiteRibbon: ItemData(1, 306, 0x3604), + ItemName.Ribbon: ItemData(1, 304, 0x3602), + ItemName.GrandRibbon: ItemData(1, 157, 0x35D4), } Usefull_Table = { - ItemName.MickyMunnyPouch: ItemData(0x1300A0, 3, 535, 0x3695), # 5000 munny per - ItemName.OletteMunnyPouch: ItemData(0x1300A1, 6, 362, 0x363C), # 2500 munny per - ItemName.HadesCupTrophy: ItemData(0x1300A2, 1, 537, 0x3696), - ItemName.UnknownDisk: ItemData(0x1300A3, 1, 462, 0x365F), - ItemName.OlympusStone: ItemData(0x1300A4, 1, 370, 0x3644), - ItemName.MaxHPUp: ItemData(0x1300A5, 20, 470, 0x3671), - ItemName.MaxMPUp: ItemData(0x1300A6, 4, 471, 0x3672), - ItemName.DriveGaugeUp: ItemData(0x1300A7, 6, 472, 0x3673), - ItemName.ArmorSlotUp: ItemData(0x1300A8, 3, 473, 0x3674), - ItemName.AccessorySlotUp: ItemData(0x1300A9, 3, 474, 0x3675), - ItemName.ItemSlotUp: ItemData(0x1300AA, 5, 463, 0x3660), + ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per + ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per + ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696), + ItemName.UnknownDisk: ItemData(1, 462, 0x365F), + ItemName.OlympusStone: ItemData(1, 370, 0x3644), + ItemName.MaxHPUp: ItemData(20, 112, 0x3671), # 470 is DUMMY 23, 112 is Encampment Area Map + ItemName.MaxMPUp: ItemData(4, 113, 0x3672), # 471 is DUMMY 24, 113 is Village Area Map + ItemName.DriveGaugeUp: ItemData(6, 114, 0x3673), # 472 is DUMMY 25, 114 is Cornerstone Hill Map + ItemName.ArmorSlotUp: ItemData(3, 116, 0x3674), # 473 is DUMMY 26, 116 is Lilliput Map + ItemName.AccessorySlotUp: ItemData(3, 117, 0x3675), # 474 is DUMMY 27, 117 is Building Site Map + ItemName.ItemSlotUp: ItemData(5, 118, 0x3660), # 463 is DUMMY 16, 118 is Mickey’s House Map } SupportAbility_Table = { - ItemName.Scan: ItemData(0x1300AB, 2, 138, 0x08A, 0, True), - ItemName.AerialRecovery: ItemData(0x1300AC, 1, 158, 0x09E, 0, True), - ItemName.ComboMaster: ItemData(0x1300AD, 1, 539, 0x21B, 0, True), - ItemName.ComboPlus: ItemData(0x1300AE, 3, 162, 0x0A2, 0, True), - ItemName.AirComboPlus: ItemData(0x1300AF, 3, 163, 0x0A3, 0, True), - ItemName.ComboBoost: ItemData(0x1300B0, 2, 390, 0x186, 0, True), - ItemName.AirComboBoost: ItemData(0x1300B1, 2, 391, 0x187, 0, True), - ItemName.ReactionBoost: ItemData(0x1300B2, 3, 392, 0x188, 0, True), - ItemName.FinishingPlus: ItemData(0x1300B3, 3, 393, 0x189, 0, True), - ItemName.NegativeCombo: ItemData(0x1300B4, 2, 394, 0x18A, 0, True), - ItemName.BerserkCharge: ItemData(0x1300B5, 2, 395, 0x18B, 0, True), - ItemName.DamageDrive: ItemData(0x1300B6, 2, 396, 0x18C, 0, True), - ItemName.DriveBoost: ItemData(0x1300B7, 2, 397, 0x18D, 0, True), - ItemName.FormBoost: ItemData(0x1300B8, 3, 398, 0x18E, 0, True), - ItemName.SummonBoost: ItemData(0x1300B9, 1, 399, 0x18F, 0, True), - ItemName.ExperienceBoost: ItemData(0x1300BA, 2, 401, 0x191, 0, True), - ItemName.Draw: ItemData(0x1300BB, 4, 405, 0x195, 0, True), - ItemName.Jackpot: ItemData(0x1300BC, 2, 406, 0x196, 0, True), - ItemName.LuckyLucky: ItemData(0x1300BD, 3, 407, 0x197, 0, True), - ItemName.DriveConverter: ItemData(0x1300BE, 2, 540, 0x21C, 0, True), - ItemName.FireBoost: ItemData(0x1300BF, 2, 408, 0x198, 0, True), - ItemName.BlizzardBoost: ItemData(0x1300C0, 2, 409, 0x199, 0, True), - ItemName.ThunderBoost: ItemData(0x1300C1, 2, 410, 0x19A, 0, True), - ItemName.ItemBoost: ItemData(0x1300C2, 2, 411, 0x19B, 0, True), - ItemName.MPRage: ItemData(0x1300C3, 2, 412, 0x19C, 0, True), - ItemName.MPHaste: ItemData(0x1300C4, 2, 413, 0x19D, 0, True), - ItemName.MPHastera: ItemData(0x1300C5, 2, 421, 0x1A5, 0, True), - ItemName.MPHastega: ItemData(0x1300C6, 1, 422, 0x1A6, 0, True), - ItemName.Defender: ItemData(0x1300C7, 2, 414, 0x19E, 0, True), - ItemName.DamageControl: ItemData(0x1300C8, 2, 542, 0x21E, 0, True), - ItemName.NoExperience: ItemData(0x1300C9, 1, 404, 0x194, 0, True), - ItemName.LightDarkness: ItemData(0x1300CA, 1, 541, 0x21D, 0, True), - ItemName.MagicLock: ItemData(0x1300CB, 1, 403, 0x193, 0, True), - ItemName.LeafBracer: ItemData(0x1300CC, 1, 402, 0x192, 0, True), - ItemName.CombinationBoost: ItemData(0x1300CD, 1, 400, 0x190, 0, True), - ItemName.OnceMore: ItemData(0x1300CE, 1, 416, 0x1A0, 0, True), - ItemName.SecondChance: ItemData(0x1300CF, 1, 415, 0x19F, 0, True), + ItemName.Scan: ItemData(2, 138, 0x08A, ability=True), + ItemName.AerialRecovery: ItemData(1, 158, 0x09E, ability=True), + ItemName.ComboMaster: ItemData(1, 539, 0x21B, ability=True), + ItemName.ComboPlus: ItemData(3, 162, 0x0A2, ability=True), + ItemName.AirComboPlus: ItemData(3, 163, 0x0A3, ability=True), + ItemName.ComboBoost: ItemData(2, 390, 0x186, ability=True), + ItemName.AirComboBoost: ItemData(2, 391, 0x187, ability=True), + ItemName.ReactionBoost: ItemData(3, 392, 0x188, ability=True), + ItemName.FinishingPlus: ItemData(3, 393, 0x189, ability=True), + ItemName.NegativeCombo: ItemData(2, 394, 0x18A, ability=True), + ItemName.BerserkCharge: ItemData(2, 395, 0x18B, ability=True), + ItemName.DamageDrive: ItemData(2, 396, 0x18C, ability=True), + ItemName.DriveBoost: ItemData(2, 397, 0x18D, ability=True), + ItemName.FormBoost: ItemData(3, 398, 0x18E, ability=True), + ItemName.SummonBoost: ItemData(1, 399, 0x18F, ability=True), + ItemName.ExperienceBoost: ItemData(2, 401, 0x191, ability=True), + ItemName.Draw: ItemData(4, 405, 0x195, ability=True), + ItemName.Jackpot: ItemData(2, 406, 0x196, ability=True), + ItemName.LuckyLucky: ItemData(3, 407, 0x197, ability=True), + ItemName.DriveConverter: ItemData(2, 540, 0x21C, ability=True), + ItemName.FireBoost: ItemData(2, 408, 0x198, ability=True), + ItemName.BlizzardBoost: ItemData(2, 409, 0x199, ability=True), + ItemName.ThunderBoost: ItemData(2, 410, 0x19A, ability=True), + ItemName.ItemBoost: ItemData(2, 411, 0x19B, ability=True), + ItemName.MPRage: ItemData(2, 412, 0x19C, ability=True), + ItemName.MPHaste: ItemData(2, 413, 0x19D, ability=True), + ItemName.MPHastera: ItemData(2, 421, 0x1A5, ability=True), + ItemName.MPHastega: ItemData(1, 422, 0x1A6, ability=True), + ItemName.Defender: ItemData(2, 414, 0x19E, ability=True), + ItemName.DamageControl: ItemData(2, 542, 0x21E, ability=True), + ItemName.NoExperience: ItemData(0, 404, 0x194, ability=True), # quantity changed to 0 because the player starts with one always. + ItemName.LightDarkness: ItemData(1, 541, 0x21D, ability=True), + ItemName.MagicLock: ItemData(1, 403, 0x193, ability=True), + ItemName.LeafBracer: ItemData(1, 402, 0x192, ability=True), + ItemName.CombinationBoost: ItemData(1, 400, 0x190, ability=True), + ItemName.OnceMore: ItemData(1, 416, 0x1A0, ability=True), + ItemName.SecondChance: ItemData(1, 415, 0x19F, ability=True), } ActionAbility_Table = { - ItemName.Guard: ItemData(0x1300D0, 1, 82, 0x052, 0, True), - ItemName.UpperSlash: ItemData(0x1300D1, 1, 137, 0x089, 0, True), - ItemName.HorizontalSlash: ItemData(0x1300D2, 1, 271, 0x10F, 0, True), - ItemName.FinishingLeap: ItemData(0x1300D3, 1, 267, 0x10B, 0, True), - ItemName.RetaliatingSlash: ItemData(0x1300D4, 1, 273, 0x111, 0, True), - ItemName.Slapshot: ItemData(0x1300D5, 1, 262, 0x106, 0, True), - ItemName.DodgeSlash: ItemData(0x1300D6, 1, 263, 0x107, 0, True), - ItemName.FlashStep: ItemData(0x1300D7, 1, 559, 0x22F, 0, True), - ItemName.SlideDash: ItemData(0x1300D8, 1, 264, 0x108, 0, True), - ItemName.VicinityBreak: ItemData(0x1300D9, 1, 562, 0x232, 0, True), - ItemName.GuardBreak: ItemData(0x1300DA, 1, 265, 0x109, 0, True), - ItemName.Explosion: ItemData(0x1300DB, 1, 266, 0x10A, 0, True), - ItemName.AerialSweep: ItemData(0x1300DC, 1, 269, 0x10D, 0, True), - ItemName.AerialDive: ItemData(0x1300DD, 1, 560, 0x230, 0, True), - ItemName.AerialSpiral: ItemData(0x1300DE, 1, 270, 0x10E, 0, True), - ItemName.AerialFinish: ItemData(0x1300DF, 1, 272, 0x110, 0, True), - ItemName.MagnetBurst: ItemData(0x1300E0, 1, 561, 0x231, 0, True), - ItemName.Counterguard: ItemData(0x1300E1, 1, 268, 0x10C, 0, True), - ItemName.AutoValor: ItemData(0x1300E2, 1, 385, 0x181, 0, True), - ItemName.AutoWisdom: ItemData(0x1300E3, 1, 386, 0x182, 0, True), - ItemName.AutoLimit: ItemData(0x1300E4, 1, 568, 0x238, 0, True), - ItemName.AutoMaster: ItemData(0x1300E5, 1, 387, 0x183, 0, True), - ItemName.AutoFinal: ItemData(0x1300E6, 1, 388, 0x184, 0, True), - ItemName.AutoSummon: ItemData(0x1300E7, 1, 389, 0x185, 0, True), - ItemName.TrinityLimit: ItemData(0x1300E8, 1, 198, 0x0C6, 0, True), + ItemName.Guard: ItemData(1, 82, 0x052, ability=True), + ItemName.UpperSlash: ItemData(1, 137, 0x089, ability=True), + ItemName.HorizontalSlash: ItemData(1, 271, 0x10F, ability=True), + ItemName.FinishingLeap: ItemData(1, 267, 0x10B, ability=True), + ItemName.RetaliatingSlash: ItemData(1, 273, 0x111, ability=True), + ItemName.Slapshot: ItemData(1, 262, 0x106, ability=True), + ItemName.DodgeSlash: ItemData(1, 263, 0x107, ability=True), + ItemName.FlashStep: ItemData(1, 559, 0x22F, ability=True), + ItemName.SlideDash: ItemData(1, 264, 0x108, ability=True), + ItemName.VicinityBreak: ItemData(1, 562, 0x232, ability=True), + ItemName.GuardBreak: ItemData(1, 265, 0x109, ability=True), + ItemName.Explosion: ItemData(1, 266, 0x10A, ability=True), + ItemName.AerialSweep: ItemData(1, 269, 0x10D, ability=True), + ItemName.AerialDive: ItemData(1, 560, 0x230, ability=True), + ItemName.AerialSpiral: ItemData(1, 270, 0x10E, ability=True), + ItemName.AerialFinish: ItemData(1, 272, 0x110, ability=True), + ItemName.MagnetBurst: ItemData(1, 561, 0x231, ability=True), + ItemName.Counterguard: ItemData(1, 268, 0x10C, ability=True), + ItemName.AutoValor: ItemData(1, 385, 0x181, ability=True), + ItemName.AutoWisdom: ItemData(1, 386, 0x182, ability=True), + ItemName.AutoLimit: ItemData(1, 568, 0x238, ability=True), + ItemName.AutoMaster: ItemData(1, 387, 0x183, ability=True), + ItemName.AutoFinal: ItemData(1, 388, 0x184, ability=True), + ItemName.AutoSummon: ItemData(1, 389, 0x185, ability=True), + ItemName.TrinityLimit: ItemData(1, 198, 0x0C6, ability=True), } -Items_Table = { - ItemName.PowerBoost: ItemData(0x1300E9, 1, 276, 0x3666), - ItemName.MagicBoost: ItemData(0x1300EA, 1, 277, 0x3667), - ItemName.DefenseBoost: ItemData(0x1300EB, 1, 278, 0x3668), - ItemName.APBoost: ItemData(0x1300EC, 1, 279, 0x3669), +Boosts_Table = { + ItemName.PowerBoost: ItemData(1, 253, 0x359D), # 276, 0x3666, market place map + ItemName.MagicBoost: ItemData(1, 586, 0x35E0), # 277, 0x3667, dark rememberance map + ItemName.DefenseBoost: ItemData(1, 590, 0x35F8), # 278, 0x3668, depths of remembrance map + ItemName.APBoost: ItemData(1, 532, 0x35FE), # 279, 0x3669, mansion map } # These items cannot be in other games so these are done locally in kh2 DonaldAbility_Table = { - ItemName.DonaldFire: ItemData(0x1300ED, 1, 165, 0xA5, 0, True), - ItemName.DonaldBlizzard: ItemData(0x1300EE, 1, 166, 0xA6, 0, True), - ItemName.DonaldThunder: ItemData(0x1300EF, 1, 167, 0xA7, 0, True), - ItemName.DonaldCure: ItemData(0x1300F0, 1, 168, 0xA8, 0, True), - ItemName.Fantasia: ItemData(0x1300F1, 1, 199, 0xC7, 0, True), - ItemName.FlareForce: ItemData(0x1300F2, 1, 200, 0xC8, 0, True), - ItemName.DonaldMPRage: ItemData(0x1300F3, 3, 412, 0x19C, 0, True), - ItemName.DonaldJackpot: ItemData(0x1300F4, 1, 406, 0x196, 0, True), - ItemName.DonaldLuckyLucky: ItemData(0x1300F5, 3, 407, 0x197, 0, True), - ItemName.DonaldFireBoost: ItemData(0x1300F6, 2, 408, 0x198, 0, True), - ItemName.DonaldBlizzardBoost: ItemData(0x1300F7, 2, 409, 0x199, 0, True), - ItemName.DonaldThunderBoost: ItemData(0x1300F8, 2, 410, 0x19A, 0, True), - ItemName.DonaldMPHaste: ItemData(0x1300F9, 1, 413, 0x19D, 0, True), - ItemName.DonaldMPHastera: ItemData(0x1300FA, 2, 421, 0x1A5, 0, True), - ItemName.DonaldMPHastega: ItemData(0x1300FB, 2, 422, 0x1A6, 0, True), - ItemName.DonaldAutoLimit: ItemData(0x1300FC, 1, 417, 0x1A1, 0, True), - ItemName.DonaldHyperHealing: ItemData(0x1300FD, 2, 419, 0x1A3, 0, True), - ItemName.DonaldAutoHealing: ItemData(0x1300FE, 1, 420, 0x1A4, 0, True), - ItemName.DonaldItemBoost: ItemData(0x1300FF, 1, 411, 0x19B, 0, True), - ItemName.DonaldDamageControl: ItemData(0x130100, 2, 542, 0x21E, 0, True), - ItemName.DonaldDraw: ItemData(0x130101, 1, 405, 0x195, 0, True), + ItemName.DonaldFire: ItemData(1, 165, 0xA5, ability=True), + ItemName.DonaldBlizzard: ItemData(1, 166, 0xA6, ability=True), + ItemName.DonaldThunder: ItemData(1, 167, 0xA7, ability=True), + ItemName.DonaldCure: ItemData(1, 168, 0xA8, ability=True), + ItemName.Fantasia: ItemData(1, 199, 0xC7, ability=True), + ItemName.FlareForce: ItemData(1, 200, 0xC8, ability=True), + ItemName.DonaldMPRage: ItemData(1, 412, 0x19C, ability=True), # originally 3 but swapped to 1 because crit checks + ItemName.DonaldJackpot: ItemData(1, 406, 0x196, ability=True), + ItemName.DonaldLuckyLucky: ItemData(3, 407, 0x197, ability=True), + ItemName.DonaldFireBoost: ItemData(2, 408, 0x198, ability=True), + ItemName.DonaldBlizzardBoost: ItemData(2, 409, 0x199, ability=True), + ItemName.DonaldThunderBoost: ItemData(2, 410, 0x19A, ability=True), + ItemName.DonaldMPHaste: ItemData(1, 413, 0x19D, ability=True), + ItemName.DonaldMPHastera: ItemData(2, 421, 0x1A5, ability=True), + ItemName.DonaldMPHastega: ItemData(2, 422, 0x1A6, ability=True), + ItemName.DonaldAutoLimit: ItemData(1, 417, 0x1A1, ability=True), + ItemName.DonaldHyperHealing: ItemData(2, 419, 0x1A3, ability=True), + ItemName.DonaldAutoHealing: ItemData(1, 420, 0x1A4, ability=True), + ItemName.DonaldItemBoost: ItemData(1, 411, 0x19B, ability=True), + ItemName.DonaldDamageControl: ItemData(2, 542, 0x21E, ability=True), + ItemName.DonaldDraw: ItemData(1, 405, 0x195, ability=True), } + GoofyAbility_Table = { - ItemName.GoofyTornado: ItemData(0x130102, 1, 423, 0x1A7, 0, True), - ItemName.GoofyTurbo: ItemData(0x130103, 1, 425, 0x1A9, 0, True), - ItemName.GoofyBash: ItemData(0x130104, 1, 429, 0x1AD, 0, True), - ItemName.TornadoFusion: ItemData(0x130105, 1, 201, 0xC9, 0, True), - ItemName.Teamwork: ItemData(0x130106, 1, 202, 0xCA, 0, True), - ItemName.GoofyDraw: ItemData(0x130107, 1, 405, 0x195, 0, True), - ItemName.GoofyJackpot: ItemData(0x130108, 1, 406, 0x196, 0, True), - ItemName.GoofyLuckyLucky: ItemData(0x130109, 1, 407, 0x197, 0, True), - ItemName.GoofyItemBoost: ItemData(0x13010A, 2, 411, 0x19B, 0, True), - ItemName.GoofyMPRage: ItemData(0x13010B, 2, 412, 0x19C, 0, True), - ItemName.GoofyDefender: ItemData(0x13010C, 2, 414, 0x19E, 0, True), - ItemName.GoofyDamageControl: ItemData(0x13010D, 3, 542, 0x21E, 0, True), - ItemName.GoofyAutoLimit: ItemData(0x13010E, 1, 417, 0x1A1, 0, True), - ItemName.GoofySecondChance: ItemData(0x13010F, 1, 415, 0x19F, 0, True), - ItemName.GoofyOnceMore: ItemData(0x130110, 1, 416, 0x1A0, 0, True), - ItemName.GoofyAutoChange: ItemData(0x130111, 1, 418, 0x1A2, 0, True), - ItemName.GoofyHyperHealing: ItemData(0x130112, 2, 419, 0x1A3, 0, True), - ItemName.GoofyAutoHealing: ItemData(0x130113, 1, 420, 0x1A4, 0, True), - ItemName.GoofyMPHaste: ItemData(0x130114, 1, 413, 0x19D, 0, True), - ItemName.GoofyMPHastera: ItemData(0x130115, 1, 421, 0x1A5, 0, True), - ItemName.GoofyMPHastega: ItemData(0x130116, 1, 422, 0x1A6, 0, True), - ItemName.GoofyProtect: ItemData(0x130117, 2, 596, 0x254, 0, True), - ItemName.GoofyProtera: ItemData(0x130118, 2, 597, 0x255, 0, True), - ItemName.GoofyProtega: ItemData(0x130119, 2, 598, 0x256, 0, True), + ItemName.GoofyTornado: ItemData(1, 423, 0x1A7, ability=True), + ItemName.GoofyTurbo: ItemData(1, 425, 0x1A9, ability=True), + ItemName.GoofyBash: ItemData(1, 429, 0x1AD, ability=True), + ItemName.TornadoFusion: ItemData(1, 201, 0xC9, ability=True), + ItemName.Teamwork: ItemData(1, 202, 0xCA, ability=True), + ItemName.GoofyDraw: ItemData(1, 405, 0x195, ability=True), + ItemName.GoofyJackpot: ItemData(1, 406, 0x196, ability=True), + ItemName.GoofyLuckyLucky: ItemData(1, 407, 0x197, ability=True), + ItemName.GoofyItemBoost: ItemData(2, 411, 0x19B, ability=True), + ItemName.GoofyMPRage: ItemData(2, 412, 0x19C, ability=True), + ItemName.GoofyDefender: ItemData(2, 414, 0x19E, ability=True), + ItemName.GoofyDamageControl: ItemData(1, 542, 0x21E, ability=True), # originally 3 but swapped to 1 because crit checks + ItemName.GoofyAutoLimit: ItemData(1, 417, 0x1A1, ability=True), + ItemName.GoofySecondChance: ItemData(1, 415, 0x19F, ability=True), + ItemName.GoofyOnceMore: ItemData(1, 416, 0x1A0, ability=True), + ItemName.GoofyAutoChange: ItemData(1, 418, 0x1A2, ability=True), + ItemName.GoofyHyperHealing: ItemData(2, 419, 0x1A3, ability=True), + ItemName.GoofyAutoHealing: ItemData(1, 420, 0x1A4, ability=True), + ItemName.GoofyMPHaste: ItemData(1, 413, 0x19D, ability=True), + ItemName.GoofyMPHastera: ItemData(1, 421, 0x1A5, ability=True), + ItemName.GoofyMPHastega: ItemData(1, 422, 0x1A6, ability=True), + ItemName.GoofyProtect: ItemData(2, 596, 0x254, ability=True), + ItemName.GoofyProtera: ItemData(2, 597, 0x255, ability=True), + ItemName.GoofyProtega: ItemData(2, 598, 0x256, ability=True), } -Misc_Table = { - ItemName.LuckyEmblem: ItemData(0x13011A, 0, 367, 0x3641), # letter item - ItemName.Victory: ItemData(0x13011B, 0, 263, 0x111), - ItemName.Bounty: ItemData(0x13011C, 0, 461, 0, 0), # Dummy 14 - # ItemName.UniversalKey:ItemData(0x130129,0,365,0x363F,0)#Tournament Poster +Wincon_Table = { + ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item + ItemName.Victory: ItemData(kh2id=263, memaddr=0x111), + ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14 + # ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster +} +Consumable_Table = { + ItemName.Potion: ItemData(1, 127, 0x36B8), # 1, 0x3580, piglets house map + ItemName.HiPotion: ItemData(1, 126, 0x36B9), # 2, 0x03581, rabbits house map + ItemName.Ether: ItemData(1, 128, 0x36BA), # 3, 0x3582, kangas house map + ItemName.Elixir: ItemData(1, 129, 0x36BB), # 4, 0x3583, spooky cave map + ItemName.Megalixir: ItemData(1, 124, 0x36BC), # 7, 0x3586, starry hill map + ItemName.Tent: ItemData(1, 512, 0x36BD), # 131,0x35E1, savannah map + ItemName.DriveRecovery: ItemData(1, 252, 0x36BE), # 274,0x3664, pride rock map + ItemName.HighDriveRecovery: ItemData(1, 511, 0x36BF), # 275,0x3665, oasis map +} + +Events_Table = { + ItemName.HostileProgramEvent, + ItemName.McpEvent, + ItemName.ASLarxeneEvent, + ItemName.DataLarxeneEvent, + ItemName.BarbosaEvent, + ItemName.GrimReaper1Event, + ItemName.GrimReaper2Event, + ItemName.DataLuxordEvent, + ItemName.DataAxelEvent, + ItemName.CerberusEvent, + ItemName.OlympusPeteEvent, + ItemName.HydraEvent, + ItemName.OcPainAndPanicCupEvent, + ItemName.OcCerberusCupEvent, + ItemName.HadesEvent, + ItemName.ASZexionEvent, + ItemName.DataZexionEvent, + ItemName.Oc2TitanCupEvent, + ItemName.Oc2GofCupEvent, + ItemName.Oc2CupsEvent, + ItemName.HadesCupEvents, + ItemName.PrisonKeeperEvent, + ItemName.OogieBoogieEvent, + ItemName.ExperimentEvent, + ItemName.ASVexenEvent, + ItemName.DataVexenEvent, + ItemName.ShanYuEvent, + ItemName.AnsemRikuEvent, + ItemName.StormRiderEvent, + ItemName.DataXigbarEvent, + ItemName.RoxasEvent, + ItemName.XigbarEvent, + ItemName.LuxordEvent, + ItemName.SaixEvent, + ItemName.XemnasEvent, + ItemName.ArmoredXemnasEvent, + ItemName.ArmoredXemnas2Event, + ItemName.FinalXemnasEvent, + ItemName.DataXemnasEvent, + ItemName.ThresholderEvent, + ItemName.BeastEvent, + ItemName.DarkThornEvent, + ItemName.XaldinEvent, + ItemName.DataXaldinEvent, + ItemName.TwinLordsEvent, + ItemName.GenieJafarEvent, + ItemName.ASLexaeusEvent, + ItemName.DataLexaeusEvent, + ItemName.ScarEvent, + ItemName.GroundShakerEvent, + ItemName.DataSaixEvent, + ItemName.HBDemyxEvent, + ItemName.ThousandHeartlessEvent, + ItemName.Mushroom13Event, + ItemName.SephiEvent, + ItemName.DataDemyxEvent, + ItemName.CorFirstFightEvent, + ItemName.CorSecondFightEvent, + ItemName.TransportEvent, + ItemName.OldPeteEvent, + ItemName.FuturePeteEvent, + ItemName.ASMarluxiaEvent, + ItemName.DataMarluxiaEvent, + ItemName.TerraEvent, + ItemName.TwilightThornEvent, + ItemName.Axel1Event, + ItemName.Axel2Event, + ItemName.DataRoxasEvent, } # Items that are prone to duping. # anchors for checking form keyblade @@ -358,185 +442,37 @@ Misc_Table = { # Equipped abilities have an offset of 0x8000 so check for if whatever || whatever+0x8000 CheckDupingItems = { "Items": { - ItemName.ProofofConnection, - ItemName.ProofofNonexistence, - ItemName.ProofofPeace, - ItemName.PromiseCharm, - ItemName.NamineSketches, - ItemName.CastleKey, - ItemName.BattlefieldsofWar, - ItemName.SwordoftheAncestor, - ItemName.BeastsClaw, - ItemName.BoneFist, - ItemName.ProudFang, - ItemName.SkillandCrossbones, - ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, - ItemName.WaytotheDawn, - ItemName.IdentityDisk, - ItemName.TornPages, - ItemName.LuckyEmblem, - ItemName.MickyMunnyPouch, - ItemName.OletteMunnyPouch, - ItemName.HadesCupTrophy, - ItemName.UnknownDisk, - ItemName.OlympusStone, + item_name for keys in [Progression_Table.keys(), Wincon_Table.keys(), Consumable_Table, [ItemName.MickeyMunnyPouch, + ItemName.OletteMunnyPouch, + ItemName.HadesCupTrophy, + ItemName.UnknownDisk, + ItemName.OlympusStone, ], Boosts_Table.keys()] + for item_name in keys + }, "Magic": { - ItemName.FireElement, - ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, - ItemName.MagnetElement, - ItemName.ReflectElement, + magic for magic in Magic_Table.keys() }, "Bitmask": { - ItemName.ValorForm, - ItemName.WisdomForm, - ItemName.LimitForm, - ItemName.MasterForm, - ItemName.FinalForm, - ItemName.Genie, - ItemName.PeterPan, - ItemName.Stitch, - ItemName.ChickenLittle, - ItemName.SecretAnsemsReport1, - ItemName.SecretAnsemsReport2, - ItemName.SecretAnsemsReport3, - ItemName.SecretAnsemsReport4, - ItemName.SecretAnsemsReport5, - ItemName.SecretAnsemsReport6, - ItemName.SecretAnsemsReport7, - ItemName.SecretAnsemsReport8, - ItemName.SecretAnsemsReport9, - ItemName.SecretAnsemsReport10, - ItemName.SecretAnsemsReport11, - ItemName.SecretAnsemsReport12, - ItemName.SecretAnsemsReport13, - + item_name for keys in [Forms_Table.keys(), Summon_Table.keys(), Reports_Table.keys()] for item_name in keys }, "Weapons": { "Keyblades": { - ItemName.Oathkeeper, - ItemName.Oblivion, - ItemName.StarSeeker, - ItemName.HiddenDragon, - ItemName.HerosCrest, - ItemName.Monochrome, - ItemName.FollowtheWind, - ItemName.CircleofLife, - ItemName.PhotonDebugger, - ItemName.GullWing, - ItemName.RumblingRose, - ItemName.GuardianSoul, - ItemName.WishingLamp, - ItemName.DecisivePumpkin, - ItemName.SleepingLion, - ItemName.SweetMemories, - ItemName.MysteriousAbyss, - ItemName.TwoBecomeOne, - ItemName.FatalCrest, - ItemName.BondofFlame, - ItemName.Fenrir, - ItemName.UltimaWeapon, - ItemName.WinnersProof, - ItemName.Pureblood, + keyblade for keyblade in Keyblade_Table.keys() }, "Staffs": { - ItemName.Centurion2, - ItemName.MeteorStaff, - ItemName.NobodyLance, - ItemName.PreciousMushroom, - ItemName.PreciousMushroom2, - ItemName.PremiumMushroom, - ItemName.RisingDragon, - ItemName.SaveTheQueen2, - ItemName.ShamansRelic, + staff for staff in Staffs_Table.keys() }, "Shields": { - ItemName.AkashicRecord, - ItemName.FrozenPride2, - ItemName.GenjiShield, - ItemName.MajesticMushroom, - ItemName.MajesticMushroom2, - ItemName.NobodyGuard, - ItemName.OgreShield, - ItemName.SaveTheKing2, - ItemName.UltimateMushroom, + shield for shield in Shields_Table.keys() } }, "Equipment": { "Accessories": { - ItemName.AbilityRing, - ItemName.EngineersRing, - ItemName.TechniciansRing, - ItemName.SkillRing, - ItemName.SkillfulRing, - ItemName.ExpertsRing, - ItemName.MastersRing, - ItemName.CosmicRing, - ItemName.ExecutivesRing, - ItemName.SardonyxRing, - ItemName.TourmalineRing, - ItemName.AquamarineRing, - ItemName.GarnetRing, - ItemName.DiamondRing, - ItemName.SilverRing, - ItemName.GoldRing, - ItemName.PlatinumRing, - ItemName.MythrilRing, - ItemName.OrichalcumRing, - ItemName.SoldierEarring, - ItemName.FencerEarring, - ItemName.MageEarring, - ItemName.SlayerEarring, - ItemName.Medal, - ItemName.MoonAmulet, - ItemName.StarCharm, - ItemName.CosmicArts, - ItemName.ShadowArchive, - ItemName.ShadowArchive2, - ItemName.FullBloom, - ItemName.FullBloom2, - ItemName.DrawRing, - ItemName.LuckyRing, + accessory for accessory in Accessory_Table.keys() }, "Armor": { - ItemName.ElvenBandana, - ItemName.DivineBandana, - ItemName.ProtectBelt, - ItemName.GaiaBelt, - ItemName.PowerBand, - ItemName.BusterBand, - ItemName.CosmicBelt, - ItemName.FireBangle, - ItemName.FiraBangle, - ItemName.FiragaBangle, - ItemName.FiragunBangle, - ItemName.BlizzardArmlet, - ItemName.BlizzaraArmlet, - ItemName.BlizzagaArmlet, - ItemName.BlizzagunArmlet, - ItemName.ThunderTrinket, - ItemName.ThundaraTrinket, - ItemName.ThundagaTrinket, - ItemName.ThundagunTrinket, - ItemName.ShockCharm, - ItemName.ShockCharm2, - ItemName.ShadowAnklet, - ItemName.DarkAnklet, - ItemName.MidnightAnklet, - ItemName.ChaosAnklet, - ItemName.ChampionBelt, - ItemName.AbasChain, - ItemName.AegisChain, - ItemName.Acrisius, - ItemName.Acrisius2, - ItemName.CosmicChain, - ItemName.PetiteRibbon, - ItemName.Ribbon, - ItemName.GrandRibbon, + armor for armor in Armor_Table.keys() } }, "Stat Increases": { @@ -549,297 +485,103 @@ CheckDupingItems = { }, "Abilities": { "Sora": { - ItemName.Scan, + item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys + }, + "Donald": { + donald_ability for donald_ability in DonaldAbility_Table.keys() + }, + "Goofy": { + goofy_ability for goofy_ability in GoofyAbility_Table.keys() + } + }, +} +progression_set = { + # abilities + item_name for keys in [ + Wincon_Table.keys(), + Progression_Table.keys(), + Forms_Table.keys(), + Magic_Table.keys(), + Summon_Table.keys(), + Movement_Table.keys(), + Keyblade_Table.keys(), + Staffs_Table.keys(), + Shields_Table.keys(), + [ ItemName.AerialRecovery, ItemName.ComboMaster, ItemName.ComboPlus, ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, ItemName.FinishingPlus, ItemName.NegativeCombo, ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, ItemName.OnceMore, ItemName.SecondChance, ItemName.Guard, - ItemName.UpperSlash, ItemName.HorizontalSlash, ItemName.FinishingLeap, - ItemName.RetaliatingSlash, ItemName.Slapshot, - ItemName.DodgeSlash, ItemName.FlashStep, ItemName.SlideDash, - ItemName.VicinityBreak, ItemName.GuardBreak, ItemName.Explosion, ItemName.AerialSweep, ItemName.AerialDive, ItemName.AerialSpiral, ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal, - ItemName.AutoSummon, ItemName.TrinityLimit, - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - }, - "Donald": { - ItemName.DonaldFire, - ItemName.DonaldBlizzard, - ItemName.DonaldThunder, - ItemName.DonaldCure, - ItemName.Fantasia, + ItemName.DriveConverter, + # Party Limits ItemName.FlareForce, - ItemName.DonaldMPRage, - ItemName.DonaldJackpot, - ItemName.DonaldLuckyLucky, - ItemName.DonaldFireBoost, - ItemName.DonaldBlizzardBoost, - ItemName.DonaldThunderBoost, - ItemName.DonaldMPHaste, - ItemName.DonaldMPHastera, - ItemName.DonaldMPHastega, - ItemName.DonaldAutoLimit, - ItemName.DonaldHyperHealing, - ItemName.DonaldAutoHealing, - ItemName.DonaldItemBoost, - ItemName.DonaldDamageControl, - ItemName.DonaldDraw, - }, - "Goofy": { - ItemName.GoofyTornado, - ItemName.GoofyTurbo, - ItemName.GoofyBash, - ItemName.TornadoFusion, + ItemName.Fantasia, ItemName.Teamwork, - ItemName.GoofyDraw, - ItemName.GoofyJackpot, - ItemName.GoofyLuckyLucky, - ItemName.GoofyItemBoost, - ItemName.GoofyMPRage, - ItemName.GoofyDefender, - ItemName.GoofyDamageControl, - ItemName.GoofyAutoLimit, - ItemName.GoofySecondChance, - ItemName.GoofyOnceMore, - ItemName.GoofyAutoChange, - ItemName.GoofyHyperHealing, - ItemName.GoofyAutoHealing, - ItemName.GoofyMPHaste, - ItemName.GoofyMPHastera, - ItemName.GoofyMPHastega, - ItemName.GoofyProtect, - ItemName.GoofyProtera, - ItemName.GoofyProtega, - } - }, - "Boosts": { - ItemName.PowerBoost, - ItemName.MagicBoost, - ItemName.DefenseBoost, - ItemName.APBoost, - } + ItemName.TornadoFusion, + ItemName.HadesCupTrophy], + Events_Table] + for item_name in keys } +party_filler_set = { + ItemName.GoofyAutoHealing, + ItemName.GoofyMPHaste, + ItemName.GoofyMPHastera, + ItemName.GoofyMPHastega, + ItemName.GoofyProtect, + ItemName.GoofyProtera, + ItemName.GoofyProtega, + ItemName.GoofyMPRage, + ItemName.GoofyDefender, + ItemName.GoofyDamageControl, -Progression_Dicts = { - # Items that are classified as progression - "Progression": { - # Wincons - ItemName.Victory, - ItemName.LuckyEmblem, - ItemName.Bounty, - ItemName.ProofofConnection, - ItemName.ProofofNonexistence, - ItemName.ProofofPeace, - ItemName.PromiseCharm, - # visit locking - ItemName.NamineSketches, - # dummy 13 - ItemName.CastleKey, - ItemName.BattlefieldsofWar, - ItemName.SwordoftheAncestor, - ItemName.BeastsClaw, - ItemName.BoneFist, - ItemName.ProudFang, - ItemName.SkillandCrossbones, - ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, - ItemName.WaytotheDawn, - ItemName.IdentityDisk, - ItemName.TornPages, - # forms - ItemName.ValorForm, - ItemName.WisdomForm, - ItemName.LimitForm, - ItemName.MasterForm, - ItemName.FinalForm, - # magic - ItemName.FireElement, - ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, - ItemName.MagnetElement, - ItemName.ReflectElement, - ItemName.Genie, - ItemName.PeterPan, - ItemName.Stitch, - ItemName.ChickenLittle, - # movement - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - # abilities - ItemName.Scan, - ItemName.AerialRecovery, - ItemName.ComboMaster, - ItemName.ComboPlus, - ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, - ItemName.FinishingPlus, - ItemName.NegativeCombo, - ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, - ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, - ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, - ItemName.OnceMore, - ItemName.SecondChance, - ItemName.Guard, - ItemName.UpperSlash, - ItemName.HorizontalSlash, - ItemName.FinishingLeap, - ItemName.RetaliatingSlash, - ItemName.Slapshot, - ItemName.DodgeSlash, - ItemName.FlashStep, - ItemName.SlideDash, - ItemName.VicinityBreak, - ItemName.GuardBreak, - ItemName.Explosion, - ItemName.AerialSweep, - ItemName.AerialDive, - ItemName.AerialSpiral, - ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, - ItemName.AutoValor, - ItemName.AutoWisdom, - ItemName.AutoLimit, - ItemName.AutoMaster, - ItemName.AutoFinal, - ItemName.AutoSummon, - ItemName.TrinityLimit, - # keyblades - ItemName.Oathkeeper, - ItemName.Oblivion, - ItemName.StarSeeker, - ItemName.HiddenDragon, - ItemName.HerosCrest, - ItemName.Monochrome, - ItemName.FollowtheWind, - ItemName.CircleofLife, - ItemName.PhotonDebugger, - ItemName.GullWing, - ItemName.RumblingRose, - ItemName.GuardianSoul, - ItemName.WishingLamp, - ItemName.DecisivePumpkin, - ItemName.SleepingLion, - ItemName.SweetMemories, - ItemName.MysteriousAbyss, - ItemName.TwoBecomeOne, - ItemName.FatalCrest, - ItemName.BondofFlame, - ItemName.Fenrir, - ItemName.UltimaWeapon, - ItemName.WinnersProof, - ItemName.Pureblood, - # Staffs - ItemName.Centurion2, - ItemName.MeteorStaff, - ItemName.NobodyLance, - ItemName.PreciousMushroom, - ItemName.PreciousMushroom2, - ItemName.PremiumMushroom, - ItemName.RisingDragon, - ItemName.SaveTheQueen2, - ItemName.ShamansRelic, - # Shields - ItemName.AkashicRecord, - ItemName.FrozenPride2, - ItemName.GenjiShield, - ItemName.MajesticMushroom, - ItemName.MajesticMushroom2, - ItemName.NobodyGuard, - ItemName.OgreShield, - ItemName.SaveTheKing2, - ItemName.UltimateMushroom, - # Party Limits - ItemName.FlareForce, - ItemName.Fantasia, - ItemName.Teamwork, - ItemName.TornadoFusion - }, - "2VisitLocking": { + ItemName.DonaldFireBoost, + ItemName.DonaldBlizzardBoost, + ItemName.DonaldThunderBoost, + ItemName.DonaldMPHaste, + ItemName.DonaldMPHastera, + ItemName.DonaldMPHastega, + ItemName.DonaldAutoHealing, + ItemName.DonaldDamageControl, + ItemName.DonaldDraw, + ItemName.DonaldMPRage, +} +useful_set = {item_name for keys in [ + SupportAbility_Table.keys(), + ActionAbility_Table.keys(), + DonaldAbility_Table.keys(), + GoofyAbility_Table.keys(), + Armor_Table.keys(), + Usefull_Table.keys(), + Accessory_Table.keys()] + for item_name in keys if item_name not in progression_set and item_name not in party_filler_set} + +visit_locking_dict = { + "2VisitLocking": [ ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, @@ -854,7 +596,7 @@ Progression_Dicts = { ItemName.IdentityDisk, ItemName.IceCream, ItemName.NamineSketches - }, + ], "AllVisitLocking": { ItemName.CastleKey: 2, ItemName.BattlefieldsofWar: 2, @@ -865,84 +607,13 @@ Progression_Dicts = { ItemName.SkillandCrossbones: 2, ItemName.Scimitar: 2, ItemName.MembershipCard: 2, - ItemName.WaytotheDawn: 1, + ItemName.WaytotheDawn: 2, ItemName.IdentityDisk: 2, ItemName.IceCream: 3, ItemName.NamineSketches: 1, } } - -exclusionItem_table = { - "Ability": { - ItemName.Scan, - ItemName.AerialRecovery, - ItemName.ComboMaster, - ItemName.ComboPlus, - ItemName.AirComboPlus, - ItemName.ComboBoost, - ItemName.AirComboBoost, - ItemName.ReactionBoost, - ItemName.FinishingPlus, - ItemName.NegativeCombo, - ItemName.BerserkCharge, - ItemName.DamageDrive, - ItemName.DriveBoost, - ItemName.FormBoost, - ItemName.SummonBoost, - ItemName.ExperienceBoost, - ItemName.Draw, - ItemName.Jackpot, - ItemName.LuckyLucky, - ItemName.DriveConverter, - ItemName.FireBoost, - ItemName.BlizzardBoost, - ItemName.ThunderBoost, - ItemName.ItemBoost, - ItemName.MPRage, - ItemName.MPHaste, - ItemName.MPHastera, - ItemName.MPHastega, - ItemName.Defender, - ItemName.DamageControl, - ItemName.NoExperience, - ItemName.LightDarkness, - ItemName.MagicLock, - ItemName.LeafBracer, - ItemName.CombinationBoost, - ItemName.DamageDrive, - ItemName.OnceMore, - ItemName.SecondChance, - ItemName.Guard, - ItemName.UpperSlash, - ItemName.HorizontalSlash, - ItemName.FinishingLeap, - ItemName.RetaliatingSlash, - ItemName.Slapshot, - ItemName.DodgeSlash, - ItemName.FlashStep, - ItemName.SlideDash, - ItemName.VicinityBreak, - ItemName.GuardBreak, - ItemName.Explosion, - ItemName.AerialSweep, - ItemName.AerialDive, - ItemName.AerialSpiral, - ItemName.AerialFinish, - ItemName.MagnetBurst, - ItemName.Counterguard, - ItemName.AutoValor, - ItemName.AutoWisdom, - ItemName.AutoLimit, - ItemName.AutoMaster, - ItemName.AutoFinal, - ItemName.AutoSummon, - ItemName.TrinityLimit, - ItemName.HighJump, - ItemName.QuickRun, - ItemName.DodgeRoll, - ItemName.AerialDodge, - ItemName.Glide, - }, +exclusion_item_table = { "StatUps": { ItemName.MaxHPUp, ItemName.MaxMPUp, @@ -951,59 +622,64 @@ exclusionItem_table = { ItemName.AccessorySlotUp, ItemName.ItemSlotUp, }, + "Ability": { + item_name for keys in [SupportAbility_Table.keys(), ActionAbility_Table.keys(), Movement_Table.keys()] for item_name in keys + } } -item_dictionary_table = {**Reports_Table, - **Progression_Table, - **Forms_Table, - **Magic_Table, - **Armor_Table, - **Movement_Table, - **Staffs_Table, - **Shields_Table, - **Keyblade_Table, - **Accessory_Table, - **Usefull_Table, - **SupportAbility_Table, - **ActionAbility_Table, - **Items_Table, - **Misc_Table, - **Items_Table, - **DonaldAbility_Table, - **GoofyAbility_Table, - } - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_dictionary_table.items() if - data.code} - -item_groups: typing.Dict[str, list] = {"Drive Form": [item_name for item_name in Forms_Table.keys()], - "Growth": [item_name for item_name in Movement_Table.keys()], - "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], - "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], - "Magic": [ItemName.FireElement, ItemName.BlizzardElement, - ItemName.ThunderElement, - ItemName.CureElement, ItemName.MagnetElement, - ItemName.ReflectElement], - "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, - ItemName.PeterPan], - "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], - "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, - ItemName.FinishingLeap], - "Visit Lock": [item_name for item_name in - Progression_Dicts["2VisitLocking"]], - "Keyblade": [item_name for item_name in Keyblade_Table.keys()], - "Fire": [ItemName.FireElement], - "Blizzard": [ItemName.BlizzardElement], - "Thunder": [ItemName.ThunderElement], - "Cure": [ItemName.CureElement], - "Magnet": [ItemName.MagnetElement], - "Reflect": [ItemName.ReflectElement], - "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, - ItemName.ProofofConnection], - "Filler": [ - ItemName.PowerBoost, ItemName.MagicBoost, - ItemName.DefenseBoost, ItemName.APBoost] - } - -# lookup_kh2id_to_name: typing.Dict[int, str] = {data.kh2id: item_name for item_name, data in -# item_dictionary_table.items() if data.kh2id} +default_itempool_option = { + item_name: ItemData.quantity for dic in [Magic_Table, Progression_Table, Summon_Table, Movement_Table, Forms_Table] for item_name, ItemData in dic.items() +} +item_dictionary_table = { + **Reports_Table, + **Progression_Table, + **Forms_Table, + **Magic_Table, + **Summon_Table, + **Armor_Table, + **Movement_Table, + **Staffs_Table, + **Shields_Table, + **Keyblade_Table, + **Accessory_Table, + **Usefull_Table, + **SupportAbility_Table, + **ActionAbility_Table, + **Boosts_Table, + **Wincon_Table, + **Boosts_Table, + **DonaldAbility_Table, + **GoofyAbility_Table, + **Consumable_Table +} +filler_items = [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost, + ItemName.Potion, ItemName.HiPotion, ItemName.Ether, ItemName.Elixir, ItemName.Megalixir, + ItemName.Tent, ItemName.DriveRecovery, ItemName.HighDriveRecovery, + ] +item_groups: typing.Dict[str, list] = { + "Drive Form": [item_name for item_name in Forms_Table.keys()], + "Growth": [item_name for item_name in Movement_Table.keys()], + "Donald Limit": [ItemName.FlareForce, ItemName.Fantasia], + "Goofy Limit": [ItemName.Teamwork, ItemName.TornadoFusion], + "Magic": [ItemName.FireElement, ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.CureElement, ItemName.MagnetElement, + ItemName.ReflectElement], + "Summon": [ItemName.ChickenLittle, ItemName.Genie, ItemName.Stitch, + ItemName.PeterPan], + "Gap Closer": [ItemName.SlideDash, ItemName.FlashStep], + "Ground Finisher": [ItemName.GuardBreak, ItemName.Explosion, + ItemName.FinishingLeap], + "Visit Lock": [item_name for item_name in + visit_locking_dict["2VisitLocking"]], + "Keyblade": [item_name for item_name in Keyblade_Table.keys()], + "Fire": [ItemName.FireElement], + "Blizzard": [ItemName.BlizzardElement], + "Thunder": [ItemName.ThunderElement], + "Cure": [ItemName.CureElement], + "Magnet": [ItemName.MagnetElement], + "Reflect": [ItemName.ReflectElement], + "Proof": [ItemName.ProofofNonexistence, ItemName.ProofofPeace, + ItemName.ProofofConnection], + "hitlist": [ItemName.Bounty], +} diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 9046dfc67b..9d7d948443 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1,7 +1,7 @@ import typing from BaseClasses import Location -from .Names import LocationName, RegionName, ItemName +from .Names import LocationName, ItemName class KH2Location(Location): @@ -9,7 +9,6 @@ class KH2Location(Location): class LocationData(typing.NamedTuple): - code: typing.Optional[int] locid: int yml: str charName: str = "Sora" @@ -18,950 +17,1072 @@ class LocationData(typing.NamedTuple): # data's addrcheck sys3 addr obtained roomid bit index is eventid LoD_Checks = { - LocationName.BambooGroveDarkShard: LocationData(0x130000, 245, "Chest"), - LocationName.BambooGroveEther: LocationData(0x130001, 497, "Chest"), - LocationName.BambooGroveMythrilShard: LocationData(0x130002, 498, "Chest"), - LocationName.EncampmentAreaMap: LocationData(0x130003, 350, "Chest"), - LocationName.Mission3: LocationData(0x130004, 417, "Chest"), - LocationName.CheckpointHiPotion: LocationData(0x130005, 21, "Chest"), - LocationName.CheckpointMythrilShard: LocationData(0x130006, 121, "Chest"), - LocationName.MountainTrailLightningShard: LocationData(0x130007, 22, "Chest"), - LocationName.MountainTrailRecoveryRecipe: LocationData(0x130008, 23, "Chest"), - LocationName.MountainTrailEther: LocationData(0x130009, 122, "Chest"), - LocationName.MountainTrailMythrilShard: LocationData(0x13000A, 123, "Chest"), - LocationName.VillageCaveAreaMap: LocationData(0x13000B, 495, "Chest"), - LocationName.VillageCaveDarkShard: LocationData(0x13000C, 125, "Chest"), - LocationName.VillageCaveAPBoost: LocationData(0x13000D, 124, "Chest"), - LocationName.VillageCaveBonus: LocationData(0x13000E, 43, "Get Bonus"), - LocationName.RidgeFrostShard: LocationData(0x13000F, 24, "Chest"), - LocationName.RidgeAPBoost: LocationData(0x130010, 126, "Chest"), - LocationName.ShanYu: LocationData(0x130011, 9, "Double Get Bonus"), - LocationName.ShanYuGetBonus: LocationData(0x130012, 9, "Second Get Bonus"), - LocationName.HiddenDragon: LocationData(0x130013, 257, "Chest"), - -} -LoD2_Checks = { - LocationName.ThroneRoomTornPages: LocationData(0x130014, 25, "Chest"), - LocationName.ThroneRoomPalaceMap: LocationData(0x130015, 127, "Chest"), - LocationName.ThroneRoomAPBoost: LocationData(0x130016, 26, "Chest"), - LocationName.ThroneRoomQueenRecipe: LocationData(0x130017, 27, "Chest"), - LocationName.ThroneRoomAPBoost2: LocationData(0x130018, 128, "Chest"), - LocationName.ThroneRoomOgreShield: LocationData(0x130019, 129, "Chest"), - LocationName.ThroneRoomMythrilCrystal: LocationData(0x13001A, 130, "Chest"), - LocationName.ThroneRoomOrichalcum: LocationData(0x13001B, 131, "Chest"), - LocationName.StormRider: LocationData(0x13001C, 10, "Get Bonus"), - LocationName.XigbarDataDefenseBoost: LocationData(0x13001D, 555, "Chest"), + LocationName.BambooGroveDarkShard: LocationData(245, "Chest"), + LocationName.BambooGroveEther: LocationData(497, "Chest"), + LocationName.BambooGroveMythrilShard: LocationData(498, "Chest"), + LocationName.EncampmentAreaMap: LocationData(350, "Chest"), + LocationName.Mission3: LocationData(417, "Chest"), + LocationName.CheckpointHiPotion: LocationData(21, "Chest"), + LocationName.CheckpointMythrilShard: LocationData(121, "Chest"), + LocationName.MountainTrailLightningShard: LocationData(22, "Chest"), + LocationName.MountainTrailRecoveryRecipe: LocationData(23, "Chest"), + LocationName.MountainTrailEther: LocationData(122, "Chest"), + LocationName.MountainTrailMythrilShard: LocationData(123, "Chest"), + LocationName.VillageCaveAreaMap: LocationData(495, "Chest"), + LocationName.VillageCaveDarkShard: LocationData(125, "Chest"), + LocationName.VillageCaveAPBoost: LocationData(124, "Chest"), + LocationName.VillageCaveBonus: LocationData(43, "Get Bonus"), + LocationName.RidgeFrostShard: LocationData(24, "Chest"), + LocationName.RidgeAPBoost: LocationData(126, "Chest"), + LocationName.ShanYu: LocationData(9, "Double Get Bonus"), + LocationName.ShanYuGetBonus: LocationData(9, "Second Get Bonus"), + LocationName.HiddenDragon: LocationData(257, "Chest"), + LocationName.ThroneRoomTornPages: LocationData(25, "Chest"), + LocationName.ThroneRoomPalaceMap: LocationData(127, "Chest"), + LocationName.ThroneRoomAPBoost: LocationData(26, "Chest"), + LocationName.ThroneRoomQueenRecipe: LocationData(27, "Chest"), + LocationName.ThroneRoomAPBoost2: LocationData(128, "Chest"), + LocationName.ThroneRoomOgreShield: LocationData(129, "Chest"), + LocationName.ThroneRoomMythrilCrystal: LocationData(130, "Chest"), + LocationName.ThroneRoomOrichalcum: LocationData(131, "Chest"), + LocationName.StormRider: LocationData(10, "Get Bonus"), + LocationName.XigbarDataDefenseBoost: LocationData(555, "Chest"), } AG_Checks = { - LocationName.AgrabahMap: LocationData(0x13001E, 353, "Chest"), - LocationName.AgrabahDarkShard: LocationData(0x13001F, 28, "Chest"), - LocationName.AgrabahMythrilShard: LocationData(0x130020, 29, "Chest"), - LocationName.AgrabahHiPotion: LocationData(0x130021, 30, "Chest"), - LocationName.AgrabahAPBoost: LocationData(0x130022, 132, "Chest"), - LocationName.AgrabahMythrilStone: LocationData(0x130023, 133, "Chest"), - LocationName.AgrabahMythrilShard2: LocationData(0x130024, 249, "Chest"), - LocationName.AgrabahSerenityShard: LocationData(0x130025, 501, "Chest"), - LocationName.BazaarMythrilGem: LocationData(0x130026, 31, "Chest"), - LocationName.BazaarPowerShard: LocationData(0x130027, 32, "Chest"), - LocationName.BazaarHiPotion: LocationData(0x130028, 33, "Chest"), - LocationName.BazaarAPBoost: LocationData(0x130029, 134, "Chest"), - LocationName.BazaarMythrilShard: LocationData(0x13002A, 135, "Chest"), - LocationName.PalaceWallsSkillRing: LocationData(0x13002B, 136, "Chest"), - LocationName.PalaceWallsMythrilStone: LocationData(0x13002C, 520, "Chest"), - LocationName.CaveEntrancePowerStone: LocationData(0x13002D, 250, "Chest"), - LocationName.CaveEntranceMythrilShard: LocationData(0x13002E, 251, "Chest"), - LocationName.ValleyofStoneMythrilStone: LocationData(0x13002F, 35, "Chest"), - LocationName.ValleyofStoneAPBoost: LocationData(0x130030, 36, "Chest"), - LocationName.ValleyofStoneMythrilShard: LocationData(0x130031, 137, "Chest"), - LocationName.ValleyofStoneHiPotion: LocationData(0x130032, 138, "Chest"), - LocationName.AbuEscort: LocationData(0x130033, 42, "Get Bonus"), - LocationName.ChasmofChallengesCaveofWondersMap: LocationData(0x130034, 487, "Chest"), - LocationName.ChasmofChallengesAPBoost: LocationData(0x130035, 37, "Chest"), - LocationName.TreasureRoom: LocationData(0x130036, 46, "Get Bonus"), - LocationName.TreasureRoomAPBoost: LocationData(0x130037, 502, "Chest"), - LocationName.TreasureRoomSerenityGem: LocationData(0x130038, 503, "Chest"), - LocationName.ElementalLords: LocationData(0x130039, 37, "Get Bonus"), - LocationName.LampCharm: LocationData(0x13003A, 300, "Chest"), - -} -AG2_Checks = { - LocationName.RuinedChamberTornPages: LocationData(0x13003B, 34, "Chest"), - LocationName.RuinedChamberRuinsMap: LocationData(0x13003C, 486, "Chest"), - LocationName.GenieJafar: LocationData(0x13003D, 15, "Get Bonus"), - LocationName.WishingLamp: LocationData(0x13003E, 303, "Chest"), - LocationName.LexaeusBonus: LocationData(0x13003F, 65, "Get Bonus"), - LocationName.LexaeusASStrengthBeyondStrength: LocationData(0x130040, 545, "Chest"), - LocationName.LexaeusDataLostIllusion: LocationData(0x130041, 550, "Chest"), + LocationName.AgrabahMap: LocationData(353, "Chest"), + LocationName.AgrabahDarkShard: LocationData(28, "Chest"), + LocationName.AgrabahMythrilShard: LocationData(29, "Chest"), + LocationName.AgrabahHiPotion: LocationData(30, "Chest"), + LocationName.AgrabahAPBoost: LocationData(132, "Chest"), + LocationName.AgrabahMythrilStone: LocationData(133, "Chest"), + LocationName.AgrabahMythrilShard2: LocationData(249, "Chest"), + LocationName.AgrabahSerenityShard: LocationData(501, "Chest"), + LocationName.BazaarMythrilGem: LocationData(31, "Chest"), + LocationName.BazaarPowerShard: LocationData(32, "Chest"), + LocationName.BazaarHiPotion: LocationData(33, "Chest"), + LocationName.BazaarAPBoost: LocationData(134, "Chest"), + LocationName.BazaarMythrilShard: LocationData(135, "Chest"), + LocationName.PalaceWallsSkillRing: LocationData(136, "Chest"), + LocationName.PalaceWallsMythrilStone: LocationData(520, "Chest"), + LocationName.CaveEntrancePowerStone: LocationData(250, "Chest"), + LocationName.CaveEntranceMythrilShard: LocationData(251, "Chest"), + LocationName.ValleyofStoneMythrilStone: LocationData(35, "Chest"), + LocationName.ValleyofStoneAPBoost: LocationData(36, "Chest"), + LocationName.ValleyofStoneMythrilShard: LocationData(137, "Chest"), + LocationName.ValleyofStoneHiPotion: LocationData(138, "Chest"), + LocationName.AbuEscort: LocationData(42, "Get Bonus"), + LocationName.ChasmofChallengesCaveofWondersMap: LocationData(487, "Chest"), + LocationName.ChasmofChallengesAPBoost: LocationData(37, "Chest"), + LocationName.TreasureRoom: LocationData(46, "Get Bonus"), + LocationName.TreasureRoomAPBoost: LocationData(502, "Chest"), + LocationName.TreasureRoomSerenityGem: LocationData(503, "Chest"), + LocationName.ElementalLords: LocationData(37, "Get Bonus"), + LocationName.LampCharm: LocationData(300, "Chest"), + LocationName.RuinedChamberTornPages: LocationData(34, "Chest"), + LocationName.RuinedChamberRuinsMap: LocationData(486, "Chest"), + LocationName.GenieJafar: LocationData(15, "Get Bonus"), + LocationName.WishingLamp: LocationData(303, "Chest"), + LocationName.LexaeusBonus: LocationData(65, "Get Bonus"), + LocationName.LexaeusASStrengthBeyondStrength: LocationData(545, "Chest"), + LocationName.LexaeusDataLostIllusion: LocationData(550, "Chest"), } DC_Checks = { - LocationName.DCCourtyardMythrilShard: LocationData(0x130042, 16, "Chest"), - LocationName.DCCourtyardStarRecipe: LocationData(0x130043, 17, "Chest"), - LocationName.DCCourtyardAPBoost: LocationData(0x130044, 18, "Chest"), - LocationName.DCCourtyardMythrilStone: LocationData(0x130045, 92, "Chest"), - LocationName.DCCourtyardBlazingStone: LocationData(0x130046, 93, "Chest"), - LocationName.DCCourtyardBlazingShard: LocationData(0x130047, 247, "Chest"), - LocationName.DCCourtyardMythrilShard2: LocationData(0x130048, 248, "Chest"), - LocationName.LibraryTornPages: LocationData(0x130049, 91, "Chest"), - LocationName.DisneyCastleMap: LocationData(0x13004A, 332, "Chest"), - LocationName.MinnieEscort: LocationData(0x13004B, 38, "Double Get Bonus"), - LocationName.MinnieEscortGetBonus: LocationData(0x13004C, 38, "Second Get Bonus"), + LocationName.DCCourtyardMythrilShard: LocationData(16, "Chest"), + LocationName.DCCourtyardStarRecipe: LocationData(17, "Chest"), + LocationName.DCCourtyardAPBoost: LocationData(18, "Chest"), + LocationName.DCCourtyardMythrilStone: LocationData(92, "Chest"), + LocationName.DCCourtyardBlazingStone: LocationData(93, "Chest"), + LocationName.DCCourtyardBlazingShard: LocationData(247, "Chest"), + LocationName.DCCourtyardMythrilShard2: LocationData(248, "Chest"), + LocationName.LibraryTornPages: LocationData(91, "Chest"), + LocationName.DisneyCastleMap: LocationData(332, "Chest"), + LocationName.MinnieEscort: LocationData(38, "Double Get Bonus"), + LocationName.MinnieEscortGetBonus: LocationData(38, "Second Get Bonus"), + LocationName.CornerstoneHillMap: LocationData(79, "Chest"), + LocationName.CornerstoneHillFrostShard: LocationData(12, "Chest"), + LocationName.PierMythrilShard: LocationData(81, "Chest"), + LocationName.PierHiPotion: LocationData(82, "Chest"), + LocationName.WaterwayMythrilStone: LocationData(83, "Chest"), + LocationName.WaterwayAPBoost: LocationData(84, "Chest"), + LocationName.WaterwayFrostStone: LocationData(85, "Chest"), + LocationName.WindowofTimeMap: LocationData(368, "Chest"), + LocationName.BoatPete: LocationData(16, "Get Bonus"), + LocationName.FuturePete: LocationData(17, "Double Get Bonus"), + LocationName.FuturePeteGetBonus: LocationData(17, "Second Get Bonus"), + LocationName.Monochrome: LocationData(261, "Chest"), + LocationName.WisdomForm: LocationData(262, "Chest"), + LocationName.MarluxiaGetBonus: LocationData(67, "Get Bonus"), + LocationName.MarluxiaASEternalBlossom: LocationData(548, "Chest"), + LocationName.MarluxiaDataLostIllusion: LocationData(553, "Chest"), + LocationName.LingeringWillBonus: LocationData(70, "Get Bonus"), + LocationName.LingeringWillProofofConnection: LocationData(587, "Chest"), + LocationName.LingeringWillManifestIllusion: LocationData(591, "Chest"), } -TR_Checks = { - LocationName.CornerstoneHillMap: LocationData(0x13004D, 79, "Chest"), - LocationName.CornerstoneHillFrostShard: LocationData(0x13004E, 12, "Chest"), - LocationName.PierMythrilShard: LocationData(0x13004F, 81, "Chest"), - LocationName.PierHiPotion: LocationData(0x130050, 82, "Chest"), - LocationName.WaterwayMythrilStone: LocationData(0x130051, 83, "Chest"), - LocationName.WaterwayAPBoost: LocationData(0x130052, 84, "Chest"), - LocationName.WaterwayFrostStone: LocationData(0x130053, 85, "Chest"), - LocationName.WindowofTimeMap: LocationData(0x130054, 368, "Chest"), - LocationName.BoatPete: LocationData(0x130055, 16, "Get Bonus"), - LocationName.FuturePete: LocationData(0x130056, 17, "Double Get Bonus"), - LocationName.FuturePeteGetBonus: LocationData(0x130057, 17, "Second Get Bonus"), - LocationName.Monochrome: LocationData(0x130058, 261, "Chest"), - LocationName.WisdomForm: LocationData(0x130059, 262, "Chest"), - LocationName.MarluxiaGetBonus: LocationData(0x13005A, 67, "Get Bonus"), - LocationName.MarluxiaASEternalBlossom: LocationData(0x13005B, 548, "Chest"), - LocationName.MarluxiaDataLostIllusion: LocationData(0x13005C, 553, "Chest"), - LocationName.LingeringWillBonus: LocationData(0x13005D, 70, "Get Bonus"), - LocationName.LingeringWillProofofConnection: LocationData(0x13005E, 587, "Chest"), - LocationName.LingeringWillManifestIllusion: LocationData(0x13005F, 591, "Chest"), -} -# the mismatch might be here -HundredAcre1_Checks = { - LocationName.PoohsHouse100AcreWoodMap: LocationData(0x130060, 313, "Chest"), - LocationName.PoohsHouseAPBoost: LocationData(0x130061, 97, "Chest"), - LocationName.PoohsHouseMythrilStone: LocationData(0x130062, 98, "Chest"), -} -HundredAcre2_Checks = { - LocationName.PigletsHouseDefenseBoost: LocationData(0x130063, 105, "Chest"), - LocationName.PigletsHouseAPBoost: LocationData(0x130064, 103, "Chest"), - LocationName.PigletsHouseMythrilGem: LocationData(0x130065, 104, "Chest"), -} -HundredAcre3_Checks = { - LocationName.RabbitsHouseDrawRing: LocationData(0x130066, 314, "Chest"), - LocationName.RabbitsHouseMythrilCrystal: LocationData(0x130067, 100, "Chest"), - LocationName.RabbitsHouseAPBoost: LocationData(0x130068, 101, "Chest"), -} -HundredAcre4_Checks = { - LocationName.KangasHouseMagicBoost: LocationData(0x130069, 108, "Chest"), - LocationName.KangasHouseAPBoost: LocationData(0x13006A, 106, "Chest"), - LocationName.KangasHouseOrichalcum: LocationData(0x13006B, 107, "Chest"), -} -HundredAcre5_Checks = { - LocationName.SpookyCaveMythrilGem: LocationData(0x13006C, 110, "Chest"), - LocationName.SpookyCaveAPBoost: LocationData(0x13006D, 111, "Chest"), - LocationName.SpookyCaveOrichalcum: LocationData(0x13006E, 112, "Chest"), - LocationName.SpookyCaveGuardRecipe: LocationData(0x13006F, 113, "Chest"), - LocationName.SpookyCaveMythrilCrystal: LocationData(0x130070, 115, "Chest"), - LocationName.SpookyCaveAPBoost2: LocationData(0x130071, 116, "Chest"), - LocationName.SweetMemories: LocationData(0x130072, 284, "Chest"), - LocationName.SpookyCaveMap: LocationData(0x130073, 485, "Chest"), -} -HundredAcre6_Checks = { - LocationName.StarryHillCosmicRing: LocationData(0x130074, 312, "Chest"), - LocationName.StarryHillStyleRecipe: LocationData(0x130075, 94, "Chest"), - LocationName.StarryHillCureElement: LocationData(0x130076, 285, "Chest"), - LocationName.StarryHillOrichalcumPlus: LocationData(0x130077, 539, "Chest"), +HundredAcre_Checks = { + LocationName.PoohsHouse100AcreWoodMap: LocationData(313, "Chest"), + LocationName.PoohsHouseAPBoost: LocationData(97, "Chest"), + LocationName.PoohsHouseMythrilStone: LocationData(98, "Chest"), + LocationName.PigletsHouseDefenseBoost: LocationData(105, "Chest"), + LocationName.PigletsHouseAPBoost: LocationData(103, "Chest"), + LocationName.PigletsHouseMythrilGem: LocationData(104, "Chest"), + LocationName.RabbitsHouseDrawRing: LocationData(314, "Chest"), + LocationName.RabbitsHouseMythrilCrystal: LocationData(100, "Chest"), + LocationName.RabbitsHouseAPBoost: LocationData(101, "Chest"), + LocationName.KangasHouseMagicBoost: LocationData(108, "Chest"), + LocationName.KangasHouseAPBoost: LocationData(106, "Chest"), + LocationName.KangasHouseOrichalcum: LocationData(107, "Chest"), + LocationName.SpookyCaveMythrilGem: LocationData(110, "Chest"), + LocationName.SpookyCaveAPBoost: LocationData(111, "Chest"), + LocationName.SpookyCaveOrichalcum: LocationData(112, "Chest"), + LocationName.SpookyCaveGuardRecipe: LocationData(113, "Chest"), + LocationName.SpookyCaveMythrilCrystal: LocationData(115, "Chest"), + LocationName.SpookyCaveAPBoost2: LocationData(116, "Chest"), + LocationName.SweetMemories: LocationData(284, "Chest"), + LocationName.SpookyCaveMap: LocationData(485, "Chest"), + LocationName.StarryHillCosmicRing: LocationData(312, "Chest"), + LocationName.StarryHillStyleRecipe: LocationData(94, "Chest"), + LocationName.StarryHillCureElement: LocationData(285, "Chest"), + LocationName.StarryHillOrichalcumPlus: LocationData(539, "Chest"), } Oc_Checks = { - LocationName.PassageMythrilShard: LocationData(0x130078, 7, "Chest"), - LocationName.PassageMythrilStone: LocationData(0x130079, 8, "Chest"), - LocationName.PassageEther: LocationData(0x13007A, 144, "Chest"), - LocationName.PassageAPBoost: LocationData(0x13007B, 145, "Chest"), - LocationName.PassageHiPotion: LocationData(0x13007C, 146, "Chest"), - LocationName.InnerChamberUnderworldMap: LocationData(0x13007D, 2, "Chest"), - LocationName.InnerChamberMythrilShard: LocationData(0x13007E, 243, "Chest"), - LocationName.Cerberus: LocationData(0x13007F, 5, "Get Bonus"), - LocationName.ColiseumMap: LocationData(0x130080, 338, "Chest"), - LocationName.Urns: LocationData(0x130081, 57, "Get Bonus"), - LocationName.UnderworldEntrancePowerBoost: LocationData(0x130082, 242, "Chest"), - LocationName.CavernsEntranceLucidShard: LocationData(0x130083, 3, "Chest"), - LocationName.CavernsEntranceAPBoost: LocationData(0x130084, 11, "Chest"), - LocationName.CavernsEntranceMythrilShard: LocationData(0x130085, 504, "Chest"), - LocationName.TheLostRoadBrightShard: LocationData(0x130086, 9, "Chest"), - LocationName.TheLostRoadEther: LocationData(0x130087, 10, "Chest"), - LocationName.TheLostRoadMythrilShard: LocationData(0x130088, 148, "Chest"), - LocationName.TheLostRoadMythrilStone: LocationData(0x130089, 149, "Chest"), - LocationName.AtriumLucidStone: LocationData(0x13008A, 150, "Chest"), - LocationName.AtriumAPBoost: LocationData(0x13008B, 151, "Chest"), - LocationName.DemyxOC: LocationData(0x13008C, 58, "Get Bonus"), - LocationName.SecretAnsemReport5: LocationData(0x13008D, 529, "Chest"), - LocationName.OlympusStone: LocationData(0x13008E, 293, "Chest"), - LocationName.TheLockCavernsMap: LocationData(0x13008F, 244, "Chest"), - LocationName.TheLockMythrilShard: LocationData(0x130090, 5, "Chest"), - LocationName.TheLockAPBoost: LocationData(0x130091, 142, "Chest"), - LocationName.PeteOC: LocationData(0x130092, 6, "Get Bonus"), - LocationName.Hydra: LocationData(0x130093, 7, "Double Get Bonus"), - LocationName.HydraGetBonus: LocationData(0x130094, 7, "Second Get Bonus"), - LocationName.HerosCrest: LocationData(0x130095, 260, "Chest"), - -} -Oc2_Checks = { - LocationName.AuronsStatue: LocationData(0x130096, 295, "Chest"), - LocationName.Hades: LocationData(0x130097, 8, "Double Get Bonus"), - LocationName.HadesGetBonus: LocationData(0x130098, 8, "Second Get Bonus"), - LocationName.GuardianSoul: LocationData(0x130099, 272, "Chest"), - LocationName.ZexionBonus: LocationData(0x13009A, 66, "Get Bonus"), - LocationName.ZexionASBookofShadows: LocationData(0x13009B, 546, "Chest"), - LocationName.ZexionDataLostIllusion: LocationData(0x13009C, 551, "Chest"), -} -Oc2Cups = { - LocationName.ProtectBeltPainandPanicCup: LocationData(0x13009D, 513, "Chest"), - LocationName.SerenityGemPainandPanicCup: LocationData(0x13009E, 540, "Chest"), - LocationName.RisingDragonCerberusCup: LocationData(0x13009F, 515, "Chest"), - LocationName.SerenityCrystalCerberusCup: LocationData(0x1300A0, 542, "Chest"), - LocationName.GenjiShieldTitanCup: LocationData(0x1300A1, 514, "Chest"), - LocationName.SkillfulRingTitanCup: LocationData(0x1300A2, 541, "Chest"), - LocationName.FatalCrestGoddessofFateCup: LocationData(0x1300A3, 516, "Chest"), - LocationName.OrichalcumPlusGoddessofFateCup: LocationData(0x1300A4, 517, "Chest"), - LocationName.HadesCupTrophyParadoxCups: LocationData(0x1300A5, 518, "Chest"), + LocationName.PassageMythrilShard: LocationData(7, "Chest"), + LocationName.PassageMythrilStone: LocationData(8, "Chest"), + LocationName.PassageEther: LocationData(144, "Chest"), + LocationName.PassageAPBoost: LocationData(145, "Chest"), + LocationName.PassageHiPotion: LocationData(146, "Chest"), + LocationName.InnerChamberUnderworldMap: LocationData(2, "Chest"), + LocationName.InnerChamberMythrilShard: LocationData(243, "Chest"), + LocationName.Cerberus: LocationData(5, "Get Bonus"), + LocationName.ColiseumMap: LocationData(338, "Chest"), + LocationName.Urns: LocationData(57, "Get Bonus"), + LocationName.UnderworldEntrancePowerBoost: LocationData(242, "Chest"), + LocationName.CavernsEntranceLucidShard: LocationData(3, "Chest"), + LocationName.CavernsEntranceAPBoost: LocationData(11, "Chest"), + LocationName.CavernsEntranceMythrilShard: LocationData(504, "Chest"), + LocationName.TheLostRoadBrightShard: LocationData(9, "Chest"), + LocationName.TheLostRoadEther: LocationData(10, "Chest"), + LocationName.TheLostRoadMythrilShard: LocationData(148, "Chest"), + LocationName.TheLostRoadMythrilStone: LocationData(149, "Chest"), + LocationName.AtriumLucidStone: LocationData(150, "Chest"), + LocationName.AtriumAPBoost: LocationData(151, "Chest"), + LocationName.DemyxOC: LocationData(58, "Get Bonus"), + LocationName.SecretAnsemReport5: LocationData(529, "Chest"), + LocationName.OlympusStone: LocationData(293, "Chest"), + LocationName.TheLockCavernsMap: LocationData(244, "Chest"), + LocationName.TheLockMythrilShard: LocationData(5, "Chest"), + LocationName.TheLockAPBoost: LocationData(142, "Chest"), + LocationName.PeteOC: LocationData(6, "Get Bonus"), + LocationName.Hydra: LocationData(7, "Double Get Bonus"), + LocationName.HydraGetBonus: LocationData(7, "Second Get Bonus"), + LocationName.HerosCrest: LocationData(260, "Chest"), + LocationName.AuronsStatue: LocationData(295, "Chest"), + LocationName.Hades: LocationData(8, "Double Get Bonus"), + LocationName.HadesGetBonus: LocationData(8, "Second Get Bonus"), + LocationName.GuardianSoul: LocationData(272, "Chest"), + LocationName.ZexionBonus: LocationData(66, "Get Bonus"), + LocationName.ZexionASBookofShadows: LocationData(546, "Chest"), + LocationName.ZexionDataLostIllusion: LocationData(551, "Chest"), + LocationName.ProtectBeltPainandPanicCup: LocationData(513, "Chest"), + LocationName.SerenityGemPainandPanicCup: LocationData(540, "Chest"), + LocationName.RisingDragonCerberusCup: LocationData(515, "Chest"), + LocationName.SerenityCrystalCerberusCup: LocationData(542, "Chest"), + LocationName.GenjiShieldTitanCup: LocationData(514, "Chest"), + LocationName.SkillfulRingTitanCup: LocationData(541, "Chest"), + LocationName.FatalCrestGoddessofFateCup: LocationData(516, "Chest"), + LocationName.OrichalcumPlusGoddessofFateCup: LocationData(517, "Chest"), + LocationName.HadesCupTrophyParadoxCups: LocationData(518, "Chest"), } BC_Checks = { - LocationName.BCCourtyardAPBoost: LocationData(0x1300A6, 39, "Chest"), - LocationName.BCCourtyardHiPotion: LocationData(0x1300A7, 40, "Chest"), - LocationName.BCCourtyardMythrilShard: LocationData(0x1300A8, 505, "Chest"), - LocationName.BellesRoomCastleMap: LocationData(0x1300A9, 46, "Chest"), - LocationName.BellesRoomMegaRecipe: LocationData(0x1300AA, 240, "Chest"), - LocationName.TheEastWingMythrilShard: LocationData(0x1300AB, 63, "Chest"), - LocationName.TheEastWingTent: LocationData(0x1300AC, 155, "Chest"), - LocationName.TheWestHallHiPotion: LocationData(0x1300AD, 41, "Chest"), - LocationName.TheWestHallPowerShard: LocationData(0x1300AE, 207, "Chest"), - LocationName.TheWestHallAPBoostPostDungeon: LocationData(0x1300AF, 158, "Chest"), - LocationName.TheWestHallBrightStone: LocationData(0x1300B0, 159, "Chest"), - LocationName.TheWestHallMythrilShard: LocationData(0x1300B1, 206, "Chest"), - LocationName.Thresholder: LocationData(0x1300B2, 2, "Get Bonus"), - LocationName.DungeonBasementMap: LocationData(0x1300B3, 239, "Chest"), - LocationName.DungeonAPBoost: LocationData(0x1300B4, 43, "Chest"), - LocationName.SecretPassageMythrilShard: LocationData(0x1300B5, 44, "Chest"), - LocationName.SecretPassageHiPotion: LocationData(0x1300B6, 168, "Chest"), - LocationName.SecretPassageLucidShard: LocationData(0x1300B7, 45, "Chest"), - LocationName.TheWestHallMythrilShard2: LocationData(0x1300B8, 208, "Chest"), - LocationName.TheWestWingMythrilShard: LocationData(0x1300B9, 42, "Chest"), - LocationName.TheWestWingTent: LocationData(0x1300BA, 164, "Chest"), - LocationName.Beast: LocationData(0x1300BB, 12, "Get Bonus"), - LocationName.TheBeastsRoomBlazingShard: LocationData(0x1300BC, 241, "Chest"), - LocationName.DarkThorn: LocationData(0x1300BD, 3, "Double Get Bonus"), - LocationName.DarkThornGetBonus: LocationData(0x1300BE, 3, "Second Get Bonus"), - LocationName.DarkThornCureElement: LocationData(0x1300BF, 299, "Chest"), - -} -BC2_Checks = { - LocationName.RumblingRose: LocationData(0x1300C0, 270, "Chest"), - LocationName.CastleWallsMap: LocationData(0x1300C1, 325, "Chest"), - LocationName.Xaldin: LocationData(0x1300C2, 4, "Double Get Bonus"), - LocationName.XaldinGetBonus: LocationData(0x1300C3, 4, "Second Get Bonus"), - LocationName.SecretAnsemReport4: LocationData(0x1300C4, 528, "Chest"), - LocationName.XaldinDataDefenseBoost: LocationData(0x1300C5, 559, "Chest"), + LocationName.BCCourtyardAPBoost: LocationData(39, "Chest"), + LocationName.BCCourtyardHiPotion: LocationData(40, "Chest"), + LocationName.BCCourtyardMythrilShard: LocationData(505, "Chest"), + LocationName.BellesRoomCastleMap: LocationData(46, "Chest"), + LocationName.BellesRoomMegaRecipe: LocationData(240, "Chest"), + LocationName.TheEastWingMythrilShard: LocationData(63, "Chest"), + LocationName.TheEastWingTent: LocationData(155, "Chest"), + LocationName.TheWestHallHiPotion: LocationData(41, "Chest"), + LocationName.TheWestHallPowerShard: LocationData(207, "Chest"), + LocationName.TheWestHallAPBoostPostDungeon: LocationData(158, "Chest"), + LocationName.TheWestHallBrightStone: LocationData(159, "Chest"), + LocationName.TheWestHallMythrilShard: LocationData(206, "Chest"), + LocationName.Thresholder: LocationData(2, "Get Bonus"), + LocationName.DungeonBasementMap: LocationData(239, "Chest"), + LocationName.DungeonAPBoost: LocationData(43, "Chest"), + LocationName.SecretPassageMythrilShard: LocationData(44, "Chest"), + LocationName.SecretPassageHiPotion: LocationData(168, "Chest"), + LocationName.SecretPassageLucidShard: LocationData(45, "Chest"), + LocationName.TheWestHallMythrilShard2: LocationData(208, "Chest"), + LocationName.TheWestWingMythrilShard: LocationData(42, "Chest"), + LocationName.TheWestWingTent: LocationData(164, "Chest"), + LocationName.Beast: LocationData(12, "Get Bonus"), + LocationName.TheBeastsRoomBlazingShard: LocationData(241, "Chest"), + LocationName.DarkThorn: LocationData(3, "Double Get Bonus"), + LocationName.DarkThornGetBonus: LocationData(3, "Second Get Bonus"), + LocationName.DarkThornCureElement: LocationData(299, "Chest"), + LocationName.RumblingRose: LocationData(270, "Chest"), + LocationName.CastleWallsMap: LocationData(325, "Chest"), + LocationName.Xaldin: LocationData(4, "Double Get Bonus"), + LocationName.XaldinGetBonus: LocationData(4, "Second Get Bonus"), + LocationName.SecretAnsemReport4: LocationData(528, "Chest"), + LocationName.XaldinDataDefenseBoost: LocationData(559, "Chest"), } SP_Checks = { - LocationName.PitCellAreaMap: LocationData(0x1300C6, 316, "Chest"), - LocationName.PitCellMythrilCrystal: LocationData(0x1300C7, 64, "Chest"), - LocationName.CanyonDarkCrystal: LocationData(0x1300C8, 65, "Chest"), - LocationName.CanyonMythrilStone: LocationData(0x1300C9, 171, "Chest"), - LocationName.CanyonMythrilGem: LocationData(0x1300CA, 253, "Chest"), - LocationName.CanyonFrostCrystal: LocationData(0x1300CB, 521, "Chest"), - LocationName.Screens: LocationData(0x1300CC, 45, "Get Bonus"), - LocationName.HallwayPowerCrystal: LocationData(0x1300CD, 49, "Chest"), - LocationName.HallwayAPBoost: LocationData(0x1300CE, 50, "Chest"), - LocationName.CommunicationsRoomIOTowerMap: LocationData(0x1300CF, 255, "Chest"), - LocationName.CommunicationsRoomGaiaBelt: LocationData(0x1300D0, 499, "Chest"), - LocationName.HostileProgram: LocationData(0x1300D1, 31, "Double Get Bonus"), - LocationName.HostileProgramGetBonus: LocationData(0x1300D2, 31, "Second Get Bonus"), - LocationName.PhotonDebugger: LocationData(0x1300D3, 267, "Chest"), - -} -SP2_Checks = { - LocationName.SolarSailer: LocationData(0x1300D4, 61, "Get Bonus"), - LocationName.CentralComputerCoreAPBoost: LocationData(0x1300D5, 177, "Chest"), - LocationName.CentralComputerCoreOrichalcumPlus: LocationData(0x1300D6, 178, "Chest"), - LocationName.CentralComputerCoreCosmicArts: LocationData(0x1300D7, 51, "Chest"), - LocationName.CentralComputerCoreMap: LocationData(0x1300D8, 488, "Chest"), - LocationName.MCP: LocationData(0x1300D9, 32, "Double Get Bonus"), - LocationName.MCPGetBonus: LocationData(0x1300DA, 32, "Second Get Bonus"), - LocationName.LarxeneBonus: LocationData(0x1300DB, 68, "Get Bonus"), - LocationName.LarxeneASCloakedThunder: LocationData(0x1300DC, 547, "Chest"), - LocationName.LarxeneDataLostIllusion: LocationData(0x1300DD, 552, "Chest"), + LocationName.PitCellAreaMap: LocationData(316, "Chest"), + LocationName.PitCellMythrilCrystal: LocationData(64, "Chest"), + LocationName.CanyonDarkCrystal: LocationData(65, "Chest"), + LocationName.CanyonMythrilStone: LocationData(171, "Chest"), + LocationName.CanyonMythrilGem: LocationData(253, "Chest"), + LocationName.CanyonFrostCrystal: LocationData(521, "Chest"), + LocationName.Screens: LocationData(45, "Get Bonus"), + LocationName.HallwayPowerCrystal: LocationData(49, "Chest"), + LocationName.HallwayAPBoost: LocationData(50, "Chest"), + LocationName.CommunicationsRoomIOTowerMap: LocationData(255, "Chest"), + LocationName.CommunicationsRoomGaiaBelt: LocationData(499, "Chest"), + LocationName.HostileProgram: LocationData(31, "Double Get Bonus"), + LocationName.HostileProgramGetBonus: LocationData(31, "Second Get Bonus"), + LocationName.PhotonDebugger: LocationData(267, "Chest"), + LocationName.SolarSailer: LocationData(61, "Get Bonus"), + LocationName.CentralComputerCoreAPBoost: LocationData(177, "Chest"), + LocationName.CentralComputerCoreOrichalcumPlus: LocationData(178, "Chest"), + LocationName.CentralComputerCoreCosmicArts: LocationData(51, "Chest"), + LocationName.CentralComputerCoreMap: LocationData(488, "Chest"), + LocationName.MCP: LocationData(32, "Double Get Bonus"), + LocationName.MCPGetBonus: LocationData(32, "Second Get Bonus"), + LocationName.LarxeneBonus: LocationData(68, "Get Bonus"), + LocationName.LarxeneASCloakedThunder: LocationData(547, "Chest"), + LocationName.LarxeneDataLostIllusion: LocationData(552, "Chest"), } HT_Checks = { - LocationName.GraveyardMythrilShard: LocationData(0x1300DE, 53, "Chest"), - LocationName.GraveyardSerenityGem: LocationData(0x1300DF, 212, "Chest"), - LocationName.FinklesteinsLabHalloweenTownMap: LocationData(0x1300E0, 211, "Chest"), - LocationName.TownSquareMythrilStone: LocationData(0x1300E1, 209, "Chest"), - LocationName.TownSquareEnergyShard: LocationData(0x1300E2, 210, "Chest"), - LocationName.HinterlandsLightningShard: LocationData(0x1300E3, 54, "Chest"), - LocationName.HinterlandsMythrilStone: LocationData(0x1300E4, 213, "Chest"), - LocationName.HinterlandsAPBoost: LocationData(0x1300E5, 214, "Chest"), - LocationName.CandyCaneLaneMegaPotion: LocationData(0x1300E6, 55, "Chest"), - LocationName.CandyCaneLaneMythrilGem: LocationData(0x1300E7, 56, "Chest"), - LocationName.CandyCaneLaneLightningStone: LocationData(0x1300E8, 216, "Chest"), - LocationName.CandyCaneLaneMythrilStone: LocationData(0x1300E9, 217, "Chest"), - LocationName.SantasHouseChristmasTownMap: LocationData(0x1300EA, 57, "Chest"), - LocationName.SantasHouseAPBoost: LocationData(0x1300EB, 58, "Chest"), - LocationName.PrisonKeeper: LocationData(0x1300EC, 18, "Get Bonus"), - LocationName.OogieBoogie: LocationData(0x1300ED, 19, "Get Bonus"), - LocationName.OogieBoogieMagnetElement: LocationData(0x1300EE, 301, "Chest"), -} -HT2_Checks = { - LocationName.Lock: LocationData(0x1300EF, 40, "Get Bonus"), - LocationName.Present: LocationData(0x1300F0, 297, "Chest"), - LocationName.DecoyPresents: LocationData(0x1300F1, 298, "Chest"), - LocationName.Experiment: LocationData(0x1300F2, 20, "Get Bonus"), - LocationName.DecisivePumpkin: LocationData(0x1300F3, 275, "Chest"), - LocationName.VexenBonus: LocationData(0x1300F4, 64, "Get Bonus"), - LocationName.VexenASRoadtoDiscovery: LocationData(0x1300F5, 544, "Chest"), - LocationName.VexenDataLostIllusion: LocationData(0x1300F6, 549, "Chest"), + LocationName.GraveyardMythrilShard: LocationData(53, "Chest"), + LocationName.GraveyardSerenityGem: LocationData(212, "Chest"), + LocationName.FinklesteinsLabHalloweenTownMap: LocationData(211, "Chest"), + LocationName.TownSquareMythrilStone: LocationData(209, "Chest"), + LocationName.TownSquareEnergyShard: LocationData(210, "Chest"), + LocationName.HinterlandsLightningShard: LocationData(54, "Chest"), + LocationName.HinterlandsMythrilStone: LocationData(213, "Chest"), + LocationName.HinterlandsAPBoost: LocationData(214, "Chest"), + LocationName.CandyCaneLaneMegaPotion: LocationData(55, "Chest"), + LocationName.CandyCaneLaneMythrilGem: LocationData(56, "Chest"), + LocationName.CandyCaneLaneLightningStone: LocationData(216, "Chest"), + LocationName.CandyCaneLaneMythrilStone: LocationData(217, "Chest"), + LocationName.SantasHouseChristmasTownMap: LocationData(57, "Chest"), + LocationName.SantasHouseAPBoost: LocationData(58, "Chest"), + LocationName.PrisonKeeper: LocationData(18, "Get Bonus"), + LocationName.OogieBoogie: LocationData(19, "Get Bonus"), + LocationName.OogieBoogieMagnetElement: LocationData(301, "Chest"), + LocationName.Lock: LocationData(40, "Get Bonus"), + LocationName.Present: LocationData(297, "Chest"), + LocationName.DecoyPresents: LocationData(298, "Chest"), + LocationName.Experiment: LocationData(20, "Get Bonus"), + LocationName.DecisivePumpkin: LocationData(275, "Chest"), + LocationName.VexenBonus: LocationData(64, "Get Bonus"), + LocationName.VexenASRoadtoDiscovery: LocationData(544, "Chest"), + LocationName.VexenDataLostIllusion: LocationData(549, "Chest"), } PR_Checks = { - LocationName.RampartNavalMap: LocationData(0x1300F7, 70, "Chest"), - LocationName.RampartMythrilStone: LocationData(0x1300F8, 219, "Chest"), - LocationName.RampartDarkShard: LocationData(0x1300F9, 220, "Chest"), - LocationName.TownDarkStone: LocationData(0x1300FA, 71, "Chest"), - LocationName.TownAPBoost: LocationData(0x1300FB, 72, "Chest"), - LocationName.TownMythrilShard: LocationData(0x1300FC, 73, "Chest"), - LocationName.TownMythrilGem: LocationData(0x1300FD, 221, "Chest"), - LocationName.CaveMouthBrightShard: LocationData(0x1300FE, 74, "Chest"), - LocationName.CaveMouthMythrilShard: LocationData(0x1300FF, 223, "Chest"), - LocationName.IsladeMuertaMap: LocationData(0x130100, 329, "Chest"), - LocationName.BoatFight: LocationData(0x130101, 62, "Get Bonus"), - LocationName.InterceptorBarrels: LocationData(0x130102, 39, "Get Bonus"), - LocationName.PowderStoreAPBoost1: LocationData(0x130103, 369, "Chest"), - LocationName.PowderStoreAPBoost2: LocationData(0x130104, 370, "Chest"), - LocationName.MoonlightNookMythrilShard: LocationData(0x130105, 75, "Chest"), - LocationName.MoonlightNookSerenityGem: LocationData(0x130106, 224, "Chest"), - LocationName.MoonlightNookPowerStone: LocationData(0x130107, 371, "Chest"), - LocationName.Barbossa: LocationData(0x130108, 21, "Double Get Bonus"), - LocationName.BarbossaGetBonus: LocationData(0x130109, 21, "Second Get Bonus"), - LocationName.FollowtheWind: LocationData(0x13010A, 263, "Chest"), - -} -PR2_Checks = { - LocationName.GrimReaper1: LocationData(0x13010B, 59, "Get Bonus"), - LocationName.InterceptorsHoldFeatherCharm: LocationData(0x13010C, 252, "Chest"), - LocationName.SeadriftKeepAPBoost: LocationData(0x13010D, 76, "Chest"), - LocationName.SeadriftKeepOrichalcum: LocationData(0x13010E, 225, "Chest"), - LocationName.SeadriftKeepMeteorStaff: LocationData(0x13010F, 372, "Chest"), - LocationName.SeadriftRowSerenityGem: LocationData(0x130110, 77, "Chest"), - LocationName.SeadriftRowKingRecipe: LocationData(0x130111, 78, "Chest"), - LocationName.SeadriftRowMythrilCrystal: LocationData(0x130112, 373, "Chest"), - LocationName.SeadriftRowCursedMedallion: LocationData(0x130113, 296, "Chest"), - LocationName.SeadriftRowShipGraveyardMap: LocationData(0x130114, 331, "Chest"), - LocationName.GrimReaper2: LocationData(0x130115, 22, "Get Bonus"), - LocationName.SecretAnsemReport6: LocationData(0x130116, 530, "Chest"), - LocationName.LuxordDataAPBoost: LocationData(0x130117, 557, "Chest"), + LocationName.RampartNavalMap: LocationData(70, "Chest"), + LocationName.RampartMythrilStone: LocationData(219, "Chest"), + LocationName.RampartDarkShard: LocationData(220, "Chest"), + LocationName.TownDarkStone: LocationData(71, "Chest"), + LocationName.TownAPBoost: LocationData(72, "Chest"), + LocationName.TownMythrilShard: LocationData(73, "Chest"), + LocationName.TownMythrilGem: LocationData(221, "Chest"), + LocationName.CaveMouthBrightShard: LocationData(74, "Chest"), + LocationName.CaveMouthMythrilShard: LocationData(223, "Chest"), + LocationName.IsladeMuertaMap: LocationData(329, "Chest"), + LocationName.BoatFight: LocationData(62, "Get Bonus"), + LocationName.InterceptorBarrels: LocationData(39, "Get Bonus"), + LocationName.PowderStoreAPBoost1: LocationData(369, "Chest"), + LocationName.PowderStoreAPBoost2: LocationData(370, "Chest"), + LocationName.MoonlightNookMythrilShard: LocationData(75, "Chest"), + LocationName.MoonlightNookSerenityGem: LocationData(224, "Chest"), + LocationName.MoonlightNookPowerStone: LocationData(371, "Chest"), + LocationName.Barbossa: LocationData(21, "Double Get Bonus"), + LocationName.BarbossaGetBonus: LocationData(21, "Second Get Bonus"), + LocationName.FollowtheWind: LocationData(263, "Chest"), + LocationName.GrimReaper1: LocationData(59, "Get Bonus"), + LocationName.InterceptorsHoldFeatherCharm: LocationData(252, "Chest"), + LocationName.SeadriftKeepAPBoost: LocationData(76, "Chest"), + LocationName.SeadriftKeepOrichalcum: LocationData(225, "Chest"), + LocationName.SeadriftKeepMeteorStaff: LocationData(372, "Chest"), + LocationName.SeadriftRowSerenityGem: LocationData(77, "Chest"), + LocationName.SeadriftRowKingRecipe: LocationData(78, "Chest"), + LocationName.SeadriftRowMythrilCrystal: LocationData(373, "Chest"), + LocationName.SeadriftRowCursedMedallion: LocationData(296, "Chest"), + LocationName.SeadriftRowShipGraveyardMap: LocationData(331, "Chest"), + LocationName.GrimReaper2: LocationData(22, "Get Bonus"), + LocationName.SecretAnsemReport6: LocationData(530, "Chest"), + LocationName.LuxordDataAPBoost: LocationData(557, "Chest"), } HB_Checks = { - LocationName.MarketplaceMap: LocationData(0x130118, 362, "Chest"), - LocationName.BoroughDriveRecovery: LocationData(0x130119, 194, "Chest"), - LocationName.BoroughAPBoost: LocationData(0x13011A, 195, "Chest"), - LocationName.BoroughHiPotion: LocationData(0x13011B, 196, "Chest"), - LocationName.BoroughMythrilShard: LocationData(0x13011C, 305, "Chest"), - LocationName.BoroughDarkShard: LocationData(0x13011D, 506, "Chest"), - LocationName.MerlinsHouseMembershipCard: LocationData(0x13011E, 256, "Chest"), - LocationName.MerlinsHouseBlizzardElement: LocationData(0x13011F, 292, "Chest"), - LocationName.Bailey: LocationData(0x130120, 47, "Get Bonus"), - LocationName.BaileySecretAnsemReport7: LocationData(0x130121, 531, "Chest"), - LocationName.BaseballCharm: LocationData(0x130122, 258, "Chest"), -} -HB2_Checks = { - LocationName.PosternCastlePerimeterMap: LocationData(0x130123, 310, "Chest"), - LocationName.PosternMythrilGem: LocationData(0x130124, 189, "Chest"), - LocationName.PosternAPBoost: LocationData(0x130125, 190, "Chest"), - LocationName.CorridorsMythrilStone: LocationData(0x130126, 200, "Chest"), - LocationName.CorridorsMythrilCrystal: LocationData(0x130127, 201, "Chest"), - LocationName.CorridorsDarkCrystal: LocationData(0x130128, 202, "Chest"), - LocationName.CorridorsAPBoost: LocationData(0x130129, 307, "Chest"), - LocationName.AnsemsStudyMasterForm: LocationData(0x13012A, 276, "Chest"), - LocationName.AnsemsStudySleepingLion: LocationData(0x13012B, 266, "Chest"), - LocationName.AnsemsStudySkillRecipe: LocationData(0x13012C, 184, "Chest"), - LocationName.AnsemsStudyUkuleleCharm: LocationData(0x13012D, 183, "Chest"), - LocationName.RestorationSiteMoonRecipe: LocationData(0x13012E, 309, "Chest"), - LocationName.RestorationSiteAPBoost: LocationData(0x13012F, 507, "Chest"), - LocationName.DemyxHB: LocationData(0x130130, 28, "Double Get Bonus"), - LocationName.DemyxHBGetBonus: LocationData(0x130131, 28, "Second Get Bonus"), - LocationName.FFFightsCureElement: LocationData(0x130132, 361, "Chest"), - LocationName.CrystalFissureTornPages: LocationData(0x130133, 179, "Chest"), - LocationName.CrystalFissureTheGreatMawMap: LocationData(0x130134, 489, "Chest"), - LocationName.CrystalFissureEnergyCrystal: LocationData(0x130135, 180, "Chest"), - LocationName.CrystalFissureAPBoost: LocationData(0x130136, 181, "Chest"), - LocationName.ThousandHeartless: LocationData(0x130137, 60, "Get Bonus"), - LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(0x130138, 525, "Chest"), - LocationName.ThousandHeartlessIceCream: LocationData(0x130139, 269, "Chest"), - LocationName.ThousandHeartlessPicture: LocationData(0x13013A, 511, "Chest"), - LocationName.PosternGullWing: LocationData(0x13013B, 491, "Chest"), - LocationName.HeartlessManufactoryCosmicChain: LocationData(0x13013C, 311, "Chest"), - LocationName.SephirothBonus: LocationData(0x13013D, 35, "Get Bonus"), - LocationName.SephirothFenrir: LocationData(0x13013E, 282, "Chest"), - LocationName.WinnersProof: LocationData(0x13013F, 588, "Chest"), - LocationName.ProofofPeace: LocationData(0x130140, 589, "Chest"), - LocationName.DemyxDataAPBoost: LocationData(0x130141, 560, "Chest"), - LocationName.CoRDepthsAPBoost: LocationData(0x130142, 562, "Chest"), - LocationName.CoRDepthsPowerCrystal: LocationData(0x130143, 563, "Chest"), - LocationName.CoRDepthsFrostCrystal: LocationData(0x130144, 564, "Chest"), - LocationName.CoRDepthsManifestIllusion: LocationData(0x130145, 565, "Chest"), - LocationName.CoRDepthsAPBoost2: LocationData(0x130146, 566, "Chest"), - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(0x130147, 580, "Chest"), - LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(0x130148, 578, "Chest"), - -} -CoR_Checks = { - LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(0x130149, 567, "Chest"), - LocationName.CoRMiningAreaSerenityGem: LocationData(0x13014A, 568, "Chest"), - LocationName.CoRMiningAreaAPBoost: LocationData(0x13014B, 569, "Chest"), - LocationName.CoRMiningAreaSerenityCrystal: LocationData(0x13014C, 570, "Chest"), - LocationName.CoRMiningAreaManifestIllusion: LocationData(0x13014D, 571, "Chest"), - LocationName.CoRMiningAreaSerenityGem2: LocationData(0x13014E, 572, "Chest"), - LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(0x13014F, 573, "Chest"), - LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(0x130150, 581, "Chest"), - LocationName.CoREngineChamberSerenityCrystal: LocationData(0x130151, 574, "Chest"), - LocationName.CoREngineChamberRemembranceCrystal: LocationData(0x130152, 575, "Chest"), - LocationName.CoREngineChamberAPBoost: LocationData(0x130153, 576, "Chest"), - LocationName.CoREngineChamberManifestIllusion: LocationData(0x130154, 577, "Chest"), - LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(0x130155, 582, "Chest"), - LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(0x130156, 579, "Chest"), - LocationName.TransporttoRemembrance: LocationData(0x130157, 72, "Get Bonus"), + LocationName.MarketplaceMap: LocationData(362, "Chest"), + LocationName.BoroughDriveRecovery: LocationData(194, "Chest"), + LocationName.BoroughAPBoost: LocationData(195, "Chest"), + LocationName.BoroughHiPotion: LocationData(196, "Chest"), + LocationName.BoroughMythrilShard: LocationData(305, "Chest"), + LocationName.BoroughDarkShard: LocationData(506, "Chest"), + LocationName.MerlinsHouseMembershipCard: LocationData(256, "Chest"), + LocationName.MerlinsHouseBlizzardElement: LocationData(292, "Chest"), + LocationName.Bailey: LocationData(47, "Get Bonus"), + LocationName.BaileySecretAnsemReport7: LocationData(531, "Chest"), + LocationName.BaseballCharm: LocationData(258, "Chest"), + LocationName.PosternCastlePerimeterMap: LocationData(310, "Chest"), + LocationName.PosternMythrilGem: LocationData(189, "Chest"), + LocationName.PosternAPBoost: LocationData(190, "Chest"), + LocationName.CorridorsMythrilStone: LocationData(200, "Chest"), + LocationName.CorridorsMythrilCrystal: LocationData(201, "Chest"), + LocationName.CorridorsDarkCrystal: LocationData(202, "Chest"), + LocationName.CorridorsAPBoost: LocationData(307, "Chest"), + LocationName.AnsemsStudyMasterForm: LocationData(276, "Chest"), + LocationName.AnsemsStudySleepingLion: LocationData(266, "Chest"), + LocationName.AnsemsStudySkillRecipe: LocationData(184, "Chest"), + LocationName.AnsemsStudyUkuleleCharm: LocationData(183, "Chest"), + LocationName.RestorationSiteMoonRecipe: LocationData(309, "Chest"), + LocationName.RestorationSiteAPBoost: LocationData(507, "Chest"), + LocationName.DemyxHB: LocationData(28, "Double Get Bonus"), + LocationName.DemyxHBGetBonus: LocationData(28, "Second Get Bonus"), + LocationName.FFFightsCureElement: LocationData(361, "Chest"), + LocationName.CrystalFissureTornPages: LocationData(179, "Chest"), + LocationName.CrystalFissureTheGreatMawMap: LocationData(489, "Chest"), + LocationName.CrystalFissureEnergyCrystal: LocationData(180, "Chest"), + LocationName.CrystalFissureAPBoost: LocationData(181, "Chest"), + LocationName.ThousandHeartless: LocationData(60, "Get Bonus"), + LocationName.ThousandHeartlessSecretAnsemReport1: LocationData(525, "Chest"), + LocationName.ThousandHeartlessIceCream: LocationData(269, "Chest"), + LocationName.ThousandHeartlessPicture: LocationData(511, "Chest"), + LocationName.PosternGullWing: LocationData(491, "Chest"), + LocationName.HeartlessManufactoryCosmicChain: LocationData(311, "Chest"), + LocationName.SephirothBonus: LocationData(35, "Get Bonus"), + LocationName.SephirothFenrir: LocationData(282, "Chest"), + LocationName.WinnersProof: LocationData(588, "Chest"), + LocationName.ProofofPeace: LocationData(589, "Chest"), + LocationName.DemyxDataAPBoost: LocationData(560, "Chest"), + LocationName.CoRDepthsAPBoost: LocationData(562, "Chest"), + LocationName.CoRDepthsPowerCrystal: LocationData(563, "Chest"), + LocationName.CoRDepthsFrostCrystal: LocationData(564, "Chest"), + LocationName.CoRDepthsManifestIllusion: LocationData(565, "Chest"), + LocationName.CoRDepthsAPBoost2: LocationData(566, "Chest"), + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap: LocationData(580, "Chest"), + LocationName.CoRMineshaftLowerLevelAPBoost: LocationData(578, "Chest"), + LocationName.CoRDepthsUpperLevelRemembranceGem: LocationData(567, "Chest"), + LocationName.CoRMiningAreaSerenityGem: LocationData(568, "Chest"), + LocationName.CoRMiningAreaAPBoost: LocationData(569, "Chest"), + LocationName.CoRMiningAreaSerenityCrystal: LocationData(570, "Chest"), + LocationName.CoRMiningAreaManifestIllusion: LocationData(571, "Chest"), + LocationName.CoRMiningAreaSerenityGem2: LocationData(572, "Chest"), + LocationName.CoRMiningAreaDarkRemembranceMap: LocationData(573, "Chest"), + LocationName.CoRMineshaftMidLevelPowerBoost: LocationData(581, "Chest"), + LocationName.CoREngineChamberSerenityCrystal: LocationData(574, "Chest"), + LocationName.CoREngineChamberRemembranceCrystal: LocationData(575, "Chest"), + LocationName.CoREngineChamberAPBoost: LocationData(576, "Chest"), + LocationName.CoREngineChamberManifestIllusion: LocationData(577, "Chest"), + LocationName.CoRMineshaftUpperLevelMagicBoost: LocationData(582, "Chest"), + LocationName.CoRMineshaftUpperLevelAPBoost: LocationData(579, "Chest"), + LocationName.TransporttoRemembrance: LocationData(72, "Get Bonus"), } PL_Checks = { - LocationName.GorgeSavannahMap: LocationData(0x130158, 492, "Chest"), - LocationName.GorgeDarkGem: LocationData(0x130159, 404, "Chest"), - LocationName.GorgeMythrilStone: LocationData(0x13015A, 405, "Chest"), - LocationName.ElephantGraveyardFrostGem: LocationData(0x13015B, 401, "Chest"), - LocationName.ElephantGraveyardMythrilStone: LocationData(0x13015C, 402, "Chest"), - LocationName.ElephantGraveyardBrightStone: LocationData(0x13015D, 403, "Chest"), - LocationName.ElephantGraveyardAPBoost: LocationData(0x13015E, 508, "Chest"), - LocationName.ElephantGraveyardMythrilShard: LocationData(0x13015F, 509, "Chest"), - LocationName.PrideRockMap: LocationData(0x130160, 418, "Chest"), - LocationName.PrideRockMythrilStone: LocationData(0x130161, 392, "Chest"), - LocationName.PrideRockSerenityCrystal: LocationData(0x130162, 393, "Chest"), - LocationName.WildebeestValleyEnergyStone: LocationData(0x130163, 396, "Chest"), - LocationName.WildebeestValleyAPBoost: LocationData(0x130164, 397, "Chest"), - LocationName.WildebeestValleyMythrilGem: LocationData(0x130165, 398, "Chest"), - LocationName.WildebeestValleyMythrilStone: LocationData(0x130166, 399, "Chest"), - LocationName.WildebeestValleyLucidGem: LocationData(0x130167, 400, "Chest"), - LocationName.WastelandsMythrilShard: LocationData(0x130168, 406, "Chest"), - LocationName.WastelandsSerenityGem: LocationData(0x130169, 407, "Chest"), - LocationName.WastelandsMythrilStone: LocationData(0x13016A, 408, "Chest"), - LocationName.JungleSerenityGem: LocationData(0x13016B, 409, "Chest"), - LocationName.JungleMythrilStone: LocationData(0x13016C, 410, "Chest"), - LocationName.JungleSerenityCrystal: LocationData(0x13016D, 411, "Chest"), - LocationName.OasisMap: LocationData(0x13016E, 412, "Chest"), - LocationName.OasisTornPages: LocationData(0x13016F, 493, "Chest"), - LocationName.OasisAPBoost: LocationData(0x130170, 413, "Chest"), - LocationName.CircleofLife: LocationData(0x130171, 264, "Chest"), - LocationName.Hyenas1: LocationData(0x130172, 49, "Get Bonus"), - LocationName.Scar: LocationData(0x130173, 29, "Get Bonus"), - LocationName.ScarFireElement: LocationData(0x130174, 302, "Chest"), - -} -PL2_Checks = { - LocationName.Hyenas2: LocationData(0x130175, 50, "Get Bonus"), - LocationName.Groundshaker: LocationData(0x130176, 30, "Double Get Bonus"), - LocationName.GroundshakerGetBonus: LocationData(0x130177, 30, "Second Get Bonus"), - LocationName.SaixDataDefenseBoost: LocationData(0x130178, 556, "Chest"), + LocationName.GorgeSavannahMap: LocationData(492, "Chest"), + LocationName.GorgeDarkGem: LocationData(404, "Chest"), + LocationName.GorgeMythrilStone: LocationData(405, "Chest"), + LocationName.ElephantGraveyardFrostGem: LocationData(401, "Chest"), + LocationName.ElephantGraveyardMythrilStone: LocationData(402, "Chest"), + LocationName.ElephantGraveyardBrightStone: LocationData(403, "Chest"), + LocationName.ElephantGraveyardAPBoost: LocationData(508, "Chest"), + LocationName.ElephantGraveyardMythrilShard: LocationData(509, "Chest"), + LocationName.PrideRockMap: LocationData(418, "Chest"), + LocationName.PrideRockMythrilStone: LocationData(392, "Chest"), + LocationName.PrideRockSerenityCrystal: LocationData(393, "Chest"), + LocationName.WildebeestValleyEnergyStone: LocationData(396, "Chest"), + LocationName.WildebeestValleyAPBoost: LocationData(397, "Chest"), + LocationName.WildebeestValleyMythrilGem: LocationData(398, "Chest"), + LocationName.WildebeestValleyMythrilStone: LocationData(399, "Chest"), + LocationName.WildebeestValleyLucidGem: LocationData(400, "Chest"), + LocationName.WastelandsMythrilShard: LocationData(406, "Chest"), + LocationName.WastelandsSerenityGem: LocationData(407, "Chest"), + LocationName.WastelandsMythrilStone: LocationData(408, "Chest"), + LocationName.JungleSerenityGem: LocationData(409, "Chest"), + LocationName.JungleMythrilStone: LocationData(410, "Chest"), + LocationName.JungleSerenityCrystal: LocationData(411, "Chest"), + LocationName.OasisMap: LocationData(412, "Chest"), + LocationName.OasisTornPages: LocationData(493, "Chest"), + LocationName.OasisAPBoost: LocationData(413, "Chest"), + LocationName.CircleofLife: LocationData(264, "Chest"), + LocationName.Hyenas1: LocationData(49, "Get Bonus"), + LocationName.Scar: LocationData(29, "Get Bonus"), + LocationName.ScarFireElement: LocationData(302, "Chest"), + LocationName.Hyenas2: LocationData(50, "Get Bonus"), + LocationName.Groundshaker: LocationData(30, "Double Get Bonus"), + LocationName.GroundshakerGetBonus: LocationData(30, "Second Get Bonus"), + LocationName.SaixDataDefenseBoost: LocationData(556, "Chest"), } STT_Checks = { - LocationName.TwilightTownMap: LocationData(0x130179, 319, "Chest"), - LocationName.MunnyPouchOlette: LocationData(0x13017A, 288, "Chest"), - LocationName.StationDusks: LocationData(0x13017B, 54, "Get Bonus", "Roxas", 14), - LocationName.StationofSerenityPotion: LocationData(0x13017C, 315, "Chest"), - LocationName.StationofCallingPotion: LocationData(0x13017D, 472, "Chest"), - LocationName.TwilightThorn: LocationData(0x13017E, 33, "Get Bonus", "Roxas", 14), - LocationName.Axel1: LocationData(0x13017F, 73, "Get Bonus", "Roxas", 14), - LocationName.JunkChampionBelt: LocationData(0x130180, 389, "Chest"), - LocationName.JunkMedal: LocationData(0x130181, 390, "Chest"), - LocationName.TheStruggleTrophy: LocationData(0x130182, 519, "Chest"), - LocationName.CentralStationPotion1: LocationData(0x130183, 428, "Chest"), - LocationName.STTCentralStationHiPotion: LocationData(0x130184, 429, "Chest"), - LocationName.CentralStationPotion2: LocationData(0x130185, 430, "Chest"), - LocationName.SunsetTerraceAbilityRing: LocationData(0x130186, 434, "Chest"), - LocationName.SunsetTerraceHiPotion: LocationData(0x130187, 435, "Chest"), - LocationName.SunsetTerracePotion1: LocationData(0x130188, 436, "Chest"), - LocationName.SunsetTerracePotion2: LocationData(0x130189, 437, "Chest"), - LocationName.MansionFoyerHiPotion: LocationData(0x13018A, 449, "Chest"), - LocationName.MansionFoyerPotion1: LocationData(0x13018B, 450, "Chest"), - LocationName.MansionFoyerPotion2: LocationData(0x13018C, 451, "Chest"), - LocationName.MansionDiningRoomElvenBandanna: LocationData(0x13018D, 455, "Chest"), - LocationName.MansionDiningRoomPotion: LocationData(0x13018E, 456, "Chest"), - LocationName.NaminesSketches: LocationData(0x13018F, 289, "Chest"), - LocationName.MansionMap: LocationData(0x130190, 483, "Chest"), - LocationName.MansionLibraryHiPotion: LocationData(0x130191, 459, "Chest"), - LocationName.Axel2: LocationData(0x130192, 34, "Get Bonus", "Roxas", 14), - LocationName.MansionBasementCorridorHiPotion: LocationData(0x130193, 463, "Chest"), - LocationName.RoxasDataMagicBoost: LocationData(0x130194, 558, "Chest"), + LocationName.TwilightTownMap: LocationData(319, "Chest"), + LocationName.MunnyPouchOlette: LocationData(288, "Chest"), + LocationName.StationDusks: LocationData(54, "Get Bonus", "Roxas", 14), + LocationName.StationofSerenityPotion: LocationData(315, "Chest"), + LocationName.StationofCallingPotion: LocationData(472, "Chest"), + LocationName.TwilightThorn: LocationData(33, "Get Bonus", "Roxas", 14), + LocationName.Axel1: LocationData(73, "Get Bonus", "Roxas", 14), + LocationName.JunkChampionBelt: LocationData(389, "Chest"), + LocationName.JunkMedal: LocationData(390, "Chest"), + LocationName.TheStruggleTrophy: LocationData(519, "Chest"), + LocationName.CentralStationPotion1: LocationData(428, "Chest"), + LocationName.STTCentralStationHiPotion: LocationData(429, "Chest"), + LocationName.CentralStationPotion2: LocationData(430, "Chest"), + LocationName.SunsetTerraceAbilityRing: LocationData(434, "Chest"), + LocationName.SunsetTerraceHiPotion: LocationData(435, "Chest"), + LocationName.SunsetTerracePotion1: LocationData(436, "Chest"), + LocationName.SunsetTerracePotion2: LocationData(437, "Chest"), + LocationName.MansionFoyerHiPotion: LocationData(449, "Chest"), + LocationName.MansionFoyerPotion1: LocationData(450, "Chest"), + LocationName.MansionFoyerPotion2: LocationData(451, "Chest"), + LocationName.MansionDiningRoomElvenBandanna: LocationData(455, "Chest"), + LocationName.MansionDiningRoomPotion: LocationData(456, "Chest"), + LocationName.NaminesSketches: LocationData(289, "Chest"), + LocationName.MansionMap: LocationData(483, "Chest"), + LocationName.MansionLibraryHiPotion: LocationData(459, "Chest"), + LocationName.Axel2: LocationData(34, "Get Bonus", "Roxas", 14), + LocationName.MansionBasementCorridorHiPotion: LocationData(463, "Chest"), + LocationName.RoxasDataMagicBoost: LocationData(558, "Chest"), } TT_Checks = { - LocationName.OldMansionPotion: LocationData(0x130195, 447, "Chest"), - LocationName.OldMansionMythrilShard: LocationData(0x130196, 448, "Chest"), - LocationName.TheWoodsPotion: LocationData(0x130197, 442, "Chest"), - LocationName.TheWoodsMythrilShard: LocationData(0x130198, 443, "Chest"), - LocationName.TheWoodsHiPotion: LocationData(0x130199, 444, "Chest"), - LocationName.TramCommonHiPotion: LocationData(0x13019A, 420, "Chest"), - LocationName.TramCommonAPBoost: LocationData(0x13019B, 421, "Chest"), - LocationName.TramCommonTent: LocationData(0x13019C, 422, "Chest"), - LocationName.TramCommonMythrilShard1: LocationData(0x13019D, 423, "Chest"), - LocationName.TramCommonPotion1: LocationData(0x13019E, 424, "Chest"), - LocationName.TramCommonMythrilShard2: LocationData(0x13019F, 425, "Chest"), - LocationName.TramCommonPotion2: LocationData(0x1301A0, 484, "Chest"), - LocationName.StationPlazaSecretAnsemReport2: LocationData(0x1301A1, 526, "Chest"), - LocationName.MunnyPouchMickey: LocationData(0x1301A2, 290, "Chest"), - LocationName.CrystalOrb: LocationData(0x1301A3, 291, "Chest"), - LocationName.CentralStationTent: LocationData(0x1301A4, 431, "Chest"), - LocationName.TTCentralStationHiPotion: LocationData(0x1301A5, 432, "Chest"), - LocationName.CentralStationMythrilShard: LocationData(0x1301A6, 433, "Chest"), - LocationName.TheTowerPotion: LocationData(0x1301A7, 465, "Chest"), - LocationName.TheTowerHiPotion: LocationData(0x1301A8, 466, "Chest"), - LocationName.TheTowerEther: LocationData(0x1301A9, 522, "Chest"), - LocationName.TowerEntrywayEther: LocationData(0x1301AA, 467, "Chest"), - LocationName.TowerEntrywayMythrilShard: LocationData(0x1301AB, 468, "Chest"), - LocationName.SorcerersLoftTowerMap: LocationData(0x1301AC, 469, "Chest"), - LocationName.TowerWardrobeMythrilStone: LocationData(0x1301AD, 470, "Chest"), - LocationName.StarSeeker: LocationData(0x1301AE, 304, "Chest"), - LocationName.ValorForm: LocationData(0x1301AF, 286, "Chest"), - -} -TT2_Checks = { - LocationName.SeifersTrophy: LocationData(0x1301B0, 294, "Chest"), - LocationName.Oathkeeper: LocationData(0x1301B1, 265, "Chest"), - LocationName.LimitForm: LocationData(0x1301B2, 543, "Chest"), -} -TT3_Checks = { - LocationName.UndergroundConcourseMythrilGem: LocationData(0x1301B3, 479, "Chest"), - LocationName.UndergroundConcourseAPBoost: LocationData(0x1301B4, 481, "Chest"), - LocationName.UndergroundConcourseOrichalcum: LocationData(0x1301B5, 480, "Chest"), - LocationName.UndergroundConcourseMythrilCrystal: LocationData(0x1301B6, 482, "Chest"), - LocationName.TunnelwayOrichalcum: LocationData(0x1301B7, 477, "Chest"), - LocationName.TunnelwayMythrilCrystal: LocationData(0x1301B8, 478, "Chest"), - LocationName.SunsetTerraceOrichalcumPlus: LocationData(0x1301B9, 438, "Chest"), - LocationName.SunsetTerraceMythrilShard: LocationData(0x1301BA, 439, "Chest"), - LocationName.SunsetTerraceMythrilCrystal: LocationData(0x1301BB, 440, "Chest"), - LocationName.SunsetTerraceAPBoost: LocationData(0x1301BC, 441, "Chest"), - LocationName.MansionNobodies: LocationData(0x1301BD, 56, "Get Bonus"), - LocationName.MansionFoyerMythrilCrystal: LocationData(0x1301BE, 452, "Chest"), - LocationName.MansionFoyerMythrilStone: LocationData(0x1301BF, 453, "Chest"), - LocationName.MansionFoyerSerenityCrystal: LocationData(0x1301C0, 454, "Chest"), - LocationName.MansionDiningRoomMythrilCrystal: LocationData(0x1301C1, 457, "Chest"), - LocationName.MansionDiningRoomMythrilStone: LocationData(0x1301C2, 458, "Chest"), - LocationName.MansionLibraryOrichalcum: LocationData(0x1301C3, 460, "Chest"), - LocationName.BeamSecretAnsemReport10: LocationData(0x1301C4, 534, "Chest"), - LocationName.MansionBasementCorridorUltimateRecipe: LocationData(0x1301C5, 464, "Chest"), - LocationName.BetwixtandBetween: LocationData(0x1301C6, 63, "Get Bonus"), - LocationName.BetwixtandBetweenBondofFlame: LocationData(0x1301C7, 317, "Chest"), - LocationName.AxelDataMagicBoost: LocationData(0x1301C8, 561, "Chest"), + LocationName.OldMansionPotion: LocationData(447, "Chest"), + LocationName.OldMansionMythrilShard: LocationData(448, "Chest"), + LocationName.TheWoodsPotion: LocationData(442, "Chest"), + LocationName.TheWoodsMythrilShard: LocationData(443, "Chest"), + LocationName.TheWoodsHiPotion: LocationData(444, "Chest"), + LocationName.TramCommonHiPotion: LocationData(420, "Chest"), + LocationName.TramCommonAPBoost: LocationData(421, "Chest"), + LocationName.TramCommonTent: LocationData(422, "Chest"), + LocationName.TramCommonMythrilShard1: LocationData(423, "Chest"), + LocationName.TramCommonPotion1: LocationData(424, "Chest"), + LocationName.TramCommonMythrilShard2: LocationData(425, "Chest"), + LocationName.TramCommonPotion2: LocationData(484, "Chest"), + LocationName.StationPlazaSecretAnsemReport2: LocationData(526, "Chest"), + LocationName.MunnyPouchMickey: LocationData(290, "Chest"), + LocationName.CrystalOrb: LocationData(291, "Chest"), + LocationName.CentralStationTent: LocationData(431, "Chest"), + LocationName.TTCentralStationHiPotion: LocationData(432, "Chest"), + LocationName.CentralStationMythrilShard: LocationData(433, "Chest"), + LocationName.TheTowerPotion: LocationData(465, "Chest"), + LocationName.TheTowerHiPotion: LocationData(466, "Chest"), + LocationName.TheTowerEther: LocationData(522, "Chest"), + LocationName.TowerEntrywayEther: LocationData(467, "Chest"), + LocationName.TowerEntrywayMythrilShard: LocationData(468, "Chest"), + LocationName.SorcerersLoftTowerMap: LocationData(469, "Chest"), + LocationName.TowerWardrobeMythrilStone: LocationData(470, "Chest"), + LocationName.StarSeeker: LocationData(304, "Chest"), + LocationName.ValorForm: LocationData(286, "Chest"), + LocationName.SeifersTrophy: LocationData(294, "Chest"), + LocationName.Oathkeeper: LocationData(265, "Chest"), + LocationName.LimitForm: LocationData(543, "Chest"), + LocationName.UndergroundConcourseMythrilGem: LocationData(479, "Chest"), + LocationName.UndergroundConcourseAPBoost: LocationData(481, "Chest"), + LocationName.UndergroundConcourseOrichalcum: LocationData(480, "Chest"), + LocationName.UndergroundConcourseMythrilCrystal: LocationData(482, "Chest"), + LocationName.TunnelwayOrichalcum: LocationData(477, "Chest"), + LocationName.TunnelwayMythrilCrystal: LocationData(478, "Chest"), + LocationName.SunsetTerraceOrichalcumPlus: LocationData(438, "Chest"), + LocationName.SunsetTerraceMythrilShard: LocationData(439, "Chest"), + LocationName.SunsetTerraceMythrilCrystal: LocationData(440, "Chest"), + LocationName.SunsetTerraceAPBoost: LocationData(441, "Chest"), + LocationName.MansionNobodies: LocationData(56, "Get Bonus"), + LocationName.MansionFoyerMythrilCrystal: LocationData(452, "Chest"), + LocationName.MansionFoyerMythrilStone: LocationData(453, "Chest"), + LocationName.MansionFoyerSerenityCrystal: LocationData(454, "Chest"), + LocationName.MansionDiningRoomMythrilCrystal: LocationData(457, "Chest"), + LocationName.MansionDiningRoomMythrilStone: LocationData(458, "Chest"), + LocationName.MansionLibraryOrichalcum: LocationData(460, "Chest"), + LocationName.BeamSecretAnsemReport10: LocationData(534, "Chest"), + LocationName.MansionBasementCorridorUltimateRecipe: LocationData(464, "Chest"), + LocationName.BetwixtandBetween: LocationData(63, "Get Bonus"), + LocationName.BetwixtandBetweenBondofFlame: LocationData(317, "Chest"), + LocationName.AxelDataMagicBoost: LocationData(561, "Chest"), } TWTNW_Checks = { - LocationName.FragmentCrossingMythrilStone: LocationData(0x1301C9, 374, "Chest"), - LocationName.FragmentCrossingMythrilCrystal: LocationData(0x1301CA, 375, "Chest"), - LocationName.FragmentCrossingAPBoost: LocationData(0x1301CB, 376, "Chest"), - LocationName.FragmentCrossingOrichalcum: LocationData(0x1301CC, 377, "Chest"), - LocationName.Roxas: LocationData(0x1301CD, 69, "Double Get Bonus"), - LocationName.RoxasGetBonus: LocationData(0x1301CE, 69, "Second Get Bonus"), - LocationName.RoxasSecretAnsemReport8: LocationData(0x1301CF, 532, "Chest"), - LocationName.TwoBecomeOne: LocationData(0x1301D0, 277, "Chest"), - LocationName.MemorysSkyscaperMythrilCrystal: LocationData(0x1301D1, 391, "Chest"), - LocationName.MemorysSkyscaperAPBoost: LocationData(0x1301D2, 523, "Chest"), - LocationName.MemorysSkyscaperMythrilStone: LocationData(0x1301D3, 524, "Chest"), - LocationName.TheBrinkofDespairDarkCityMap: LocationData(0x1301D4, 335, "Chest"), - LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(0x1301D5, 500, "Chest"), - LocationName.NothingsCallMythrilGem: LocationData(0x1301D6, 378, "Chest"), - LocationName.NothingsCallOrichalcum: LocationData(0x1301D7, 379, "Chest"), - LocationName.TwilightsViewCosmicBelt: LocationData(0x1301D8, 336, "Chest"), -} -TWTNW2_Checks = { - LocationName.XigbarBonus: LocationData(0x1301D9, 23, "Get Bonus"), - LocationName.XigbarSecretAnsemReport3: LocationData(0x1301DA, 527, "Chest"), - LocationName.NaughtsSkywayMythrilGem: LocationData(0x1301DB, 380, "Chest"), - LocationName.NaughtsSkywayOrichalcum: LocationData(0x1301DC, 381, "Chest"), - LocationName.NaughtsSkywayMythrilCrystal: LocationData(0x1301DD, 382, "Chest"), - LocationName.Oblivion: LocationData(0x1301DE, 278, "Chest"), - LocationName.CastleThatNeverWasMap: LocationData(0x1301DF, 496, "Chest"), - LocationName.Luxord: LocationData(0x1301E0, 24, "Double Get Bonus"), - LocationName.LuxordGetBonus: LocationData(0x1301E1, 24, "Second Get Bonus"), - LocationName.LuxordSecretAnsemReport9: LocationData(0x1301E2, 533, "Chest"), - LocationName.SaixBonus: LocationData(0x1301E3, 25, "Get Bonus"), - LocationName.SaixSecretAnsemReport12: LocationData(0x1301E4, 536, "Chest"), - LocationName.PreXemnas1SecretAnsemReport11: LocationData(0x1301E5, 535, "Chest"), - LocationName.RuinandCreationsPassageMythrilStone: LocationData(0x1301E6, 385, "Chest"), - LocationName.RuinandCreationsPassageAPBoost: LocationData(0x1301E7, 386, "Chest"), - LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(0x1301E8, 387, "Chest"), - LocationName.RuinandCreationsPassageOrichalcum: LocationData(0x1301E9, 388, "Chest"), - LocationName.Xemnas1: LocationData(0x1301EA, 26, "Double Get Bonus"), - LocationName.Xemnas1GetBonus: LocationData(0x1301EB, 26, "Second Get Bonus"), - LocationName.Xemnas1SecretAnsemReport13: LocationData(0x1301EC, 537, "Chest"), - LocationName.FinalXemnas: LocationData(0x1301ED, 71, "Get Bonus"), - LocationName.XemnasDataPowerBoost: LocationData(0x1301EE, 554, "Chest"), + LocationName.FragmentCrossingMythrilStone: LocationData(374, "Chest"), + LocationName.FragmentCrossingMythrilCrystal: LocationData(375, "Chest"), + LocationName.FragmentCrossingAPBoost: LocationData(376, "Chest"), + LocationName.FragmentCrossingOrichalcum: LocationData(377, "Chest"), + LocationName.Roxas: LocationData(69, "Double Get Bonus"), + LocationName.RoxasGetBonus: LocationData(69, "Second Get Bonus"), + LocationName.RoxasSecretAnsemReport8: LocationData(532, "Chest"), + LocationName.TwoBecomeOne: LocationData(277, "Chest"), + LocationName.MemorysSkyscaperMythrilCrystal: LocationData(391, "Chest"), + LocationName.MemorysSkyscaperAPBoost: LocationData(523, "Chest"), + LocationName.MemorysSkyscaperMythrilStone: LocationData(524, "Chest"), + LocationName.TheBrinkofDespairDarkCityMap: LocationData(335, "Chest"), + LocationName.TheBrinkofDespairOrichalcumPlus: LocationData(500, "Chest"), + LocationName.NothingsCallMythrilGem: LocationData(378, "Chest"), + LocationName.NothingsCallOrichalcum: LocationData(379, "Chest"), + LocationName.TwilightsViewCosmicBelt: LocationData(336, "Chest"), + LocationName.XigbarBonus: LocationData(23, "Get Bonus"), + LocationName.XigbarSecretAnsemReport3: LocationData(527, "Chest"), + LocationName.NaughtsSkywayMythrilGem: LocationData(380, "Chest"), + LocationName.NaughtsSkywayOrichalcum: LocationData(381, "Chest"), + LocationName.NaughtsSkywayMythrilCrystal: LocationData(382, "Chest"), + LocationName.Oblivion: LocationData(278, "Chest"), + LocationName.CastleThatNeverWasMap: LocationData(496, "Chest"), + LocationName.Luxord: LocationData(24, "Double Get Bonus"), + LocationName.LuxordGetBonus: LocationData(24, "Second Get Bonus"), + LocationName.LuxordSecretAnsemReport9: LocationData(533, "Chest"), + LocationName.SaixBonus: LocationData(25, "Get Bonus"), + LocationName.SaixSecretAnsemReport12: LocationData(536, "Chest"), + LocationName.PreXemnas1SecretAnsemReport11: LocationData(535, "Chest"), + LocationName.RuinandCreationsPassageMythrilStone: LocationData(385, "Chest"), + LocationName.RuinandCreationsPassageAPBoost: LocationData(386, "Chest"), + LocationName.RuinandCreationsPassageMythrilCrystal: LocationData(387, "Chest"), + LocationName.RuinandCreationsPassageOrichalcum: LocationData(388, "Chest"), + LocationName.Xemnas1: LocationData(26, "Double Get Bonus"), + LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"), + LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"), + LocationName.FinalXemnas: LocationData(71, "Get Bonus"), + LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"), } SoraLevels = { - LocationName.Lvl1: LocationData(0x1301EF, 1, "Levels"), - LocationName.Lvl2: LocationData(0x1301F0, 2, "Levels"), - LocationName.Lvl3: LocationData(0x1301F1, 3, "Levels"), - LocationName.Lvl4: LocationData(0x1301F2, 4, "Levels"), - LocationName.Lvl5: LocationData(0x1301F3, 5, "Levels"), - LocationName.Lvl6: LocationData(0x1301F4, 6, "Levels"), - LocationName.Lvl7: LocationData(0x1301F5, 7, "Levels"), - LocationName.Lvl8: LocationData(0x1301F6, 8, "Levels"), - LocationName.Lvl9: LocationData(0x1301F7, 9, "Levels"), - LocationName.Lvl10: LocationData(0x1301F8, 10, "Levels"), - LocationName.Lvl11: LocationData(0x1301F9, 11, "Levels"), - LocationName.Lvl12: LocationData(0x1301FA, 12, "Levels"), - LocationName.Lvl13: LocationData(0x1301FB, 13, "Levels"), - LocationName.Lvl14: LocationData(0x1301FC, 14, "Levels"), - LocationName.Lvl15: LocationData(0x1301FD, 15, "Levels"), - LocationName.Lvl16: LocationData(0x1301FE, 16, "Levels"), - LocationName.Lvl17: LocationData(0x1301FF, 17, "Levels"), - LocationName.Lvl18: LocationData(0x130200, 18, "Levels"), - LocationName.Lvl19: LocationData(0x130201, 19, "Levels"), - LocationName.Lvl20: LocationData(0x130202, 20, "Levels"), - LocationName.Lvl21: LocationData(0x130203, 21, "Levels"), - LocationName.Lvl22: LocationData(0x130204, 22, "Levels"), - LocationName.Lvl23: LocationData(0x130205, 23, "Levels"), - LocationName.Lvl24: LocationData(0x130206, 24, "Levels"), - LocationName.Lvl25: LocationData(0x130207, 25, "Levels"), - LocationName.Lvl26: LocationData(0x130208, 26, "Levels"), - LocationName.Lvl27: LocationData(0x130209, 27, "Levels"), - LocationName.Lvl28: LocationData(0x13020A, 28, "Levels"), - LocationName.Lvl29: LocationData(0x13020B, 29, "Levels"), - LocationName.Lvl30: LocationData(0x13020C, 30, "Levels"), - LocationName.Lvl31: LocationData(0x13020D, 31, "Levels"), - LocationName.Lvl32: LocationData(0x13020E, 32, "Levels"), - LocationName.Lvl33: LocationData(0x13020F, 33, "Levels"), - LocationName.Lvl34: LocationData(0x130210, 34, "Levels"), - LocationName.Lvl35: LocationData(0x130211, 35, "Levels"), - LocationName.Lvl36: LocationData(0x130212, 36, "Levels"), - LocationName.Lvl37: LocationData(0x130213, 37, "Levels"), - LocationName.Lvl38: LocationData(0x130214, 38, "Levels"), - LocationName.Lvl39: LocationData(0x130215, 39, "Levels"), - LocationName.Lvl40: LocationData(0x130216, 40, "Levels"), - LocationName.Lvl41: LocationData(0x130217, 41, "Levels"), - LocationName.Lvl42: LocationData(0x130218, 42, "Levels"), - LocationName.Lvl43: LocationData(0x130219, 43, "Levels"), - LocationName.Lvl44: LocationData(0x13021A, 44, "Levels"), - LocationName.Lvl45: LocationData(0x13021B, 45, "Levels"), - LocationName.Lvl46: LocationData(0x13021C, 46, "Levels"), - LocationName.Lvl47: LocationData(0x13021D, 47, "Levels"), - LocationName.Lvl48: LocationData(0x13021E, 48, "Levels"), - LocationName.Lvl49: LocationData(0x13021F, 49, "Levels"), - LocationName.Lvl50: LocationData(0x130220, 50, "Levels"), - LocationName.Lvl51: LocationData(0x130221, 51, "Levels"), - LocationName.Lvl52: LocationData(0x130222, 52, "Levels"), - LocationName.Lvl53: LocationData(0x130223, 53, "Levels"), - LocationName.Lvl54: LocationData(0x130224, 54, "Levels"), - LocationName.Lvl55: LocationData(0x130225, 55, "Levels"), - LocationName.Lvl56: LocationData(0x130226, 56, "Levels"), - LocationName.Lvl57: LocationData(0x130227, 57, "Levels"), - LocationName.Lvl58: LocationData(0x130228, 58, "Levels"), - LocationName.Lvl59: LocationData(0x130229, 59, "Levels"), - LocationName.Lvl60: LocationData(0x13022A, 60, "Levels"), - LocationName.Lvl61: LocationData(0x13022B, 61, "Levels"), - LocationName.Lvl62: LocationData(0x13022C, 62, "Levels"), - LocationName.Lvl63: LocationData(0x13022D, 63, "Levels"), - LocationName.Lvl64: LocationData(0x13022E, 64, "Levels"), - LocationName.Lvl65: LocationData(0x13022F, 65, "Levels"), - LocationName.Lvl66: LocationData(0x130230, 66, "Levels"), - LocationName.Lvl67: LocationData(0x130231, 67, "Levels"), - LocationName.Lvl68: LocationData(0x130232, 68, "Levels"), - LocationName.Lvl69: LocationData(0x130233, 69, "Levels"), - LocationName.Lvl70: LocationData(0x130234, 70, "Levels"), - LocationName.Lvl71: LocationData(0x130235, 71, "Levels"), - LocationName.Lvl72: LocationData(0x130236, 72, "Levels"), - LocationName.Lvl73: LocationData(0x130237, 73, "Levels"), - LocationName.Lvl74: LocationData(0x130238, 74, "Levels"), - LocationName.Lvl75: LocationData(0x130239, 75, "Levels"), - LocationName.Lvl76: LocationData(0x13023A, 76, "Levels"), - LocationName.Lvl77: LocationData(0x13023B, 77, "Levels"), - LocationName.Lvl78: LocationData(0x13023C, 78, "Levels"), - LocationName.Lvl79: LocationData(0x13023D, 79, "Levels"), - LocationName.Lvl80: LocationData(0x13023E, 80, "Levels"), - LocationName.Lvl81: LocationData(0x13023F, 81, "Levels"), - LocationName.Lvl82: LocationData(0x130240, 82, "Levels"), - LocationName.Lvl83: LocationData(0x130241, 83, "Levels"), - LocationName.Lvl84: LocationData(0x130242, 84, "Levels"), - LocationName.Lvl85: LocationData(0x130243, 85, "Levels"), - LocationName.Lvl86: LocationData(0x130244, 86, "Levels"), - LocationName.Lvl87: LocationData(0x130245, 87, "Levels"), - LocationName.Lvl88: LocationData(0x130246, 88, "Levels"), - LocationName.Lvl89: LocationData(0x130247, 89, "Levels"), - LocationName.Lvl90: LocationData(0x130248, 90, "Levels"), - LocationName.Lvl91: LocationData(0x130249, 91, "Levels"), - LocationName.Lvl92: LocationData(0x13024A, 92, "Levels"), - LocationName.Lvl93: LocationData(0x13024B, 93, "Levels"), - LocationName.Lvl94: LocationData(0x13024C, 94, "Levels"), - LocationName.Lvl95: LocationData(0x13024D, 95, "Levels"), - LocationName.Lvl96: LocationData(0x13024E, 96, "Levels"), - LocationName.Lvl97: LocationData(0x13024F, 97, "Levels"), - LocationName.Lvl98: LocationData(0x130250, 98, "Levels"), - LocationName.Lvl99: LocationData(0x130251, 99, "Levels"), + LocationName.Lvl2: LocationData(2, "Levels"), + LocationName.Lvl3: LocationData(3, "Levels"), + LocationName.Lvl4: LocationData(4, "Levels"), + LocationName.Lvl5: LocationData(5, "Levels"), + LocationName.Lvl6: LocationData(6, "Levels"), + LocationName.Lvl7: LocationData(7, "Levels"), + LocationName.Lvl8: LocationData(8, "Levels"), + LocationName.Lvl9: LocationData(9, "Levels"), + LocationName.Lvl10: LocationData(10, "Levels"), + LocationName.Lvl11: LocationData(11, "Levels"), + LocationName.Lvl12: LocationData(12, "Levels"), + LocationName.Lvl13: LocationData(13, "Levels"), + LocationName.Lvl14: LocationData(14, "Levels"), + LocationName.Lvl15: LocationData(15, "Levels"), + LocationName.Lvl16: LocationData(16, "Levels"), + LocationName.Lvl17: LocationData(17, "Levels"), + LocationName.Lvl18: LocationData(18, "Levels"), + LocationName.Lvl19: LocationData(19, "Levels"), + LocationName.Lvl20: LocationData(20, "Levels"), + LocationName.Lvl21: LocationData(21, "Levels"), + LocationName.Lvl22: LocationData(22, "Levels"), + LocationName.Lvl23: LocationData(23, "Levels"), + LocationName.Lvl24: LocationData(24, "Levels"), + LocationName.Lvl25: LocationData(25, "Levels"), + LocationName.Lvl26: LocationData(26, "Levels"), + LocationName.Lvl27: LocationData(27, "Levels"), + LocationName.Lvl28: LocationData(28, "Levels"), + LocationName.Lvl29: LocationData(29, "Levels"), + LocationName.Lvl30: LocationData(30, "Levels"), + LocationName.Lvl31: LocationData(31, "Levels"), + LocationName.Lvl32: LocationData(32, "Levels"), + LocationName.Lvl33: LocationData(33, "Levels"), + LocationName.Lvl34: LocationData(34, "Levels"), + LocationName.Lvl35: LocationData(35, "Levels"), + LocationName.Lvl36: LocationData(36, "Levels"), + LocationName.Lvl37: LocationData(37, "Levels"), + LocationName.Lvl38: LocationData(38, "Levels"), + LocationName.Lvl39: LocationData(39, "Levels"), + LocationName.Lvl40: LocationData(40, "Levels"), + LocationName.Lvl41: LocationData(41, "Levels"), + LocationName.Lvl42: LocationData(42, "Levels"), + LocationName.Lvl43: LocationData(43, "Levels"), + LocationName.Lvl44: LocationData(44, "Levels"), + LocationName.Lvl45: LocationData(45, "Levels"), + LocationName.Lvl46: LocationData(46, "Levels"), + LocationName.Lvl47: LocationData(47, "Levels"), + LocationName.Lvl48: LocationData(48, "Levels"), + LocationName.Lvl49: LocationData(49, "Levels"), + LocationName.Lvl50: LocationData(50, "Levels"), + LocationName.Lvl51: LocationData(51, "Levels"), + LocationName.Lvl52: LocationData(52, "Levels"), + LocationName.Lvl53: LocationData(53, "Levels"), + LocationName.Lvl54: LocationData(54, "Levels"), + LocationName.Lvl55: LocationData(55, "Levels"), + LocationName.Lvl56: LocationData(56, "Levels"), + LocationName.Lvl57: LocationData(57, "Levels"), + LocationName.Lvl58: LocationData(58, "Levels"), + LocationName.Lvl59: LocationData(59, "Levels"), + LocationName.Lvl60: LocationData(60, "Levels"), + LocationName.Lvl61: LocationData(61, "Levels"), + LocationName.Lvl62: LocationData(62, "Levels"), + LocationName.Lvl63: LocationData(63, "Levels"), + LocationName.Lvl64: LocationData(64, "Levels"), + LocationName.Lvl65: LocationData(65, "Levels"), + LocationName.Lvl66: LocationData(66, "Levels"), + LocationName.Lvl67: LocationData(67, "Levels"), + LocationName.Lvl68: LocationData(68, "Levels"), + LocationName.Lvl69: LocationData(69, "Levels"), + LocationName.Lvl70: LocationData(70, "Levels"), + LocationName.Lvl71: LocationData(71, "Levels"), + LocationName.Lvl72: LocationData(72, "Levels"), + LocationName.Lvl73: LocationData(73, "Levels"), + LocationName.Lvl74: LocationData(74, "Levels"), + LocationName.Lvl75: LocationData(75, "Levels"), + LocationName.Lvl76: LocationData(76, "Levels"), + LocationName.Lvl77: LocationData(77, "Levels"), + LocationName.Lvl78: LocationData(78, "Levels"), + LocationName.Lvl79: LocationData(79, "Levels"), + LocationName.Lvl80: LocationData(80, "Levels"), + LocationName.Lvl81: LocationData(81, "Levels"), + LocationName.Lvl82: LocationData(82, "Levels"), + LocationName.Lvl83: LocationData(83, "Levels"), + LocationName.Lvl84: LocationData(84, "Levels"), + LocationName.Lvl85: LocationData(85, "Levels"), + LocationName.Lvl86: LocationData(86, "Levels"), + LocationName.Lvl87: LocationData(87, "Levels"), + LocationName.Lvl88: LocationData(88, "Levels"), + LocationName.Lvl89: LocationData(89, "Levels"), + LocationName.Lvl90: LocationData(90, "Levels"), + LocationName.Lvl91: LocationData(91, "Levels"), + LocationName.Lvl92: LocationData(92, "Levels"), + LocationName.Lvl93: LocationData(93, "Levels"), + LocationName.Lvl94: LocationData(94, "Levels"), + LocationName.Lvl95: LocationData(95, "Levels"), + LocationName.Lvl96: LocationData(96, "Levels"), + LocationName.Lvl97: LocationData(97, "Levels"), + LocationName.Lvl98: LocationData(98, "Levels"), + LocationName.Lvl99: LocationData(99, "Levels"), } Form_Checks = { - LocationName.Valorlvl2: LocationData(0x130253, 2, "Forms", 1), - LocationName.Valorlvl3: LocationData(0x130254, 3, "Forms", 1), - LocationName.Valorlvl4: LocationData(0x130255, 4, "Forms", 1), - LocationName.Valorlvl5: LocationData(0x130256, 5, "Forms", 1), - LocationName.Valorlvl6: LocationData(0x130257, 6, "Forms", 1), - LocationName.Valorlvl7: LocationData(0x130258, 7, "Forms", 1), + LocationName.Valorlvl2: LocationData(2, "Forms", 1), + LocationName.Valorlvl3: LocationData(3, "Forms", 1), + LocationName.Valorlvl4: LocationData(4, "Forms", 1), + LocationName.Valorlvl5: LocationData(5, "Forms", 1), + LocationName.Valorlvl6: LocationData(6, "Forms", 1), + LocationName.Valorlvl7: LocationData(7, "Forms", 1), - LocationName.Wisdomlvl2: LocationData(0x13025A, 2, "Forms", 2), - LocationName.Wisdomlvl3: LocationData(0x13025B, 3, "Forms", 2), - LocationName.Wisdomlvl4: LocationData(0x13025C, 4, "Forms", 2), - LocationName.Wisdomlvl5: LocationData(0x13025D, 5, "Forms", 2), - LocationName.Wisdomlvl6: LocationData(0x13025E, 6, "Forms", 2), - LocationName.Wisdomlvl7: LocationData(0x13025F, 7, "Forms", 2), + LocationName.Wisdomlvl2: LocationData(2, "Forms", 2), + LocationName.Wisdomlvl3: LocationData(3, "Forms", 2), + LocationName.Wisdomlvl4: LocationData(4, "Forms", 2), + LocationName.Wisdomlvl5: LocationData(5, "Forms", 2), + LocationName.Wisdomlvl6: LocationData(6, "Forms", 2), + LocationName.Wisdomlvl7: LocationData(7, "Forms", 2), - LocationName.Limitlvl2: LocationData(0x130261, 2, "Forms", 3), - LocationName.Limitlvl3: LocationData(0x130262, 3, "Forms", 3), - LocationName.Limitlvl4: LocationData(0x130263, 4, "Forms", 3), - LocationName.Limitlvl5: LocationData(0x130264, 5, "Forms", 3), - LocationName.Limitlvl6: LocationData(0x130265, 6, "Forms", 3), - LocationName.Limitlvl7: LocationData(0x130266, 7, "Forms", 3), + LocationName.Limitlvl2: LocationData(2, "Forms", 3), + LocationName.Limitlvl3: LocationData(3, "Forms", 3), + LocationName.Limitlvl4: LocationData(4, "Forms", 3), + LocationName.Limitlvl5: LocationData(5, "Forms", 3), + LocationName.Limitlvl6: LocationData(6, "Forms", 3), + LocationName.Limitlvl7: LocationData(7, "Forms", 3), - LocationName.Masterlvl2: LocationData(0x130268, 2, "Forms", 4), - LocationName.Masterlvl3: LocationData(0x130269, 3, "Forms", 4), - LocationName.Masterlvl4: LocationData(0x13026A, 4, "Forms", 4), - LocationName.Masterlvl5: LocationData(0x13026B, 5, "Forms", 4), - LocationName.Masterlvl6: LocationData(0x13026C, 6, "Forms", 4), - LocationName.Masterlvl7: LocationData(0x13026D, 7, "Forms", 4), + LocationName.Masterlvl2: LocationData(2, "Forms", 4), + LocationName.Masterlvl3: LocationData(3, "Forms", 4), + LocationName.Masterlvl4: LocationData(4, "Forms", 4), + LocationName.Masterlvl5: LocationData(5, "Forms", 4), + LocationName.Masterlvl6: LocationData(6, "Forms", 4), + LocationName.Masterlvl7: LocationData(7, "Forms", 4), - LocationName.Finallvl2: LocationData(0x13026F, 2, "Forms", 5), - LocationName.Finallvl3: LocationData(0x130270, 3, "Forms", 5), - LocationName.Finallvl4: LocationData(0x130271, 4, "Forms", 5), - LocationName.Finallvl5: LocationData(0x130272, 5, "Forms", 5), - LocationName.Finallvl6: LocationData(0x130273, 6, "Forms", 5), - LocationName.Finallvl7: LocationData(0x130274, 7, "Forms", 5), + LocationName.Finallvl2: LocationData(2, "Forms", 5), + LocationName.Finallvl3: LocationData(3, "Forms", 5), + LocationName.Finallvl4: LocationData(4, "Forms", 5), + LocationName.Finallvl5: LocationData(5, "Forms", 5), + LocationName.Finallvl6: LocationData(6, "Forms", 5), + LocationName.Finallvl7: LocationData(7, "Forms", 5), +} +Summon_Checks = { + LocationName.Summonlvl2: LocationData(2, "Summons"), + LocationName.Summonlvl3: LocationData(3, "Summons"), + LocationName.Summonlvl4: LocationData(4, "Summons"), + LocationName.Summonlvl5: LocationData(5, "Summons"), + LocationName.Summonlvl6: LocationData(6, "Summons"), + LocationName.Summonlvl7: LocationData(7, "Summons"), } GoA_Checks = { - LocationName.GardenofAssemblageMap: LocationData(0x130275, 585, "Chest"), - LocationName.GoALostIllusion: LocationData(0x130276, 586, "Chest"), - LocationName.ProofofNonexistence: LocationData(0x130277, 590, "Chest"), + LocationName.GardenofAssemblageMap: LocationData(585, "Chest"), + LocationName.GoALostIllusion: LocationData(586, "Chest"), + LocationName.ProofofNonexistence: LocationData(590, "Chest"), } Keyblade_Slots = { - LocationName.FAKESlot: LocationData(0x130278, 116, "Keyblade"), - LocationName.DetectionSaberSlot: LocationData(0x130279, 83, "Keyblade"), - LocationName.EdgeofUltimaSlot: LocationData(0x13027A, 84, "Keyblade"), - LocationName.KingdomKeySlot: LocationData(0x13027B, 80, "Keyblade"), - LocationName.OathkeeperSlot: LocationData(0x13027C, 81, "Keyblade"), - LocationName.OblivionSlot: LocationData(0x13027D, 82, "Keyblade"), - LocationName.StarSeekerSlot: LocationData(0x13027E, 123, "Keyblade"), - LocationName.HiddenDragonSlot: LocationData(0x13027F, 124, "Keyblade"), - LocationName.HerosCrestSlot: LocationData(0x130280, 127, "Keyblade"), - LocationName.MonochromeSlot: LocationData(0x130281, 128, "Keyblade"), - LocationName.FollowtheWindSlot: LocationData(0x130282, 129, "Keyblade"), - LocationName.CircleofLifeSlot: LocationData(0x130283, 130, "Keyblade"), - LocationName.PhotonDebuggerSlot: LocationData(0x130284, 131, "Keyblade"), - LocationName.GullWingSlot: LocationData(0x130285, 132, "Keyblade"), - LocationName.RumblingRoseSlot: LocationData(0x130286, 133, "Keyblade"), - LocationName.GuardianSoulSlot: LocationData(0x130287, 134, "Keyblade"), - LocationName.WishingLampSlot: LocationData(0x130288, 135, "Keyblade"), - LocationName.DecisivePumpkinSlot: LocationData(0x130289, 136, "Keyblade"), - LocationName.SweetMemoriesSlot: LocationData(0x13028A, 138, "Keyblade"), - LocationName.MysteriousAbyssSlot: LocationData(0x13028B, 139, "Keyblade"), - LocationName.SleepingLionSlot: LocationData(0x13028C, 137, "Keyblade"), - LocationName.BondofFlameSlot: LocationData(0x13028D, 141, "Keyblade"), - LocationName.TwoBecomeOneSlot: LocationData(0x13028E, 148, "Keyblade"), - LocationName.FatalCrestSlot: LocationData(0x13028F, 140, "Keyblade"), - LocationName.FenrirSlot: LocationData(0x130290, 142, "Keyblade"), - LocationName.UltimaWeaponSlot: LocationData(0x130291, 143, "Keyblade"), - LocationName.WinnersProofSlot: LocationData(0x130292, 149, "Keyblade"), - LocationName.PurebloodSlot: LocationData(0x1302DB, 85, "Keyblade"), -} -# checks are given when talking to the computer in the GoA -Critical_Checks = { - LocationName.Crit_1: LocationData(0x130293, 1, "Critical"), - LocationName.Crit_2: LocationData(0x130294, 1, "Critical"), - LocationName.Crit_3: LocationData(0x130295, 1, "Critical"), - LocationName.Crit_4: LocationData(0x130296, 1, "Critical"), - LocationName.Crit_5: LocationData(0x130297, 1, "Critical"), - LocationName.Crit_6: LocationData(0x130298, 1, "Critical"), - LocationName.Crit_7: LocationData(0x130299, 1, "Critical"), + LocationName.FAKESlot: LocationData(116, "Keyblade"), + LocationName.DetectionSaberSlot: LocationData(83, "Keyblade"), + LocationName.EdgeofUltimaSlot: LocationData(84, "Keyblade"), + LocationName.KingdomKeySlot: LocationData(80, "Keyblade"), + LocationName.OathkeeperSlot: LocationData(81, "Keyblade"), + LocationName.OblivionSlot: LocationData(82, "Keyblade"), + LocationName.StarSeekerSlot: LocationData(123, "Keyblade"), + LocationName.HiddenDragonSlot: LocationData(124, "Keyblade"), + LocationName.HerosCrestSlot: LocationData(127, "Keyblade"), + LocationName.MonochromeSlot: LocationData(128, "Keyblade"), + LocationName.FollowtheWindSlot: LocationData(129, "Keyblade"), + LocationName.CircleofLifeSlot: LocationData(130, "Keyblade"), + LocationName.PhotonDebuggerSlot: LocationData(131, "Keyblade"), + LocationName.GullWingSlot: LocationData(132, "Keyblade"), + LocationName.RumblingRoseSlot: LocationData(133, "Keyblade"), + LocationName.GuardianSoulSlot: LocationData(134, "Keyblade"), + LocationName.WishingLampSlot: LocationData(135, "Keyblade"), + LocationName.DecisivePumpkinSlot: LocationData(136, "Keyblade"), + LocationName.SweetMemoriesSlot: LocationData(138, "Keyblade"), + LocationName.MysteriousAbyssSlot: LocationData(139, "Keyblade"), + LocationName.SleepingLionSlot: LocationData(137, "Keyblade"), + LocationName.BondofFlameSlot: LocationData(141, "Keyblade"), + LocationName.TwoBecomeOneSlot: LocationData(148, "Keyblade"), + LocationName.FatalCrestSlot: LocationData(140, "Keyblade"), + LocationName.FenrirSlot: LocationData(142, "Keyblade"), + LocationName.UltimaWeaponSlot: LocationData(143, "Keyblade"), + LocationName.WinnersProofSlot: LocationData(149, "Keyblade"), + LocationName.PurebloodSlot: LocationData(85, "Keyblade"), } Donald_Checks = { - LocationName.DonaldScreens: LocationData(0x13029A, 45, "Get Bonus", "Donald", 2), - LocationName.DonaldDemyxHBGetBonus: LocationData(0x13029B, 28, "Get Bonus", "Donald", 2), - LocationName.DonaldDemyxOC: LocationData(0x13029C, 58, "Get Bonus", "Donald", 2), - LocationName.DonaldBoatPete: LocationData(0x13029D, 16, "Double Get Bonus", "Donald", 2), - LocationName.DonaldBoatPeteGetBonus: LocationData(0x13029E, 16, "Second Get Bonus", "Donald", 2), - LocationName.DonaldPrisonKeeper: LocationData(0x13029F, 18, "Get Bonus", "Donald", 2), - LocationName.DonaldScar: LocationData(0x1302A0, 29, "Get Bonus", "Donald", 2), - LocationName.DonaldSolarSailer: LocationData(0x1302A1, 61, "Get Bonus", "Donald", 2), - LocationName.DonaldExperiment: LocationData(0x1302A2, 20, "Get Bonus", "Donald", 2), - LocationName.DonaldBoatFight: LocationData(0x1302A3, 62, "Get Bonus", "Donald", 2), - LocationName.DonaldMansionNobodies: LocationData(0x1302A4, 56, "Get Bonus", "Donald", 2), - LocationName.DonaldThresholder: LocationData(0x1302A5, 2, "Get Bonus", "Donald", 2), - LocationName.DonaldXaldinGetBonus: LocationData(0x1302A6, 4, "Get Bonus", "Donald", 2), - LocationName.DonaladGrimReaper2: LocationData(0x1302A7, 22, "Get Bonus", "Donald", 2), + LocationName.DonaldScreens: LocationData(45, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxHBGetBonus: LocationData(28, "Get Bonus", "Donald", 2), + LocationName.DonaldDemyxOC: LocationData(58, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatPete: LocationData(16, "Double Get Bonus", "Donald", 2), + LocationName.DonaldBoatPeteGetBonus: LocationData(16, "Second Get Bonus", "Donald", 2), + LocationName.DonaldPrisonKeeper: LocationData(18, "Get Bonus", "Donald", 2), + LocationName.DonaldScar: LocationData(29, "Get Bonus", "Donald", 2), + LocationName.DonaldSolarSailer: LocationData(61, "Get Bonus", "Donald", 2), + LocationName.DonaldExperiment: LocationData(20, "Get Bonus", "Donald", 2), + LocationName.DonaldBoatFight: LocationData(62, "Get Bonus", "Donald", 2), + LocationName.DonaldMansionNobodies: LocationData(56, "Get Bonus", "Donald", 2), + LocationName.DonaldThresholder: LocationData(2, "Get Bonus", "Donald", 2), + LocationName.DonaldXaldinGetBonus: LocationData(4, "Get Bonus", "Donald", 2), + LocationName.DonaladGrimReaper2: LocationData(22, "Get Bonus", "Donald", 2), - LocationName.CometStaff: LocationData(0x1302A8, 90, "Keyblade", "Donald"), - LocationName.HammerStaff: LocationData(0x1302A9, 87, "Keyblade", "Donald"), - LocationName.LordsBroom: LocationData(0x1302AA, 91, "Keyblade", "Donald"), - LocationName.MagesStaff: LocationData(0x1302AB, 86, "Keyblade", "Donald"), - LocationName.MeteorStaff: LocationData(0x1302AC, 89, "Keyblade", "Donald"), - LocationName.NobodyLance: LocationData(0x1302AD, 94, "Keyblade", "Donald"), - LocationName.PreciousMushroom: LocationData(0x1302AE, 154, "Keyblade", "Donald"), - LocationName.PreciousMushroom2: LocationData(0x1302AF, 155, "Keyblade", "Donald"), - LocationName.PremiumMushroom: LocationData(0x1302B0, 156, "Keyblade", "Donald"), - LocationName.RisingDragon: LocationData(0x1302B1, 93, "Keyblade", "Donald"), - LocationName.SaveTheQueen2: LocationData(0x1302B2, 146, "Keyblade", "Donald"), - LocationName.ShamansRelic: LocationData(0x1302B3, 95, "Keyblade", "Donald"), - LocationName.VictoryBell: LocationData(0x1302B4, 88, "Keyblade", "Donald"), - LocationName.WisdomWand: LocationData(0x1302B5, 92, "Keyblade", "Donald"), - LocationName.Centurion2: LocationData(0x1302B6, 151, "Keyblade", "Donald"), - LocationName.DonaldAbuEscort: LocationData(0x1302B7, 42, "Get Bonus", "Donald", 2), - LocationName.DonaldStarting1: LocationData(0x1302B8, 2, "Critical", "Donald"), - LocationName.DonaldStarting2: LocationData(0x1302B9, 2, "Critical", "Donald"), + LocationName.CometStaff: LocationData(90, "Keyblade", "Donald"), + LocationName.HammerStaff: LocationData(87, "Keyblade", "Donald"), + LocationName.LordsBroom: LocationData(91, "Keyblade", "Donald"), + LocationName.MagesStaff: LocationData(86, "Keyblade", "Donald"), + LocationName.MeteorStaff: LocationData(89, "Keyblade", "Donald"), + LocationName.NobodyLance: LocationData(94, "Keyblade", "Donald"), + LocationName.PreciousMushroom: LocationData(154, "Keyblade", "Donald"), + LocationName.PreciousMushroom2: LocationData(155, "Keyblade", "Donald"), + LocationName.PremiumMushroom: LocationData(156, "Keyblade", "Donald"), + LocationName.RisingDragon: LocationData(93, "Keyblade", "Donald"), + LocationName.SaveTheQueen2: LocationData(146, "Keyblade", "Donald"), + LocationName.ShamansRelic: LocationData(95, "Keyblade", "Donald"), + LocationName.VictoryBell: LocationData(88, "Keyblade", "Donald"), + LocationName.WisdomWand: LocationData(92, "Keyblade", "Donald"), + LocationName.Centurion2: LocationData(151, "Keyblade", "Donald"), + LocationName.DonaldAbuEscort: LocationData(42, "Get Bonus", "Donald", 2), + # LocationName.DonaldStarting1: LocationData(2, "Critical", "Donald"), + # LocationName.DonaldStarting2: LocationData(2, "Critical", "Donald"), } Goofy_Checks = { - LocationName.GoofyBarbossa: LocationData(0x1302BA, 21, "Double Get Bonus", "Goofy", 3), - LocationName.GoofyBarbossaGetBonus: LocationData(0x1302BB, 21, "Second Get Bonus", "Goofy", 3), - LocationName.GoofyGrimReaper1: LocationData(0x1302BC, 59, "Get Bonus", "Goofy", 3), - LocationName.GoofyHostileProgram: LocationData(0x1302BD, 31, "Get Bonus", "Goofy", 3), - LocationName.GoofyHyenas1: LocationData(0x1302BE, 49, "Get Bonus", "Goofy", 3), - LocationName.GoofyHyenas2: LocationData(0x1302BF, 50, "Get Bonus", "Goofy", 3), - LocationName.GoofyLock: LocationData(0x1302C0, 40, "Get Bonus", "Goofy", 3), - LocationName.GoofyOogieBoogie: LocationData(0x1302C1, 19, "Get Bonus", "Goofy", 3), - LocationName.GoofyPeteOC: LocationData(0x1302C2, 6, "Get Bonus", "Goofy", 3), - LocationName.GoofyFuturePete: LocationData(0x1302C3, 17, "Get Bonus", "Goofy", 3), - LocationName.GoofyShanYu: LocationData(0x1302C4, 9, "Get Bonus", "Goofy", 3), - LocationName.GoofyStormRider: LocationData(0x1302C5, 10, "Get Bonus", "Goofy", 3), - LocationName.GoofyBeast: LocationData(0x1302C6, 12, "Get Bonus", "Goofy", 3), - LocationName.GoofyInterceptorBarrels: LocationData(0x1302C7, 39, "Get Bonus", "Goofy", 3), - LocationName.GoofyTreasureRoom: LocationData(0x1302C8, 46, "Get Bonus", "Goofy", 3), - LocationName.GoofyZexion: LocationData(0x1302C9, 66, "Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossa: LocationData(21, "Double Get Bonus", "Goofy", 3), + LocationName.GoofyBarbossaGetBonus: LocationData(21, "Second Get Bonus", "Goofy", 3), + LocationName.GoofyGrimReaper1: LocationData(59, "Get Bonus", "Goofy", 3), + LocationName.GoofyHostileProgram: LocationData(31, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas1: LocationData(49, "Get Bonus", "Goofy", 3), + LocationName.GoofyHyenas2: LocationData(50, "Get Bonus", "Goofy", 3), + LocationName.GoofyLock: LocationData(40, "Get Bonus", "Goofy", 3), + LocationName.GoofyOogieBoogie: LocationData(19, "Get Bonus", "Goofy", 3), + LocationName.GoofyPeteOC: LocationData(6, "Get Bonus", "Goofy", 3), + LocationName.GoofyFuturePete: LocationData(17, "Get Bonus", "Goofy", 3), + LocationName.GoofyShanYu: LocationData(9, "Get Bonus", "Goofy", 3), + LocationName.GoofyStormRider: LocationData(10, "Get Bonus", "Goofy", 3), + LocationName.GoofyBeast: LocationData(12, "Get Bonus", "Goofy", 3), + LocationName.GoofyInterceptorBarrels: LocationData(39, "Get Bonus", "Goofy", 3), + LocationName.GoofyTreasureRoom: LocationData(46, "Get Bonus", "Goofy", 3), + LocationName.GoofyZexion: LocationData(66, "Get Bonus", "Goofy", 3), - LocationName.AdamantShield: LocationData(0x1302CA, 100, "Keyblade", "Goofy"), - LocationName.AkashicRecord: LocationData(0x1302CB, 107, "Keyblade", "Goofy"), - LocationName.ChainGear: LocationData(0x1302CC, 101, "Keyblade", "Goofy"), - LocationName.DreamCloud: LocationData(0x1302CD, 104, "Keyblade", "Goofy"), - LocationName.FallingStar: LocationData(0x1302CE, 103, "Keyblade", "Goofy"), - LocationName.FrozenPride2: LocationData(0x1302CF, 158, "Keyblade", "Goofy"), - LocationName.GenjiShield: LocationData(0x1302D0, 106, "Keyblade", "Goofy"), - LocationName.KnightDefender: LocationData(0x1302D1, 105, "Keyblade", "Goofy"), - LocationName.KnightsShield: LocationData(0x1302D2, 99, "Keyblade", "Goofy"), - LocationName.MajesticMushroom: LocationData(0x1302D3, 161, "Keyblade", "Goofy"), - LocationName.MajesticMushroom2: LocationData(0x1302D4, 162, "Keyblade", "Goofy"), - LocationName.NobodyGuard: LocationData(0x1302D5, 108, "Keyblade", "Goofy"), - LocationName.OgreShield: LocationData(0x1302D6, 102, "Keyblade", "Goofy"), - LocationName.SaveTheKing2: LocationData(0x1302D7, 147, "Keyblade", "Goofy"), - LocationName.UltimateMushroom: LocationData(0x1302D8, 163, "Keyblade", "Goofy"), - LocationName.GoofyStarting1: LocationData(0x1302D9, 3, "Critical", "Goofy"), - LocationName.GoofyStarting2: LocationData(0x1302DA, 3, "Critical", "Goofy"), + LocationName.AdamantShield: LocationData(100, "Keyblade", "Goofy"), + LocationName.AkashicRecord: LocationData(107, "Keyblade", "Goofy"), + LocationName.ChainGear: LocationData(101, "Keyblade", "Goofy"), + LocationName.DreamCloud: LocationData(104, "Keyblade", "Goofy"), + LocationName.FallingStar: LocationData(103, "Keyblade", "Goofy"), + LocationName.FrozenPride2: LocationData(158, "Keyblade", "Goofy"), + LocationName.GenjiShield: LocationData(106, "Keyblade", "Goofy"), + LocationName.KnightDefender: LocationData(105, "Keyblade", "Goofy"), + LocationName.KnightsShield: LocationData(99, "Keyblade", "Goofy"), + LocationName.MajesticMushroom: LocationData(161, "Keyblade", "Goofy"), + LocationName.MajesticMushroom2: LocationData(162, "Keyblade", "Goofy"), + LocationName.NobodyGuard: LocationData(108, "Keyblade", "Goofy"), + LocationName.OgreShield: LocationData(102, "Keyblade", "Goofy"), + LocationName.SaveTheKing2: LocationData(147, "Keyblade", "Goofy"), + LocationName.UltimateMushroom: LocationData(163, "Keyblade", "Goofy"), + # LocationName.GoofyStarting1: LocationData(3, "Critical", "Goofy"), + # LocationName.GoofyStarting2: LocationData(3, "Critical", "Goofy"), +} + +Atlantica_Checks = { + LocationName.UnderseaKingdomMap: LocationData(367, "Chest"), + LocationName.MysteriousAbyss: LocationData(287, "Chest"), # needs 2 magnets + LocationName.MusicalBlizzardElement: LocationData(279, "Chest"), # 2 magnets all thunders + LocationName.MusicalOrichalcumPlus: LocationData(538, "Chest"), # 2 magnets all thunders +} + +event_location_to_item = { + LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent, + LocationName.McpEventLocation: ItemName.McpEvent, + # LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent, + LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent, + LocationName.BarbosaEventLocation: ItemName.BarbosaEvent, + LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event, + LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event, + LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent, + LocationName.DataAxelEventLocation: ItemName.DataAxelEvent, + LocationName.CerberusEventLocation: ItemName.CerberusEvent, + LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent, + LocationName.HydraEventLocation: ItemName.HydraEvent, + LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent, + LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent, + LocationName.HadesEventLocation: ItemName.HadesEvent, + # LocationName.ASZexionEventLocation: ItemName.ASZexionEvent, + LocationName.DataZexionEventLocation: ItemName.DataZexionEvent, + LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent, + LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent, + # LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation, + LocationName.HadesCupEventLocations: ItemName.HadesCupEvents, + LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent, + LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent, + LocationName.ExperimentEventLocation: ItemName.ExperimentEvent, + # LocationName.ASVexenEventLocation: ItemName.ASVexenEvent, + LocationName.DataVexenEventLocation: ItemName.DataVexenEvent, + LocationName.ShanYuEventLocation: ItemName.ShanYuEvent, + LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent, + LocationName.StormRiderEventLocation: ItemName.StormRiderEvent, + LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent, + LocationName.RoxasEventLocation: ItemName.RoxasEvent, + LocationName.XigbarEventLocation: ItemName.XigbarEvent, + LocationName.LuxordEventLocation: ItemName.LuxordEvent, + LocationName.SaixEventLocation: ItemName.SaixEvent, + LocationName.XemnasEventLocation: ItemName.XemnasEvent, + LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent, + LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event, + # LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent, + LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent, + LocationName.ThresholderEventLocation: ItemName.ThresholderEvent, + LocationName.BeastEventLocation: ItemName.BeastEvent, + LocationName.DarkThornEventLocation: ItemName.DarkThornEvent, + LocationName.XaldinEventLocation: ItemName.XaldinEvent, + LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent, + LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent, + LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent, + # LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent, + LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent, + LocationName.ScarEventLocation: ItemName.ScarEvent, + LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent, + LocationName.DataSaixEventLocation: ItemName.DataSaixEvent, + LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent, + LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent, + LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event, + LocationName.SephiEventLocation: ItemName.SephiEvent, + LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent, + LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent, + LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent, + LocationName.TransportEventLocation: ItemName.TransportEvent, + LocationName.OldPeteEventLocation: ItemName.OldPeteEvent, + LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent, + # LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent, + LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent, + LocationName.TerraEventLocation: ItemName.TerraEvent, + LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent, + LocationName.Axel1EventLocation: ItemName.Axel1Event, + LocationName.Axel2EventLocation: ItemName.Axel2Event, + LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent, +} +all_weapon_slot = { + LocationName.FAKESlot, + LocationName.DetectionSaberSlot, + LocationName.EdgeofUltimaSlot, + LocationName.KingdomKeySlot, + LocationName.OathkeeperSlot, + LocationName.OblivionSlot, + LocationName.StarSeekerSlot, + LocationName.HiddenDragonSlot, + LocationName.HerosCrestSlot, + LocationName.MonochromeSlot, + LocationName.FollowtheWindSlot, + LocationName.CircleofLifeSlot, + LocationName.PhotonDebuggerSlot, + LocationName.GullWingSlot, + LocationName.RumblingRoseSlot, + LocationName.GuardianSoulSlot, + LocationName.WishingLampSlot, + LocationName.DecisivePumpkinSlot, + LocationName.SweetMemoriesSlot, + LocationName.MysteriousAbyssSlot, + LocationName.SleepingLionSlot, + LocationName.BondofFlameSlot, + LocationName.TwoBecomeOneSlot, + LocationName.FatalCrestSlot, + LocationName.FenrirSlot, + LocationName.UltimaWeaponSlot, + LocationName.WinnersProofSlot, + LocationName.PurebloodSlot, + + LocationName.Centurion2, + LocationName.CometStaff, + LocationName.HammerStaff, + LocationName.LordsBroom, + LocationName.MagesStaff, + LocationName.MeteorStaff, + LocationName.NobodyLance, + LocationName.PreciousMushroom, + LocationName.PreciousMushroom2, + LocationName.PremiumMushroom, + LocationName.RisingDragon, + LocationName.SaveTheQueen2, + LocationName.ShamansRelic, + LocationName.VictoryBell, + LocationName.WisdomWand, + + LocationName.AdamantShield, + LocationName.AkashicRecord, + LocationName.ChainGear, + LocationName.DreamCloud, + LocationName.FallingStar, + LocationName.FrozenPride2, + LocationName.GenjiShield, + LocationName.KnightDefender, + LocationName.KnightsShield, + LocationName.MajesticMushroom, + LocationName.MajesticMushroom2, + LocationName.NobodyGuard, + LocationName.OgreShield, + LocationName.SaveTheKing2, + LocationName.UltimateMushroom, } + +all_locations = { + **TWTNW_Checks, + **TT_Checks, + **STT_Checks, + **PL_Checks, + **HB_Checks, + **HT_Checks, + **PR_Checks, + **PR_Checks, + **SP_Checks, + **BC_Checks, + **Oc_Checks, + **HundredAcre_Checks, + **DC_Checks, + **AG_Checks, + **LoD_Checks, + **SoraLevels, + **Form_Checks, + **GoA_Checks, + **Keyblade_Slots, + **Donald_Checks, + **Goofy_Checks, + **Atlantica_Checks, + **Summon_Checks, +} + +popups_set = { + LocationName.SweetMemories, + LocationName.SpookyCaveMap, + LocationName.StarryHillCureElement, + LocationName.StarryHillOrichalcumPlus, + LocationName.AgrabahMap, + LocationName.LampCharm, + LocationName.WishingLamp, + LocationName.DarkThornCureElement, + LocationName.RumblingRose, + LocationName.CastleWallsMap, + LocationName.SecretAnsemReport4, + LocationName.DisneyCastleMap, + LocationName.WindowofTimeMap, + LocationName.Monochrome, + LocationName.WisdomForm, + LocationName.LingeringWillProofofConnection, + LocationName.LingeringWillManifestIllusion, + LocationName.OogieBoogieMagnetElement, + LocationName.Present, + LocationName.DecoyPresents, + LocationName.DecisivePumpkin, + LocationName.MarketplaceMap, + LocationName.MerlinsHouseMembershipCard, + LocationName.MerlinsHouseBlizzardElement, + LocationName.BaileySecretAnsemReport7, + LocationName.BaseballCharm, + LocationName.AnsemsStudyMasterForm, + LocationName.AnsemsStudySkillRecipe, + LocationName.AnsemsStudySleepingLion, + LocationName.FFFightsCureElement, + LocationName.ThousandHeartlessSecretAnsemReport1, + LocationName.ThousandHeartlessIceCream, + LocationName.ThousandHeartlessPicture, + LocationName.WinnersProof, + LocationName.ProofofPeace, + LocationName.SephirothFenrir, + LocationName.EncampmentAreaMap, + LocationName.Mission3, + LocationName.VillageCaveAreaMap, + LocationName.HiddenDragon, + LocationName.ColiseumMap, + LocationName.SecretAnsemReport6, + LocationName.OlympusStone, + LocationName.HerosCrest, + LocationName.AuronsStatue, + LocationName.GuardianSoul, + LocationName.ProtectBeltPainandPanicCup, + LocationName.SerenityGemPainandPanicCup, + LocationName.RisingDragonCerberusCup, + LocationName.SerenityCrystalCerberusCup, + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.FatalCrestGoddessofFateCup, + LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.HadesCupTrophyParadoxCups, + LocationName.IsladeMuertaMap, + LocationName.FollowtheWind, + LocationName.SeadriftRowCursedMedallion, + LocationName.SeadriftRowShipGraveyardMap, + LocationName.SecretAnsemReport5, + LocationName.CircleofLife, + LocationName.ScarFireElement, + LocationName.TwilightTownMap, + LocationName.MunnyPouchOlette, + LocationName.JunkChampionBelt, + LocationName.JunkMedal, + LocationName.TheStruggleTrophy, + LocationName.NaminesSketches, + LocationName.MansionMap, + LocationName.PhotonDebugger, + LocationName.StationPlazaSecretAnsemReport2, + LocationName.MunnyPouchMickey, + LocationName.CrystalOrb, + LocationName.StarSeeker, + LocationName.ValorForm, + LocationName.SeifersTrophy, + LocationName.Oathkeeper, + LocationName.LimitForm, + LocationName.BeamSecretAnsemReport10, + LocationName.BetwixtandBetweenBondofFlame, + LocationName.TwoBecomeOne, + LocationName.RoxasSecretAnsemReport8, + LocationName.XigbarSecretAnsemReport3, + LocationName.Oblivion, + LocationName.CastleThatNeverWasMap, + LocationName.LuxordSecretAnsemReport9, + LocationName.SaixSecretAnsemReport12, + LocationName.PreXemnas1SecretAnsemReport11, + LocationName.Xemnas1SecretAnsemReport13, + LocationName.XemnasDataPowerBoost, + LocationName.AxelDataMagicBoost, + LocationName.RoxasDataMagicBoost, + LocationName.SaixDataDefenseBoost, + LocationName.DemyxDataAPBoost, + LocationName.LuxordDataAPBoost, + LocationName.VexenDataLostIllusion, + LocationName.LarxeneDataLostIllusion, + LocationName.XaldinDataDefenseBoost, + LocationName.MarluxiaDataLostIllusion, + LocationName.LexaeusDataLostIllusion, + LocationName.XigbarDataDefenseBoost, + LocationName.VexenASRoadtoDiscovery, + LocationName.LarxeneASCloakedThunder, + LocationName.ZexionASBookofShadows, + LocationName.ZexionDataLostIllusion, + LocationName.LexaeusASStrengthBeyondStrength, + LocationName.MarluxiaASEternalBlossom, + LocationName.UnderseaKingdomMap, + LocationName.MysteriousAbyss, + LocationName.MusicalBlizzardElement, + LocationName.MusicalOrichalcumPlus, } exclusion_table = { - "Popups": { - LocationName.SweetMemories, - LocationName.SpookyCaveMap, - LocationName.StarryHillCureElement, - LocationName.StarryHillOrichalcumPlus, - LocationName.AgrabahMap, - LocationName.LampCharm, - LocationName.WishingLamp, - LocationName.DarkThornCureElement, - LocationName.RumblingRose, - LocationName.CastleWallsMap, - LocationName.SecretAnsemReport4, - LocationName.DisneyCastleMap, - LocationName.WindowofTimeMap, - LocationName.Monochrome, - LocationName.WisdomForm, + "SuperBosses": { + LocationName.LingeringWillBonus, LocationName.LingeringWillProofofConnection, LocationName.LingeringWillManifestIllusion, - LocationName.OogieBoogieMagnetElement, - LocationName.Present, - LocationName.DecoyPresents, - LocationName.DecisivePumpkin, - LocationName.MarketplaceMap, - LocationName.MerlinsHouseMembershipCard, - LocationName.MerlinsHouseBlizzardElement, - LocationName.BaileySecretAnsemReport7, - LocationName.BaseballCharm, - LocationName.AnsemsStudyMasterForm, - LocationName.AnsemsStudySkillRecipe, - LocationName.AnsemsStudySleepingLion, - LocationName.FFFightsCureElement, - LocationName.ThousandHeartlessSecretAnsemReport1, - LocationName.ThousandHeartlessIceCream, - LocationName.ThousandHeartlessPicture, - LocationName.WinnersProof, - LocationName.ProofofPeace, + LocationName.SephirothBonus, LocationName.SephirothFenrir, - LocationName.EncampmentAreaMap, - LocationName.Mission3, - LocationName.VillageCaveAreaMap, - LocationName.HiddenDragon, - LocationName.ColiseumMap, - LocationName.SecretAnsemReport6, - LocationName.OlympusStone, - LocationName.HerosCrest, - LocationName.AuronsStatue, - LocationName.GuardianSoul, - LocationName.ProtectBeltPainandPanicCup, - LocationName.SerenityGemPainandPanicCup, - LocationName.RisingDragonCerberusCup, - LocationName.SerenityCrystalCerberusCup, - LocationName.GenjiShieldTitanCup, - LocationName.SkillfulRingTitanCup, - LocationName.FatalCrestGoddessofFateCup, - LocationName.OrichalcumPlusGoddessofFateCup, - LocationName.HadesCupTrophyParadoxCups, - LocationName.IsladeMuertaMap, - LocationName.FollowtheWind, - LocationName.SeadriftRowCursedMedallion, - LocationName.SeadriftRowShipGraveyardMap, - LocationName.SecretAnsemReport5, - LocationName.CircleofLife, - LocationName.ScarFireElement, - LocationName.TwilightTownMap, - LocationName.MunnyPouchOlette, - LocationName.JunkChampionBelt, - LocationName.JunkMedal, - LocationName.TheStruggleTrophy, - LocationName.NaminesSketches, - LocationName.MansionMap, - LocationName.PhotonDebugger, - LocationName.StationPlazaSecretAnsemReport2, - LocationName.MunnyPouchMickey, - LocationName.CrystalOrb, - LocationName.StarSeeker, - LocationName.ValorForm, - LocationName.SeifersTrophy, - LocationName.Oathkeeper, - LocationName.LimitForm, - LocationName.BeamSecretAnsemReport10, - LocationName.BetwixtandBetweenBondofFlame, - LocationName.TwoBecomeOne, - LocationName.RoxasSecretAnsemReport8, - LocationName.XigbarSecretAnsemReport3, - LocationName.Oblivion, - LocationName.CastleThatNeverWasMap, - LocationName.LuxordSecretAnsemReport9, - LocationName.SaixSecretAnsemReport12, - LocationName.PreXemnas1SecretAnsemReport11, - LocationName.Xemnas1SecretAnsemReport13, - LocationName.XemnasDataPowerBoost, - LocationName.AxelDataMagicBoost, - LocationName.RoxasDataMagicBoost, - LocationName.SaixDataDefenseBoost, - LocationName.DemyxDataAPBoost, - LocationName.LuxordDataAPBoost, - LocationName.VexenDataLostIllusion, - LocationName.LarxeneDataLostIllusion, - LocationName.XaldinDataDefenseBoost, - LocationName.MarluxiaDataLostIllusion, - LocationName.LexaeusDataLostIllusion, - LocationName.XigbarDataDefenseBoost, - LocationName.VexenASRoadtoDiscovery, - LocationName.LarxeneASCloakedThunder, - LocationName.ZexionASBookofShadows, - LocationName.ZexionDataLostIllusion, - LocationName.LexaeusASStrengthBeyondStrength, - LocationName.MarluxiaASEternalBlossom - }, - "Datas": { LocationName.XemnasDataPowerBoost, LocationName.AxelDataMagicBoost, LocationName.RoxasDataMagicBoost, @@ -985,13 +1106,7 @@ exclusion_table = { LocationName.ZexionDataLostIllusion, LocationName.ZexionBonus, LocationName.ZexionASBookofShadows, - }, - "SuperBosses": { - LocationName.LingeringWillBonus, - LocationName.LingeringWillProofofConnection, - LocationName.LingeringWillManifestIllusion, - LocationName.SephirothBonus, - LocationName.SephirothFenrir, + LocationName.GoofyZexion, }, # 23 checks spread through 50 levels @@ -1148,15 +1263,6 @@ exclusion_table = { LocationName.Lvl98, LocationName.Lvl99, }, - "Critical": { - LocationName.Crit_1, - LocationName.Crit_2, - LocationName.Crit_3, - LocationName.Crit_4, - LocationName.Crit_5, - LocationName.Crit_6, - LocationName.Crit_7, - }, "Hitlist": [ LocationName.XemnasDataPowerBoost, LocationName.AxelDataMagicBoost, @@ -1179,9 +1285,11 @@ exclusion_table = { LocationName.Limitlvl7, LocationName.Masterlvl7, LocationName.Finallvl7, + LocationName.Summonlvl7, LocationName.TransporttoRemembrance, LocationName.OrichalcumPlusGoddessofFateCup, LocationName.HadesCupTrophyParadoxCups, + LocationName.MusicalOrichalcumPlus, ], "Cups": { LocationName.ProtectBeltPainandPanicCup, @@ -1194,6 +1302,12 @@ exclusion_table = { LocationName.OrichalcumPlusGoddessofFateCup, LocationName.HadesCupTrophyParadoxCups, }, + "Atlantica": { + LocationName.MysteriousAbyss, + LocationName.MusicalOrichalcumPlus, + LocationName.MusicalBlizzardElement, + LocationName.UnderseaKingdomMap, + }, "WeaponSlots": { LocationName.FAKESlot: ItemName.ValorForm, LocationName.DetectionSaberSlot: ItemName.MasterForm, @@ -1244,536 +1358,6 @@ exclusion_table = { LocationName.Centurion2: ItemName.Centurion2, }, "Chests": { - LocationName.BambooGroveDarkShard, - LocationName.BambooGroveEther, - LocationName.BambooGroveMythrilShard, - LocationName.CheckpointHiPotion, - LocationName.CheckpointMythrilShard, - LocationName.MountainTrailLightningShard, - LocationName.MountainTrailRecoveryRecipe, - LocationName.MountainTrailEther, - LocationName.MountainTrailMythrilShard, - LocationName.VillageCaveAPBoost, - LocationName.VillageCaveDarkShard, - LocationName.RidgeFrostShard, - LocationName.RidgeAPBoost, - LocationName.ThroneRoomTornPages, - LocationName.ThroneRoomPalaceMap, - LocationName.ThroneRoomAPBoost, - LocationName.ThroneRoomQueenRecipe, - LocationName.ThroneRoomAPBoost2, - LocationName.ThroneRoomOgreShield, - LocationName.ThroneRoomMythrilCrystal, - LocationName.ThroneRoomOrichalcum, - LocationName.AgrabahDarkShard, - LocationName.AgrabahMythrilShard, - LocationName.AgrabahHiPotion, - LocationName.AgrabahAPBoost, - LocationName.AgrabahMythrilStone, - LocationName.AgrabahMythrilShard2, - LocationName.AgrabahSerenityShard, - LocationName.BazaarMythrilGem, - LocationName.BazaarPowerShard, - LocationName.BazaarHiPotion, - LocationName.BazaarAPBoost, - LocationName.BazaarMythrilShard, - LocationName.PalaceWallsSkillRing, - LocationName.PalaceWallsMythrilStone, - LocationName.CaveEntrancePowerStone, - LocationName.CaveEntranceMythrilShard, - LocationName.ValleyofStoneMythrilStone, - LocationName.ValleyofStoneAPBoost, - LocationName.ValleyofStoneMythrilShard, - LocationName.ValleyofStoneHiPotion, - LocationName.ChasmofChallengesCaveofWondersMap, - LocationName.ChasmofChallengesAPBoost, - LocationName.TreasureRoomAPBoost, - LocationName.TreasureRoomSerenityGem, - LocationName.RuinedChamberTornPages, - LocationName.RuinedChamberRuinsMap, - LocationName.DCCourtyardMythrilShard, - LocationName.DCCourtyardStarRecipe, - LocationName.DCCourtyardAPBoost, - LocationName.DCCourtyardMythrilStone, - LocationName.DCCourtyardBlazingStone, - LocationName.DCCourtyardBlazingShard, - LocationName.DCCourtyardMythrilShard2, - LocationName.LibraryTornPages, - LocationName.CornerstoneHillMap, - LocationName.CornerstoneHillFrostShard, - LocationName.PierMythrilShard, - LocationName.PierHiPotion, - LocationName.WaterwayMythrilStone, - LocationName.WaterwayAPBoost, - LocationName.WaterwayFrostStone, - LocationName.PoohsHouse100AcreWoodMap, - LocationName.PoohsHouseAPBoost, - LocationName.PoohsHouseMythrilStone, - LocationName.PigletsHouseDefenseBoost, - LocationName.PigletsHouseAPBoost, - LocationName.PigletsHouseMythrilGem, - LocationName.RabbitsHouseDrawRing, - LocationName.RabbitsHouseMythrilCrystal, - LocationName.RabbitsHouseAPBoost, - LocationName.KangasHouseMagicBoost, - LocationName.KangasHouseAPBoost, - LocationName.KangasHouseOrichalcum, - LocationName.SpookyCaveMythrilGem, - LocationName.SpookyCaveAPBoost, - LocationName.SpookyCaveOrichalcum, - LocationName.SpookyCaveGuardRecipe, - LocationName.SpookyCaveMythrilCrystal, - LocationName.SpookyCaveAPBoost2, - LocationName.StarryHillCosmicRing, - LocationName.StarryHillStyleRecipe, - LocationName.RampartNavalMap, - LocationName.RampartMythrilStone, - LocationName.RampartDarkShard, - LocationName.TownDarkStone, - LocationName.TownAPBoost, - LocationName.TownMythrilShard, - LocationName.TownMythrilGem, - LocationName.CaveMouthBrightShard, - LocationName.CaveMouthMythrilShard, - LocationName.PowderStoreAPBoost1, - LocationName.PowderStoreAPBoost2, - LocationName.MoonlightNookMythrilShard, - LocationName.MoonlightNookSerenityGem, - LocationName.MoonlightNookPowerStone, - LocationName.InterceptorsHoldFeatherCharm, - LocationName.SeadriftKeepAPBoost, - LocationName.SeadriftKeepOrichalcum, - LocationName.SeadriftKeepMeteorStaff, - LocationName.SeadriftRowSerenityGem, - LocationName.SeadriftRowKingRecipe, - LocationName.SeadriftRowMythrilCrystal, - LocationName.PassageMythrilShard, - LocationName.PassageMythrilStone, - LocationName.PassageEther, - LocationName.PassageAPBoost, - LocationName.PassageHiPotion, - LocationName.InnerChamberUnderworldMap, - LocationName.InnerChamberMythrilShard, - LocationName.UnderworldEntrancePowerBoost, - LocationName.CavernsEntranceLucidShard, - LocationName.CavernsEntranceAPBoost, - LocationName.CavernsEntranceMythrilShard, - LocationName.TheLostRoadBrightShard, - LocationName.TheLostRoadEther, - LocationName.TheLostRoadMythrilShard, - LocationName.TheLostRoadMythrilStone, - LocationName.AtriumLucidStone, - LocationName.AtriumAPBoost, - LocationName.TheLockCavernsMap, - LocationName.TheLockMythrilShard, - LocationName.TheLockAPBoost, - LocationName.BCCourtyardAPBoost, - LocationName.BCCourtyardHiPotion, - LocationName.BCCourtyardMythrilShard, - LocationName.BellesRoomCastleMap, - LocationName.BellesRoomMegaRecipe, - LocationName.TheEastWingMythrilShard, - LocationName.TheEastWingTent, - LocationName.TheWestHallHiPotion, - LocationName.TheWestHallPowerShard, - LocationName.TheWestHallMythrilShard2, - LocationName.TheWestHallBrightStone, - LocationName.TheWestHallMythrilShard, - LocationName.DungeonBasementMap, - LocationName.DungeonAPBoost, - LocationName.SecretPassageMythrilShard, - LocationName.SecretPassageHiPotion, - LocationName.SecretPassageLucidShard, - LocationName.TheWestHallAPBoostPostDungeon, - LocationName.TheWestWingMythrilShard, - LocationName.TheWestWingTent, - LocationName.TheBeastsRoomBlazingShard, - LocationName.PitCellAreaMap, - LocationName.PitCellMythrilCrystal, - LocationName.CanyonDarkCrystal, - LocationName.CanyonMythrilStone, - LocationName.CanyonMythrilGem, - LocationName.CanyonFrostCrystal, - LocationName.HallwayPowerCrystal, - LocationName.HallwayAPBoost, - LocationName.CommunicationsRoomIOTowerMap, - LocationName.CommunicationsRoomGaiaBelt, - LocationName.CentralComputerCoreAPBoost, - LocationName.CentralComputerCoreOrichalcumPlus, - LocationName.CentralComputerCoreCosmicArts, - LocationName.CentralComputerCoreMap, - LocationName.GraveyardMythrilShard, - LocationName.GraveyardSerenityGem, - LocationName.FinklesteinsLabHalloweenTownMap, - LocationName.TownSquareMythrilStone, - LocationName.TownSquareEnergyShard, - LocationName.HinterlandsLightningShard, - LocationName.HinterlandsMythrilStone, - LocationName.HinterlandsAPBoost, - LocationName.CandyCaneLaneMegaPotion, - LocationName.CandyCaneLaneMythrilGem, - LocationName.CandyCaneLaneLightningStone, - LocationName.CandyCaneLaneMythrilStone, - LocationName.SantasHouseChristmasTownMap, - LocationName.SantasHouseAPBoost, - LocationName.BoroughDriveRecovery, - LocationName.BoroughAPBoost, - LocationName.BoroughHiPotion, - LocationName.BoroughMythrilShard, - LocationName.BoroughDarkShard, - LocationName.PosternCastlePerimeterMap, - LocationName.PosternMythrilGem, - LocationName.PosternAPBoost, - LocationName.CorridorsMythrilStone, - LocationName.CorridorsMythrilCrystal, - LocationName.CorridorsDarkCrystal, - LocationName.CorridorsAPBoost, - LocationName.AnsemsStudyUkuleleCharm, - LocationName.RestorationSiteMoonRecipe, - LocationName.RestorationSiteAPBoost, - LocationName.CoRDepthsAPBoost, - LocationName.CoRDepthsPowerCrystal, - LocationName.CoRDepthsFrostCrystal, - LocationName.CoRDepthsManifestIllusion, - LocationName.CoRDepthsAPBoost2, - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, - LocationName.CoRMineshaftLowerLevelAPBoost, - LocationName.CrystalFissureTornPages, - LocationName.CrystalFissureTheGreatMawMap, - LocationName.CrystalFissureEnergyCrystal, - LocationName.CrystalFissureAPBoost, - LocationName.PosternGullWing, - LocationName.HeartlessManufactoryCosmicChain, - LocationName.CoRDepthsUpperLevelRemembranceGem, - LocationName.CoRMiningAreaSerenityGem, - LocationName.CoRMiningAreaAPBoost, - LocationName.CoRMiningAreaSerenityCrystal, - LocationName.CoRMiningAreaManifestIllusion, - LocationName.CoRMiningAreaSerenityGem2, - LocationName.CoRMiningAreaDarkRemembranceMap, - LocationName.CoRMineshaftMidLevelPowerBoost, - LocationName.CoREngineChamberSerenityCrystal, - LocationName.CoREngineChamberRemembranceCrystal, - LocationName.CoREngineChamberAPBoost, - LocationName.CoREngineChamberManifestIllusion, - LocationName.CoRMineshaftUpperLevelMagicBoost, - LocationName.CoRMineshaftUpperLevelAPBoost, - LocationName.GorgeSavannahMap, - LocationName.GorgeDarkGem, - LocationName.GorgeMythrilStone, - LocationName.ElephantGraveyardFrostGem, - LocationName.ElephantGraveyardMythrilStone, - LocationName.ElephantGraveyardBrightStone, - LocationName.ElephantGraveyardAPBoost, - LocationName.ElephantGraveyardMythrilShard, - LocationName.PrideRockMap, - LocationName.PrideRockMythrilStone, - LocationName.PrideRockSerenityCrystal, - LocationName.WildebeestValleyEnergyStone, - LocationName.WildebeestValleyAPBoost, - LocationName.WildebeestValleyMythrilGem, - LocationName.WildebeestValleyMythrilStone, - LocationName.WildebeestValleyLucidGem, - LocationName.WastelandsMythrilShard, - LocationName.WastelandsSerenityGem, - LocationName.WastelandsMythrilStone, - LocationName.JungleSerenityGem, - LocationName.JungleMythrilStone, - LocationName.JungleSerenityCrystal, - LocationName.OasisMap, - LocationName.OasisTornPages, - LocationName.OasisAPBoost, - LocationName.StationofCallingPotion, - LocationName.CentralStationPotion1, - LocationName.STTCentralStationHiPotion, - LocationName.CentralStationPotion2, - LocationName.SunsetTerraceAbilityRing, - LocationName.SunsetTerraceHiPotion, - LocationName.SunsetTerracePotion1, - LocationName.SunsetTerracePotion2, - LocationName.MansionFoyerHiPotion, - LocationName.MansionFoyerPotion1, - LocationName.MansionFoyerPotion2, - LocationName.MansionDiningRoomElvenBandanna, - LocationName.MansionDiningRoomPotion, - LocationName.MansionLibraryHiPotion, - LocationName.MansionBasementCorridorHiPotion, - LocationName.OldMansionPotion, - LocationName.OldMansionMythrilShard, - LocationName.TheWoodsPotion, - LocationName.TheWoodsMythrilShard, - LocationName.TheWoodsHiPotion, - LocationName.TramCommonHiPotion, - LocationName.TramCommonAPBoost, - LocationName.TramCommonTent, - LocationName.TramCommonMythrilShard1, - LocationName.TramCommonPotion1, - LocationName.TramCommonMythrilShard2, - LocationName.TramCommonPotion2, - LocationName.CentralStationTent, - LocationName.TTCentralStationHiPotion, - LocationName.CentralStationMythrilShard, - LocationName.TheTowerPotion, - LocationName.TheTowerHiPotion, - LocationName.TheTowerEther, - LocationName.TowerEntrywayEther, - LocationName.TowerEntrywayMythrilShard, - LocationName.SorcerersLoftTowerMap, - LocationName.TowerWardrobeMythrilStone, - LocationName.UndergroundConcourseMythrilGem, - LocationName.UndergroundConcourseAPBoost, - LocationName.UndergroundConcourseMythrilCrystal, - LocationName.UndergroundConcourseOrichalcum, - LocationName.TunnelwayOrichalcum, - LocationName.TunnelwayMythrilCrystal, - LocationName.SunsetTerraceOrichalcumPlus, - LocationName.SunsetTerraceMythrilShard, - LocationName.SunsetTerraceMythrilCrystal, - LocationName.SunsetTerraceAPBoost, - LocationName.MansionFoyerMythrilCrystal, - LocationName.MansionFoyerMythrilStone, - LocationName.MansionFoyerSerenityCrystal, - LocationName.MansionDiningRoomMythrilCrystal, - LocationName.MansionDiningRoomMythrilStone, - LocationName.MansionLibraryOrichalcum, - LocationName.MansionBasementCorridorUltimateRecipe, - LocationName.FragmentCrossingMythrilStone, - LocationName.FragmentCrossingMythrilCrystal, - LocationName.FragmentCrossingAPBoost, - LocationName.FragmentCrossingOrichalcum, - LocationName.MemorysSkyscaperMythrilCrystal, - LocationName.MemorysSkyscaperAPBoost, - LocationName.MemorysSkyscaperMythrilStone, - LocationName.TheBrinkofDespairDarkCityMap, - LocationName.TheBrinkofDespairOrichalcumPlus, - LocationName.NothingsCallMythrilGem, - LocationName.NothingsCallOrichalcum, - LocationName.TwilightsViewCosmicBelt, - LocationName.NaughtsSkywayMythrilGem, - LocationName.NaughtsSkywayOrichalcum, - LocationName.NaughtsSkywayMythrilCrystal, - LocationName.RuinandCreationsPassageMythrilStone, - LocationName.RuinandCreationsPassageAPBoost, - LocationName.RuinandCreationsPassageMythrilCrystal, - LocationName.RuinandCreationsPassageOrichalcum, - LocationName.GardenofAssemblageMap, - LocationName.GoALostIllusion, - LocationName.ProofofNonexistence, + location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest" } } - -AllWeaponSlot = { - LocationName.FAKESlot, - LocationName.DetectionSaberSlot, - LocationName.EdgeofUltimaSlot, - LocationName.KingdomKeySlot, - LocationName.OathkeeperSlot, - LocationName.OblivionSlot, - LocationName.StarSeekerSlot, - LocationName.HiddenDragonSlot, - LocationName.HerosCrestSlot, - LocationName.MonochromeSlot, - LocationName.FollowtheWindSlot, - LocationName.CircleofLifeSlot, - LocationName.PhotonDebuggerSlot, - LocationName.GullWingSlot, - LocationName.RumblingRoseSlot, - LocationName.GuardianSoulSlot, - LocationName.WishingLampSlot, - LocationName.DecisivePumpkinSlot, - LocationName.SweetMemoriesSlot, - LocationName.MysteriousAbyssSlot, - LocationName.SleepingLionSlot, - LocationName.BondofFlameSlot, - LocationName.TwoBecomeOneSlot, - LocationName.FatalCrestSlot, - LocationName.FenrirSlot, - LocationName.UltimaWeaponSlot, - LocationName.WinnersProofSlot, - LocationName.PurebloodSlot, - LocationName.Centurion2, - LocationName.CometStaff, - LocationName.HammerStaff, - LocationName.LordsBroom, - LocationName.MagesStaff, - LocationName.MeteorStaff, - LocationName.NobodyLance, - LocationName.PreciousMushroom, - LocationName.PreciousMushroom2, - LocationName.PremiumMushroom, - LocationName.RisingDragon, - LocationName.SaveTheQueen2, - LocationName.ShamansRelic, - LocationName.VictoryBell, - LocationName.WisdomWand, - - LocationName.AdamantShield, - LocationName.AkashicRecord, - LocationName.ChainGear, - LocationName.DreamCloud, - LocationName.FallingStar, - LocationName.FrozenPride2, - LocationName.GenjiShield, - LocationName.KnightDefender, - LocationName.KnightsShield, - LocationName.MajesticMushroom, - LocationName.MajesticMushroom2, - LocationName.NobodyGuard, - LocationName.OgreShield, - LocationName.SaveTheKing2, - LocationName.UltimateMushroom, } -RegionTable = { - "FirstVisits": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.Pl_Region, - RegionName.STT_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "SecondVisits": { - RegionName.LoD2_Region, - RegionName.Ag2_Region, - RegionName.Tr_Region, - RegionName.Pr2_Region, - RegionName.Oc2_Region, - RegionName.Bc2_Region, - RegionName.Sp2_Region, - RegionName.Ht2_Region, - RegionName.Hb2_Region, - RegionName.Pl2_Region, - RegionName.STT_Region, - RegionName.Twtnw2_Region, - }, - "ValorRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "WisdomRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, - "LimitRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - RegionName.STT_Region, - }, - "MasterRegion": { - RegionName.LoD_Region, - RegionName.Ag_Region, - RegionName.Dc_Region, - RegionName.Pr_Region, - RegionName.Oc_Region, - RegionName.Bc_Region, - RegionName.Sp_Region, - RegionName.Ht_Region, - RegionName.Hb_Region, - RegionName.TT_Region, - RegionName.Twtnw_Region, - }, # could add lod2 and bc2 as an option since those spawns are rng - "FinalRegion": { - RegionName.TT3_Region, - RegionName.Twtnw_PostRoxas, - RegionName.Twtnw2_Region, - } -} - -all_locations = { - **TWTNW_Checks, - **TWTNW2_Checks, - **TT_Checks, - **TT2_Checks, - **TT3_Checks, - **STT_Checks, - **PL_Checks, - **PL2_Checks, - **CoR_Checks, - **HB_Checks, - **HB2_Checks, - **HT_Checks, - **HT2_Checks, - **PR_Checks, - **PR2_Checks, - **PR_Checks, - **PR2_Checks, - **SP_Checks, - **SP2_Checks, - **BC_Checks, - **BC2_Checks, - **Oc_Checks, - **Oc2_Checks, - **Oc2Cups, - **HundredAcre1_Checks, - **HundredAcre2_Checks, - **HundredAcre3_Checks, - **HundredAcre4_Checks, - **HundredAcre5_Checks, - **HundredAcre6_Checks, - **DC_Checks, - **TR_Checks, - **AG_Checks, - **AG2_Checks, - **LoD_Checks, - **LoD2_Checks, - **SoraLevels, - **Form_Checks, - **GoA_Checks, - **Keyblade_Slots, - **Critical_Checks, - **Donald_Checks, - **Goofy_Checks, -} - -location_table = {} - - -def setup_locations(): - totallocation_table = {**TWTNW_Checks, **TWTNW2_Checks, **TT_Checks, **TT2_Checks, **TT3_Checks, **STT_Checks, - **PL_Checks, **PL2_Checks, **CoR_Checks, **HB_Checks, **HB2_Checks, - **PR_Checks, **PR2_Checks, **PR_Checks, **PR2_Checks, **SP_Checks, **SP2_Checks, **BC_Checks, - **BC2_Checks, **HT_Checks, **HT2_Checks, - **Oc_Checks, **Oc2_Checks, **Oc2Cups, **Critical_Checks, **Donald_Checks, **Goofy_Checks, - **HundredAcre1_Checks, **HundredAcre2_Checks, **HundredAcre3_Checks, **HundredAcre4_Checks, - **HundredAcre5_Checks, **HundredAcre6_Checks, - **DC_Checks, **TR_Checks, **AG_Checks, **AG2_Checks, **LoD_Checks, **LoD2_Checks, - **SoraLevels, - **Form_Checks, **GoA_Checks, **Keyblade_Slots} - return totallocation_table - - -lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if - data.code} diff --git a/worlds/kh2/Logic.py b/worlds/kh2/Logic.py new file mode 100644 index 0000000000..1f13aa5f02 --- /dev/null +++ b/worlds/kh2/Logic.py @@ -0,0 +1,642 @@ +from .Names import ItemName, RegionName, LocationName + +# this file contains the dicts,lists and sets used for making rules in rules.py +base_tools = [ + ItemName.FinishingPlus, + ItemName.Guard, + ItemName.AerialRecovery +] +gap_closer = [ + ItemName.SlideDash, + ItemName.FlashStep +] +defensive_tool = [ + ItemName.ReflectElement, + ItemName.Guard +] +form_list = [ + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm, + ItemName.FinalForm +] +form_list_without_final = [ + ItemName.ValorForm, + ItemName.WisdomForm, + ItemName.LimitForm, + ItemName.MasterForm +] +ground_finisher = [ + ItemName.GuardBreak, + ItemName.Explosion, + ItemName.FinishingLeap +] +party_limit = [ + ItemName.Fantasia, + ItemName.FlareForce, + ItemName.Teamwork, + ItemName.TornadoFusion +] +donald_limit = [ + ItemName.Fantasia, + ItemName.FlareForce +] +aerial_move = [ + ItemName.AerialDive, + ItemName.AerialSpiral, + ItemName.HorizontalSlash, + ItemName.AerialSweep, + ItemName.AerialFinish +] +level_3_form_loc = [ + LocationName.Valorlvl3, + LocationName.Wisdomlvl3, + LocationName.Limitlvl3, + LocationName.Masterlvl3, + LocationName.Finallvl3 +] +black_magic = [ + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement +] +magic = [ + ItemName.FireElement, + ItemName.BlizzardElement, + ItemName.ThunderElement, + ItemName.ReflectElement, + ItemName.CureElement, + ItemName.MagnetElement +] +summons = [ + ItemName.ChickenLittle, + ItemName.Stitch, + ItemName.Genie, + ItemName.PeterPan +] +three_proofs = [ + ItemName.ProofofConnection, + ItemName.ProofofPeace, + ItemName.ProofofNonexistence +] + +auto_form_dict = { + ItemName.FinalForm: ItemName.AutoFinal, + ItemName.MasterForm: ItemName.AutoMaster, + ItemName.LimitForm: ItemName.AutoLimit, + ItemName.WisdomForm: ItemName.AutoWisdom, + ItemName.ValorForm: ItemName.AutoValor, +} + +# could use comprehension for getting a list of the region objects but eh I like this more +drive_form_list = [RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, RegionName.Final, RegionName.Summon] + +easy_data_xigbar_tools = { + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.AerialDive: 1, + ItemName.HorizontalSlash: 1, + ItemName.AirComboPlus: 2, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, +} +normal_data_xigbar_tools = { + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.HorizontalSlash: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, +} + +easy_data_lex_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1 +} +normal_data_lex_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 1, +} + +easy_data_marluxia_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialRecovery: 1, +} +normal_data_marluxia_tools = { + ItemName.Guard: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 1, + ItemName.AerialRecovery: 1, +} +easy_terra_tools = { + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.FireElement: 3, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.DodgeRoll: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3 +} +normal_terra_tools = { + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.Guard: 1, + ItemName.DodgeRoll: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2 +} +hard_terra_tools = { + ItemName.Explosion: 1, + ItemName.ComboPlus: 2, + ItemName.DodgeRoll: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.Guard: 1 +} +easy_data_luxord_tools = { + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, +} +easy_data_zexion = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.QuickRun: 3, +} +normal_data_zexion = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.QuickRun: 3 +} +hard_data_zexion = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.QuickRun: 2, +} +easy_data_xaldin = { + ItemName.FireElement: 3, + ItemName.AirComboPlus: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.HighJump: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.MagnetElement: 1, + ItemName.HorizontalSlash: 1, + ItemName.AerialDive: 1, + ItemName.AerialSpiral: 1, + ItemName.BerserkCharge: 1 +} +normal_data_xaldin = { + ItemName.FireElement: 3, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.HighJump: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.MagnetElement: 1, + ItemName.HorizontalSlash: 1, + ItemName.AerialDive: 1, + ItemName.AerialSpiral: 1, +} +hard_data_xaldin = { + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.HighJump: 2, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.MagnetElement: 1, + ItemName.AerialDive: 1 +} +easy_data_larxene = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1 +} +normal_data_larxene = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +hard_data_larxene = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, +} +easy_data_vexen = { + ItemName.FireElement: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +normal_data_vexen = { + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.Guard: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +hard_data_vexen = { + ItemName.FireElement: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, + ItemName.AerialDodge: 2, + ItemName.Glide: 2, + ItemName.DodgeRoll: 3, + ItemName.QuickRun: 3, +} +easy_thousand_heartless_rules = { + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.Guard: 1, + ItemName.MagnetElement: 2, +} +normal_thousand_heartless_rules = { + ItemName.LimitForm: 1, + ItemName.Guard: 1, +} +easy_data_demyx = { + ItemName.FormBoost: 1, + ItemName.ReflectElement: 2, + ItemName.FireElement: 3, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.FinishingPlus: 1, +} +normal_data_demyx = { + ItemName.ReflectElement: 2, + ItemName.FireElement: 3, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.FinishingPlus: 1, +} +hard_data_demyx = { + ItemName.ReflectElement: 1, + ItemName.FireElement: 2, + ItemName.FlareForce: 1, + ItemName.Guard: 1, + ItemName.FinishingPlus: 1, +} +easy_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, +} +normal_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, +} +hard_sephiroth_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, +} + +not_hard_cor_tools_dict = { + ItemName.ReflectElement: 3, + ItemName.Stitch: 1, + ItemName.ChickenLittle: 1, + ItemName.MagnetElement: 2, + ItemName.Explosion: 1, + ItemName.FinishingLeap: 1, + ItemName.ThunderElement: 2, +} +transport_tools_dict = { + ItemName.ReflectElement: 3, + ItemName.Stitch: 1, + ItemName.ChickenLittle: 1, + ItemName.MagnetElement: 2, + ItemName.Explosion: 1, + ItemName.FinishingLeap: 1, + ItemName.ThunderElement: 3, + ItemName.Fantasia: 1, + ItemName.FlareForce: 1, + ItemName.Genie: 1, +} +easy_data_saix = { + ItemName.Guard: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.ThunderElement: 1, + ItemName.BlizzardElement: 1, + ItemName.FlareForce: 1, + ItemName.Fantasia: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1 +} +normal_data_saix = { + ItemName.Guard: 1, + ItemName.ThunderElement: 1, + ItemName.BlizzardElement: 1, + ItemName.FireElement: 3, + ItemName.ReflectElement: 3, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +hard_data_saix = { + ItemName.Guard: 1, + ItemName.BlizzardElement: 1, + ItemName.ReflectElement: 1, + ItemName.AerialDodge: 3, + ItemName.Glide: 3, +} +easy_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, +} +normal_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, +} +hard_data_roxas_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, +} +easy_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.BlizzardElement: 3, +} +normal_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.GuardBreak: 1, + ItemName.Explosion: 1, + ItemName.DodgeRoll: 3, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 3, +} +hard_data_axel_tools = { + ItemName.Guard: 1, + ItemName.ReflectElement: 1, + ItemName.DodgeRoll: 2, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 2, +} +easy_roxas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.LimitForm: 1, + ItemName.ThunderElement: 1, + ItemName.ReflectElement: 2, + ItemName.GuardBreak: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 1 +} +normal_roxas_tools = { + ItemName.ThunderElement: 1, + ItemName.ReflectElement: 2, + ItemName.GuardBreak: 1, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.BlizzardElement: 1 +} +easy_xigbar_tools = { + ItemName.HorizontalSlash: 1, + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Glide: 2, + ItemName.AerialDodge: 2, + ItemName.QuickRun: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1, +} +normal_xigbar_tools = { + ItemName.FireElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Glide: 2, + ItemName.AerialDodge: 2, + ItemName.QuickRun: 2, + ItemName.ReflectElement: 1, + ItemName.Guard: 1 +} +easy_luxord_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_luxord_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_saix_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_saix_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_xemnas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.LimitForm: 1, +} +normal_xemnas_tools = { + ItemName.AerialDodge: 1, + ItemName.Glide: 1, + ItemName.QuickRun: 2, + ItemName.Guard: 1, + ItemName.ReflectElement: 2, +} +easy_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.TrinityLimit: 1, + ItemName.SecondChance: 1, + ItemName.OnceMore: 1, + ItemName.LimitForm: 1, +} +normal_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 3, + ItemName.SlideDash: 1, + ItemName.FlashStep: 1, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.LimitForm: 1, +} +hard_data_xemnas = { + ItemName.ComboMaster: 1, + ItemName.Slapshot: 1, + ItemName.ReflectElement: 2, + ItemName.FinishingPlus: 1, + ItemName.Guard: 1, + ItemName.LimitForm: 1, +} +final_leveling_access = { + LocationName.MemorysSkyscaperMythrilCrystal, + LocationName.GrimReaper2, + LocationName.Xaldin, + LocationName.StormRider, + LocationName.SunsetTerraceAbilityRing +} + +multi_form_region_access = { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, +} +limit_form_region_access = { + ItemName.CastleKey, + ItemName.BattlefieldsofWar, + ItemName.SwordoftheAncestor, + ItemName.BeastsClaw, + ItemName.BoneFist, + ItemName.SkillandCrossbones, + ItemName.Scimitar, + ItemName.MembershipCard, + ItemName.IceCream, + ItemName.WaytotheDawn, + ItemName.IdentityDisk, + ItemName.NamineSketches +} diff --git a/worlds/kh2/Names/ItemName.py b/worlds/kh2/Names/ItemName.py index 57cfcbe060..d7dbdb0ad3 100644 --- a/worlds/kh2/Names/ItemName.py +++ b/worlds/kh2/Names/ItemName.py @@ -12,8 +12,7 @@ SecretAnsemsReport10 = "Secret Ansem's Report 10" SecretAnsemsReport11 = "Secret Ansem's Report 11" SecretAnsemsReport12 = "Secret Ansem's Report 12" SecretAnsemsReport13 = "Secret Ansem's Report 13" - -# progression +# proofs, visit unlocks and forms ProofofConnection = "Proof of Connection" ProofofNonexistence = "Proof of Nonexistence" ProofofPeace = "Proof of Peace" @@ -32,51 +31,33 @@ IdentityDisk = "Identity Disk" NamineSketches = "Namine Sketches" CastleKey = "Disney Castle Key" TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" -TornPages = "Torn Page" ValorForm = "Valor Form" WisdomForm = "Wisdom Form" LimitForm = "Limit Form" MasterForm = "Master Form" FinalForm = "Final Form" - +AntiForm = "Anti Form" # magic and summons -FireElement = "Fire Element" - -BlizzardElement = "Blizzard Element" - -ThunderElement = "Thunder Element" - -CureElement = "Cure Element" - -MagnetElement = "Magnet Element" - -ReflectElement = "Reflect Element" +FireElement = "Fire Element" +BlizzardElement = "Blizzard Element" +ThunderElement = "Thunder Element" +CureElement = "Cure Element" +MagnetElement = "Magnet Element" +ReflectElement = "Reflect Element" Genie = "Genie" PeterPan = "Peter Pan" Stitch = "Stitch" ChickenLittle = "Chicken Little" -#movement +# movement HighJump = "High Jump" - - QuickRun = "Quick Run" - - AerialDodge = "Aerial Dodge" - - Glide = "Glide" - - DodgeRoll = "Dodge Roll" - -#keyblades +# keyblades Oathkeeper = "Oathkeeper" Oblivion = "Oblivion" StarSeeker = "Star Seeker" @@ -109,7 +90,6 @@ MagesStaff = "Mages Staff" MeteorStaff = "Meteor Staff" CometStaff = "Comet Staff" Centurion2 = "Centurion+" -MeteorStaff = "Meteor Staff" NobodyLance = "Nobody Lance" PreciousMushroom = "Precious Mushroom" PreciousMushroom2 = "Precious Mushroom+" @@ -203,7 +183,7 @@ Ribbon = "Ribbon" GrandRibbon = "Grand Ribbon" # usefull and stat incre -MickyMunnyPouch = "Mickey Munny Pouch" +MickeyMunnyPouch = "Mickey Munny Pouch" OletteMunnyPouch = "Olette Munny Pouch" HadesCupTrophy = "Hades Cup Trophy" UnknownDisk = "Unknown Disk" @@ -253,7 +233,6 @@ LightDarkness = "Light & Darkness" MagicLock = "Magic Lock-On" LeafBracer = "Leaf Bracer" CombinationBoost = "Combination Boost" -DamageDrive = "Damage Drive" OnceMore = "Once More" SecondChance = "Second Chance" @@ -313,10 +292,6 @@ DonaldLuckyLucky = "Donald Lucky Lucky" DonaldFireBoost = "Donald Fire Boost" DonaldBlizzardBoost = "Donald Blizzard Boost" DonaldThunderBoost = "Donald Thunder Boost" -DonaldFireBoost = "Donald Fire Boost" -DonaldBlizzardBoost = "Donald Blizzard Boost" -DonaldThunderBoost = "Donald Thunder Boost" -DonaldMPRage = "Donald MP Rage" DonaldMPHastera = "Donald MP Hastera" DonaldAutoLimit = "Donald Auto Limit" DonaldHyperHealing = "Donald Hyper Healing" @@ -324,14 +299,7 @@ DonaldAutoHealing = "Donald Auto Healing" DonaldMPHastega = "Donald MP Hastega" DonaldItemBoost = "Donald Item Boost" DonaldDamageControl = "Donald Damage Control" -DonaldHyperHealing = "Donald Hyper Healing" -DonaldMPRage = "Donald MP Rage" DonaldMPHaste = "Donald MP Haste" -DonaldMPHastera = "Donald MP Hastera" -DonaldMPHastega = "Donald MP Hastega" -DonaldMPHaste = "Donald MP Haste" -DonaldDamageControl = "Donald Damage Control" -DonaldMPHastera = "Donald MP Hastera" DonaldDraw = "Donald Draw" # goofy abili @@ -353,27 +321,18 @@ GoofyOnceMore = "Goofy Once More" GoofyAutoChange = "Goofy Auto Change" GoofyHyperHealing = "Goofy Hyper Healing" GoofyAutoHealing = "Goofy Auto Healing" -GoofyDefender = "Goofy Defender" -GoofyHyperHealing = "Goofy Hyper Healing" GoofyMPHaste = "Goofy MP Haste" GoofyMPHastera = "Goofy MP Hastera" -GoofyMPRage = "Goofy MP Rage" GoofyMPHastega = "Goofy MP Hastega" -GoofyItemBoost = "Goofy Item Boost" -GoofyDamageControl = "Goofy Damage Control" -GoofyProtect = "Goofy Protect" -GoofyProtera = "Goofy Protera" -GoofyProtega = "Goofy Protega" -GoofyDamageControl = "Goofy Damage Control" GoofyProtect = "Goofy Protect" GoofyProtera = "Goofy Protera" GoofyProtega = "Goofy Protega" Victory = "Victory" LuckyEmblem = "Lucky Emblem" -Bounty="Bounty" +Bounty = "Bounty" -UniversalKey="Universal Key" +# UniversalKey = "Universal Key" # Keyblade Slots FAKESlot = "FAKE (Slot)" DetectionSaberSlot = "Detection Saber (Slot)" @@ -402,3 +361,73 @@ FatalCrestSlot = "Fatal Crest (Slot)" FenrirSlot = "Fenrir (Slot)" UltimaWeaponSlot = "Ultima Weapon (Slot)" WinnersProofSlot = "Winner's Proof (Slot)" + +# events +HostileProgramEvent = "Hostile Program Event" +McpEvent = "Master Control Program Event" +ASLarxeneEvent = "AS Larxene Event" +DataLarxeneEvent = "Data Larxene Event" +BarbosaEvent = "Barbosa Event" +GrimReaper1Event = "Grim Reaper 1 Event" +GrimReaper2Event = "Grim Reaper 2 Event" +DataLuxordEvent = "Data Luxord Event" +DataAxelEvent = "Data Axel Event" +CerberusEvent = "Cerberus Event" +OlympusPeteEvent = "Olympus Pete Event" +HydraEvent = "Hydra Event" +OcPainAndPanicCupEvent = "Pain and Panic Cup Event" +OcCerberusCupEvent = "Cerberus Cup Event" +HadesEvent = "Hades Event" +ASZexionEvent = "AS Zexion Event" +DataZexionEvent = "Data Zexion Event" +Oc2TitanCupEvent = "Titan Cup Event" +Oc2GofCupEvent = "Goddess of Fate Cup Event" +Oc2CupsEvent = "Olympus Coliseum Cups Event" +HadesCupEvents = "Olympus Coliseum Hade's Paradox Event" +PrisonKeeperEvent = "Prison Keeper Event" +OogieBoogieEvent = "Oogie Boogie Event" +ExperimentEvent = "The Experiment Event" +ASVexenEvent = "AS Vexen Event" +DataVexenEvent = "Data Vexen Event" +ShanYuEvent = "Shan Yu Event" +AnsemRikuEvent = "Ansem Riku Event" +StormRiderEvent = "Storm Rider Event" +DataXigbarEvent = "Data Xigbar Event" +RoxasEvent = "Roxas Event" +XigbarEvent = "Xigbar Event" +LuxordEvent = "Luxord Event" +SaixEvent = "Saix Event" +XemnasEvent = "Xemnas Event" +ArmoredXemnasEvent = "Armored Xemnas Event" +ArmoredXemnas2Event = "Armored Xemnas 2 Event" +FinalXemnasEvent = "Final Xemnas Event" +DataXemnasEvent = "Data Xemnas Event" +ThresholderEvent = "Thresholder Event" +BeastEvent = "Beast Event" +DarkThornEvent = "Dark Thorn Event" +XaldinEvent = "Xaldin Event" +DataXaldinEvent = "Data Xaldin Event" +TwinLordsEvent = "Twin Lords Event" +GenieJafarEvent = "Genie Jafar Event" +ASLexaeusEvent = "AS Lexaeus Event" +DataLexaeusEvent = "Data Lexaeus Event" +ScarEvent = "Scar Event" +GroundShakerEvent = "Groundshaker Event" +DataSaixEvent = "Data Saix Event" +HBDemyxEvent = "Hollow Bastion Demyx Event" +ThousandHeartlessEvent = "Thousand Heartless Event" +Mushroom13Event = "Mushroom 13 Event" +SephiEvent = "Sephiroth Event" +DataDemyxEvent = "Data Demyx Event" +CorFirstFightEvent = "Cavern of Rememberance:Fight 1 Event" +CorSecondFightEvent = "Cavern of Rememberance:Fight 2 Event" +TransportEvent = "Transport to Rememberance Event" +OldPeteEvent = "Old Pete Event" +FuturePeteEvent = "Future Pete Event" +ASMarluxiaEvent = "AS Marluxia Event" +DataMarluxiaEvent = "Data Marluxia Event" +TerraEvent = "Terra Event" +TwilightThornEvent = "Twilight Thorn Event" +Axel1Event = "Axel 1 Event" +Axel2Event = "Axel 2 Event" +DataRoxasEvent = "Data Roxas Event" diff --git a/worlds/kh2/Names/LocationName.py b/worlds/kh2/Names/LocationName.py index 1a6c4d07fb..bcaf664558 100644 --- a/worlds/kh2/Names/LocationName.py +++ b/worlds/kh2/Names/LocationName.py @@ -27,7 +27,7 @@ ThroneRoomOgreShield = "(LoD2) Throne Room Ogre Shield" ThroneRoomMythrilCrystal = "(LoD2) Throne Room Mythril Crystal" ThroneRoomOrichalcum = "(LoD2) Throne Room Orichalcum" StormRider = "(LoD2) Storm Rider Bonus: Sora Slot 1" -XigbarDataDefenseBoost = "Data Xigbar" +XigbarDataDefenseBoost = "(Post LoD2: Summit) Data Xigbar" AgrabahMap = "(AG) Agrabah Map" AgrabahDarkShard = "(AG) Agrabah Dark Shard" @@ -62,9 +62,10 @@ RuinedChamberTornPages = "(AG2) Ruined Chamber Torn Pages RuinedChamberRuinsMap = "(AG2) Ruined Chamber Ruins Map" GenieJafar = "(AG2) Genie Jafar" WishingLamp = "(AG2) Wishing Lamp" -LexaeusBonus = "Lexaeus Bonus: Sora Slot 1" -LexaeusASStrengthBeyondStrength = "AS Lexaeus" -LexaeusDataLostIllusion = "Data Lexaeus" +LexaeusBonus = "(Post AG2: Peddler's Shop) Lexaeus Bonus: Sora Slot 1" +LexaeusASStrengthBeyondStrength = "(Post AG2: Peddler's Shop) AS Lexaeus" +LexaeusDataLostIllusion = "(Post AG2: Peddler's Shop) Data Lexaeus" + DCCourtyardMythrilShard = "(DC) Courtyard Mythril Shard" DCCourtyardStarRecipe = "(DC) Courtyard Star Recipe" DCCourtyardAPBoost = "(DC) Courtyard AP Boost" @@ -89,12 +90,15 @@ FuturePete = "(TR) Future Pete Bonus: Sora Sl FuturePeteGetBonus = "(TR) Future Pete Bonus: Sora Slot 2" Monochrome = "(TR) Monochrome" WisdomForm = "(TR) Wisdom Form" -MarluxiaGetBonus = "Marluxia Bonus: Sora Slot 1" -MarluxiaASEternalBlossom = "AS Marluxia" -MarluxiaDataLostIllusion = "Data Marluxia" -LingeringWillBonus = "Lingering Will Bonus: Sora Slot 1" -LingeringWillProofofConnection = "Lingering Will Proof of Connection" -LingeringWillManifestIllusion = "Lingering Will Manifest Illusion" + +MarluxiaGetBonus = "(Post TR:Hall of the Cornerstone) Marluxia Bonus: Sora Slot 1" +MarluxiaASEternalBlossom = "(Post TR:Hall of the Cornerstone) AS Marluxia" +MarluxiaDataLostIllusion = "(Post TR:Hall of the Cornerstone) Data Marluxia" + +LingeringWillBonus = "(Post TR:Hall of the Cornerstone) Lingering Will Bonus: Sora Slot 1" +LingeringWillProofofConnection = "(Post TR:Hall of the Cornerstone) Lingering Will Proof of Connection" +LingeringWillManifestIllusion = "(Post TR:Hall of the Cornerstone) Lingering Will Manifest Illusion" + PoohsHouse100AcreWoodMap = "(100Acre) Pooh's House 100 Acre Wood Map" PoohsHouseAPBoost = "(100Acre) Pooh's House AP Boost" PoohsHouseMythrilStone = "(100Acre) Pooh's House Mythril Stone" @@ -119,6 +123,7 @@ StarryHillCosmicRing = "(100Acre) Starry Hill Cosmic Ri StarryHillStyleRecipe = "(100Acre) Starry Hill Style Recipe" StarryHillCureElement = "(100Acre) Starry Hill Cure Element" StarryHillOrichalcumPlus = "(100Acre) Starry Hill Orichalcum+" + PassageMythrilShard = "(OC) Passage Mythril Shard" PassageMythrilStone = "(OC) Passage Mythril Stone" PassageEther = "(OC) Passage Ether" @@ -162,9 +167,9 @@ SkillfulRingTitanCup = "Skillful Ring Titan Cup" FatalCrestGoddessofFateCup = "Fatal Crest Goddess of Fate Cup" OrichalcumPlusGoddessofFateCup = "Orichalcum+ Goddess of Fate Cup" HadesCupTrophyParadoxCups = "Hades Cup Trophy Paradox Cups" -ZexionBonus = "Zexion Bonus: Sora Slot 1" -ZexionASBookofShadows = "AS Zexion" -ZexionDataLostIllusion = "Data Zexion" +ZexionBonus = "(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Sora Slot 1" +ZexionASBookofShadows = "(Post OC2: Cave of the Dead Inner Chamber) AS Zexion" +ZexionDataLostIllusion = "(Post OC2: Cave of the Dead Inner Chamber) Data Zexion" BCCourtyardAPBoost = "(BC) Courtyard AP Boost" @@ -198,7 +203,7 @@ CastleWallsMap = "(BC2) Castle Walls Map" Xaldin = "(BC2) Xaldin Bonus: Sora Slot 1" XaldinGetBonus = "(BC2) Xaldin Bonus: Sora Slot 2" SecretAnsemReport4 = "(BC2) Secret Ansem Report 4 (Xaldin)" -XaldinDataDefenseBoost = "Data Xaldin" +XaldinDataDefenseBoost = "(Post BC2: Ballroom) Data Xaldin" @@ -223,9 +228,9 @@ CentralComputerCoreCosmicArts = "(SP2) Central Computer Core Cos CentralComputerCoreMap = "(SP2) Central Computer Core Map" MCP = "(SP2) MCP Bonus: Sora Slot 1" MCPGetBonus = "(SP2) MCP Bonus: Sora Slot 2" -LarxeneBonus = "Larxene Bonus: Sora Slot 1" -LarxeneASCloakedThunder = "AS Larxene" -LarxeneDataLostIllusion = "Data Larxene" +LarxeneBonus = "(Post SP2: Central Computer Core) Larxene Bonus: Sora Slot 1" +LarxeneASCloakedThunder = "(Post SP2: Central Computer Core) AS Larxene" +LarxeneDataLostIllusion = "(Post SP2: Central Computer Core) Data Larxene" GraveyardMythrilShard = "(HT) Graveyard Mythril Shard" GraveyardSerenityGem = "(HT) Graveyard Serenity Gem" @@ -249,9 +254,9 @@ Present = "(HT2) Present" DecoyPresents = "(HT2) Decoy Presents" Experiment = "(HT2) Experiment Bonus: Sora Slot 1" DecisivePumpkin = "(HT2) Decisive Pumpkin" -VexenBonus = "Vexen Bonus: Sora Slot 1" -VexenASRoadtoDiscovery = "AS Vexen" -VexenDataLostIllusion = "Data Vexen" +VexenBonus = "(Post HT2: Yuletide Hill) Vexen Bonus: Sora Slot 1" +VexenASRoadtoDiscovery = "(Post HT2: Yuletide Hill) AS Vexen" +VexenDataLostIllusion = "(Post HT2: Yuletide Hill) Data Vexen" RampartNavalMap = "(PR) Rampart Naval Map" RampartMythrilStone = "(PR) Rampart Mythril Stone" @@ -286,7 +291,7 @@ SeadriftRowShipGraveyardMap = "(PR2) Seadrift Row Ship Graveya GrimReaper2 = "(PR2) Grim Reaper 2 Bonus: Sora Slot 1" SecretAnsemReport6 = "(PR2) Secret Ansem Report 6 (Grim Reaper 2)" -LuxordDataAPBoost = "Data Luxord" +LuxordDataAPBoost = "(Post PR2: Treasure Heap) Data Luxord" MarketplaceMap = "(HB) Marketplace Map" BoroughDriveRecovery = "(HB) Borough Drive Recovery" @@ -329,7 +334,7 @@ SephirothBonus = "Sephiroth Bonus: Sora Slot 1" SephirothFenrir = "Sephiroth Fenrir" WinnersProof = "(HB2) Winner's Proof" ProofofPeace = "(HB2) Proof of Peace" -DemyxDataAPBoost = "Data Demyx" +DemyxDataAPBoost = "(Post HB2: Restoration Site) Data Demyx" CoRDepthsAPBoost = "(CoR) Depths AP Boost" CoRDepthsPowerCrystal = "(CoR) Depths Power Crystal" @@ -386,7 +391,7 @@ ScarFireElement = "(PL) Scar Fire Element" Hyenas2 = "(PL2) Hyenas 2 Bonus: Sora Slot 1" Groundshaker = "(PL2) Groundshaker Bonus: Sora Slot 1" GroundshakerGetBonus = "(PL2) Groundshaker Bonus: Sora Slot 2" -SaixDataDefenseBoost = "Data Saix" +SaixDataDefenseBoost = "(Post PL2: Peak) Data Saix" TwilightTownMap = "(STT) Twilight Town Map" MunnyPouchOlette = "(STT) Munny Pouch Olette" @@ -415,7 +420,7 @@ MansionMap = "(STT) Mansion Map" MansionLibraryHiPotion = "(STT) Mansion Library Hi-Potion" Axel2 = "(STT) Axel 2" MansionBasementCorridorHiPotion = "(STT) Mansion Basement Corridor Hi-Potion" -RoxasDataMagicBoost = "Data Roxas" +RoxasDataMagicBoost = "(Post STT: Mansion Pod Room) Data Roxas" OldMansionPotion = "(TT) Old Mansion Potion" OldMansionMythrilShard = "(TT) Old Mansion Mythril Shard" @@ -468,46 +473,46 @@ BeamSecretAnsemReport10 = "(TT3) Beam Secret Ansem Report MansionBasementCorridorUltimateRecipe = "(TT3) Mansion Basement Corridor Ultimate Recipe" BetwixtandBetween = "(TT3) Betwixt and Between" BetwixtandBetweenBondofFlame = "(TT3) Betwixt and Between Bond of Flame" -AxelDataMagicBoost = "Data Axel" +AxelDataMagicBoost = "(Post TT3: Betwixt and Between) Data Axel" FragmentCrossingMythrilStone = "(TWTNW) Fragment Crossing Mythril Stone" FragmentCrossingMythrilCrystal = "(TWTNW) Fragment Crossing Mythril Crystal" FragmentCrossingAPBoost = "(TWTNW) Fragment Crossing AP Boost" FragmentCrossingOrichalcum = "(TWTNW) Fragment Crossing Orichalcum" -Roxas = "(TWTNW) Roxas Bonus: Sora Slot 1" -RoxasGetBonus = "(TWTNW) Roxas Bonus: Sora Slot 2" -RoxasSecretAnsemReport8 = "(TWTNW) Roxas Secret Ansem Report 8" -TwoBecomeOne = "(TWTNW) Two Become One" -MemorysSkyscaperMythrilCrystal = "(TWTNW) Memory's Skyscaper Mythril Crystal" -MemorysSkyscaperAPBoost = "(TWTNW) Memory's Skyscaper AP Boost" -MemorysSkyscaperMythrilStone = "(TWTNW) Memory's Skyscaper Mythril Stone" -TheBrinkofDespairDarkCityMap = "(TWTNW) The Brink of Despair Dark City Map" -TheBrinkofDespairOrichalcumPlus = "(TWTNW) The Brink of Despair Orichalcum+" -NothingsCallMythrilGem = "(TWTNW) Nothing's Call Mythril Gem" -NothingsCallOrichalcum = "(TWTNW) Nothing's Call Orichalcum" -TwilightsViewCosmicBelt = "(TWTNW) Twilight's View Cosmic Belt" -XigbarBonus = "(TWTNW) Xigbar Bonus: Sora Slot 1" -XigbarSecretAnsemReport3 = "(TWTNW) Xigbar Secret Ansem Report 3" -NaughtsSkywayMythrilGem = "(TWTNW) Naught's Skyway Mythril Gem" -NaughtsSkywayOrichalcum = "(TWTNW) Naught's Skyway Orichalcum" -NaughtsSkywayMythrilCrystal = "(TWTNW) Naught's Skyway Mythril Crystal" -Oblivion = "(TWTNW) Oblivion" -CastleThatNeverWasMap = "(TWTNW) Castle That Never Was Map" -Luxord = "(TWTNW) Luxord" -LuxordGetBonus = "(TWTNW) Luxord Bonus: Sora Slot 1" -LuxordSecretAnsemReport9 = "(TWTNW) Luxord Secret Ansem Report 9" -SaixBonus = "(TWTNW) Saix Bonus: Sora Slot 1" -SaixSecretAnsemReport12 = "(TWTNW) Saix Secret Ansem Report 12" -PreXemnas1SecretAnsemReport11 = "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)" -RuinandCreationsPassageMythrilStone = "(TWTNW) Ruin and Creation's Passage Mythril Stone" -RuinandCreationsPassageAPBoost = "(TWTNW) Ruin and Creation's Passage AP Boost" -RuinandCreationsPassageMythrilCrystal = "(TWTNW) Ruin and Creation's Passage Mythril Crystal" -RuinandCreationsPassageOrichalcum = "(TWTNW) Ruin and Creation's Passage Orichalcum" -Xemnas1 = "(TWTNW) Xemnas 1 Bonus: Sora Slot 1" -Xemnas1GetBonus = "(TWTNW) Xemnas 1 Bonus: Sora Slot 2" -Xemnas1SecretAnsemReport13 = "(TWTNW) Xemnas 1 Secret Ansem Report 13" +Roxas = "(TWTNW2) Roxas Bonus: Sora Slot 1" +RoxasGetBonus = "(TWTNW2) Roxas Bonus: Sora Slot 2" +RoxasSecretAnsemReport8 = "(TWTNW2) Roxas Secret Ansem Report 8" +TwoBecomeOne = "(TWTNW2) Two Become One" +MemorysSkyscaperMythrilCrystal = "(TWTNW2) Memory's Skyscaper Mythril Crystal" +MemorysSkyscaperAPBoost = "(TWTNW2) Memory's Skyscaper AP Boost" +MemorysSkyscaperMythrilStone = "(TWTNW2) Memory's Skyscaper Mythril Stone" +TheBrinkofDespairDarkCityMap = "(TWTNW2) The Brink of Despair Dark City Map" +TheBrinkofDespairOrichalcumPlus = "(TWTNW2) The Brink of Despair Orichalcum+" +NothingsCallMythrilGem = "(TWTNW2) Nothing's Call Mythril Gem" +NothingsCallOrichalcum = "(TWTNW2) Nothing's Call Orichalcum" +TwilightsViewCosmicBelt = "(TWTNW2) Twilight's View Cosmic Belt" +XigbarBonus = "(TWTNW2) Xigbar Bonus: Sora Slot 1" +XigbarSecretAnsemReport3 = "(TWTNW2) Xigbar Secret Ansem Report 3" +NaughtsSkywayMythrilGem = "(TWTNW2) Naught's Skyway Mythril Gem" +NaughtsSkywayOrichalcum = "(TWTNW2) Naught's Skyway Orichalcum" +NaughtsSkywayMythrilCrystal = "(TWTNW2) Naught's Skyway Mythril Crystal" +Oblivion = "(TWTNW2) Oblivion" +CastleThatNeverWasMap = "(TWTNW2) Castle That Never Was Map" +Luxord = "(TWTNW2) Luxord Bonus: Sora Slot 2" +LuxordGetBonus = "(TWTNW2) Luxord Bonus: Sora Slot 1" +LuxordSecretAnsemReport9 = "(TWTNW2) Luxord Secret Ansem Report 9" +SaixBonus = "(TWTNW2) Saix Bonus: Sora Slot 1" +SaixSecretAnsemReport12 = "(TWTNW2) Saix Secret Ansem Report 12" +PreXemnas1SecretAnsemReport11 = "(TWTNW3) Secret Ansem Report 11 (Pre-Xemnas 1)" +RuinandCreationsPassageMythrilStone = "(TWTNW3) Ruin and Creation's Passage Mythril Stone" +RuinandCreationsPassageAPBoost = "(TWTNW3) Ruin and Creation's Passage AP Boost" +RuinandCreationsPassageMythrilCrystal = "(TWTNW3) Ruin and Creation's Passage Mythril Crystal" +RuinandCreationsPassageOrichalcum = "(TWTNW3) Ruin and Creation's Passage Orichalcum" +Xemnas1 = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 1" +Xemnas1GetBonus = "(TWTNW3) Xemnas 1 Bonus: Sora Slot 2" +Xemnas1SecretAnsemReport13 = "(TWTNW3) Xemnas 1 Secret Ansem Report 13" FinalXemnas = "Final Xemnas" -XemnasDataPowerBoost = "Data Xemnas" +XemnasDataPowerBoost = "(Post TWTNW3: The Altar of Naught) Data Xemnas" Lvl1 ="Level 01" Lvl2 ="Level 02" Lvl3 ="Level 03" @@ -605,7 +610,7 @@ Lvl94 ="Level 94" Lvl95 ="Level 95" Lvl96 ="Level 96" Lvl97 ="Level 97" -Lvl98 ="Level 98" +Lvl98 ="Level 98" Lvl99 ="Level 99" Valorlvl1 ="Valor level 1" Valorlvl2 ="Valor level 2" @@ -643,13 +648,28 @@ Finallvl5 ="Final level 5" Finallvl6 ="Final level 6" Finallvl7 ="Final level 7" +Summonlvl2="Summon level 2" +Summonlvl3="Summon level 3" +Summonlvl4="Summon level 4" +Summonlvl5="Summon level 5" +Summonlvl6="Summon level 6" +Summonlvl7="Summon level 7" + + GardenofAssemblageMap ="Garden of Assemblage Map" GoALostIllusion ="GoA Lost Illusion" ProofofNonexistence ="Proof of Nonexistence Location" -test= "test" - +UnderseaKingdomMap ="(AT) Undersea Kingdom Map" +MysteriousAbyss ="(AT) Mysterious Abyss" +MusicalBlizzardElement ="(AT) Musical Blizzard Element" +MusicalOrichalcumPlus ="(AT) Musical Orichalcum+" +DonaldStarting1 ="Donald Starting Item 1" +DonaldStarting2 ="Donald Starting Item 2" +GoofyStarting1 ="Goofy Starting Item 1" +GoofyStarting2 ="Goofy Starting Item 2" +# TODO: remove in 4.3 Crit_1 ="Critical Starting Ability 1" Crit_2 ="Critical Starting Ability 2" Crit_3 ="Critical Starting Ability 3" @@ -657,14 +677,9 @@ Crit_4 ="Critical Starting Ability 4" Crit_5 ="Critical Starting Ability 5" Crit_6 ="Critical Starting Ability 6" Crit_7 ="Critical Starting Ability 7" -DonaldStarting1 ="Donald Starting Item 1" -DonaldStarting2 ="Donald Starting Item 2" -GoofyStarting1 ="Goofy Starting Item 1" -GoofyStarting2 ="Goofy Starting Item 2" - DonaldScreens ="(SP) Screens Bonus: Donald Slot 1" -DonaldDemyxHBGetBonus ="(HB) Demyx Bonus: Donald Slot 1" +DonaldDemyxHBGetBonus ="(HB2) Demyx Bonus: Donald Slot 1" DonaldDemyxOC ="(OC) Demyx Bonus: Donald Slot 1" DonaldBoatPete ="(TR) Boat Pete Bonus: Donald Slot 1" DonaldBoatPeteGetBonus ="(TR) Boat Pete Bonus: Donald Slot 2" @@ -694,7 +709,7 @@ GoofyStormRider ="(LoD2) Storm Rider Bonus: Goofy S GoofyBeast ="(BC) Beast Bonus: Goofy Slot 1" GoofyInterceptorBarrels ="(PR) Interceptor Barrels Bonus: Goofy Slot 1" GoofyTreasureRoom ="(AG) Treasure Room Heartless Bonus: Goofy Slot 1" -GoofyZexion ="Zexion Bonus: Goofy Slot 1" +GoofyZexion ="(Post OC2: Cave of the Dead Inner Chamber) Zexion Bonus: Goofy Slot 1" AdamantShield ="Adamant Shield Slot" @@ -760,4 +775,86 @@ UltimaWeaponSlot ="Ultima Weapon Slot" WinnersProofSlot ="Winner's Proof Slot" PurebloodSlot ="Pureblood Slot" -#Final_Region ="Final Form" +Mushroom13_1 = "(Post TWTNW3: Memory's Skyscraper) Mushroom XIII No. 1" +Mushroom13_2 = "(Post HT2: Christmas Tree Plaza) Mushroom XIII No. 2" +Mushroom13_3 = "(Post BC2: Bridge) Mushroom XIII No. 3" +Mushroom13_4 = "(Post LOD2: Palace Gates) Mushroom XIII No. 4" +Mushroom13_5 = "(Post AG2: Treasure Room) Mushroom XIII No. 5" +Mushroom13_6 = "(Post OC2: Atrium) Mushroom XIII No. 6" +Mushroom13_7 = "(Post TT3: Tunnel way) Mushroom XIII No. 7" +Mushroom13_8 = "(Post TT3: Tower) Mushroom XIII No. 8" +Mushroom13_9 = "(Post HB2: Castle Gates) Mushroom XIII No. 9" +Mushroom13_10 = "(Post PR2: Moonlight Nook) Mushroom XIII No. 10" +Mushroom13_11 = "(Post TR: Waterway) Mushroom XIII No. 11" +Mushroom13_12 = "(Post TT3: Old Mansion) Mushroom XIII No. 12" + + +HostileProgramEventLocation = "Hostile Program Event Location" +McpEventLocation = "Master Control Program Event Location" +ASLarxeneEventLocation = "AS Larxene Event Location" +DataLarxeneEventLocation = "Data Larxene Event Location" +BarbosaEventLocation = "Barbosa Event Location" +GrimReaper1EventLocation = "Grim Reaper 1 Event Location" +GrimReaper2EventLocation = "Grim Reaper 2 Event Location" +DataLuxordEventLocation = "Data Luxord Event Location" +DataAxelEventLocation = "Data Axel Event Location" +CerberusEventLocation = "Cerberus Event Location" +OlympusPeteEventLocation = "Olympus Pete Event Location" +HydraEventLocation = "Hydra Event Location" +OcPainAndPanicCupEventLocation = "Pain and Panic Cup Event Location" +OcCerberusCupEventLocation = "Cerberus Cup Event Location" +HadesEventLocation = "Hades Event Location" +ASZexionEventLocation = "AS Zexion Event Location" +DataZexionEventLocation = "Data Zexion Event Location" +Oc2TitanCupEventLocation = "Titan Cup Event Location" +Oc2GofCupEventLocation = "Goddess of Fate Cup Event Location" +Oc2CupsEventLocation = "Olympus Coliseum Cups Event Location" +HadesCupEventLocations = "Olympus Coliseum Hade's Paradox Event Location" +PrisonKeeperEventLocation = "Prison Keeper Event Location" +OogieBoogieEventLocation = "Oogie Boogie Event Location" +ExperimentEventLocation = "The Experiment Event Location" +ASVexenEventLocation = "AS Vexen Event Location" +DataVexenEventLocation = "Data Vexen Event Location" +ShanYuEventLocation = "Shan Yu Event Location" +AnsemRikuEventLocation = "Ansem Riku Event Location" +StormRiderEventLocation = "Storm Rider Event Location" +DataXigbarEventLocation = "Data Xigbar Event Location" +RoxasEventLocation = "Roxas Event Location" +XigbarEventLocation = "Xigbar Event Location" +LuxordEventLocation = "Luxord Event Location" +SaixEventLocation = "Saix Event Location" +XemnasEventLocation = "Xemnas Event Location" +ArmoredXemnasEventLocation = "Armored Xemnas Event Location" +ArmoredXemnas2EventLocation = "Armored Xemnas 2 Event Location" +FinalXemnasEventLocation = "Final Xemnas Event Location" +DataXemnasEventLocation = "Data Xemnas Event Location" +ThresholderEventLocation = "Thresholder Event Location" +BeastEventLocation = "Beast Event Location" +DarkThornEventLocation = "Dark Thorn Event Location" +XaldinEventLocation = "Xaldin Event Location" +DataXaldinEventLocation = "Data Xaldin Event Location" +TwinLordsEventLocation = "Twin Lords Event Location" +GenieJafarEventLocation = "Genie Jafar Event Location" +ASLexaeusEventLocation = "AS Lexaeus Event Location" +DataLexaeusEventLocation = "Data Lexaeus Event Location" +ScarEventLocation = "Scar Event Location" +GroundShakerEventLocation = "Groundshaker Event Location" +DataSaixEventLocation = "Data Saix Event Location" +HBDemyxEventLocation = "Hollow Bastion Demyx Event Location" +ThousandHeartlessEventLocation = "Thousand Heartless Event Location" +Mushroom13EventLocation = "Mushroom 13 Event Location" +SephiEventLocation = "Sephiroth Event Location" +DataDemyxEventLocation = "Data Demyx Event Location" +CorFirstFightEventLocation = "Cavern of Rememberance:Fight 1 Event Location" +CorSecondFightEventLocation = "Cavern of Rememberance:Fight 2 Event Location" +TransportEventLocation = "Transport to Rememberance Event Location" +OldPeteEventLocation = "Old Pete Event Location" +FuturePeteEventLocation = "Future Pete Event Location" +ASMarluxiaEventLocation = "AS Marluxia Event Location" +DataMarluxiaEventLocation = "Data Marluxia Event Location" +TerraEventLocation = "Terra Event Location" +TwilightThornEventLocation = "Twilight Thorn Event Location" +Axel1EventLocation = "Axel 1 Event Location" +Axel2EventLocation = "Axel 2 Event Location" +DataRoxasEventLocation = "Data Roxas Event Location" + diff --git a/worlds/kh2/Names/RegionName.py b/worlds/kh2/Names/RegionName.py index d07b5d3de3..63ba6acdb8 100644 --- a/worlds/kh2/Names/RegionName.py +++ b/worlds/kh2/Names/RegionName.py @@ -1,90 +1,156 @@ -LoD_Region ="Land of Dragons" -LoD2_Region ="Land of Dragons 2" +Ha1 = "Pooh's House" +Ha2 = "Piglet's House" +Ha3 = "Rabbit's House" +Ha4 = "Roo's House" +Ha5 = "Spooky Cave" +Ha6 = "Starry Hill" -Ag_Region ="Agrabah" -Ag2_Region ="Agrabah 2" +SoraLevels = "Sora's Levels" +GoA = "Garden Of Assemblage" +Keyblade = "Weapon Slots" -Dc_Region ="Disney Castle" -Tr_Region ="Timeless River" +Valor = "Valor Form" +Wisdom = "Wisdom Form" +Limit = "Limit Form" +Master = "Master Form" +Final = "Final Form" +Summon = "Summons" +# sp +Sp = "Space Paranoids" +HostileProgram = "Hostile Program" +Sp2 = "Space Paranoids 2" +Mcp = "Master Control Program" +ASLarxene = "AS Larxene" +DataLarxene = "Data Larxene" -HundredAcre1_Region ="Pooh's House" -HundredAcre2_Region ="Piglet's House" -HundredAcre3_Region ="Rabbit's House" -HundredAcre4_Region ="Roo's House" -HundredAcre5_Region ="Spookey Cave" -HundredAcre6_Region ="Starry Hill" +# pr +Pr = "Port Royal" +Barbosa = "Barbosa" +Pr2 = "Port Royal 2" +GrimReaper1 = "Grim Reaper 1" +GrimReaper2 = "Grim Reaper 2" +DataLuxord = "Data Luxord" -Pr_Region ="Port Royal" -Pr2_Region ="Port Royal 2" -Gr2_Region ="Grim Reaper 2" +# tt +Tt = "Twilight Town" +Tt2 = "Twilight Town 2" +Tt3 = "Twilight Town 3" +DataAxel = "Data Axel" -Oc_Region ="Olympus Coliseum" -Oc2_Region ="Olympus Coliseum 2" -Oc2_pain_and_panic_Region ="Pain and Panic Cup" -Oc2_titan_Region ="Titan Cup" -Oc2_cerberus_Region ="Cerberus Cup" -Oc2_gof_Region ="Goddest of Fate Cup" -Oc2Cups_Region ="Olympus Coliseum Cups" -HadesCups_Region ="Olympus Coliseum Hade's Paradox" +# oc +Oc = "Olympus Coliseum" +Cerberus = "Cerberus" +OlympusPete = "Olympus Pete" +Hydra = "Hydra" +OcPainAndPanicCup = "Pain and Panic Cup" +OcCerberusCup = "Cerberus Cup" +Oc2 = "Olympus Coliseum 2" +Hades = "Hades" +ASZexion = "AS Zexion" +DataZexion = "Data Zexion" +Oc2TitanCup = "Titan Cup" +Oc2GofCup = "Goddess of Fate Cup" +Oc2Cups = "Olympus Coliseum Cups" +HadesCups = "Olympus Coliseum Hade's Paradox" -Bc_Region ="Beast's Castle" -Bc2_Region ="Beast's Castle 2" -Xaldin_Region ="Xaldin" +# ht +Ht = "Holloween Town" +PrisonKeeper = "Prison Keeper" +OogieBoogie = "Oogie Boogie" +Ht2 = "Holloween Town 2" +Experiment = "The Experiment" +ASVexen = "AS Vexen" +DataVexen = "Data Vexen" -Sp_Region ="Space Paranoids" -Sp2_Region ="Space Paranoids 2" -Mcp_Region ="Master Control Program" +# lod +LoD = "Land of Dragons" +ShanYu = "Shan Yu" +LoD2 = "Land of Dragons 2" +AnsemRiku = "Ansem Riku" +StormRider = "Storm Rider" +DataXigbar = "Data Xigbar" -Ht_Region ="Holloween Town" -Ht2_Region ="Holloween Town 2" +# twtnw +Twtnw = "The World That Never Was (Pre Roxas)" +Roxas = "Roxas" +Xigbar = "Xigbar" +Luxord = "Luxord" +Saix = "Saix" +Twtnw2 = "The World That Never Was (Second Visit)" # Post riku transformation +Xemnas = "Xemnas" +ArmoredXemnas = "Armored Xemnas" +ArmoredXemnas2 = "Armored Xemnas 2" +FinalXemnas = "Final Xemnas" +DataXemnas = "Data Xemnas" -Hb_Region ="Hollow Bastion" -Hb2_Region ="Hollow Bastion 2" -ThousandHeartless_Region ="Thousand Hearless" -Mushroom13_Region ="Mushroom 13" -CoR_Region ="Cavern of Rememberance" -Transport_Region ="Transport to Rememberance" +# bc +Bc = "Beast's Castle" +Thresholder = "Thresholder" +Beast = "Beast" +DarkThorn = "Dark Thorn" +Bc2 = "Beast's Castle 2" +Xaldin = "Xaldin" +DataXaldin = "Data Xaldin" -Pl_Region ="Pride Lands" -Pl2_Region ="Pride Lands 2" +# ag +Ag = "Agrabah" +TwinLords = "Twin Lords" +Ag2 = "Agrabah 2" +GenieJafar = "Genie Jafar" +ASLexaeus = "AS Lexaeus" +DataLexaeus = "Data Lexaeus" -STT_Region ="Simulated Twilight Town" +# pl +Pl = "Pride Lands" +Scar = "Scar" +Pl2 = "Pride Lands 2" +GroundShaker = "Groundshaker" +DataSaix = "Data Saix" -TT_Region ="Twlight Town" -TT2_Region ="Twlight Town 2" -TT3_Region ="Twlight Town 3" +# hb +Hb = "Hollow Bastion" +Hb2 = "Hollow Bastion 2" +HBDemyx = "Hollow Bastion Demyx" +ThousandHeartless = "Thousand Heartless" +Mushroom13 = "Mushroom 13" +Sephi = "Sephiroth" +DataDemyx = "Data Demyx" -Twtnw_Region ="The World That Never Was (First Visit)" -Twtnw_PostRoxas ="The World That Never Was (Post Roxas)" -Twtnw_PostXigbar ="The World That Never Was (Post Xigbar)" -Twtnw2_Region ="The World That Never Was (Second Visit)" #before riku transformation +# CoR +CoR = "Cavern of Rememberance" +CorFirstFight = "Cavern of Rememberance:Fight 1" +CorSecondFight = "Cavern of Rememberance:Fight 2" +Transport = "Transport to Rememberance" -SoraLevels_Region ="Sora's Levels" -GoA_Region ="Garden Of Assemblage" -Keyblade_Region ="Keyblade Slots" +# dc +Dc = "Disney Castle" +Tr = "Timeless River" +OldPete = "Old Pete" +FuturePete = "Future Pete" +ASMarluxia = "AS Marluxia" +DataMarluxia = "Data Marluxia" +Terra = "Terra" -Valor_Region ="Valor Form" -Wisdom_Region ="Wisdom Form" -Limit_Region ="Limit Form" -Master_Region ="Master Form" -Final_Region ="Final Form" +# stt +Stt = "Simulated Twilight Town" +TwilightThorn = "Twilight Thorn" +Axel1 = "Axel 1" +Axel2 = "Axel 2" +DataRoxas = "Data Roxas" -Terra_Region ="Lingering Will" -Sephi_Region ="Sephiroth" -Marluxia_Region ="Marluxia" -Larxene_Region ="Larxene" -Vexen_Region ="Vexen" -Lexaeus_Region ="Lexaeus" -Zexion_Region ="Zexion" +AtlanticaSongOne = "Atlantica First Song" +AtlanticaSongTwo = "Atlantica Second Song" +AtlanticaSongThree = "Atlantica Third Song" +AtlanticaSongFour = "Atlantica Fourth Song" -LevelsVS1 ="Levels Region (1 Visit Locking Item)" -LevelsVS3 ="Levels Region (3 Visit Locking Items)" -LevelsVS6 ="Levels Region (6 Visit Locking Items)" -LevelsVS9 ="Levels Region (9 Visit Locking Items)" -LevelsVS12 ="Levels Region (12 Visit Locking Items)" -LevelsVS15 ="Levels Region (15 Visit Locking Items)" -LevelsVS18 ="Levels Region (18 Visit Locking Items)" -LevelsVS21 ="Levels Region (21 Visit Locking Items)" -LevelsVS24 ="Levels Region (24 Visit Locking Items)" -LevelsVS26 ="Levels Region (26 Visit Locking Items)" +LevelsVS1 = "Levels Region (1 Visit Locking Item)" +LevelsVS3 = "Levels Region (3 Visit Locking Items)" +LevelsVS6 = "Levels Region (6 Visit Locking Items)" +LevelsVS9 = "Levels Region (9 Visit Locking Items)" +LevelsVS12 = "Levels Region (12 Visit Locking Items)" +LevelsVS15 = "Levels Region (15 Visit Locking Items)" +LevelsVS18 = "Levels Region (18 Visit Locking Items)" +LevelsVS21 = "Levels Region (21 Visit Locking Items)" +LevelsVS24 = "Levels Region (24 Visit Locking Items)" +LevelsVS26 = "Levels Region (26 Visit Locking Items)" diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index c3334dbb99..6b0418c997 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -5,7 +5,7 @@ import os import Utils import zipfile -from .Items import item_dictionary_table, CheckDupingItems +from .Items import item_dictionary_table from .Locations import all_locations, SoraLevels, exclusion_table from .XPValues import lvlStats, formExp, soraExp from worlds.Files import APContainer @@ -15,7 +15,7 @@ class KH2Container(APContainer): game: str = 'Kingdom Hearts 2' def __init__(self, patch_data: dict, base_path: str, output_directory: str, - player=None, player_name: str = "", server: str = ""): + player=None, player_name: str = "", server: str = ""): self.patch_data = patch_data self.file_path = base_path container_path = os.path.join(output_directory, base_path + ".zip") @@ -24,12 +24,6 @@ class KH2Container(APContainer): def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: for filename, yml in self.patch_data.items(): opened_zipfile.writestr(filename, yml) - for root, dirs, files in os.walk(os.path.join(os.path.dirname(__file__), "mod_template")): - for file in files: - opened_zipfile.write(os.path.join(root, file), - os.path.relpath(os.path.join(root, file), - os.path.join(os.path.dirname(__file__), "mod_template"))) - # opened_zipfile.writestr(self.zpf_path, self.patch_data) super().write_contents(opened_zipfile) @@ -59,13 +53,6 @@ def patch_kh2(self, output_directory): formexp = None formName = None levelsetting = list() - slotDataDuping = set() - for values in CheckDupingItems.values(): - if isinstance(values, set): - slotDataDuping = slotDataDuping.union(values) - else: - for inner_values in values.values(): - slotDataDuping = slotDataDuping.union(inner_values) if self.multiworld.Keyblade_Minimum[self.player].value > self.multiworld.Keyblade_Maximum[self.player].value: logging.info( @@ -89,14 +76,19 @@ def patch_kh2(self, output_directory): levelsetting.extend(exclusion_table["Level99Sanity"]) mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.get_file_safe_player_name(self.player)}" - + all_valid_locations = {location for location, data in all_locations.items()} for location in self.multiworld.get_filled_locations(self.player): - - data = all_locations[location.name] - if location.item.player == self.player: - itemcode = item_dictionary_table[location.item.name].kh2id + if location.name in all_valid_locations: + data = all_locations[location.name] else: - itemcode = 90 # castle map + continue + if location.item: + if location.item.player == self.player: + itemcode = item_dictionary_table[location.item.name].kh2id + else: + itemcode = 90 # castle map + else: + itemcode = 90 if data.yml == "Chest": self.formattedTrsr[data.locid] = {"ItemId": itemcode} @@ -129,8 +121,8 @@ def patch_kh2(self, output_directory): elif data.yml == "Keyblade": self.formattedItem["Stats"].append({ "Id": data.locid, - "Attack": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), - "Magic": self.multiworld.per_slot_randoms[self.player].randint(keyblademin, keyblademax), + "Attack": self.random.randint(keyblademin, keyblademax), + "Magic": self.random.randint(keyblademin, keyblademax), "Defense": 0, "Ability": itemcode, "AbilityPoints": 0, @@ -154,7 +146,8 @@ def patch_kh2(self, output_directory): 2: self.multiworld.Wisdom_Form_EXP[self.player].value, 3: self.multiworld.Limit_Form_EXP[self.player].value, 4: self.multiworld.Master_Form_EXP[self.player].value, - 5: self.multiworld.Final_Form_EXP[self.player].value} + 5: self.multiworld.Final_Form_EXP[self.player].value + } formexp = formDictExp[data.charName] formName = formDict[data.charName] self.formattedFmlv[formName] = [] @@ -174,7 +167,7 @@ def patch_kh2(self, output_directory): "GrowthAbilityLevel": 0, }) - # Summons have no checks on them so done fully locally + # Summons have no actual locations so done down here. self.formattedFmlv["Summon"] = [] for x in range(1, 7): self.formattedFmlv["Summon"].append({ @@ -185,17 +178,18 @@ def patch_kh2(self, output_directory): "GrowthAbilityLevel": 0, }) # levels done down here because of optional settings that can take locations out of the pool. - self.i = 1 + self.i = 2 for location in SoraLevels: - increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + increaseStat(self.random.randint(0, 3)) if location in levelsetting: data = self.multiworld.get_location(location, self.player) - if data.item.player == self.player: - itemcode = item_dictionary_table[data.item.name].kh2id - else: - itemcode = 90 # castle map + if data.item: + if data.item.player == self.player: + itemcode = item_dictionary_table[data.item.name].kh2id + else: + itemcode = 90 # castle map else: - increaseStat(self.multiworld.per_slot_randoms[self.player].randint(0, 3)) + increaseStat(self.random.randint(0, 3)) itemcode = 0 self.formattedLvup["Sora"][self.i] = { "Exp": int(soraExp[self.i] / self.multiworld.Sora_Level_EXP[self.player].value), @@ -229,6 +223,193 @@ def patch_kh2(self, output_directory): "GeneralResistance": 100, "Unknown": 0 }) + self.formattedLvup["Sora"][1] = { + "Exp": int(soraExp[1] / self.multiworld.Sora_Level_EXP[self.player].value), + "Strength": 2, + "Magic": 6, + "Defense": 2, + "Ap": 0, + "SwordAbility": 0, + "ShieldAbility": 0, + "StaffAbility": 0, + "Padding": 0, + "Character": "Sora", + "Level": 1 + } + self.mod_yml = { + "assets": [ + { + 'method': 'binarc', + 'name': '00battle.bin', + 'source': [ + { + 'method': 'listpatch', + 'name': 'fmlv', + 'source': [ + { + 'name': 'FmlvList.yml', + 'type': 'fmlv' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'lvup', + 'source': [ + { + 'name': 'LvupList.yml', + 'type': 'lvup' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'bons', + 'source': [ + { + 'name': 'BonsList.yml', + 'type': 'bons' + } + ], + 'type': 'List' + } + ] + }, + { + 'method': 'binarc', + 'name': '03system.bin', + 'source': [ + { + 'method': 'listpatch', + 'name': 'trsr', + 'source': [ + { + 'name': 'TrsrList.yml', + 'type': 'trsr' + } + ], + 'type': 'List' + }, + { + 'method': 'listpatch', + 'name': 'item', + 'source': [ + { + 'name': 'ItemList.yml', + 'type': 'item' + } + ], + 'type': 'List' + } + ] + }, + { + 'name': 'msg/us/po.bar', + 'multi': [ + { + 'name': 'msg/fr/po.bar' + }, + { + 'name': 'msg/gr/po.bar' + }, + { + 'name': 'msg/it/po.bar' + }, + { + 'name': 'msg/sp/po.bar' + } + ], + 'method': 'binarc', + 'source': [ + { + 'name': 'po', + 'type': 'list', + 'method': 'kh2msg', + 'source': [ + { + 'name': 'po.yml', + 'language': 'en' + } + ] + } + ] + }, + { + 'name': 'msg/us/sys.bar', + 'multi': [ + { + 'name': 'msg/fr/sys.bar' + }, + { + 'name': 'msg/gr/sys.bar' + }, + { + 'name': 'msg/it/sys.bar' + }, + { + 'name': 'msg/sp/sys.bar' + } + ], + 'method': 'binarc', + 'source': [ + { + 'name': 'sys', + 'type': 'list', + 'method': 'kh2msg', + 'source': [ + { + 'name': 'sys.yml', + 'language': 'en' + } + ] + } + ] + }, + ], + 'title': 'Randomizer Seed' + } + + goal_to_text = { + 0: "Three Proofs", + 1: "Lucky Emblem", + 2: "Hitlist", + 3: "Lucky Emblem and Hitlist", + } + lucky_emblem_text = { + 0: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.", + 1: f"Lucky Emblem Required: {self.multiworld.LuckyEmblemsRequired[self.player]} out of {self.multiworld.LuckyEmblemsAmount[self.player]}", + 2: "Your Goal is not Lucky Emblem. It is Hitlist or Three Proofs.", + 3: f"Lucky Emblem Required: {self.multiworld.LuckyEmblemsRequired[self.player]} out of {self.multiworld.LuckyEmblemsAmount[self.player]}" + } + hitlist_text = { + 0: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs", + 1: "Your Goal is not Hitlist. It is Lucky Emblem or Three Proofs", + 2: f"Bounties Required: {self.multiworld.BountyRequired[self.player]} out of {self.multiworld.BountyAmount[self.player]}", + 3: f"Bounties Required: {self.multiworld.BountyRequired[self.player]} out of {self.multiworld.BountyAmount[self.player]}", + } + + self.pooh_text = [ + { + 'id': 18326, + 'en': f"Your goal is {goal_to_text[self.multiworld.Goal[self.player].value]}" + }, + { + 'id': 18327, + 'en': lucky_emblem_text[self.multiworld.Goal[self.player].value] + }, + { + 'id': 18328, + 'en': hitlist_text[self.multiworld.Goal[self.player].value] + } + ] + self.level_depth_text = [ + { + 'id': 0x3BF1, + 'en': f"Your Level Depth is {self.multiworld.LevelDepth[self.player].current_option_name}" + } + ] mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) openkhmod = { @@ -237,8 +418,11 @@ def patch_kh2(self, output_directory): "BonsList.yml": yaml.dump(self.formattedBons, line_break="\n"), "ItemList.yml": yaml.dump(self.formattedItem, line_break="\n"), "FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"), + "mod.yml": yaml.dump(self.mod_yml, line_break="\n"), + "po.yml": yaml.dump(self.pooh_text, line_break="\n"), + "sys.yml": yaml.dump(self.level_depth_text, line_break="\n"), } mod = KH2Container(openkhmod, mod_dir, output_directory, self.player, - self.multiworld.get_file_safe_player_name(self.player)) + self.multiworld.get_file_safe_player_name(self.player)) mod.write() diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 7a6f106aa9..7ba7c0082d 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -1,7 +1,8 @@ -from Options import Choice, Option, Range, Toggle, OptionSet -import typing +from dataclasses import dataclass -from worlds.kh2 import SupportAbility_Table, ActionAbility_Table +from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool + +from worlds.kh2 import default_itempool_option class SoraEXP(Range): @@ -107,23 +108,61 @@ class Visitlocking(Choice): First and Second Visit Locking: One item for First Visit Two For Second Visit""" display_name = "Visit locking" option_no_visit_locking = 0 # starts with 25 visit locking - option_second_visit_locking = 1 # starts with 13 (no icecream/picture) + option_second_visit_locking = 1 # starts with 12 visit locking option_first_and_second_visit_locking = 2 # starts with nothing default = 2 +class FightLogic(Choice): + """ + The level of logic to use when determining what fights in each KH2 world are beatable. + + Easy: For Players not very comfortable doing things without a lot of tools. + + Normal: For Players somewhat comfortable doing fights with some of the tools. + + Hard: For Players comfortable doing fights with almost no tools. + """ + display_name = "Fight Logic" + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 + + +class FinalFormLogic(Choice): + """Determines forcing final form logic + + No Light and Darkness: Light and Darkness is not in logic. + Light And Darkness: Final Forcing with light and darkness is in logic. + Just a Form: All that requires final forcing is another form. + """ + display_name = "Final Form Logic" + option_no_light_and_darkness = 0 + option_light_and_darkness = 1 + option_just_a_form = 2 + default = 1 + + +class AutoFormLogic(Toggle): + """ Have Auto Forms levels in logic. + """ + display_name = "Auto Form Logic" + default = False + + class RandomVisitLockingItem(Range): """Start with random amount of visit locking items.""" display_name = "Random Visit Locking Item" range_start = 0 range_end = 25 - default = 3 + default = 0 class SuperBosses(Toggle): - """Terra, Sephiroth and Data Fights Toggle.""" + """Terra Sephiroth and Data Fights Toggle.""" display_name = "Super Bosses" - default = False + default = True class Cups(Choice): @@ -135,7 +174,7 @@ class Cups(Choice): option_no_cups = 0 option_cups = 1 option_cups_and_hades_paradox = 2 - default = 1 + default = 0 class LevelDepth(Choice): @@ -157,67 +196,71 @@ class LevelDepth(Choice): default = 0 +class DonaldGoofyStatsanity(Toggle): + """Toggles if on Donald and Goofy's Get Bonus locations can be any item""" + display_name = "Donald & Goofy Statsanity" + default = True + + +class AtlanticaToggle(Toggle): + """Atlantica Toggle""" + display_name = "Atlantica Toggle" + default = False + + class PromiseCharm(Toggle): - """Add Promise Charm to the Pool""" + """Add Promise Charm to the pool""" display_name = "Promise Charm" default = False -class KeybladeAbilities(Choice): - """ - Action: Action Abilities in the Keyblade Slot Pool. - - Support: Support Abilities in the Keyblade Slot Pool. - - Both: Action and Support Abilities in the Keyblade Slot Pool.""" - display_name = "Keyblade Abilities" - option_support = 0 - option_action = 1 - option_both = 2 - default = 0 - - -class BlacklistKeyblade(OptionSet): - """Black List these Abilities on Keyblades""" - display_name = "Blacklist Keyblade Abilities" - valid_keys = set(SupportAbility_Table.keys()).union(ActionAbility_Table.keys()) +class AntiForm(Toggle): + """Add Anti Form to the pool""" + display_name = "Anti Form" + default = False class Goal(Choice): """Win Condition - Three Proofs: Get a Gold Crown on Sora's Head. + Three Proofs: Find the 3 Proofs to unlock the final door. - Lucky Emblem Hunt: Find Required Amount of Lucky Emblems . + Lucky Emblem Hunt: Find required amount of Lucky Emblems. - Hitlist (Bounty Hunt): Find Required Amount of Bounties""" + Hitlist (Bounty Hunt): Find required amount of Bounties. + + Lucky Emblem and Hitlist: Find the required amount of Lucky Emblems and Bounties.""" display_name = "Goal" option_three_proofs = 0 option_lucky_emblem_hunt = 1 option_hitlist = 2 - default = 0 + option_hitlist_and_lucky_emblem = 3 + default = 1 class FinalXemnas(Toggle): """Kill Final Xemnas to Beat the Game. - This is in addition to your Goal. I.E. get three proofs+kill final Xemnas""" + + This is in addition to your Goal. + + I.E. get three proofs+kill final Xemnas""" display_name = "Final Xemnas" default = True class LuckyEmblemsRequired(Range): - """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas Door. + """Number of Lucky Emblems to collect to Win/Unlock Final Xemnas' Door. - If Goal is not Lucky Emblem Hunt this does nothing.""" + If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing.""" display_name = "Lucky Emblems Required" range_start = 1 range_end = 60 - default = 30 + default = 35 class LuckyEmblemsAmount(Range): """Number of Lucky Emblems that are in the pool. - If Goal is not Lucky Emblem Hunt this does nothing.""" + If Goal is not Lucky Emblem Hunt or Lucky Emblem and Hitlist this does nothing.""" display_name = "Lucky Emblems Available" range_start = 1 range_end = 60 @@ -227,48 +270,103 @@ class LuckyEmblemsAmount(Range): class BountyRequired(Range): """Number of Bounties to collect to Win/Unlock Final Xemnas Door. - If Goal is not Hitlist this does nothing.""" + If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing.""" display_name = "Bounties Required" range_start = 1 - range_end = 24 + range_end = 26 default = 7 class BountyAmount(Range): """Number of Bounties that are in the pool. - If Goal is not Hitlist this does nothing.""" + If Goal is not Hitlist or Lucky Emblem and Hitlist this does nothing.""" display_name = "Bounties Available" range_start = 1 - range_end = 24 - default = 13 + range_end = 26 + default = 10 -KH2_Options: typing.Dict[str, type(Option)] = { - "LevelDepth": LevelDepth, - "Sora_Level_EXP": SoraEXP, - "Valor_Form_EXP": ValorEXP, - "Wisdom_Form_EXP": WisdomEXP, - "Limit_Form_EXP": LimitEXP, - "Master_Form_EXP": MasterEXP, - "Final_Form_EXP": FinalEXP, - "Summon_EXP": SummonEXP, - "Schmovement": Schmovement, - "RandomGrowth": RandomGrowth, - "Promise_Charm": PromiseCharm, - "Goal": Goal, - "FinalXemnas": FinalXemnas, - "LuckyEmblemsAmount": LuckyEmblemsAmount, - "LuckyEmblemsRequired": LuckyEmblemsRequired, - "BountyAmount": BountyAmount, - "BountyRequired": BountyRequired, - "Keyblade_Minimum": KeybladeMin, - "Keyblade_Maximum": KeybladeMax, - "Visitlocking": Visitlocking, - "RandomVisitLockingItem": RandomVisitLockingItem, - "SuperBosses": SuperBosses, - "KeybladeAbilities": KeybladeAbilities, - "BlacklistKeyblade": BlacklistKeyblade, - "Cups": Cups, +class BountyStartHint(Toggle): + """Start with Bounties Hinted""" + display_name = "Start with Bounties Hinted" + default = False -} + +class WeaponSlotStartHint(Toggle): + """Start with Weapon Slots' Hinted""" + display_name = "Start with Weapon Slots Hinted" + default = False + + +class CorSkipToggle(Toggle): + """Toggle for Cor skip. + + Tools depend on which difficulty was chosen on Fight Difficulty. + + Toggle does not negate fight logic but is an alternative. + + Final Chest is also can be put into logic with this skip. + """ + display_name = "CoR Skip Toggle." + default = False + + +class CustomItemPoolQuantity(ItemDict): + """Add more of an item into the itempool. Note: You cannot take out items from the pool.""" + display_name = "Custom Item Pool" + verify_item_name = True + default = default_itempool_option + + +class FillerItemsLocal(Toggle): + """Make all dynamic filler classified items local. Recommended when playing with games with fewer locations than kh2""" + display_name = "Local Filler Items" + default = True + + +class SummonLevelLocationToggle(Toggle): + """Toggle Summon levels to have locations.""" + display_name = "Summon Level Locations" + default = False + + +# shamelessly stolen from the messanger +@dataclass +class KingdomHearts2Options(PerGameCommonOptions): + start_inventory: StartInventoryPool + LevelDepth: LevelDepth + Sora_Level_EXP: SoraEXP + Valor_Form_EXP: ValorEXP + Wisdom_Form_EXP: WisdomEXP + Limit_Form_EXP: LimitEXP + Master_Form_EXP: MasterEXP + Final_Form_EXP: FinalEXP + Summon_EXP: SummonEXP + Schmovement: Schmovement + RandomGrowth: RandomGrowth + AntiForm: AntiForm + Promise_Charm: PromiseCharm + Goal: Goal + FinalXemnas: FinalXemnas + LuckyEmblemsAmount: LuckyEmblemsAmount + LuckyEmblemsRequired: LuckyEmblemsRequired + BountyAmount: BountyAmount + BountyRequired: BountyRequired + BountyStartingHintToggle: BountyStartHint + Keyblade_Minimum: KeybladeMin + Keyblade_Maximum: KeybladeMax + WeaponSlotStartHint: WeaponSlotStartHint + FightLogic: FightLogic + FinalFormLogic: FinalFormLogic + AutoFormLogic: AutoFormLogic + DonaldGoofyStatsanity: DonaldGoofyStatsanity + FillerItemsLocal: FillerItemsLocal + Visitlocking: Visitlocking + RandomVisitLockingItem: RandomVisitLockingItem + SuperBosses: SuperBosses + Cups: Cups + SummonLevelLocationToggle: SummonLevelLocationToggle + AtlanticaToggle: AtlanticaToggle + CorSkipToggle: CorSkipToggle + CustomItemPoolQuantity: CustomItemPoolQuantity diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index 36fc0c046b..aceab97f37 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1,35 +1,22 @@ import typing -from BaseClasses import MultiWorld, Region, Entrance +from BaseClasses import MultiWorld, Region -from .Locations import KH2Location, RegionTable -from .Names import LocationName, ItemName, RegionName +from .Locations import KH2Location, event_location_to_item +from . import LocationName, RegionName, Events_Table - -def create_regions(world, player: int, active_locations): - menu_region = create_region(world, player, active_locations, 'Menu', None) - - goa_region_locations = [ - LocationName.Crit_1, - LocationName.Crit_2, - LocationName.Crit_3, - LocationName.Crit_4, - LocationName.Crit_5, - LocationName.Crit_6, - LocationName.Crit_7, +KH2REGIONS: typing.Dict[str, typing.List[str]] = { + "Menu": [], + RegionName.GoA: [ LocationName.GardenofAssemblageMap, LocationName.GoALostIllusion, LocationName.ProofofNonexistence, - LocationName.DonaldStarting1, - LocationName.DonaldStarting2, - LocationName.GoofyStarting1, - LocationName.GoofyStarting2, - ] - - goa_region = create_region(world, player, active_locations, RegionName.GoA_Region, - goa_region_locations) - - lod_Region_locations = [ + # LocationName.DonaldStarting1, + # LocationName.DonaldStarting2, + # LocationName.GoofyStarting1, + # LocationName.GoofyStarting2 + ], + RegionName.LoD: [ LocationName.BambooGroveDarkShard, LocationName.BambooGroveEther, LocationName.BambooGroveMythrilShard, @@ -47,14 +34,16 @@ def create_regions(world, player: int, active_locations): LocationName.VillageCaveBonus, LocationName.RidgeFrostShard, LocationName.RidgeAPBoost, + ], + RegionName.ShanYu: [ LocationName.ShanYu, LocationName.ShanYuGetBonus, LocationName.HiddenDragon, LocationName.GoofyShanYu, - ] - lod_Region = create_region(world, player, active_locations, RegionName.LoD_Region, - lod_Region_locations) - lod2_Region_locations = [ + LocationName.ShanYuEventLocation + ], + RegionName.LoD2: [], + RegionName.AnsemRiku: [ LocationName.ThroneRoomTornPages, LocationName.ThroneRoomPalaceMap, LocationName.ThroneRoomAPBoost, @@ -63,13 +52,18 @@ def create_regions(world, player: int, active_locations): LocationName.ThroneRoomOgreShield, LocationName.ThroneRoomMythrilCrystal, LocationName.ThroneRoomOrichalcum, + LocationName.AnsemRikuEventLocation, + ], + RegionName.StormRider: [ LocationName.StormRider, - LocationName.XigbarDataDefenseBoost, LocationName.GoofyStormRider, - ] - lod2_Region = create_region(world, player, active_locations, RegionName.LoD2_Region, - lod2_Region_locations) - ag_region_locations = [ + LocationName.StormRiderEventLocation + ], + RegionName.DataXigbar: [ + LocationName.XigbarDataDefenseBoost, + LocationName.DataXigbarEventLocation + ], + RegionName.Ag: [ LocationName.AgrabahMap, LocationName.AgrabahDarkShard, LocationName.AgrabahMythrilShard, @@ -97,30 +91,30 @@ def create_regions(world, player: int, active_locations): LocationName.TreasureRoom, LocationName.TreasureRoomAPBoost, LocationName.TreasureRoomSerenityGem, + LocationName.GoofyTreasureRoom, + LocationName.DonaldAbuEscort + ], + RegionName.TwinLords: [ LocationName.ElementalLords, LocationName.LampCharm, - LocationName.GoofyTreasureRoom, - LocationName.DonaldAbuEscort, - ] - ag_region = create_region(world, player, active_locations, RegionName.Ag_Region, - ag_region_locations) - ag2_region_locations = [ + LocationName.TwinLordsEventLocation + ], + RegionName.Ag2: [ LocationName.RuinedChamberTornPages, LocationName.RuinedChamberRuinsMap, + ], + RegionName.GenieJafar: [ LocationName.GenieJafar, LocationName.WishingLamp, - ] - ag2_region = create_region(world, player, active_locations, RegionName.Ag2_Region, - ag2_region_locations) - lexaeus_region_locations = [ + LocationName.GenieJafarEventLocation, + ], + RegionName.DataLexaeus: [ LocationName.LexaeusBonus, LocationName.LexaeusASStrengthBeyondStrength, LocationName.LexaeusDataLostIllusion, - ] - lexaeus_region = create_region(world, player, active_locations, RegionName.Lexaeus_Region, - lexaeus_region_locations) - - dc_region_locations = [ + LocationName.DataLexaeusEventLocation + ], + RegionName.Dc: [ LocationName.DCCourtyardMythrilShard, LocationName.DCCourtyardStarRecipe, LocationName.DCCourtyardAPBoost, @@ -131,74 +125,65 @@ def create_regions(world, player: int, active_locations): LocationName.LibraryTornPages, LocationName.DisneyCastleMap, LocationName.MinnieEscort, - LocationName.MinnieEscortGetBonus, - ] - dc_region = create_region(world, player, active_locations, RegionName.Dc_Region, - dc_region_locations) - tr_region_locations = [ + LocationName.MinnieEscortGetBonus + ], + RegionName.Tr: [ LocationName.CornerstoneHillMap, LocationName.CornerstoneHillFrostShard, LocationName.PierMythrilShard, LocationName.PierHiPotion, + ], + RegionName.OldPete: [ LocationName.WaterwayMythrilStone, LocationName.WaterwayAPBoost, LocationName.WaterwayFrostStone, LocationName.WindowofTimeMap, LocationName.BoatPete, + LocationName.DonaldBoatPete, + LocationName.DonaldBoatPeteGetBonus, + LocationName.OldPeteEventLocation, + ], + RegionName.FuturePete: [ LocationName.FuturePete, LocationName.FuturePeteGetBonus, LocationName.Monochrome, LocationName.WisdomForm, - LocationName.DonaldBoatPete, - LocationName.DonaldBoatPeteGetBonus, LocationName.GoofyFuturePete, - ] - tr_region = create_region(world, player, active_locations, RegionName.Tr_Region, - tr_region_locations) - marluxia_region_locations = [ + LocationName.FuturePeteEventLocation + ], + RegionName.DataMarluxia: [ LocationName.MarluxiaGetBonus, LocationName.MarluxiaASEternalBlossom, LocationName.MarluxiaDataLostIllusion, - ] - marluxia_region = create_region(world, player, active_locations, RegionName.Marluxia_Region, - marluxia_region_locations) - terra_region_locations = [ + LocationName.DataMarluxiaEventLocation + ], + RegionName.Terra: [ LocationName.LingeringWillBonus, LocationName.LingeringWillProofofConnection, LocationName.LingeringWillManifestIllusion, - ] - terra_region = create_region(world, player, active_locations, RegionName.Terra_Region, - terra_region_locations) - - hundred_acre1_region_locations = [ + LocationName.TerraEventLocation + ], + RegionName.Ha1: [ LocationName.PoohsHouse100AcreWoodMap, LocationName.PoohsHouseAPBoost, - LocationName.PoohsHouseMythrilStone, - ] - hundred_acre1_region = create_region(world, player, active_locations, RegionName.HundredAcre1_Region, - hundred_acre1_region_locations) - hundred_acre2_region_locations = [ + LocationName.PoohsHouseMythrilStone + ], + RegionName.Ha2: [ LocationName.PigletsHouseDefenseBoost, LocationName.PigletsHouseAPBoost, - LocationName.PigletsHouseMythrilGem, - ] - hundred_acre2_region = create_region(world, player, active_locations, RegionName.HundredAcre2_Region, - hundred_acre2_region_locations) - hundred_acre3_region_locations = [ + LocationName.PigletsHouseMythrilGem + ], + RegionName.Ha3: [ LocationName.RabbitsHouseDrawRing, LocationName.RabbitsHouseMythrilCrystal, LocationName.RabbitsHouseAPBoost, - ] - hundred_acre3_region = create_region(world, player, active_locations, RegionName.HundredAcre3_Region, - hundred_acre3_region_locations) - hundred_acre4_region_locations = [ + ], + RegionName.Ha4: [ LocationName.KangasHouseMagicBoost, LocationName.KangasHouseAPBoost, LocationName.KangasHouseOrichalcum, - ] - hundred_acre4_region = create_region(world, player, active_locations, RegionName.HundredAcre4_Region, - hundred_acre4_region_locations) - hundred_acre5_region_locations = [ + ], + RegionName.Ha5: [ LocationName.SpookyCaveMythrilGem, LocationName.SpookyCaveAPBoost, LocationName.SpookyCaveOrichalcum, @@ -206,19 +191,15 @@ def create_regions(world, player: int, active_locations): LocationName.SpookyCaveMythrilCrystal, LocationName.SpookyCaveAPBoost2, LocationName.SweetMemories, - LocationName.SpookyCaveMap, - ] - hundred_acre5_region = create_region(world, player, active_locations, RegionName.HundredAcre5_Region, - hundred_acre5_region_locations) - hundred_acre6_region_locations = [ + LocationName.SpookyCaveMap + ], + RegionName.Ha6: [ LocationName.StarryHillCosmicRing, LocationName.StarryHillStyleRecipe, LocationName.StarryHillCureElement, - LocationName.StarryHillOrichalcumPlus, - ] - hundred_acre6_region = create_region(world, player, active_locations, RegionName.HundredAcre6_Region, - hundred_acre6_region_locations) - pr_region_locations = [ + LocationName.StarryHillOrichalcumPlus + ], + RegionName.Pr: [ LocationName.RampartNavalMap, LocationName.RampartMythrilStone, LocationName.RampartDarkShard, @@ -236,17 +217,20 @@ def create_regions(world, player: int, active_locations): LocationName.MoonlightNookMythrilShard, LocationName.MoonlightNookSerenityGem, LocationName.MoonlightNookPowerStone, + LocationName.DonaldBoatFight, + LocationName.GoofyInterceptorBarrels, + + ], + RegionName.Barbosa: [ LocationName.Barbossa, LocationName.BarbossaGetBonus, LocationName.FollowtheWind, - LocationName.DonaldBoatFight, LocationName.GoofyBarbossa, LocationName.GoofyBarbossaGetBonus, - LocationName.GoofyInterceptorBarrels, - ] - pr_region = create_region(world, player, active_locations, RegionName.Pr_Region, - pr_region_locations) - pr2_region_locations = [ + LocationName.BarbosaEventLocation, + ], + RegionName.Pr2: [], + RegionName.GrimReaper1: [ LocationName.GrimReaper1, LocationName.InterceptorsHoldFeatherCharm, LocationName.SeadriftKeepAPBoost, @@ -258,19 +242,19 @@ def create_regions(world, player: int, active_locations): LocationName.SeadriftRowCursedMedallion, LocationName.SeadriftRowShipGraveyardMap, LocationName.GoofyGrimReaper1, - - ] - pr2_region = create_region(world, player, active_locations, RegionName.Pr2_Region, - pr2_region_locations) - gr2_region_locations = [ + LocationName.GrimReaper1EventLocation, + ], + RegionName.GrimReaper2: [ LocationName.DonaladGrimReaper2, LocationName.GrimReaper2, LocationName.SecretAnsemReport6, + LocationName.GrimReaper2EventLocation, + ], + RegionName.DataLuxord: [ LocationName.LuxordDataAPBoost, - ] - gr2_region = create_region(world, player, active_locations, RegionName.Gr2_Region, - gr2_region_locations) - oc_region_locations = [ + LocationName.DataLuxordEventLocation + ], + RegionName.Oc: [ LocationName.PassageMythrilShard, LocationName.PassageMythrilStone, LocationName.PassageEther, @@ -278,6 +262,8 @@ def create_regions(world, player: int, active_locations): LocationName.PassageHiPotion, LocationName.InnerChamberUnderworldMap, LocationName.InnerChamberMythrilShard, + ], + RegionName.Cerberus: [ LocationName.Cerberus, LocationName.ColiseumMap, LocationName.Urns, @@ -297,56 +283,61 @@ def create_regions(world, player: int, active_locations): LocationName.TheLockCavernsMap, LocationName.TheLockMythrilShard, LocationName.TheLockAPBoost, + LocationName.CerberusEventLocation + ], + RegionName.OlympusPete: [ LocationName.PeteOC, + LocationName.DonaldDemyxOC, + LocationName.GoofyPeteOC, + LocationName.OlympusPeteEventLocation + ], + RegionName.Hydra: [ LocationName.Hydra, LocationName.HydraGetBonus, LocationName.HerosCrest, - LocationName.DonaldDemyxOC, - LocationName.GoofyPeteOC, - ] - oc_region = create_region(world, player, active_locations, RegionName.Oc_Region, - oc_region_locations) - oc2_region_locations = [ + LocationName.HydraEventLocation + ], + RegionName.Oc2: [ LocationName.AuronsStatue, + ], + RegionName.Hades: [ LocationName.Hades, LocationName.HadesGetBonus, LocationName.GuardianSoul, - - ] - oc2_region = create_region(world, player, active_locations, RegionName.Oc2_Region, - oc2_region_locations) - oc2_pain_and_panic_locations = [ + LocationName.HadesEventLocation + ], + RegionName.OcPainAndPanicCup: [ LocationName.ProtectBeltPainandPanicCup, LocationName.SerenityGemPainandPanicCup, - ] - oc2_titan_locations = [ - LocationName.GenjiShieldTitanCup, - LocationName.SkillfulRingTitanCup, - ] - oc2_cerberus_locations = [ + LocationName.OcPainAndPanicCupEventLocation + ], + RegionName.OcCerberusCup: [ LocationName.RisingDragonCerberusCup, LocationName.SerenityCrystalCerberusCup, - ] - oc2_gof_cup_locations = [ + LocationName.OcCerberusCupEventLocation + ], + RegionName.Oc2TitanCup: [ + LocationName.GenjiShieldTitanCup, + LocationName.SkillfulRingTitanCup, + LocationName.Oc2TitanCupEventLocation + ], + RegionName.Oc2GofCup: [ LocationName.FatalCrestGoddessofFateCup, LocationName.OrichalcumPlusGoddessofFateCup, + LocationName.Oc2GofCupEventLocation, + ], + RegionName.HadesCups: [ LocationName.HadesCupTrophyParadoxCups, - ] - zexion_region_locations = [ + LocationName.HadesCupEventLocations + ], + RegionName.DataZexion: [ LocationName.ZexionBonus, LocationName.ZexionASBookofShadows, LocationName.ZexionDataLostIllusion, LocationName.GoofyZexion, - ] - oc2_pain_and_panic_cup = create_region(world, player, active_locations, RegionName.Oc2_pain_and_panic_Region, - oc2_pain_and_panic_locations) - oc2_titan_cup = create_region(world, player, active_locations, RegionName.Oc2_titan_Region, oc2_titan_locations) - oc2_cerberus_cup = create_region(world, player, active_locations, RegionName.Oc2_cerberus_Region, - oc2_cerberus_locations) - oc2_gof_cup = create_region(world, player, active_locations, RegionName.Oc2_gof_Region, oc2_gof_cup_locations) - zexion_region = create_region(world, player, active_locations, RegionName.Zexion_Region, zexion_region_locations) - - bc_region_locations = [ + LocationName.DataZexionEventLocation + ], + RegionName.Bc: [ LocationName.BCCourtyardAPBoost, LocationName.BCCourtyardHiPotion, LocationName.BCCourtyardMythrilShard, @@ -359,6 +350,8 @@ def create_regions(world, player: int, active_locations): LocationName.TheWestHallMythrilShard2, LocationName.TheWestHallBrightStone, LocationName.TheWestHallMythrilShard, + ], + RegionName.Thresholder: [ LocationName.Thresholder, LocationName.DungeonBasementMap, LocationName.DungeonAPBoost, @@ -368,33 +361,37 @@ def create_regions(world, player: int, active_locations): LocationName.TheWestHallAPBoostPostDungeon, LocationName.TheWestWingMythrilShard, LocationName.TheWestWingTent, + LocationName.DonaldThresholder, + LocationName.ThresholderEventLocation + ], + RegionName.Beast: [ LocationName.Beast, LocationName.TheBeastsRoomBlazingShard, + LocationName.GoofyBeast, + LocationName.BeastEventLocation + ], + RegionName.DarkThorn: [ LocationName.DarkThorn, LocationName.DarkThornGetBonus, LocationName.DarkThornCureElement, - LocationName.DonaldThresholder, - LocationName.GoofyBeast, - ] - bc_region = create_region(world, player, active_locations, RegionName.Bc_Region, - bc_region_locations) - bc2_region_locations = [ + LocationName.DarkThornEventLocation, + ], + RegionName.Bc2: [ LocationName.RumblingRose, - LocationName.CastleWallsMap, - - ] - bc2_region = create_region(world, player, active_locations, RegionName.Bc2_Region, - bc2_region_locations) - xaldin_region_locations = [ + LocationName.CastleWallsMap + ], + RegionName.Xaldin: [ LocationName.Xaldin, LocationName.XaldinGetBonus, LocationName.DonaldXaldinGetBonus, LocationName.SecretAnsemReport4, + LocationName.XaldinEventLocation + ], + RegionName.DataXaldin: [ LocationName.XaldinDataDefenseBoost, - ] - xaldin_region = create_region(world, player, active_locations, RegionName.Xaldin_Region, - xaldin_region_locations) - sp_region_locations = [ + LocationName.DataXaldinEventLocation + ], + RegionName.Sp: [ LocationName.PitCellAreaMap, LocationName.PitCellMythrilCrystal, LocationName.CanyonDarkCrystal, @@ -406,41 +403,35 @@ def create_regions(world, player: int, active_locations): LocationName.HallwayAPBoost, LocationName.CommunicationsRoomIOTowerMap, LocationName.CommunicationsRoomGaiaBelt, + LocationName.DonaldScreens, + ], + RegionName.HostileProgram: [ LocationName.HostileProgram, LocationName.HostileProgramGetBonus, LocationName.PhotonDebugger, - LocationName.DonaldScreens, LocationName.GoofyHostileProgram, - - ] - sp_region = create_region(world, player, active_locations, RegionName.Sp_Region, - sp_region_locations) - sp2_region_locations = [ + LocationName.HostileProgramEventLocation + ], + RegionName.Sp2: [ LocationName.SolarSailer, LocationName.CentralComputerCoreAPBoost, LocationName.CentralComputerCoreOrichalcumPlus, LocationName.CentralComputerCoreCosmicArts, LocationName.CentralComputerCoreMap, - - LocationName.DonaldSolarSailer, - ] - - sp2_region = create_region(world, player, active_locations, RegionName.Sp2_Region, - sp2_region_locations) - mcp_region_locations = [ + LocationName.DonaldSolarSailer + ], + RegionName.Mcp: [ LocationName.MCP, LocationName.MCPGetBonus, - ] - mcp_region = create_region(world, player, active_locations, RegionName.Mcp_Region, - mcp_region_locations) - larxene_region_locations = [ + LocationName.McpEventLocation + ], + RegionName.DataLarxene: [ LocationName.LarxeneBonus, LocationName.LarxeneASCloakedThunder, LocationName.LarxeneDataLostIllusion, - ] - larxene_region = create_region(world, player, active_locations, RegionName.Larxene_Region, - larxene_region_locations) - ht_region_locations = [ + LocationName.DataLarxeneEventLocation + ], + RegionName.Ht: [ LocationName.GraveyardMythrilShard, LocationName.GraveyardSerenityGem, LocationName.FinklesteinsLabHalloweenTownMap, @@ -455,34 +446,37 @@ def create_regions(world, player: int, active_locations): LocationName.CandyCaneLaneMythrilStone, LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost, + ], + RegionName.PrisonKeeper: [ LocationName.PrisonKeeper, + LocationName.DonaldPrisonKeeper, + LocationName.PrisonKeeperEventLocation, + ], + RegionName.OogieBoogie: [ LocationName.OogieBoogie, LocationName.OogieBoogieMagnetElement, - LocationName.DonaldPrisonKeeper, LocationName.GoofyOogieBoogie, - ] - ht_region = create_region(world, player, active_locations, RegionName.Ht_Region, - ht_region_locations) - ht2_region_locations = [ + LocationName.OogieBoogieEventLocation + ], + RegionName.Ht2: [ LocationName.Lock, LocationName.Present, LocationName.DecoyPresents, + LocationName.GoofyLock + ], + RegionName.Experiment: [ LocationName.Experiment, LocationName.DecisivePumpkin, - LocationName.DonaldExperiment, - LocationName.GoofyLock, - ] - ht2_region = create_region(world, player, active_locations, RegionName.Ht2_Region, - ht2_region_locations) - vexen_region_locations = [ + LocationName.ExperimentEventLocation, + ], + RegionName.DataVexen: [ LocationName.VexenBonus, LocationName.VexenASRoadtoDiscovery, LocationName.VexenDataLostIllusion, - ] - vexen_region = create_region(world, player, active_locations, RegionName.Vexen_Region, - vexen_region_locations) - hb_region_locations = [ + LocationName.DataVexenEventLocation + ], + RegionName.Hb: [ LocationName.MarketplaceMap, LocationName.BoroughDriveRecovery, LocationName.BoroughAPBoost, @@ -493,11 +487,9 @@ def create_regions(world, player: int, active_locations): LocationName.MerlinsHouseBlizzardElement, LocationName.Bailey, LocationName.BaileySecretAnsemReport7, - LocationName.BaseballCharm, - ] - hb_region = create_region(world, player, active_locations, RegionName.Hb_Region, - hb_region_locations) - hb2_region_locations = [ + LocationName.BaseballCharm + ], + RegionName.Hb2: [ LocationName.PosternCastlePerimeterMap, LocationName.PosternMythrilGem, LocationName.PosternAPBoost, @@ -511,18 +503,9 @@ def create_regions(world, player: int, active_locations): LocationName.AnsemsStudyUkuleleCharm, LocationName.RestorationSiteMoonRecipe, LocationName.RestorationSiteAPBoost, - LocationName.CoRDepthsAPBoost, - LocationName.CoRDepthsPowerCrystal, - LocationName.CoRDepthsFrostCrystal, - LocationName.CoRDepthsManifestIllusion, - LocationName.CoRDepthsAPBoost2, - LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, - LocationName.CoRMineshaftLowerLevelAPBoost, + ], + RegionName.HBDemyx: [ LocationName.DonaldDemyxHBGetBonus, - ] - hb2_region = create_region(world, player, active_locations, RegionName.Hb2_Region, - hb2_region_locations) - onek_region_locations = [ LocationName.DemyxHB, LocationName.DemyxHBGetBonus, LocationName.FFFightsCureElement, @@ -530,30 +513,41 @@ def create_regions(world, player: int, active_locations): LocationName.CrystalFissureTheGreatMawMap, LocationName.CrystalFissureEnergyCrystal, LocationName.CrystalFissureAPBoost, + LocationName.HBDemyxEventLocation, + ], + RegionName.ThousandHeartless: [ LocationName.ThousandHeartless, LocationName.ThousandHeartlessSecretAnsemReport1, LocationName.ThousandHeartlessIceCream, LocationName.ThousandHeartlessPicture, LocationName.PosternGullWing, LocationName.HeartlessManufactoryCosmicChain, + LocationName.ThousandHeartlessEventLocation, + ], + RegionName.DataDemyx: [ LocationName.DemyxDataAPBoost, - ] - onek_region = create_region(world, player, active_locations, RegionName.ThousandHeartless_Region, - onek_region_locations) - mushroom_region_locations = [ + LocationName.DataDemyxEventLocation, + ], + RegionName.Mushroom13: [ LocationName.WinnersProof, LocationName.ProofofPeace, - ] - mushroom_region = create_region(world, player, active_locations, RegionName.Mushroom13_Region, - mushroom_region_locations) - sephi_region_locations = [ + LocationName.Mushroom13EventLocation, + ], + RegionName.Sephi: [ LocationName.SephirothBonus, LocationName.SephirothFenrir, - ] - sephi_region = create_region(world, player, active_locations, RegionName.Sephi_Region, - sephi_region_locations) - - cor_region_locations = [ + LocationName.SephiEventLocation + ], + RegionName.CoR: [ + LocationName.CoRDepthsAPBoost, + LocationName.CoRDepthsPowerCrystal, + LocationName.CoRDepthsFrostCrystal, + LocationName.CoRDepthsManifestIllusion, + LocationName.CoRDepthsAPBoost2, + LocationName.CoRMineshaftLowerLevelDepthsofRemembranceMap, + LocationName.CoRMineshaftLowerLevelAPBoost, + ], + RegionName.CorFirstFight: [ LocationName.CoRDepthsUpperLevelRemembranceGem, LocationName.CoRMiningAreaSerenityGem, LocationName.CoRMiningAreaAPBoost, @@ -561,22 +555,23 @@ def create_regions(world, player: int, active_locations): LocationName.CoRMiningAreaManifestIllusion, LocationName.CoRMiningAreaSerenityGem2, LocationName.CoRMiningAreaDarkRemembranceMap, + LocationName.CorFirstFightEventLocation, + ], + RegionName.CorSecondFight: [ LocationName.CoRMineshaftMidLevelPowerBoost, LocationName.CoREngineChamberSerenityCrystal, LocationName.CoREngineChamberRemembranceCrystal, LocationName.CoREngineChamberAPBoost, LocationName.CoREngineChamberManifestIllusion, LocationName.CoRMineshaftUpperLevelMagicBoost, - ] - cor_region = create_region(world, player, active_locations, RegionName.CoR_Region, - cor_region_locations) - transport_region_locations = [ - LocationName.CoRMineshaftUpperLevelAPBoost, + LocationName.CorSecondFightEventLocation, + ], + RegionName.Transport: [ + LocationName.CoRMineshaftUpperLevelAPBoost, # last chest LocationName.TransporttoRemembrance, - ] - transport_region = create_region(world, player, active_locations, RegionName.Transport_Region, - transport_region_locations) - pl_region_locations = [ + LocationName.TransportEventLocation, + ], + RegionName.Pl: [ LocationName.GorgeSavannahMap, LocationName.GorgeDarkGem, LocationName.GorgeMythrilStone, @@ -604,31 +599,40 @@ def create_regions(world, player: int, active_locations): LocationName.OasisAPBoost, LocationName.CircleofLife, LocationName.Hyenas1, + + LocationName.GoofyHyenas1 + ], + RegionName.Scar: [ LocationName.Scar, LocationName.ScarFireElement, LocationName.DonaldScar, - LocationName.GoofyHyenas1, - - ] - pl_region = create_region(world, player, active_locations, RegionName.Pl_Region, - pl_region_locations) - pl2_region_locations = [ + LocationName.ScarEventLocation, + ], + RegionName.Pl2: [ LocationName.Hyenas2, + LocationName.GoofyHyenas2 + ], + RegionName.GroundShaker: [ LocationName.Groundshaker, LocationName.GroundshakerGetBonus, + LocationName.GroundShakerEventLocation, + ], + RegionName.DataSaix: [ LocationName.SaixDataDefenseBoost, - LocationName.GoofyHyenas2, - ] - pl2_region = create_region(world, player, active_locations, RegionName.Pl2_Region, - pl2_region_locations) - - stt_region_locations = [ + LocationName.DataSaixEventLocation + ], + RegionName.Stt: [ LocationName.TwilightTownMap, LocationName.MunnyPouchOlette, LocationName.StationDusks, LocationName.StationofSerenityPotion, LocationName.StationofCallingPotion, + ], + RegionName.TwilightThorn: [ LocationName.TwilightThorn, + LocationName.TwilightThornEventLocation + ], + RegionName.Axel1: [ LocationName.Axel1, LocationName.JunkChampionBelt, LocationName.JunkMedal, @@ -648,14 +652,18 @@ def create_regions(world, player: int, active_locations): LocationName.NaminesSketches, LocationName.MansionMap, LocationName.MansionLibraryHiPotion, + LocationName.Axel1EventLocation + ], + RegionName.Axel2: [ LocationName.Axel2, LocationName.MansionBasementCorridorHiPotion, + LocationName.Axel2EventLocation + ], + RegionName.DataRoxas: [ LocationName.RoxasDataMagicBoost, - ] - stt_region = create_region(world, player, active_locations, RegionName.STT_Region, - stt_region_locations) - - tt_region_locations = [ + LocationName.DataRoxasEventLocation + ], + RegionName.Tt: [ LocationName.OldMansionPotion, LocationName.OldMansionMythrilShard, LocationName.TheWoodsPotion, @@ -682,18 +690,14 @@ def create_regions(world, player: int, active_locations): LocationName.SorcerersLoftTowerMap, LocationName.TowerWardrobeMythrilStone, LocationName.StarSeeker, - LocationName.ValorForm, - ] - tt_region = create_region(world, player, active_locations, RegionName.TT_Region, - tt_region_locations) - tt2_region_locations = [ + LocationName.ValorForm + ], + RegionName.Tt2: [ LocationName.SeifersTrophy, LocationName.Oathkeeper, - LocationName.LimitForm, - ] - tt2_region = create_region(world, player, active_locations, RegionName.TT2_Region, - tt2_region_locations) - tt3_region_locations = [ + LocationName.LimitForm + ], + RegionName.Tt3: [ LocationName.UndergroundConcourseMythrilGem, LocationName.UndergroundConcourseAPBoost, LocationName.UndergroundConcourseMythrilCrystal, @@ -715,22 +719,19 @@ def create_regions(world, player: int, active_locations): LocationName.MansionBasementCorridorUltimateRecipe, LocationName.BetwixtandBetween, LocationName.BetwixtandBetweenBondofFlame, + LocationName.DonaldMansionNobodies + ], + RegionName.DataAxel: [ LocationName.AxelDataMagicBoost, - LocationName.DonaldMansionNobodies, - ] - tt3_region = create_region(world, player, active_locations, RegionName.TT3_Region, - tt3_region_locations) - - twtnw_region_locations = [ + LocationName.DataAxelEventLocation, + ], + RegionName.Twtnw: [ LocationName.FragmentCrossingMythrilStone, LocationName.FragmentCrossingMythrilCrystal, LocationName.FragmentCrossingAPBoost, - LocationName.FragmentCrossingOrichalcum, - ] - - twtnw_region = create_region(world, player, active_locations, RegionName.Twtnw_Region, - twtnw_region_locations) - twtnw_postroxas_region_locations = [ + LocationName.FragmentCrossingOrichalcum + ], + RegionName.Roxas: [ LocationName.Roxas, LocationName.RoxasGetBonus, LocationName.RoxasSecretAnsemReport8, @@ -743,11 +744,9 @@ def create_regions(world, player: int, active_locations): LocationName.NothingsCallMythrilGem, LocationName.NothingsCallOrichalcum, LocationName.TwilightsViewCosmicBelt, - - ] - twtnw_postroxas_region = create_region(world, player, active_locations, RegionName.Twtnw_PostRoxas, - twtnw_postroxas_region_locations) - twtnw_postxigbar_region_locations = [ + LocationName.RoxasEventLocation + ], + RegionName.Xigbar: [ LocationName.XigbarBonus, LocationName.XigbarSecretAnsemReport3, LocationName.NaughtsSkywayMythrilGem, @@ -755,80 +754,100 @@ def create_regions(world, player: int, active_locations): LocationName.NaughtsSkywayMythrilCrystal, LocationName.Oblivion, LocationName.CastleThatNeverWasMap, + LocationName.XigbarEventLocation, + ], + RegionName.Luxord: [ LocationName.Luxord, LocationName.LuxordGetBonus, LocationName.LuxordSecretAnsemReport9, - ] - twtnw_postxigbar_region = create_region(world, player, active_locations, RegionName.Twtnw_PostXigbar, - twtnw_postxigbar_region_locations) - twtnw2_region_locations = [ + LocationName.LuxordEventLocation, + ], + RegionName.Saix: [ LocationName.SaixBonus, LocationName.SaixSecretAnsemReport12, + LocationName.SaixEventLocation, + ], + RegionName.Twtnw2: [ LocationName.PreXemnas1SecretAnsemReport11, LocationName.RuinandCreationsPassageMythrilStone, LocationName.RuinandCreationsPassageAPBoost, LocationName.RuinandCreationsPassageMythrilCrystal, - LocationName.RuinandCreationsPassageOrichalcum, + LocationName.RuinandCreationsPassageOrichalcum + ], + RegionName.Xemnas: [ LocationName.Xemnas1, LocationName.Xemnas1GetBonus, LocationName.Xemnas1SecretAnsemReport13, - LocationName.FinalXemnas, - LocationName.XemnasDataPowerBoost, - ] - twtnw2_region = create_region(world, player, active_locations, RegionName.Twtnw2_Region, - twtnw2_region_locations) + LocationName.XemnasEventLocation - valor_region_locations = [ + ], + RegionName.ArmoredXemnas: [ + LocationName.ArmoredXemnasEventLocation + ], + RegionName.ArmoredXemnas2: [ + LocationName.ArmoredXemnas2EventLocation + ], + RegionName.FinalXemnas: [ + LocationName.FinalXemnas + ], + RegionName.DataXemnas: [ + LocationName.XemnasDataPowerBoost, + LocationName.DataXemnasEventLocation + ], + RegionName.AtlanticaSongOne: [ + LocationName.UnderseaKingdomMap + ], + RegionName.AtlanticaSongTwo: [ + + ], + RegionName.AtlanticaSongThree: [ + LocationName.MysteriousAbyss + ], + RegionName.AtlanticaSongFour: [ + LocationName.MusicalBlizzardElement, + LocationName.MusicalOrichalcumPlus + ], + RegionName.Valor: [ LocationName.Valorlvl2, LocationName.Valorlvl3, LocationName.Valorlvl4, LocationName.Valorlvl5, LocationName.Valorlvl6, - LocationName.Valorlvl7, - ] - valor_region = create_region(world, player, active_locations, RegionName.Valor_Region, - valor_region_locations) - wisdom_region_locations = [ + LocationName.Valorlvl7 + ], + RegionName.Wisdom: [ LocationName.Wisdomlvl2, LocationName.Wisdomlvl3, LocationName.Wisdomlvl4, LocationName.Wisdomlvl5, LocationName.Wisdomlvl6, - LocationName.Wisdomlvl7, - ] - wisdom_region = create_region(world, player, active_locations, RegionName.Wisdom_Region, - wisdom_region_locations) - limit_region_locations = [ + LocationName.Wisdomlvl7 + ], + RegionName.Limit: [ LocationName.Limitlvl2, LocationName.Limitlvl3, LocationName.Limitlvl4, LocationName.Limitlvl5, LocationName.Limitlvl6, - LocationName.Limitlvl7, - ] - limit_region = create_region(world, player, active_locations, RegionName.Limit_Region, - limit_region_locations) - master_region_locations = [ + LocationName.Limitlvl7 + ], + RegionName.Master: [ LocationName.Masterlvl2, LocationName.Masterlvl3, LocationName.Masterlvl4, LocationName.Masterlvl5, LocationName.Masterlvl6, - LocationName.Masterlvl7, - ] - master_region = create_region(world, player, active_locations, RegionName.Master_Region, - master_region_locations) - final_region_locations = [ + LocationName.Masterlvl7 + ], + RegionName.Final: [ LocationName.Finallvl2, LocationName.Finallvl3, LocationName.Finallvl4, LocationName.Finallvl5, LocationName.Finallvl6, - LocationName.Finallvl7, - ] - final_region = create_region(world, player, active_locations, RegionName.Final_Region, - final_region_locations) - keyblade_region_locations = [ + LocationName.Finallvl7 + ], + RegionName.Keyblade: [ LocationName.FAKESlot, LocationName.DetectionSaberSlot, LocationName.EdgeofUltimaSlot, @@ -887,356 +906,256 @@ def create_regions(world, player: int, active_locations): LocationName.NobodyGuard, LocationName.OgreShield, LocationName.SaveTheKing2, - LocationName.UltimateMushroom, - ] - keyblade_region = create_region(world, player, active_locations, RegionName.Keyblade_Region, - keyblade_region_locations) + LocationName.UltimateMushroom + ], +} +level_region_list = [ + RegionName.LevelsVS1, + RegionName.LevelsVS3, + RegionName.LevelsVS6, + RegionName.LevelsVS9, + RegionName.LevelsVS12, + RegionName.LevelsVS15, + RegionName.LevelsVS18, + RegionName.LevelsVS21, + RegionName.LevelsVS24, + RegionName.LevelsVS26, +] - world.regions += [ - lod_Region, - lod2_Region, - ag_region, - ag2_region, - lexaeus_region, - dc_region, - tr_region, - terra_region, - marluxia_region, - hundred_acre1_region, - hundred_acre2_region, - hundred_acre3_region, - hundred_acre4_region, - hundred_acre5_region, - hundred_acre6_region, - pr_region, - pr2_region, - gr2_region, - oc_region, - oc2_region, - oc2_pain_and_panic_cup, - oc2_titan_cup, - oc2_cerberus_cup, - oc2_gof_cup, - zexion_region, - bc_region, - bc2_region, - xaldin_region, - sp_region, - sp2_region, - mcp_region, - larxene_region, - ht_region, - ht2_region, - vexen_region, - hb_region, - hb2_region, - onek_region, - mushroom_region, - sephi_region, - cor_region, - transport_region, - pl_region, - pl2_region, - stt_region, - tt_region, - tt2_region, - tt3_region, - twtnw_region, - twtnw_postroxas_region, - twtnw_postxigbar_region, - twtnw2_region, - goa_region, - menu_region, - valor_region, - wisdom_region, - limit_region, - master_region, - final_region, - keyblade_region, - ] + +def create_regions(self): # Level region depends on level depth. # for every 5 levels there should be +3 visit locking - levelVL1 = [] - levelVL3 = [] - levelVL6 = [] - levelVL9 = [] - levelVL12 = [] - levelVL15 = [] - levelVL18 = [] - levelVL21 = [] - levelVL24 = [] - levelVL26 = [] # level 50 - if world.LevelDepth[player] == "level_50": - levelVL1 = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, LocationName.Lvl10] - levelVL3 = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, LocationName.Lvl17, - LocationName.Lvl20, ] - levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, LocationName.Lvl30] - levelVL9 = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, LocationName.Lvl39, LocationName.Lvl41] - levelVL12 = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] - levelVL15 = [LocationName.Lvl50] + multiworld = self.multiworld + player = self.player + active_locations = self.location_name_to_id + + for level_region_name in level_region_list: + KH2REGIONS[level_region_name] = [] + if multiworld.LevelDepth[player] == "level_50": + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl4, LocationName.Lvl7, LocationName.Lvl9, + LocationName.Lvl10] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl17, + LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28, + LocationName.Lvl30] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl32, LocationName.Lvl34, LocationName.Lvl36, + LocationName.Lvl39, LocationName.Lvl41] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl44, LocationName.Lvl46, LocationName.Lvl48] + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl50] + # level 99 - elif world.LevelDepth[player] == "level_99": - levelVL1 = [LocationName.Lvl7, LocationName.Lvl9, ] - levelVL3 = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, LocationName.Lvl20] - levelVL6 = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] - levelVL9 = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, LocationName.Lvl39] - levelVL12 = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, LocationName.Lvl49] - levelVL15 = [LocationName.Lvl53, LocationName.Lvl59] - levelVL18 = [LocationName.Lvl65] - levelVL21 = [LocationName.Lvl73] - levelVL24 = [LocationName.Lvl85] - levelVL26 = [LocationName.Lvl99] + elif multiworld.LevelDepth[player] == "level_99": + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl7, LocationName.Lvl9] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl12, LocationName.Lvl15, LocationName.Lvl17, + LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl23, LocationName.Lvl25, LocationName.Lvl28] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl33, LocationName.Lvl36, + LocationName.Lvl39] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl44, LocationName.Lvl47, + LocationName.Lvl49] + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl53, LocationName.Lvl59] + KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl65] + KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl73] + KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl85] + KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl99] # level sanity # has to be [] instead of {} for in - elif world.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: - levelVL1 = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, LocationName.Lvl6, - LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] - levelVL3 = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, LocationName.Lvl14, LocationName.Lvl15, - LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, LocationName.Lvl19, LocationName.Lvl20] - levelVL6 = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, LocationName.Lvl24, LocationName.Lvl25, - LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, LocationName.Lvl29, LocationName.Lvl30] - levelVL9 = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, LocationName.Lvl34, LocationName.Lvl35, - LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, LocationName.Lvl39, LocationName.Lvl40] - levelVL12 = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, LocationName.Lvl44, LocationName.Lvl45, - LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, LocationName.Lvl49, LocationName.Lvl50] + elif multiworld.LevelDepth[player] in ["level_50_sanity", "level_99_sanity"]: + KH2REGIONS[RegionName.LevelsVS1] = [LocationName.Lvl2, LocationName.Lvl3, LocationName.Lvl4, LocationName.Lvl5, + LocationName.Lvl6, + LocationName.Lvl7, LocationName.Lvl8, LocationName.Lvl9, LocationName.Lvl10] + KH2REGIONS[RegionName.LevelsVS3] = [LocationName.Lvl11, LocationName.Lvl12, LocationName.Lvl13, + LocationName.Lvl14, LocationName.Lvl15, + LocationName.Lvl16, LocationName.Lvl17, LocationName.Lvl18, + LocationName.Lvl19, LocationName.Lvl20] + KH2REGIONS[RegionName.LevelsVS6] = [LocationName.Lvl21, LocationName.Lvl22, LocationName.Lvl23, + LocationName.Lvl24, LocationName.Lvl25, + LocationName.Lvl26, LocationName.Lvl27, LocationName.Lvl28, + LocationName.Lvl29, LocationName.Lvl30] + KH2REGIONS[RegionName.LevelsVS9] = [LocationName.Lvl31, LocationName.Lvl32, LocationName.Lvl33, + LocationName.Lvl34, LocationName.Lvl35, + LocationName.Lvl36, LocationName.Lvl37, LocationName.Lvl38, + LocationName.Lvl39, LocationName.Lvl40] + KH2REGIONS[RegionName.LevelsVS12] = [LocationName.Lvl41, LocationName.Lvl42, LocationName.Lvl43, + LocationName.Lvl44, LocationName.Lvl45, + LocationName.Lvl46, LocationName.Lvl47, LocationName.Lvl48, + LocationName.Lvl49, LocationName.Lvl50] # level 99 sanity - if world.LevelDepth[player] == "level_99_sanity": - levelVL15 = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, LocationName.Lvl54, - LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, LocationName.Lvl58, - LocationName.Lvl59, LocationName.Lvl60] - levelVL18 = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, LocationName.Lvl64, - LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, LocationName.Lvl68, - LocationName.Lvl69, LocationName.Lvl70] - levelVL21 = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, LocationName.Lvl74, - LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, LocationName.Lvl78, - LocationName.Lvl79, LocationName.Lvl80] - levelVL24 = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, LocationName.Lvl84, - LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, LocationName.Lvl88, - LocationName.Lvl89, LocationName.Lvl90] - levelVL26 = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, LocationName.Lvl94, - LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, LocationName.Lvl98, - LocationName.Lvl99] - - level_regionVL1 = create_region(world, player, active_locations, RegionName.LevelsVS1, - levelVL1) - level_regionVL3 = create_region(world, player, active_locations, RegionName.LevelsVS3, - levelVL3) - level_regionVL6 = create_region(world, player, active_locations, RegionName.LevelsVS6, - levelVL6) - level_regionVL9 = create_region(world, player, active_locations, RegionName.LevelsVS9, - levelVL9) - level_regionVL12 = create_region(world, player, active_locations, RegionName.LevelsVS12, - levelVL12) - level_regionVL15 = create_region(world, player, active_locations, RegionName.LevelsVS15, - levelVL15) - level_regionVL18 = create_region(world, player, active_locations, RegionName.LevelsVS18, - levelVL18) - level_regionVL21 = create_region(world, player, active_locations, RegionName.LevelsVS21, - levelVL21) - level_regionVL24 = create_region(world, player, active_locations, RegionName.LevelsVS24, - levelVL24) - level_regionVL26 = create_region(world, player, active_locations, RegionName.LevelsVS26, - levelVL26) - world.regions += [level_regionVL1, level_regionVL3, level_regionVL6, level_regionVL9, level_regionVL12, - level_regionVL15, level_regionVL18, level_regionVL21, level_regionVL24, level_regionVL26] + if multiworld.LevelDepth[player] == "level_99_sanity": + KH2REGIONS[RegionName.LevelsVS15] = [LocationName.Lvl51, LocationName.Lvl52, LocationName.Lvl53, + LocationName.Lvl54, + LocationName.Lvl55, LocationName.Lvl56, LocationName.Lvl57, + LocationName.Lvl58, + LocationName.Lvl59, LocationName.Lvl60] + KH2REGIONS[RegionName.LevelsVS18] = [LocationName.Lvl61, LocationName.Lvl62, LocationName.Lvl63, + LocationName.Lvl64, + LocationName.Lvl65, LocationName.Lvl66, LocationName.Lvl67, + LocationName.Lvl68, + LocationName.Lvl69, LocationName.Lvl70] + KH2REGIONS[RegionName.LevelsVS21] = [LocationName.Lvl71, LocationName.Lvl72, LocationName.Lvl73, + LocationName.Lvl74, + LocationName.Lvl75, LocationName.Lvl76, LocationName.Lvl77, + LocationName.Lvl78, + LocationName.Lvl79, LocationName.Lvl80] + KH2REGIONS[RegionName.LevelsVS24] = [LocationName.Lvl81, LocationName.Lvl82, LocationName.Lvl83, + LocationName.Lvl84, + LocationName.Lvl85, LocationName.Lvl86, LocationName.Lvl87, + LocationName.Lvl88, + LocationName.Lvl89, LocationName.Lvl90] + KH2REGIONS[RegionName.LevelsVS26] = [LocationName.Lvl91, LocationName.Lvl92, LocationName.Lvl93, + LocationName.Lvl94, + LocationName.Lvl95, LocationName.Lvl96, LocationName.Lvl97, + LocationName.Lvl98, LocationName.Lvl99] + KH2REGIONS[RegionName.Summon] = [] + if multiworld.SummonLevelLocationToggle[player]: + KH2REGIONS[RegionName.Summon] = [LocationName.Summonlvl2, + LocationName.Summonlvl3, + LocationName.Summonlvl4, + LocationName.Summonlvl5, + LocationName.Summonlvl6, + LocationName.Summonlvl7] + multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in + KH2REGIONS.items()] + # fill the event locations with events + multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table}) + for location, item in event_location_to_item.items(): + multiworld.get_location(location, player).place_locked_item( + multiworld.worlds[player].create_item(item)) -def connect_regions(world: MultiWorld, player: int): +def connect_regions(self): + multiworld = self.multiworld + player = self.player # connecting every first visit to the GoA + KH2RegionConnections: typing.Dict[str, typing.Set[str]] = { + "Menu": {RegionName.GoA}, + RegionName.GoA: {RegionName.Sp, RegionName.Pr, RegionName.Tt, RegionName.Oc, RegionName.Ht, + RegionName.LoD, + RegionName.Twtnw, RegionName.Bc, RegionName.Ag, RegionName.Pl, RegionName.Hb, + RegionName.Dc, RegionName.Stt, + RegionName.Ha1, RegionName.Keyblade, RegionName.LevelsVS1, + RegionName.Valor, RegionName.Wisdom, RegionName.Limit, RegionName.Master, + RegionName.Final, RegionName.Summon, RegionName.AtlanticaSongOne}, + RegionName.LoD: {RegionName.ShanYu}, + RegionName.ShanYu: {RegionName.LoD2}, + RegionName.LoD2: {RegionName.AnsemRiku}, + RegionName.AnsemRiku: {RegionName.StormRider}, + RegionName.StormRider: {RegionName.DataXigbar}, + RegionName.Ag: {RegionName.TwinLords}, + RegionName.TwinLords: {RegionName.Ag2}, + RegionName.Ag2: {RegionName.GenieJafar}, + RegionName.GenieJafar: {RegionName.DataLexaeus}, + RegionName.Dc: {RegionName.Tr}, + RegionName.Tr: {RegionName.OldPete}, + RegionName.OldPete: {RegionName.FuturePete}, + RegionName.FuturePete: {RegionName.Terra, RegionName.DataMarluxia}, + RegionName.Ha1: {RegionName.Ha2}, + RegionName.Ha2: {RegionName.Ha3}, + RegionName.Ha3: {RegionName.Ha4}, + RegionName.Ha4: {RegionName.Ha5}, + RegionName.Ha5: {RegionName.Ha6}, + RegionName.Pr: {RegionName.Barbosa}, + RegionName.Barbosa: {RegionName.Pr2}, + RegionName.Pr2: {RegionName.GrimReaper1}, + RegionName.GrimReaper1: {RegionName.GrimReaper2}, + RegionName.GrimReaper2: {RegionName.DataLuxord}, + RegionName.Oc: {RegionName.Cerberus}, + RegionName.Cerberus: {RegionName.OlympusPete}, + RegionName.OlympusPete: {RegionName.Hydra}, + RegionName.Hydra: {RegionName.OcPainAndPanicCup, RegionName.OcCerberusCup, RegionName.Oc2}, + RegionName.Oc2: {RegionName.Hades}, + RegionName.Hades: {RegionName.Oc2TitanCup, RegionName.Oc2GofCup, RegionName.DataZexion}, + RegionName.Oc2GofCup: {RegionName.HadesCups}, + RegionName.Bc: {RegionName.Thresholder}, + RegionName.Thresholder: {RegionName.Beast}, + RegionName.Beast: {RegionName.DarkThorn}, + RegionName.DarkThorn: {RegionName.Bc2}, + RegionName.Bc2: {RegionName.Xaldin}, + RegionName.Xaldin: {RegionName.DataXaldin}, + RegionName.Sp: {RegionName.HostileProgram}, + RegionName.HostileProgram: {RegionName.Sp2}, + RegionName.Sp2: {RegionName.Mcp}, + RegionName.Mcp: {RegionName.DataLarxene}, + RegionName.Ht: {RegionName.PrisonKeeper}, + RegionName.PrisonKeeper: {RegionName.OogieBoogie}, + RegionName.OogieBoogie: {RegionName.Ht2}, + RegionName.Ht2: {RegionName.Experiment}, + RegionName.Experiment: {RegionName.DataVexen}, + RegionName.Hb: {RegionName.Hb2}, + RegionName.Hb2: {RegionName.CoR, RegionName.HBDemyx}, + RegionName.HBDemyx: {RegionName.ThousandHeartless}, + RegionName.ThousandHeartless: {RegionName.Mushroom13, RegionName.DataDemyx, RegionName.Sephi}, + RegionName.CoR: {RegionName.CorFirstFight}, + RegionName.CorFirstFight: {RegionName.CorSecondFight}, + RegionName.CorSecondFight: {RegionName.Transport}, + RegionName.Pl: {RegionName.Scar}, + RegionName.Scar: {RegionName.Pl2}, + RegionName.Pl2: {RegionName.GroundShaker}, + RegionName.GroundShaker: {RegionName.DataSaix}, + RegionName.Stt: {RegionName.TwilightThorn}, + RegionName.TwilightThorn: {RegionName.Axel1}, + RegionName.Axel1: {RegionName.Axel2}, + RegionName.Axel2: {RegionName.DataRoxas}, + RegionName.Tt: {RegionName.Tt2}, + RegionName.Tt2: {RegionName.Tt3}, + RegionName.Tt3: {RegionName.DataAxel}, + RegionName.Twtnw: {RegionName.Roxas}, + RegionName.Roxas: {RegionName.Xigbar}, + RegionName.Xigbar: {RegionName.Luxord}, + RegionName.Luxord: {RegionName.Saix}, + RegionName.Saix: {RegionName.Twtnw2}, + RegionName.Twtnw2: {RegionName.Xemnas}, + RegionName.Xemnas: {RegionName.ArmoredXemnas, RegionName.DataXemnas}, + RegionName.ArmoredXemnas: {RegionName.ArmoredXemnas2}, + RegionName.ArmoredXemnas2: {RegionName.FinalXemnas}, + RegionName.LevelsVS1: {RegionName.LevelsVS3}, + RegionName.LevelsVS3: {RegionName.LevelsVS6}, + RegionName.LevelsVS6: {RegionName.LevelsVS9}, + RegionName.LevelsVS9: {RegionName.LevelsVS12}, + RegionName.LevelsVS12: {RegionName.LevelsVS15}, + RegionName.LevelsVS15: {RegionName.LevelsVS18}, + RegionName.LevelsVS18: {RegionName.LevelsVS21}, + RegionName.LevelsVS21: {RegionName.LevelsVS24}, + RegionName.LevelsVS24: {RegionName.LevelsVS26}, + RegionName.AtlanticaSongOne: {RegionName.AtlanticaSongTwo}, + RegionName.AtlanticaSongTwo: {RegionName.AtlanticaSongThree}, + RegionName.AtlanticaSongThree: {RegionName.AtlanticaSongFour}, + } - names: typing.Dict[str, int] = {} - - connect(world, player, names, "Menu", RegionName.Keyblade_Region) - connect(world, player, names, "Menu", RegionName.GoA_Region) - - connect(world, player, names, RegionName.GoA_Region, RegionName.LoD_Region, - lambda state: state.kh_lod_unlocked(player, 1)) - connect(world, player, names, RegionName.LoD_Region, RegionName.LoD2_Region, - lambda state: state.kh_lod_unlocked(player, 2)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Oc_Region, - lambda state: state.kh_oc_unlocked(player, 1)) - connect(world, player, names, RegionName.Oc_Region, RegionName.Oc2_Region, - lambda state: state.kh_oc_unlocked(player, 2)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Zexion_Region, - lambda state: state.kh_datazexion(player)) - - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_pain_and_panic_Region, - lambda state: state.kh_painandpanic(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_cerberus_Region, - lambda state: state.kh_cerberuscup(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_titan_Region, - lambda state: state.kh_titan(player)) - connect(world, player, names, RegionName.Oc2_Region, RegionName.Oc2_gof_Region, - lambda state: state.kh_gof(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Ag_Region, - lambda state: state.kh_ag_unlocked(player, 1)) - connect(world, player, names, RegionName.Ag_Region, RegionName.Ag2_Region, - lambda state: state.kh_ag_unlocked(player, 2) - and (state.has(ItemName.FireElement, player) - and state.has(ItemName.BlizzardElement, player) - and state.has(ItemName.ThunderElement, player))) - connect(world, player, names, RegionName.Ag2_Region, RegionName.Lexaeus_Region, - lambda state: state.kh_datalexaeus(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Dc_Region, - lambda state: state.kh_dc_unlocked(player, 1)) - connect(world, player, names, RegionName.Dc_Region, RegionName.Tr_Region, - lambda state: state.kh_dc_unlocked(player, 2)) - connect(world, player, names, RegionName.Tr_Region, RegionName.Marluxia_Region, - lambda state: state.kh_datamarluxia(player)) - connect(world, player, names, RegionName.Tr_Region, RegionName.Terra_Region, lambda state: state.kh_terra(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Pr_Region, - lambda state: state.kh_pr_unlocked(player, 1)) - connect(world, player, names, RegionName.Pr_Region, RegionName.Pr2_Region, - lambda state: state.kh_pr_unlocked(player, 2)) - connect(world, player, names, RegionName.Pr2_Region, RegionName.Gr2_Region, - lambda state: state.kh_gr2(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Bc_Region, - lambda state: state.kh_bc_unlocked(player, 1)) - connect(world, player, names, RegionName.Bc_Region, RegionName.Bc2_Region, - lambda state: state.kh_bc_unlocked(player, 2)) - connect(world, player, names, RegionName.Bc2_Region, RegionName.Xaldin_Region, - lambda state: state.kh_xaldin(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Sp_Region, - lambda state: state.kh_sp_unlocked(player, 1)) - connect(world, player, names, RegionName.Sp_Region, RegionName.Sp2_Region, - lambda state: state.kh_sp_unlocked(player, 2)) - connect(world, player, names, RegionName.Sp2_Region, RegionName.Mcp_Region, - lambda state: state.kh_mcp(player)) - connect(world, player, names, RegionName.Mcp_Region, RegionName.Larxene_Region, - lambda state: state.kh_datalarxene(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Ht_Region, - lambda state: state.kh_ht_unlocked(player, 1)) - connect(world, player, names, RegionName.Ht_Region, RegionName.Ht2_Region, - lambda state: state.kh_ht_unlocked(player, 2)) - connect(world, player, names, RegionName.Ht2_Region, RegionName.Vexen_Region, - lambda state: state.kh_datavexen(player)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Hb_Region, - lambda state: state.kh_hb_unlocked(player, 1)) - connect(world, player, names, RegionName.Hb_Region, RegionName.Hb2_Region, - lambda state: state.kh_hb_unlocked(player, 2)) - connect(world, player, names, RegionName.Hb2_Region, RegionName.ThousandHeartless_Region, - lambda state: state.kh_onek(player)) - connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Mushroom13_Region, - lambda state: state.has(ItemName.ProofofPeace, player)) - connect(world, player, names, RegionName.ThousandHeartless_Region, RegionName.Sephi_Region, - lambda state: state.kh_sephi(player)) - - connect(world, player, names, RegionName.Hb2_Region, RegionName.CoR_Region, lambda state: state.kh_cor(player)) - connect(world, player, names, RegionName.CoR_Region, RegionName.Transport_Region, lambda state: - state.has(ItemName.HighJump, player, 3) - and state.has(ItemName.AerialDodge, player, 3) - and state.has(ItemName.Glide, player, 3)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Pl_Region, - lambda state: state.kh_pl_unlocked(player, 1)) - connect(world, player, names, RegionName.Pl_Region, RegionName.Pl2_Region, - lambda state: state.kh_pl_unlocked(player, 2) and ( - state.has(ItemName.BerserkCharge, player) or state.kh_reflect(player))) - - connect(world, player, names, RegionName.GoA_Region, RegionName.STT_Region, - lambda state: state.kh_stt_unlocked(player, 1)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.TT_Region, - lambda state: state.kh_tt_unlocked(player, 1)) - connect(world, player, names, RegionName.TT_Region, RegionName.TT2_Region, - lambda state: state.kh_tt_unlocked(player, 2)) - connect(world, player, names, RegionName.TT2_Region, RegionName.TT3_Region, - lambda state: state.kh_tt_unlocked(player, 3)) - - connect(world, player, names, RegionName.GoA_Region, RegionName.Twtnw_Region, - lambda state: state.kh_twtnw_unlocked(player, 0)) - connect(world, player, names, RegionName.Twtnw_Region, RegionName.Twtnw_PostRoxas, - lambda state: state.kh_roxastools(player)) - connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw_PostXigbar, - lambda state: state.kh_basetools(player) and (state.kh_donaldlimit(player) or ( - state.has(ItemName.FinalForm, player) and state.has(ItemName.FireElement, player)))) - connect(world, player, names, RegionName.Twtnw_PostRoxas, RegionName.Twtnw2_Region, - lambda state: state.kh_twtnw_unlocked(player, 1)) - - hundredacrevisits = {RegionName.HundredAcre1_Region: 0, RegionName.HundredAcre2_Region: 1, - RegionName.HundredAcre3_Region: 2, - RegionName.HundredAcre4_Region: 3, RegionName.HundredAcre5_Region: 4, - RegionName.HundredAcre6_Region: 5} - for visit, tornpage in hundredacrevisits.items(): - connect(world, player, names, RegionName.GoA_Region, visit, - lambda state: (state.has(ItemName.TornPages, player, tornpage))) - - connect(world, player, names, RegionName.GoA_Region, RegionName.LevelsVS1, - lambda state: state.kh_visit_locking_amount(player, 1)) - connect(world, player, names, RegionName.LevelsVS1, RegionName.LevelsVS3, - lambda state: state.kh_visit_locking_amount(player, 3)) - connect(world, player, names, RegionName.LevelsVS3, RegionName.LevelsVS6, - lambda state: state.kh_visit_locking_amount(player, 6)) - connect(world, player, names, RegionName.LevelsVS6, RegionName.LevelsVS9, - lambda state: state.kh_visit_locking_amount(player, 9)) - connect(world, player, names, RegionName.LevelsVS9, RegionName.LevelsVS12, - lambda state: state.kh_visit_locking_amount(player, 12)) - connect(world, player, names, RegionName.LevelsVS12, RegionName.LevelsVS15, - lambda state: state.kh_visit_locking_amount(player, 15)) - connect(world, player, names, RegionName.LevelsVS15, RegionName.LevelsVS18, - lambda state: state.kh_visit_locking_amount(player, 18)) - connect(world, player, names, RegionName.LevelsVS18, RegionName.LevelsVS21, - lambda state: state.kh_visit_locking_amount(player, 21)) - connect(world, player, names, RegionName.LevelsVS21, RegionName.LevelsVS24, - lambda state: state.kh_visit_locking_amount(player, 24)) - connect(world, player, names, RegionName.LevelsVS24, RegionName.LevelsVS26, - lambda state: state.kh_visit_locking_amount(player, 25)) # 25 because of goa twtnw bugs with visit locking. - - for region in RegionTable["ValorRegion"]: - connect(world, player, names, region, RegionName.Valor_Region, - lambda state: state.has(ItemName.ValorForm, player)) - for region in RegionTable["WisdomRegion"]: - connect(world, player, names, region, RegionName.Wisdom_Region, - lambda state: state.has(ItemName.WisdomForm, player)) - for region in RegionTable["LimitRegion"]: - connect(world, player, names, region, RegionName.Limit_Region, - lambda state: state.has(ItemName.LimitForm, player)) - for region in RegionTable["MasterRegion"]: - connect(world, player, names, region, RegionName.Master_Region, - lambda state: state.has(ItemName.MasterForm, player) and state.has(ItemName.DriveConverter, player)) - for region in RegionTable["FinalRegion"]: - connect(world, player, names, region, RegionName.Final_Region, - lambda state: state.has(ItemName.FinalForm, player)) + for source, target in KH2RegionConnections.items(): + source_region = multiworld.get_region(source, player) + source_region.add_exits(target) -# shamelessly stolen from the sa2b -def connect(world: MultiWorld, player: int, used_names: typing.Dict[str, int], source: str, target: str, - rule: typing.Optional[typing.Callable] = None): - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) +# cave fight:fire/guard +# hades escape logic:fire,blizzard,slide dash, base tools +# windows:chicken little.fire element,base tools +# chasm of challenges:reflect, blizzard, trinity limit,chicken little +# living bones: magnet +# some things for barbosa(PR), chicken little +# hyneas(magnet,reflect) +# tt2: reflect,chicken,form, guard,aerial recovery,finising plus, +# corridors,dancers:chicken little or stitch +demyx tools +# 1k: guard,once more,limit form, +# snipers +before: stitch, magnet, finishing leap, base tools, reflect +# dragoons:stitch, magnet, base tools, reflect +# oc2 tournament thing: stitch, magnet, base tools, reflera +# lock,shock and barrel: reflect, base tools +# carpet section: magnera, reflect, base tools, +# sp2: reflera, stitch, basse tools, reflera, thundara, fantasia/duck flare,once more. +# tt3: stitch/chicken little, magnera,reflera,base tools,finishing leap,limit form +# cor - if target not in used_names: - used_names[target] = 1 - name = target - else: - used_names[target] += 1 - name = target + (' ' * used_names[target]) - - connection = Entrance(player, name, source_region) - - if rule: - connection.access_rule = rule - - source_region.exits.append(connection) - connection.connect(target_region) - - -def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None): - ret = Region(name, player, world) +def create_region(multiworld, player: int, active_locations, name: str, locations=None): + ret = Region(name, player, multiworld) if locations: - for location in locations: - loc_id = active_locations.get(location, 0) - if loc_id: - location = KH2Location(player, location, loc_id.code, ret) - ret.locations.append(location) + loc_to_id = {loc: active_locations.get(loc, 0) for loc in locations if active_locations.get(loc, None)} + ret.add_locations(loc_to_id, KH2Location) + loc_to_event = {loc: active_locations.get(loc, None) for loc in locations if + not active_locations.get(loc, None)} + ret.add_locations(loc_to_event, KH2Location) return ret diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index b86ae4a2db..18375231a5 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -1,96 +1,1163 @@ +from typing import Dict, Callable, TYPE_CHECKING -from BaseClasses import MultiWorld +from BaseClasses import CollectionState +from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table +from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks +from .Names import LocationName, ItemName, RegionName +from worlds.generic.Rules import add_rule, forbid_items, add_item_rule +from .Logic import * -from .Items import exclusionItem_table -from .Locations import STT_Checks, exclusion_table -from .Names import LocationName, ItemName -from ..generic.Rules import add_rule, forbid_items, set_rule +# I don't know what is going on here, but it works. +if TYPE_CHECKING: + from . import KH2World +else: + KH2World = object -def set_rules(world: MultiWorld, player: int): +# Shamelessly Stolen from Messanger - add_rule(world.get_location(LocationName.RoxasDataMagicBoost, player), - lambda state: state.kh_dataroxas(player)) - add_rule(world.get_location(LocationName.DemyxDataAPBoost, player), - lambda state: state.kh_datademyx(player)) - add_rule(world.get_location(LocationName.SaixDataDefenseBoost, player), - lambda state: state.kh_datasaix(player)) - add_rule(world.get_location(LocationName.XaldinDataDefenseBoost, player), - lambda state: state.kh_dataxaldin(player)) - add_rule(world.get_location(LocationName.XemnasDataPowerBoost, player), - lambda state: state.kh_dataxemnas(player)) - add_rule(world.get_location(LocationName.XigbarDataDefenseBoost, player), - lambda state: state.kh_dataxigbar(player)) - add_rule(world.get_location(LocationName.VexenDataLostIllusion, player), - lambda state: state.kh_dataaxel(player)) - add_rule(world.get_location(LocationName.LuxordDataAPBoost, player), - lambda state: state.kh_dataluxord(player)) - for slot, weapon in exclusion_table["WeaponSlots"].items(): - add_rule(world.get_location(slot, player), lambda state: state.has(weapon, player)) - formLogicTable = { - ItemName.ValorForm: [LocationName.Valorlvl4, - LocationName.Valorlvl5, - LocationName.Valorlvl6, - LocationName.Valorlvl7], - ItemName.WisdomForm: [LocationName.Wisdomlvl4, - LocationName.Wisdomlvl5, - LocationName.Wisdomlvl6, - LocationName.Wisdomlvl7], - ItemName.LimitForm: [LocationName.Limitlvl4, - LocationName.Limitlvl5, - LocationName.Limitlvl6, - LocationName.Limitlvl7], - ItemName.MasterForm: [LocationName.Masterlvl4, - LocationName.Masterlvl5, - LocationName.Masterlvl6, - LocationName.Masterlvl7], - ItemName.FinalForm: [LocationName.Finallvl4, - LocationName.Finallvl5, - LocationName.Finallvl6, - LocationName.Finallvl7] - } +class KH2Rules: + player: int + world: KH2World + # World Rules: Rules for the visit locks + # Location Rules: Deterministic of player settings. + # Form Rules: Rules for Drive Forms and Summon levels. These Are Locations + # Fight Rules: Rules for fights. These are regions in the worlds. + world_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] - for form in formLogicTable: - for i in range(4): - location = world.get_location(formLogicTable[form][i], player) - set_rule(location, lambda state, i=i + 1, form=form: state.kh_amount_of_forms(player, i, form)) + fight_rules: Dict[str, Callable[[CollectionState], bool]] - if world.Goal[player] == "three_proofs": - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_three_proof_unlocked(player)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) + def __init__(self, world: KH2World) -> None: + self.player = world.player + self.world = world + self.multiworld = world.multiworld + + def lod_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.SwordoftheAncestor, self.player, Amount) + + def oc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BattlefieldsofWar, self.player, Amount) + + def twtnw_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.WaytotheDawn, self.player, Amount) + + def ht_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BoneFist, self.player, Amount) + + def tt_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.IceCream, self.player, Amount) + + def pr_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.SkillandCrossbones, self.player, Amount) + + def sp_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.IdentityDisk, self.player, Amount) + + def stt_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.NamineSketches, self.player, Amount) + + def dc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.CastleKey, self.player, Amount) # Using Dummy 13 for this + + def hb_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.MembershipCard, self.player, Amount) + + def pl_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.ProudFang, self.player, Amount) + + def ag_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.Scimitar, self.player, Amount) + + def bc_unlocked(self, state: CollectionState, Amount) -> bool: + return state.has(ItemName.BeastsClaw, self.player, Amount) + + def at_three_unlocked(self, state: CollectionState) -> bool: + return state.has(ItemName.MagnetElement, self.player, 2) + + def at_four_unlocked(self, state: CollectionState) -> bool: + return state.has(ItemName.ThunderElement, self.player, 3) + + def hundred_acre_unlocked(self, state: CollectionState, amount) -> bool: + return state.has(ItemName.TornPages, self.player, amount) + + def level_locking_unlock(self, state: CollectionState, amount): + return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]]) + + def summon_levels_unlocked(self, state: CollectionState, amount) -> bool: + return amount <= sum([state.count(item_name, self.player) for item_name in summons]) + + def kh2_list_count_sum(self, item_name_set: list, state: CollectionState) -> int: + """ + Returns the sum of state.count() for each item in the list. + """ + return sum( + [state.count(item_name, self.player) for item_name in item_name_set] + ) + + def kh2_list_any_sum(self, list_of_item_name_list: list, state: CollectionState) -> int: + """ + Returns sum that increments by 1 if state.has_any + """ + return sum( + [1 for item_list in list_of_item_name_list if + state.has_any(set(item_list), self.player)] + ) + + def kh2_dict_count(self, item_name_to_count: dict, state: CollectionState) -> bool: + """ + simplifies count to a dictionary. + """ + return all( + [state.count(item_name, self.player) >= item_amount for item_name, item_amount in + item_name_to_count.items()] + ) + + def kh2_dict_one_count(self, item_name_to_count: dict, state: CollectionState) -> int: + """ + simplifies count to a dictionary. + """ + return sum( + [1 for item_name, item_amount in + item_name_to_count.items() if state.count(item_name, self.player) >= item_amount] + ) + + def kh2_can_reach_any(self, loc_set: list, state: CollectionState): + """ + Can reach any locations in the set. + """ + return any( + [self.kh2_can_reach(location, state) for location in + loc_set] + ) + + def kh2_can_reach_all(self, loc_list: list, state: CollectionState): + """ + Can reach all locations in the set. + """ + return all( + [self.kh2_can_reach(location, state) for location in loc_list] + ) + + def kh2_can_reach(self, loc: str, state: CollectionState): + """ + Returns bool instead of collection state. + """ + return state.can_reach(self.multiworld.get_location(loc, self.player), "location", self.player) + + def kh2_has_all(self, items: list, state: CollectionState): + """If state has at least one of all.""" + return state.has_all(set(items), self.player) + + def kh2_has_any(self, items: list, state: CollectionState): + return state.has_any(set(items), self.player) + + def form_list_unlock(self, state: CollectionState, parent_form_list, level_required, fight_logic=False) -> bool: + form_access = {parent_form_list} + if self.multiworld.AutoFormLogic[self.player] and state.has(ItemName.SecondChance, self.player) and not fight_logic: + if parent_form_list == ItemName.MasterForm: + if state.has(ItemName.DriveConverter, self.player): + form_access.add(auto_form_dict[parent_form_list]) + else: + form_access.add(auto_form_dict[parent_form_list]) + return state.has_any(form_access, self.player) \ + and self.get_form_level_requirement(state, level_required) + + def get_form_level_requirement(self, state, amount): + forms_available = 0 + form_list = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, + ItemName.FinalForm] + if self.world.multiworld.FinalFormLogic[self.player] != "no_light_and_darkness": + if self.world.multiworld.FinalFormLogic[self.player] == "light_and_darkness": + if state.has(ItemName.LightDarkness, self.player) and state.has_any(set(form_list), self.player): + forms_available += 1 + form_list.remove(ItemName.FinalForm) + else: # self.multiworld.FinalFormLogic=="just a form" + form_list.remove(ItemName.FinalForm) + if state.has_any(form_list, self.player): + forms_available += 1 + forms_available += sum([1 for form in form_list if state.has(form, self.player)]) + return forms_available >= amount + + +class KH2WorldRules(KH2Rules): + def __init__(self, kh2world: KH2World) -> None: + # These Rules are Always in effect + super().__init__(kh2world) + self.region_rules = { + RegionName.LoD: lambda state: self.lod_unlocked(state, 1), + RegionName.LoD2: lambda state: self.lod_unlocked(state, 2), + + RegionName.Oc: lambda state: self.oc_unlocked(state, 1), + RegionName.Oc2: lambda state: self.oc_unlocked(state, 2), + + RegionName.Twtnw2: lambda state: self.twtnw_unlocked(state, 2), + # These will be swapped and First Visit lock for twtnw is in development. + # RegionName.Twtnw1: lambda state: self.lod_unlocked(state, 2), + + RegionName.Ht: lambda state: self.ht_unlocked(state, 1), + RegionName.Ht2: lambda state: self.ht_unlocked(state, 2), + + RegionName.Tt: lambda state: self.tt_unlocked(state, 1), + RegionName.Tt2: lambda state: self.tt_unlocked(state, 2), + RegionName.Tt3: lambda state: self.tt_unlocked(state, 3), + + RegionName.Pr: lambda state: self.pr_unlocked(state, 1), + RegionName.Pr2: lambda state: self.pr_unlocked(state, 2), + + RegionName.Sp: lambda state: self.sp_unlocked(state, 1), + RegionName.Sp2: lambda state: self.sp_unlocked(state, 2), + + RegionName.Stt: lambda state: self.stt_unlocked(state, 1), + + RegionName.Dc: lambda state: self.dc_unlocked(state, 1), + RegionName.Tr: lambda state: self.dc_unlocked(state, 2), + # Terra is a fight and can have more than just this requirement. + # RegionName.Terra: lambda state:state.has(ItemName.ProofofConnection,self.player), + + RegionName.Hb: lambda state: self.hb_unlocked(state, 1), + RegionName.Hb2: lambda state: self.hb_unlocked(state, 2), + RegionName.Mushroom13: lambda state: state.has(ItemName.ProofofPeace, self.player), + + RegionName.Pl: lambda state: self.pl_unlocked(state, 1), + RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), + + RegionName.Ag: lambda state: self.ag_unlocked(state, 1), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2), + + RegionName.Bc: lambda state: self.bc_unlocked(state, 1), + RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), + + RegionName.AtlanticaSongThree: lambda state: self.at_three_unlocked(state), + RegionName.AtlanticaSongFour: lambda state: self.at_four_unlocked(state), + + RegionName.Ha1: lambda state: True, + RegionName.Ha2: lambda state: self.hundred_acre_unlocked(state, 1), + RegionName.Ha3: lambda state: self.hundred_acre_unlocked(state, 2), + RegionName.Ha4: lambda state: self.hundred_acre_unlocked(state, 3), + RegionName.Ha5: lambda state: self.hundred_acre_unlocked(state, 4), + RegionName.Ha6: lambda state: self.hundred_acre_unlocked(state, 5), + + RegionName.LevelsVS1: lambda state: self.level_locking_unlock(state, 1), + RegionName.LevelsVS3: lambda state: self.level_locking_unlock(state, 3), + RegionName.LevelsVS6: lambda state: self.level_locking_unlock(state, 6), + RegionName.LevelsVS9: lambda state: self.level_locking_unlock(state, 9), + RegionName.LevelsVS12: lambda state: self.level_locking_unlock(state, 12), + RegionName.LevelsVS15: lambda state: self.level_locking_unlock(state, 15), + RegionName.LevelsVS18: lambda state: self.level_locking_unlock(state, 18), + RegionName.LevelsVS21: lambda state: self.level_locking_unlock(state, 21), + RegionName.LevelsVS24: lambda state: self.level_locking_unlock(state, 24), + RegionName.LevelsVS26: lambda state: self.level_locking_unlock(state, 26), + } + + def set_kh2_rules(self) -> None: + for region_name, rules in self.region_rules.items(): + region = self.multiworld.get_region(region_name, self.player) + for entrance in region.entrances: + entrance.access_rule = rules + + self.set_kh2_goal() + + weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player) + for location in weapon_region.locations: + add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player)) + if location.name in Goofy_Checks: + add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) + elif location.name in Donald_Checks: + add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) + + def set_kh2_goal(self): + + final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) + if self.multiworld.Goal[self.player] == "three_proofs": + final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: self.kh2_has_all(three_proofs, state) + # lucky emblem hunt + elif self.multiworld.Goal[self.player] == "lucky_emblem_hunt": + final_xemnas_location.access_rule = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + # hitlist if == 2 + elif self.multiworld.Goal[self.player] == "hitlist": + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - world.completion_condition[player] = lambda state: state.kh_three_proof_unlocked(player) - # lucky emblem hunt - elif world.Goal[player] == "lucky_emblem_hunt": - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) - else: - world.completion_condition[player] = lambda state: state.kh_lucky_emblem_unlocked(player, world.LuckyEmblemsRequired[player].value) - # hitlist if == 2 - else: - add_rule(world.get_location(LocationName.FinalXemnas, player), - lambda state: state.kh_hitlist(player, world.BountyRequired[player].value)) - if world.FinalXemnas[player]: - world.completion_condition[player] = lambda state: state.kh_victory(player) - else: - world.completion_condition[player] = lambda state: state.kh_hitlist(player, world.BountyRequired[player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + if self.multiworld.FinalXemnas[self.player]: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) + else: + self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) - # Forbid Abilities on popups due to game limitations - for location in exclusion_table["Popups"]: - forbid_items(world.get_location(location, player), exclusionItem_table["Ability"]) - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) - for location in STT_Checks: - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) +class KH2FormRules(KH2Rules): + #: Dict[str, Callable[[CollectionState], bool]] + def __init__(self, world: KH2World) -> None: + super().__init__(world) + # access rules on where you can level a form. - # Santa's house also breaks with stat ups - for location in {LocationName.SantasHouseChristmasTownMap, LocationName.SantasHouseAPBoost}: - forbid_items(world.get_location(location, player), exclusionItem_table["StatUps"]) + self.form_rules = { + LocationName.Valorlvl2: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 0), + LocationName.Valorlvl3: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 1), + LocationName.Valorlvl4: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 2), + LocationName.Valorlvl5: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 3), + LocationName.Valorlvl6: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 4), + LocationName.Valorlvl7: lambda state: self.form_list_unlock(state, ItemName.ValorForm, 5), + LocationName.Wisdomlvl2: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 0), + LocationName.Wisdomlvl3: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 1), + LocationName.Wisdomlvl4: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 2), + LocationName.Wisdomlvl5: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 3), + LocationName.Wisdomlvl6: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 4), + LocationName.Wisdomlvl7: lambda state: self.form_list_unlock(state, ItemName.WisdomForm, 5), + LocationName.Limitlvl2: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 0), + LocationName.Limitlvl3: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 1), + LocationName.Limitlvl4: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 2), + LocationName.Limitlvl5: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 3), + LocationName.Limitlvl6: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 4), + LocationName.Limitlvl7: lambda state: self.form_list_unlock(state, ItemName.LimitForm, 5), + LocationName.Masterlvl2: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 0), + LocationName.Masterlvl3: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 1), + LocationName.Masterlvl4: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 2), + LocationName.Masterlvl5: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 3), + LocationName.Masterlvl6: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 4), + LocationName.Masterlvl7: lambda state: self.form_list_unlock(state, ItemName.MasterForm, 5), + LocationName.Finallvl2: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 0), + LocationName.Finallvl3: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 1), + LocationName.Finallvl4: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 2), + LocationName.Finallvl5: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 3), + LocationName.Finallvl6: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 4), + LocationName.Finallvl7: lambda state: self.form_list_unlock(state, ItemName.FinalForm, 5), + LocationName.Summonlvl2: lambda state: self.summon_levels_unlocked(state, 1), + LocationName.Summonlvl3: lambda state: self.summon_levels_unlocked(state, 1), + LocationName.Summonlvl4: lambda state: self.summon_levels_unlocked(state, 2), + LocationName.Summonlvl5: lambda state: self.summon_levels_unlocked(state, 3), + LocationName.Summonlvl6: lambda state: self.summon_levels_unlocked(state, 4), + LocationName.Summonlvl7: lambda state: self.summon_levels_unlocked(state, 4), + } + self.form_region_rules = { + RegionName.Valor: lambda state: self.multi_form_region_access(), + RegionName.Wisdom: lambda state: self.multi_form_region_access(), + RegionName.Limit: lambda state: self.limit_form_region_access(), + RegionName.Master: lambda state: self.multi_form_region_access(), + RegionName.Final: lambda state: self.final_form_region_access(state) + } - add_rule(world.get_location(LocationName.TransporttoRemembrance, player), - lambda state: state.kh_transport(player)) + def final_form_region_access(self, state: CollectionState) -> bool: + """ + Can reach one of TT3,Twtnw post Roxas, BC2, LoD2 or PR2 + """ + # tt3 start, can beat roxas, can beat gr2, can beat xaldin, can beat storm rider. + + return any( + self.multiworld.get_location(location, self.player).can_reach(state) for location in + final_leveling_access + ) + + @staticmethod + def limit_form_region_access() -> bool: + """ + returns true since twtnw always is open and has enemies + """ + return True + + @staticmethod + def multi_form_region_access() -> bool: + """ + returns true since twtnw always is open and has enemies + Valor, Wisdom and Master Form region access. + Note: This does not account for having the drive form. See form_list_unlock + """ + # todo: if boss enemy start the player with oc stone because of cerb + return True + + def set_kh2_form_rules(self): + for region_name in drive_form_list: + if region_name == RegionName.Summon and not self.world.options.SummonLevelLocationToggle: + continue + # could get the location of each of these, but I feel like that would be less optimal + region = self.multiworld.get_region(region_name, self.player) + # if region_name in form_region_rules + if region_name != RegionName.Summon: + for entrance in region.entrances: + entrance.access_rule = self.form_region_rules[region_name] + for loc in region.locations: + loc.access_rule = self.form_rules[loc.name] + + +class KH2FightRules(KH2Rules): + player: int + world: KH2World + region_rules: Dict[str, Callable[[CollectionState], bool]] + location_rules: Dict[str, Callable[[CollectionState], bool]] + + # cor logic + # have 3 things for the logic + # region:movement_rules and (fight_rules or skip rules) + # if skip rules are of return false + def __init__(self, world: KH2World) -> None: + super().__init__(world) + self.fight_logic = self.multiworld.FightLogic[self.player].current_key + + self.fight_region_rules = { + RegionName.ShanYu: lambda state: self.get_shan_yu_rules(state), + RegionName.AnsemRiku: lambda state: self.get_ansem_riku_rules(state), + RegionName.StormRider: lambda state: self.get_storm_rider_rules(state), + RegionName.DataXigbar: lambda state: self.get_data_xigbar_rules(state), + RegionName.TwinLords: lambda state: self.get_fire_lord_rules(state) and self.get_blizzard_lord_rules(state), + RegionName.GenieJafar: lambda state: self.get_genie_jafar_rules(state), + RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state), + RegionName.OldPete: lambda state: self.get_old_pete_rules(), + RegionName.FuturePete: lambda state: self.get_future_pete_rules(state), + RegionName.Terra: lambda state: self.get_terra_rules(state), + RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state), + RegionName.Barbosa: lambda state: self.get_barbosa_rules(state), + RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(), + RegionName.GrimReaper2: lambda state: self.get_grim_reaper2_rules(state), + RegionName.DataLuxord: lambda state: self.get_data_luxord_rules(state), + RegionName.Cerberus: lambda state: self.get_cerberus_rules(state), + RegionName.OlympusPete: lambda state: self.get_olympus_pete_rules(state), + RegionName.Hydra: lambda state: self.get_hydra_rules(state), + RegionName.Hades: lambda state: self.get_hades_rules(state), + RegionName.DataZexion: lambda state: self.get_data_zexion_rules(state), + RegionName.OcPainAndPanicCup: lambda state: self.get_pain_and_panic_cup_rules(state), + RegionName.OcCerberusCup: lambda state: self.get_cerberus_cup_rules(state), + RegionName.Oc2TitanCup: lambda state: self.get_titan_cup_rules(state), + RegionName.Oc2GofCup: lambda state: self.get_goddess_of_fate_cup_rules(state), + RegionName.HadesCups: lambda state: self.get_hades_cup_rules(state), + RegionName.Thresholder: lambda state: self.get_thresholder_rules(state), + RegionName.Beast: lambda state: self.get_beast_rules(), + RegionName.DarkThorn: lambda state: self.get_dark_thorn_rules(state), + RegionName.Xaldin: lambda state: self.get_xaldin_rules(state), + RegionName.DataXaldin: lambda state: self.get_data_xaldin_rules(state), + RegionName.HostileProgram: lambda state: self.get_hostile_program_rules(state), + RegionName.Mcp: lambda state: self.get_mcp_rules(state), + RegionName.DataLarxene: lambda state: self.get_data_larxene_rules(state), + RegionName.PrisonKeeper: lambda state: self.get_prison_keeper_rules(state), + RegionName.OogieBoogie: lambda state: self.get_oogie_rules(), + RegionName.Experiment: lambda state: self.get_experiment_rules(state), + RegionName.DataVexen: lambda state: self.get_data_vexen_rules(state), + RegionName.HBDemyx: lambda state: self.get_demyx_rules(state), + RegionName.ThousandHeartless: lambda state: self.get_thousand_heartless_rules(state), + RegionName.DataDemyx: lambda state: self.get_data_demyx_rules(state), + RegionName.Sephi: lambda state: self.get_sephiroth_rules(state), + RegionName.CorFirstFight: lambda state: self.get_cor_first_fight_movement_rules(state) and (self.get_cor_first_fight_rules(state) or self.get_cor_skip_first_rules(state)), + RegionName.CorSecondFight: lambda state: self.get_cor_second_fight_movement_rules(state), + RegionName.Transport: lambda state: self.get_transport_movement_rules(state), + RegionName.Scar: lambda state: self.get_scar_rules(state), + RegionName.GroundShaker: lambda state: self.get_groundshaker_rules(state), + RegionName.DataSaix: lambda state: self.get_data_saix_rules(state), + RegionName.TwilightThorn: lambda state: self.get_twilight_thorn_rules(), + RegionName.Axel1: lambda state: self.get_axel_one_rules(), + RegionName.Axel2: lambda state: self.get_axel_two_rules(), + RegionName.DataRoxas: lambda state: self.get_data_roxas_rules(state), + RegionName.DataAxel: lambda state: self.get_data_axel_rules(state), + RegionName.Roxas: lambda state: self.get_roxas_rules(state) and self.twtnw_unlocked(state, 1), + RegionName.Xigbar: lambda state: self.get_xigbar_rules(state), + RegionName.Luxord: lambda state: self.get_luxord_rules(state), + RegionName.Saix: lambda state: self.get_saix_rules(state), + RegionName.Xemnas: lambda state: self.get_xemnas_rules(state), + RegionName.ArmoredXemnas: lambda state: self.get_armored_xemnas_one_rules(state), + RegionName.ArmoredXemnas2: lambda state: self.get_armored_xemnas_two_rules(state), + RegionName.FinalXemnas: lambda state: self.get_final_xemnas_rules(state), + RegionName.DataXemnas: lambda state: self.get_data_xemnas_rules(state), + } + + def set_kh2_fight_rules(self) -> None: + for region_name, rules in self.fight_region_rules.items(): + region = self.multiworld.get_region(region_name, self.player) + for entrance in region.entrances: + entrance.access_rule = rules + + for loc_name in [LocationName.TransportEventLocation, LocationName.TransporttoRemembrance]: + location = self.multiworld.get_location(loc_name, self.player) + add_rule(location, lambda state: self.get_transport_fight_rules(state)) + + def get_shan_yu_rules(self, state: CollectionState) -> bool: + # easy: gap closer, defensive tool,drive form + # normal: 2 out of easy + # hard: defensive tool or drive form + shan_yu_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 1 + } + return shan_yu_rules[self.fight_logic] + + def get_ansem_riku_rules(self, state: CollectionState) -> bool: + # easy: gap closer,defensive tool,ground finisher/limit form + # normal: defensive tool and (gap closer/ground finisher/limit form) + # hard: defensive tool or limit form + ansem_riku_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, [ItemName.LimitForm], ground_finisher], state) >= 2, + "hard": self.kh2_has_any([ItemName.ReflectElement, ItemName.Guard, ItemName.LimitForm], state), + } + return ansem_riku_rules[self.fight_logic] + + def get_storm_rider_rules(self, state: CollectionState) -> bool: + # easy: has defensive tool,drive form, party limit,aerial move + # normal: has 3 of those things + # hard: has 2 of those things + storm_rider_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 4, + "normal": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 3, + "hard": self.kh2_list_any_sum([defensive_tool, party_limit, aerial_move, form_list], state) >= 2, + } + return storm_rider_rules[self.fight_logic] + + def get_data_xigbar_rules(self, state: CollectionState) -> bool: + # easy:final 7,firaga,2 air combo plus,air gap closer, finishing plus,guard,reflega,horizontal slash,donald limit + # normal:final 7,firaga,finishing plus,guard,reflect horizontal slash,donald limit + # hard:((final 5, fira) or donald limit), finishing plus,guard/reflect + data_xigbar_rules = { + "easy": self.kh2_dict_count(easy_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state), + "normal": self.kh2_dict_count(normal_data_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_has_any(donald_limit, state), + "hard": ((self.form_list_unlock(state, ItemName.FinalForm, 3, True) and state.has(ItemName.FireElement, self.player, 2)) or self.kh2_has_any(donald_limit, state)) + and state.has(ItemName.FinishingPlus, self.player) and self.kh2_has_any(defensive_tool, state) + } + return data_xigbar_rules[self.fight_logic] + + def get_fire_lord_rules(self, state: CollectionState) -> bool: + # easy: drive form,defensive tool,one black magic,party limit + # normal: 3 of those things + # hard:2 of those things + # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split. + fire_lords_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2, + } + return fire_lords_rules[self.fight_logic] + + def get_blizzard_lord_rules(self, state: CollectionState) -> bool: + # easy: drive form,defensive tool,one black magic,party limit + # normal: 3 of those things + # hard:2 of those things + # duplicate of the other because in boss rando there will be to bosses in arena and these bosses can be split. + blizzard_lords_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, black_magic, party_limit], state) >= 2, + } + return blizzard_lords_rules[self.fight_logic] + + def get_genie_jafar_rules(self, state: CollectionState) -> bool: + # easy: defensive tool,black magic,ground finisher,finishing plus + # normal: defensive tool, ground finisher,finishing plus + # hard: defensive tool,finishing plus + genie_jafar_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, black_magic, ground_finisher, {ItemName.FinishingPlus}], state) >= 4, + "normal": self.kh2_list_any_sum([defensive_tool, ground_finisher, {ItemName.FinishingPlus}], state) >= 3, + "hard": self.kh2_list_any_sum([defensive_tool, {ItemName.FinishingPlus}], state) >= 2, + } + return genie_jafar_rules[self.fight_logic] + + def get_data_lexaeus_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,final 7,firaga,reflera,donald limit, guard + # normal:one gap closer,final 5,fira,reflect, donald limit,guard + # hard:defensive tool,gap closer + data_lexaues_rules = { + "easy": self.kh2_dict_count(easy_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_lex_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer], state) >= 2, + } + return data_lexaues_rules[self.fight_logic] + + @staticmethod + def get_old_pete_rules(): + # fight is free. + return True + + def get_future_pete_rules(self, state: CollectionState) -> bool: + # easy:defensive option,gap closer,drive form + # norma:2 of those things + # hard 1 of those things + future_pete_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, form_list], state) >= 1, + } + return future_pete_rules[self.fight_logic] + + def get_data_marluxia_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,final 7,firaga,reflera,donald limit, guard + # normal:one gap closer,final 5,fira,reflect, donald limit,guard + # hard:defensive tool,gap closer + data_marluxia_rules = { + "easy": self.kh2_dict_count(easy_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_marluxia_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, gap_closer, [ItemName.AerialRecovery]], state) >= 3, + } + return data_marluxia_rules[self.fight_logic] + + def get_terra_rules(self, state: CollectionState) -> bool: + # easy:scom,gap closers,explosion,2 combo pluses,final 7,firaga, donald limits,reflect,guard,3 dodge roll,3 aerial dodge and 3glide + # normal:gap closers,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard,donald limit, guard + # hard:1 gap closer,explosion,2 combo pluses,2 dodge roll,2 aerial dodge and lvl 2glide,guard + terra_rules = { + "easy": self.kh2_dict_count(easy_terra_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_terra_tools, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "hard": self.kh2_dict_count(hard_terra_tools, state) and self.kh2_list_any_sum([gap_closer], state) >= 1, + } + return terra_rules[self.fight_logic] + + def get_barbosa_rules(self, state: CollectionState) -> bool: + # easy:blizzara and thundara or one of each,defensive tool + # normal:(blizzard or thunder) and defensive tool + # hard: defensive tool + barbosa_rules = { + "easy": self.kh2_list_count_sum([ItemName.BlizzardElement, ItemName.ThunderElement], state) >= 2 and self.kh2_list_any_sum([defensive_tool], state) >= 1, + "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.BlizzardElement, ItemName.ThunderElement}], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return barbosa_rules[self.fight_logic] + + @staticmethod + def get_grim_reaper1_rules(): + # fight is free. + return True + + def get_grim_reaper2_rules(self, state: CollectionState) -> bool: + # easy:master form,thunder,defensive option + # normal:master form/stitch,thunder,defensive option + # hard:any black magic,defensive option. + gr2_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.ThunderElement}], state) >= 2, + "normal": self.kh2_list_any_sum([defensive_tool, {ItemName.MasterForm, ItemName.Stitch}, {ItemName.ThunderElement}], state) >= 3, + "hard": self.kh2_list_any_sum([black_magic, defensive_tool], state) >= 2 + } + return gr2_rules[self.fight_logic] + + def get_data_luxord_rules(self, state: CollectionState) -> bool: + # easy:gap closers,reflega,aerial dodge lvl 2,glide lvl 2,guard + # normal:1 gap closer,reflect,aerial dodge lvl 1,glide lvl 1,guard + # hard:quick run,defensive option + data_luxord_rules = { + "easy": self.kh2_dict_count(easy_data_luxord_tools, state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.AerialDodge, ItemName.Glide, ItemName.Guard], state) and self.kh2_has_any(defensive_tool, state), + "hard": self.kh2_list_any_sum([{ItemName.QuickRun}, defensive_tool], state) + } + return data_luxord_rules[self.fight_logic] + + def get_cerberus_rules(self, state: CollectionState) -> bool: + # easy,normal:defensive option, offensive magic + # hard:defensive option + cerberus_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2, + "normal": self.kh2_list_any_sum([defensive_tool, black_magic], state) >= 2, + "hard": self.kh2_has_any(defensive_tool, state), + } + return cerberus_rules[self.fight_logic] + + def get_pain_and_panic_cup_rules(self, state: CollectionState) -> bool: + # easy:2 party limit,reflect + # normal:1 party limit,reflect + # hard:reflect + pain_and_panic_rules = { + "easy": self.kh2_list_count_sum(party_limit, state) >= 2 and state.has(ItemName.ReflectElement, self.player), + "normal": self.kh2_list_count_sum(party_limit, state) >= 1 and state.has(ItemName.ReflectElement, self.player), + "hard": state.has(ItemName.ReflectElement, self.player) + } + return pain_and_panic_rules[self.fight_logic] and (self.kh2_has_all([ItemName.FuturePeteEvent], state) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_cerberus_cup_rules(self, state: CollectionState) -> bool: + # easy:3 drive forms,reflect + # normal:2 drive forms,reflect + # hard:reflect + cerberus_cup_rules = { + "easy": self.kh2_can_reach_any([LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5, LocationName.Finallvl5], state) and state.has(ItemName.ReflectElement, self.player), + "normal": self.kh2_can_reach_any([LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4, LocationName.Finallvl4], state) and state.has(ItemName.ReflectElement, self.player), + "hard": state.has(ItemName.ReflectElement, self.player) + } + return cerberus_cup_rules[self.fight_logic] and (self.kh2_has_all([ItemName.ScarEvent, ItemName.OogieBoogieEvent, ItemName.TwinLordsEvent], state) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_titan_cup_rules(self, state: CollectionState) -> bool: + # easy:4 summons,reflera + # normal:4 summons,reflera + # hard:2 summons,reflera + titan_cup_rules = { + "easy": self.kh2_list_count_sum(summons, state) >= 4 and state.has(ItemName.ReflectElement, self.player, 2), + "normal": self.kh2_list_count_sum(summons, state) >= 3 and state.has(ItemName.ReflectElement, self.player, 2), + "hard": self.kh2_list_count_sum(summons, state) >= 2 and state.has(ItemName.ReflectElement, self.player, 2), + } + return titan_cup_rules[self.fight_logic] and (state.has(ItemName.HadesEvent, self.player) or state.has(ItemName.HadesCupTrophy, self.player)) + + def get_goddess_of_fate_cup_rules(self, state: CollectionState) -> bool: + # can beat all the other cups+xemnas 1 + return self.kh2_has_all([ItemName.OcPainAndPanicCupEvent, ItemName.OcCerberusCupEvent, ItemName.Oc2TitanCupEvent], state) + + def get_hades_cup_rules(self, state: CollectionState) -> bool: + # can beat goddess of fate cup + return state.has(ItemName.Oc2GofCupEvent, self.player) + + def get_olympus_pete_rules(self, state: CollectionState) -> bool: + # easy:gap closer,defensive option,drive form + # normal:2 of those things + # hard:1 of those things + olympus_pete_rules = { + "easy": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([gap_closer, defensive_tool, form_list], state) >= 1, + } + return olympus_pete_rules[self.fight_logic] + + def get_hydra_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive option,offensive magic + # normal 2 of those things + # hard: one of those things + hydra_rules = { + "easy": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 3, + "normal": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([black_magic, defensive_tool, form_list], state) >= 1, + } + return hydra_rules[self.fight_logic] + + def get_hades_rules(self, state: CollectionState) -> bool: + # easy:drive form,summon,gap closer,defensive option + # normal:3 of those things + # hard:2 of those things + hades_rules = { + "easy": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 3, + "hard": self.kh2_list_any_sum([gap_closer, summons, defensive_tool, form_list], state) >= 2, + } + return hades_rules[self.fight_logic] + + def get_data_zexion_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega ,guard,2 gap closers,quick run level 3 + # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,quick run level 3 + # hard:final 5,fira, donald limit, reflect,gap closer,quick run level 2 + data_zexion_rules = { + "easy": self.kh2_dict_count(easy_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_zexion, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + } + return data_zexion_rules[self.fight_logic] + + def get_thresholder_rules(self, state: CollectionState) -> bool: + # easy:drive form,black magic,defensive tool + # normal:2 of those things + # hard:defensive tool or drive form + thresholder_rules = { + "easy": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 3, + "normal": self.kh2_list_any_sum([form_list, black_magic, defensive_tool], state) >= 2, + "hard": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 1, + } + return thresholder_rules[self.fight_logic] + + @staticmethod + def get_beast_rules(): + # fight is free + return True + + def get_dark_thorn_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive tool,gap closer + # normal:drive form,defensive tool + # hard:defensive tool + dark_thorn_rules = { + "easy": self.kh2_list_any_sum([form_list, gap_closer, defensive_tool], state) >= 3, + "normal": self.kh2_list_any_sum([form_list, defensive_tool], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return dark_thorn_rules[self.fight_logic] + + def get_xaldin_rules(self, state: CollectionState) -> bool: + # easy:guard,2 aerial modifier,valor/master/final + # normal:guard,1 aerial modifier + # hard:guard + xaldin_rules = { + "easy": self.kh2_list_any_sum([[ItemName.Guard], [ItemName.ValorForm, ItemName.MasterForm, ItemName.FinalForm]], state) >= 2 and self.kh2_list_count_sum(aerial_move, state) >= 2, + "normal": self.kh2_list_any_sum([aerial_move], state) >= 1 and state.has(ItemName.Guard, self.player), + "hard": state.has(ItemName.Guard, self.player), + } + return xaldin_rules[self.fight_logic] + + def get_data_xaldin_rules(self, state: CollectionState) -> bool: + # easy:final 7,firaga,2 air combo plus, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash,berserk charge + # normal:final 7,firaga, finishing plus,guard,reflega,donald limit,high jump aerial dodge glide lvl 3,magnet,aerial dive,aerial spiral,hori slash + # hard:final 5, fira, party limit, finishing plus,guard,high jump aerial dodge glide lvl 2,magnet,aerial dive + data_xaldin_rules = { + "easy": self.kh2_dict_count(easy_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_xaldin, state) and self.form_list_unlock(state, ItemName.FinalForm, 3, True) and self.kh2_has_any(party_limit, state), + } + return data_xaldin_rules[self.fight_logic] + + def get_hostile_program_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,drive form,summon + # normal:3 of those things + # hard: 2 of those things + hostile_program_rules = { + "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3, + "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2, + } + return hostile_program_rules[self.fight_logic] + + def get_mcp_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,drive form,summon + # normal:3 of those things + # hard: 2 of those things + mcp_rules = { + "easy": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 3, + "hard": self.kh2_list_any_sum([donald_limit, form_list, summons, {ItemName.ReflectElement}], state) >= 2, + } + return mcp_rules[self.fight_logic] + + def get_data_larxene_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3 + # normal:final 7,firaga, donald limit, Reflega ,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3 + # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2 + data_larxene_rules = { + "easy": self.kh2_dict_count(easy_data_larxene, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_larxene, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_larxene, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True), + } + return data_larxene_rules[self.fight_logic] + + def get_prison_keeper_rules(self, state: CollectionState) -> bool: + # easy:defensive tool,drive form, party limit + # normal:two of those things + # hard:one of those things + prison_keeper_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 1, + } + return prison_keeper_rules[self.fight_logic] + + @staticmethod + def get_oogie_rules(): + # fight is free + return True + + def get_experiment_rules(self, state: CollectionState) -> bool: + # easy:drive form,defensive tool,summon,party limit + # normal:3 of those things + # hard 2 of those things + experiment_rules = { + "easy": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 4, + "normal": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 3, + "hard": self.kh2_list_any_sum([form_list, defensive_tool, party_limit, summons], state) >= 2, + } + return experiment_rules[self.fight_logic] + + def get_data_vexen_rules(self, state: CollectionState) -> bool: + # easy: final 7,firaga,scom,both donald limits, Reflega,guard,2 gap closers,2 ground finishers,aerial dodge 3,glide 3,dodge roll 3,quick run 3 + # normal:final 7,firaga, donald limit, Reflega,guard,1 gap closers,1 ground finisher,aerial dodge 3,glide 3,dodge roll 3,quick run 3 + # hard:final 5,fira, donald limit, reflect,gap closer,aerial dodge 2,glide 2,dodge roll 2,quick run 2 + data_vexen_rules = { + "easy": self.kh2_dict_count(easy_data_vexen, state) and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "normal": self.kh2_dict_count(normal_data_vexen, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5, True), + "hard": self.kh2_dict_count(hard_data_vexen, state) and self.kh2_list_any_sum([gap_closer, donald_limit], state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 3, True), + } + return data_vexen_rules[self.fight_logic] + + def get_demyx_rules(self, state: CollectionState) -> bool: + # defensive option,drive form,party limit + # defensive option,drive form + # defensive option + demyx_rules = { + "easy": self.kh2_list_any_sum([defensive_tool, form_list, party_limit], state) >= 3, + "normal": self.kh2_list_any_sum([defensive_tool, form_list], state) >= 2, + "hard": self.kh2_list_any_sum([defensive_tool], state) >= 1, + } + return demyx_rules[self.fight_logic] + + def get_thousand_heartless_rules(self, state: CollectionState) -> bool: + # easy:scom,limit form,guard,magnera + # normal:limit form, guard + # hard:guard + thousand_heartless_rules = { + "easy": self.kh2_dict_count(easy_thousand_heartless_rules, state), + "normal": self.kh2_dict_count(normal_thousand_heartless_rules, state), + "hard": state.has(ItemName.Guard, self.player), + } + return thousand_heartless_rules[self.fight_logic] + + def get_data_demyx_rules(self, state: CollectionState) -> bool: + # easy:wisdom 7,1 form boosts,reflera,firaga,duck flare,guard,scom,finishing plus + # normal:remove form boost and scom + # hard:wisdom 6,reflect,guard,duck flare,fira,finishing plus + data_demyx_rules = { + "easy": self.kh2_dict_count(easy_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True), + "normal": self.kh2_dict_count(normal_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 5, True), + "hard": self.kh2_dict_count(hard_data_demyx, state) and self.form_list_unlock(state, ItemName.WisdomForm, 4, True), + } + return data_demyx_rules[self.fight_logic] + + def get_sephiroth_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus + sephiroth_rules = { + "easy": self.kh2_dict_count(easy_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_sephiroth_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_sephiroth_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + } + return sephiroth_rules[self.fight_logic] + + def get_cor_first_fight_movement_rules(self, state: CollectionState) -> bool: + # easy: quick run 3 or wisdom 5 (wisdom has qr 3) + # normal: quick run 2 and aerial dodge 1 or wisdom 5 (wisdom has qr 3) + # hard: (quick run 1, aerial dodge 1) or (wisdom form and aerial dodge 1) + cor_first_fight_movement_rules = { + "easy": state.has(ItemName.QuickRun, self.player, 3) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True), + "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 1}, state) or self.form_list_unlock(state, ItemName.WisdomForm, 3, True), + "hard": self.kh2_has_all([ItemName.AerialDodge, ItemName.QuickRun], state) or self.kh2_has_all([ItemName.AerialDodge, ItemName.WisdomForm], state), + } + return cor_first_fight_movement_rules[self.fight_logic] + + def get_cor_first_fight_rules(self, state: CollectionState) -> bool: + # easy:have 5 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara) + # normal:have 3 of these things (reflega,stitch and chicken,final form,magnera,explosion,thundara) + # hard: reflect,stitch or chicken,final form + cor_first_fight_rules = { + "easy": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 5 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 4 and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + "normal": self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 3 or self.kh2_dict_one_count(not_hard_cor_tools_dict, state) >= 2 and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + "hard": state.has(ItemName.ReflectElement, self.player) and self.kh2_has_any([ItemName.Stitch, ItemName.ChickenLittle], state) and self.form_list_unlock(state, ItemName.FinalForm, 1, True), + } + return cor_first_fight_rules[self.fight_logic] + + def get_cor_skip_first_rules(self, state: CollectionState) -> bool: + # if option is not allow skips return false else run rules + if not self.multiworld.CorSkipToggle[self.player]: + return False + # easy: aerial dodge 3,master form,fire + # normal: aerial dodge 2, master form,fire + # hard:void cross(quick run 3,aerial dodge 1) + # or (quick run 2,aerial dodge 2 and magic) + # or (final form and (magic or combo master)) + # or (master form and (reflect or fire or thunder or combo master) + # wall rise(aerial dodge 1 and (final form lvl 3 or glide 2) or (master form and (1 of black magic or combo master) + void_cross_rules = { + "easy": state.has(ItemName.AerialDodge, self.player, 3) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state), + "normal": state.has(ItemName.AerialDodge, self.player, 2) and self.kh2_has_all([ItemName.MasterForm, ItemName.FireElement], state), + "hard": (self.kh2_dict_count({ItemName.QuickRun: 3, ItemName.AerialDodge: 1}, state)) \ + or (self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.FinalForm, self.player) and (self.kh2_has_any(magic, state) or state.has(ItemName.ComboMaster, self.player))) \ + or (state.has(ItemName.MasterForm, self.player) and (self.kh2_has_any([ItemName.ReflectElement, ItemName.FireElement, ItemName.ComboMaster], state))) + } + wall_rise_rules = { + "easy": True, + "normal": True, + "hard": state.has(ItemName.AerialDodge, self.player) and (self.form_list_unlock(state, ItemName.FinalForm, 1, True) or state.has(ItemName.Glide, self.player, 2)) + } + return void_cross_rules[self.fight_logic] and wall_rise_rules[self.fight_logic] + + def get_cor_second_fight_movement_rules(self, state: CollectionState) -> bool: + # easy: quick run 2, aerial dodge 3 or master form 5 + # normal: quick run 2, aerial dodge 2 or master 5 + # hard: (glide 1,aerial dodge 1 any magic) or (master 3 any magic) or glide 1 and aerial dodge 2 + + cor_second_fight_movement_rules = { + "easy": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 3}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True), + "normal": self.kh2_dict_count({ItemName.QuickRun: 2, ItemName.AerialDodge: 2}, state) or self.form_list_unlock(state, ItemName.MasterForm, 3, True), + "hard": (self.kh2_has_all([ItemName.Glide, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.MasterForm, self.player) and self.kh2_has_any(magic, state)) \ + or (state.has(ItemName.Glide, self.player) and state.has(ItemName.AerialDodge, self.player, 2)), + } + return cor_second_fight_movement_rules[self.fight_logic] + + def get_transport_fight_rules(self, state: CollectionState) -> bool: + # easy: reflega,stitch and chicken,final form,magnera,explosion,finishing leap,thundaga,2 donald limits + # normal: 7 of those things + # hard: 5 of those things + transport_fight_rules = { + "easy": self.kh2_dict_count(transport_tools_dict, state), + "normal": self.kh2_dict_one_count(transport_tools_dict, state) >= 7, + "hard": self.kh2_dict_one_count(transport_tools_dict, state) >= 5, + } + return transport_fight_rules[self.fight_logic] + + def get_transport_movement_rules(self, state: CollectionState) -> bool: + # easy:high jump 3,aerial dodge 3,glide 3 + # normal: high jump 2,glide 3,aerial dodge 2 + # hard: (hj 2,glide 2,ad 1,any magic) or hj 1,glide 2,ad 3 any magic or (any magic master form,ad) or hj lvl 1,glide 3,ad 1 + transport_movement_rules = { + "easy": self.kh2_dict_count({ItemName.HighJump: 3, ItemName.AerialDodge: 3, ItemName.Glide: 3}, state), + "normal": self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 2, ItemName.Glide: 3}, state), + "hard": (self.kh2_dict_count({ItemName.HighJump: 2, ItemName.AerialDodge: 1, ItemName.Glide: 2}, state) and self.kh2_has_any(magic, state)) \ + or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 2, ItemName.AerialDodge: 3}, state) and self.kh2_has_any(magic, state)) \ + or (self.kh2_dict_count({ItemName.HighJump: 1, ItemName.Glide: 3, ItemName.AerialDodge: 1}, state)) \ + or (self.kh2_has_all([ItemName.MasterForm, ItemName.AerialDodge], state) and self.kh2_has_any(magic, state)), + } + return transport_movement_rules[self.fight_logic] + + def get_scar_rules(self, state: CollectionState) -> bool: + # easy: reflect,thunder,fire + # normal:reflect,fire + # hard:reflect + scar_rules = { + "easy": self.kh2_has_all([ItemName.ReflectElement, ItemName.ThunderElement, ItemName.FireElement], state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FireElement], state), + "hard": state.has(ItemName.ReflectElement, self.player), + } + return scar_rules[self.fight_logic] + + def get_groundshaker_rules(self, state: CollectionState) -> bool: + # easy:berserk charge,cure,2 air combo plus,reflect + # normal:berserk charge,reflect,cure + # hard:berserk charge or 2 air combo plus. reflect + groundshaker_rules = { + "easy": state.has(ItemName.AirComboPlus, self.player, 2) and self.kh2_has_all([ItemName.BerserkCharge, ItemName.CureElement, ItemName.ReflectElement], state), + "normal": self.kh2_has_all([ItemName.BerserkCharge, ItemName.ReflectElement, ItemName.CureElement], state), + "hard": (state.has(ItemName.BerserkCharge, self.player) or state.has(ItemName.AirComboPlus, self.player, 2)) and state.has(ItemName.ReflectElement, self.player), + } + return groundshaker_rules[self.fight_logic] + + def get_data_saix_rules(self, state: CollectionState) -> bool: + # easy:guard,2 gap closers,thunder,blizzard,2 donald limit,reflega,2 ground finisher,aerial dodge 3,glide 3,final 7,firaga,scom + # normal:guard,1 gap closers,thunder,blizzard,1 donald limit,reflega,1 ground finisher,aerial dodge 3,glide 3,final 7,firaga + # hard:aerial dodge 3,glide 3,guard,reflect,blizzard,1 gap closer,1 ground finisher + easy_data_rules = { + "easy": self.kh2_dict_count(easy_data_saix, state) and self.form_list_unlock(state, ItemName.FinalForm, 5), + "normal": self.kh2_dict_count(normal_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher, donald_limit], state) >= 3 and self.form_list_unlock(state, ItemName.FinalForm, 5), + "hard": self.kh2_dict_count(hard_data_saix, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return easy_data_rules[self.fight_logic] + + @staticmethod + def get_twilight_thorn_rules() -> bool: + return True + + @staticmethod + def get_axel_one_rules() -> bool: + return True + + @staticmethod + def get_axel_two_rules() -> bool: + return True + + def get_data_roxas_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus + data_roxas_rules = { + "easy": self.kh2_dict_count(easy_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_roxas_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_roxas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return data_roxas_rules[self.fight_logic] + + def get_data_axel_rules(self, state: CollectionState) -> bool: + # easy:both gap closers,limit 5,reflega,guard,both 2 ground finishers,3 dodge roll,finishing plus,scom,blizzaga + # normal:both gap closers,limit 5,reflera,guard,both 2 ground finishers,3 dodge roll,finishing plus,blizzaga + # hard:1 gap closers,reflect, guard,both 1 ground finisher,2 dodge roll,finishing plus,blizzara + data_axel_rules = { + "easy": self.kh2_dict_count(easy_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit], state) >= 1, + "normal": self.kh2_dict_count(normal_data_axel_tools, state) and self.kh2_can_reach(LocationName.Limitlvl5, state) and self.kh2_list_any_sum([donald_limit, gap_closer], state) >= 2, + "hard": self.kh2_dict_count(hard_data_axel_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2 + } + return data_axel_rules[self.fight_logic] + + def get_roxas_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1, limit form,thunder,reflera,guard break,2 gap closers,finishing plus,blizzard + # normal:thunder,reflera,guard break,2 gap closers,finishing plus,blizzard + # hard:guard + roxas_rules = { + "easy": self.kh2_dict_count(easy_roxas_tools, state), + "normal": self.kh2_dict_count(normal_roxas_tools, state), + "hard": state.has(ItemName.Guard, self.player), + } + return roxas_rules[self.fight_logic] + + def get_xigbar_rules(self, state: CollectionState) -> bool: + # easy:final 4,horizontal slash,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect + # normal:final 4,fira,finishing plus,glide 2,aerial dodge 2,quick run 2,guard,reflect + # hard:guard,quick run,finishing plus + xigbar_rules = { + "easy": self.kh2_dict_count(easy_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1) and self.kh2_has_any([ItemName.LightDarkness, ItemName.FinalForm], state), + "normal": self.kh2_dict_count(normal_xigbar_tools, state) and self.form_list_unlock(state, ItemName.FinalForm, 1), + "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun, ItemName.FinishingPlus], state), + } + return xigbar_rules[self.fight_logic] + + def get_luxord_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:quick run,guard + luxord_rules = { + "easy": self.kh2_dict_count(easy_luxord_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_luxord_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard, ItemName.QuickRun], state) + } + return luxord_rules[self.fight_logic] + + def get_saix_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:,guard + + saix_rules = { + "easy": self.kh2_dict_count(easy_saix_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_saix_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard], state) + } + return saix_rules[self.fight_logic] + + def get_xemnas_rules(self, state: CollectionState) -> bool: + # easy:aerial dodge 1,glide 1,quickrun 2,guard,reflera,2 gap closers,ground finisher,limit form + # normal:aerial dodge 1,glide 1,quickrun 2,guard,reflera,1 gap closers,ground finisher + # hard:,guard + xemnas_rules = { + "easy": self.kh2_dict_count(easy_xemnas_tools, state) and self.kh2_has_any(ground_finisher, state), + "normal": self.kh2_dict_count(normal_xemnas_tools, state) and self.kh2_list_any_sum([gap_closer, ground_finisher], state) >= 2, + "hard": self.kh2_has_all([ItemName.Guard], state) + } + return xemnas_rules[self.fight_logic] + + def get_armored_xemnas_one_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,1 gap closer,ground finisher + # normal:reflect,gap closer,ground finisher + # hard:reflect + armored_xemnas_one_rules = { + "easy": self.kh2_list_any_sum([donald_limit, gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3, + "hard": state.has(ItemName.ReflectElement, self.player), + } + return armored_xemnas_one_rules[self.fight_logic] + + def get_armored_xemnas_two_rules(self, state: CollectionState) -> bool: + # easy:donald limit,reflect,1 gap closer,ground finisher + # normal:reflect,gap closer,ground finisher + # hard:reflect + armored_xemnas_two_rules = { + "easy": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}, {ItemName.ThunderElement}], state) >= 4, + "normal": self.kh2_list_any_sum([gap_closer, ground_finisher, {ItemName.ReflectElement}], state) >= 3, + "hard": state.has(ItemName.ReflectElement, self.player), + } + return armored_xemnas_two_rules[self.fight_logic] + + def get_final_xemnas_rules(self, state: CollectionState) -> bool: + # easy:reflera,limit form,finishing plus,gap closer,guard + # normal:reflect,finishing plus,guard + # hard:guard + final_xemnas_rules = { + "easy": self.kh2_has_all([ItemName.LimitForm, ItemName.FinishingPlus, ItemName.Guard], state) and state.has(ItemName.ReflectElement, self.player, 2) and self.kh2_has_any(gap_closer, state), + "normal": self.kh2_has_all([ItemName.ReflectElement, ItemName.FinishingPlus, ItemName.Guard], state), + "hard": state.has(ItemName.Guard, self.player), + } + return final_xemnas_rules[self.fight_logic] + + def get_data_xemnas_rules(self, state: CollectionState) -> bool: + # easy:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5,scom,trinity limit + # normal:combo master,slapshot,reflega,2 ground finishers,both gap closers,finishing plus,guard,limit 5, + # hard:combo master,slapshot,reflera,1 ground finishers,1 gap closers,finishing plus,guard,limit form + data_xemnas_rules = { + "easy": self.kh2_dict_count(easy_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state), + "normal": self.kh2_dict_count(normal_data_xemnas, state) and self.kh2_list_count_sum(ground_finisher, state) >= 2 and self.kh2_can_reach(LocationName.Limitlvl5, state), + "hard": self.kh2_dict_count(hard_data_xemnas, state) and self.kh2_list_any_sum([ground_finisher, gap_closer], state) >= 2 + } + return data_xemnas_rules[self.fight_logic] diff --git a/worlds/kh2/WorldLocations.py b/worlds/kh2/WorldLocations.py index 172874c2b7..6df18fc800 100644 --- a/worlds/kh2/WorldLocations.py +++ b/worlds/kh2/WorldLocations.py @@ -96,6 +96,10 @@ DC_Checks = { LocationName.LingeringWillBonus: WorldLocationData(0x370C, 6), LocationName.LingeringWillProofofConnection: WorldLocationData(0x370C, 6), LocationName.LingeringWillManifestIllusion: WorldLocationData(0x370C, 6), + + 'Lingering Will Bonus: Sora Slot 1': WorldLocationData(14092, 6), + 'Lingering Will Proof of Connection': WorldLocationData(14092, 6), + 'Lingering Will Manifest Illusion': WorldLocationData(14092, 6), } TR_Checks = { LocationName.CornerstoneHillMap: WorldLocationData(0x23B2, 0), @@ -226,6 +230,8 @@ BC_Checks = { LocationName.DonaldXaldinGetBonus: WorldLocationData(0x3704, 4), LocationName.SecretAnsemReport4: WorldLocationData(0x1D31, 2), LocationName.XaldinDataDefenseBoost: WorldLocationData(0x1D34, 7), + + 'Data Xaldin': WorldLocationData(7476, 7), } SP_Checks = { LocationName.PitCellAreaMap: WorldLocationData(0x23CA, 2), @@ -351,6 +357,7 @@ HB_Checks = { LocationName.RestorationSiteMoonRecipe: WorldLocationData(0x23C9, 3), LocationName.RestorationSiteAPBoost: WorldLocationData(0x23DB, 2), LocationName.DemyxHB: WorldLocationData(0x3707, 4), + '(HB) Demyx Bonus: Donald Slot 1': WorldLocationData(14087, 4), LocationName.DemyxHBGetBonus: WorldLocationData(0x3707, 4), LocationName.DonaldDemyxHBGetBonus: WorldLocationData(0x3707, 4), LocationName.FFFightsCureElement: WorldLocationData(0x1D14, 6), @@ -409,6 +416,25 @@ HB_Checks = { LocationName.VexenASRoadtoDiscovery: WorldLocationData(0x370C, 0), LocationName.VexenDataLostIllusion: WorldLocationData(0x370C, 0), # LocationName.DemyxDataAPBoost: WorldLocationData(0x1D26, 5), + + 'Lexaeus Bonus: Sora Slot 1': WorldLocationData(14092, 1), + 'AS Lexaeus': WorldLocationData(14092, 1), + 'Data Lexaeus': WorldLocationData(14092, 1), + 'Marluxia Bonus: Sora Slot 1': WorldLocationData(14092, 3), + 'AS Marluxia': WorldLocationData(14092, 3), + 'Data Marluxia': WorldLocationData(14092, 3), + 'Zexion Bonus: Sora Slot 1': WorldLocationData(14092, 2), + 'Zexion Bonus: Goofy Slot 1': WorldLocationData(14092, 2), + 'AS Zexion': WorldLocationData(14092, 2), + 'Data Zexion': WorldLocationData(14092, 2), + 'Larxene Bonus: Sora Slot 1': WorldLocationData(14092, 4), + 'AS Larxene': WorldLocationData(14092, 4), + 'Data Larxene': WorldLocationData(14092, 4), + 'Vexen Bonus: Sora Slot 1': WorldLocationData(14092, 0), + 'AS Vexen': WorldLocationData(14092, 0), + 'Data Vexen': WorldLocationData(14092, 0), + 'Data Demyx': WorldLocationData(7462, 5), + LocationName.GardenofAssemblageMap: WorldLocationData(0x23DF, 1), LocationName.GoALostIllusion: WorldLocationData(0x23DF, 2), LocationName.ProofofNonexistence: WorldLocationData(0x23DF, 3), @@ -549,50 +575,97 @@ TT_Checks = { LocationName.BetwixtandBetween: WorldLocationData(0x370B, 7), LocationName.BetwixtandBetweenBondofFlame: WorldLocationData(0x1CE9, 1), LocationName.AxelDataMagicBoost: WorldLocationData(0x1CEB, 4), + + 'Data Axel': WorldLocationData(7403, 4), } TWTNW_Checks = { - LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), - LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), - LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), - LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), - LocationName.Roxas: WorldLocationData(0x370C, 5), - LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), - LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), - LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), - LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), - LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), - LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), - LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), - LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), - LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), - LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), - LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), - LocationName.XigbarBonus: WorldLocationData(0x3706, 7), - LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), - LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), - LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), - LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), - LocationName.Oblivion: WorldLocationData(0x1ED2, 4), - LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), - LocationName.Luxord: WorldLocationData(0x3707, 0), - LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), - LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), - LocationName.SaixBonus: WorldLocationData(0x3707, 1), - LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), - LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), - LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), - LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), - LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), - LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), - LocationName.Xemnas1: WorldLocationData(0x3707, 2), - LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), - LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), - LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), - LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), - LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), - LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), - LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), - LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), + LocationName.FragmentCrossingMythrilStone: WorldLocationData(0x23CB, 4), + LocationName.FragmentCrossingMythrilCrystal: WorldLocationData(0x23CB, 5), + LocationName.FragmentCrossingAPBoost: WorldLocationData(0x23CB, 6), + LocationName.FragmentCrossingOrichalcum: WorldLocationData(0x23CB, 7), + LocationName.Roxas: WorldLocationData(0x370C, 5), + LocationName.RoxasGetBonus: WorldLocationData(0x370C, 5), + LocationName.RoxasSecretAnsemReport8: WorldLocationData(0x1ED1, 1), + LocationName.TwoBecomeOne: WorldLocationData(0x1ED1, 1), + LocationName.MemorysSkyscaperMythrilCrystal: WorldLocationData(0x23CD, 3), + LocationName.MemorysSkyscaperAPBoost: WorldLocationData(0x23DC, 0), + LocationName.MemorysSkyscaperMythrilStone: WorldLocationData(0x23DC, 1), + LocationName.TheBrinkofDespairDarkCityMap: WorldLocationData(0x23CA, 5), + LocationName.TheBrinkofDespairOrichalcumPlus: WorldLocationData(0x23DA, 2), + LocationName.NothingsCallMythrilGem: WorldLocationData(0x23CC, 0), + LocationName.NothingsCallOrichalcum: WorldLocationData(0x23CC, 1), + LocationName.TwilightsViewCosmicBelt: WorldLocationData(0x23CA, 6), + LocationName.XigbarBonus: WorldLocationData(0x3706, 7), + LocationName.XigbarSecretAnsemReport3: WorldLocationData(0x1ED2, 2), + LocationName.NaughtsSkywayMythrilGem: WorldLocationData(0x23CC, 2), + LocationName.NaughtsSkywayOrichalcum: WorldLocationData(0x23CC, 3), + LocationName.NaughtsSkywayMythrilCrystal: WorldLocationData(0x23CC, 4), + LocationName.Oblivion: WorldLocationData(0x1ED2, 4), + LocationName.CastleThatNeverWasMap: WorldLocationData(0x1ED2, 4), + LocationName.Luxord: WorldLocationData(0x3707, 0), + LocationName.LuxordGetBonus: WorldLocationData(0x3707, 0), + LocationName.LuxordSecretAnsemReport9: WorldLocationData(0x1ED2, 7), + LocationName.SaixBonus: WorldLocationData(0x3707, 1), + LocationName.SaixSecretAnsemReport12: WorldLocationData(0x1ED3, 2), + LocationName.PreXemnas1SecretAnsemReport11: WorldLocationData(0x1ED3, 6), + LocationName.RuinandCreationsPassageMythrilStone: WorldLocationData(0x23CC, 7), + LocationName.RuinandCreationsPassageAPBoost: WorldLocationData(0x23CD, 0), + LocationName.RuinandCreationsPassageMythrilCrystal: WorldLocationData(0x23CD, 1), + LocationName.RuinandCreationsPassageOrichalcum: WorldLocationData(0x23CD, 2), + LocationName.Xemnas1: WorldLocationData(0x3707, 2), + LocationName.Xemnas1GetBonus: WorldLocationData(0x3707, 2), + LocationName.Xemnas1SecretAnsemReport13: WorldLocationData(0x1ED4, 5), + LocationName.FinalXemnas: WorldLocationData(0x1ED8, 1), + LocationName.XemnasDataPowerBoost: WorldLocationData(0x1EDA, 2), + LocationName.XigbarDataDefenseBoost: WorldLocationData(0x1ED9, 7), + LocationName.SaixDataDefenseBoost: WorldLocationData(0x1EDA, 0), + LocationName.LuxordDataAPBoost: WorldLocationData(0x1EDA, 1), + LocationName.RoxasDataMagicBoost: WorldLocationData(0x1ED9, 6), + + "(TWTNW) Roxas Bonus: Sora Slot 1": WorldLocationData(14092, 5), + "(TWTNW) Roxas Bonus: Sora Slot 2": WorldLocationData(14092, 5), + "(TWTNW) Roxas Secret Ansem Report 8": WorldLocationData(7889, 1), + "(TWTNW) Two Become One": WorldLocationData(7889, 1), + "(TWTNW) Memory's Skyscaper Mythril Crystal": WorldLocationData(9165, 3), + "(TWTNW) Memory's Skyscaper AP Boost": WorldLocationData(9180, 0), + "(TWTNW) Memory's Skyscaper Mythril Stone": WorldLocationData(9180, 1), + "(TWTNW) The Brink of Despair Dark City Map": WorldLocationData(9162, 5), + "(TWTNW) The Brink of Despair Orichalcum+": WorldLocationData(9178, 2), + "(TWTNW) Nothing's Call Mythril Gem": WorldLocationData(9164, 0), + "(TWTNW) Nothing's Call Orichalcum": WorldLocationData(9164, 1), + "(TWTNW) Twilight's View Cosmic Belt": WorldLocationData(9162, 6), + "(TWTNW) Xigbar Bonus: Sora Slot 1": WorldLocationData(14086, 7), + "(TWTNW) Xigbar Secret Ansem Report 3": WorldLocationData(7890, 2), + "(TWTNW) Naught's Skyway Mythril Gem": WorldLocationData(9164, 2), + "(TWTNW) Naught's Skyway Orichalcum": WorldLocationData(9164, 3), + "(TWTNW) Naught's Skyway Mythril Crystal": WorldLocationData(9164, 4), + "(TWTNW) Oblivion": WorldLocationData(7890, 4), + "(TWTNW) Castle That Never Was Map": WorldLocationData(7890, 4), + "(TWTNW) Luxord": WorldLocationData(14087, 0), + "(TWTNW) Luxord Bonus: Sora Slot 1": WorldLocationData(14087, 0), + "(TWTNW) Luxord Secret Ansem Report 9": WorldLocationData(7890, 7), + "(TWTNW) Saix Bonus: Sora Slot 1": WorldLocationData(14087, 1), + "(TWTNW) Saix Secret Ansem Report 12": WorldLocationData(7891, 2), + "(TWTNW) Secret Ansem Report 11 (Pre-Xemnas 1)": WorldLocationData(7891, 6), + "(TWTNW) Ruin and Creation's Passage Mythril Stone": WorldLocationData(9164, 7), + "(TWTNW) Ruin and Creation's Passage AP Boost": WorldLocationData(9165, 0), + "(TWTNW) Ruin and Creation's Passage Mythril Crystal": WorldLocationData(9165, 1), + "(TWTNW) Ruin and Creation's Passage Orichalcum": WorldLocationData(9165, 2), + "(TWTNW) Xemnas 1 Bonus: Sora Slot 1": WorldLocationData(14087, 2), + "(TWTNW) Xemnas 1 Bonus: Sora Slot 2": WorldLocationData(14087, 2), + "(TWTNW) Xemnas 1 Secret Ansem Report 13": WorldLocationData(7892, 5), + "Data Xemnas": WorldLocationData(7898, 2), + "Data Xigbar": WorldLocationData(7897, 7), + "Data Saix": WorldLocationData(7898, 0), + "Data Luxord": WorldLocationData(7898, 1), + "Data Roxas": WorldLocationData(7897, 6), + +} +Atlantica_Checks = { + LocationName.UnderseaKingdomMap: WorldLocationData(0x1DF4, 2), + LocationName.MysteriousAbyss: WorldLocationData(0x1DF5, 3), + LocationName.MusicalOrichalcumPlus: WorldLocationData(0x1DF4, 1), + LocationName.MusicalBlizzardElement: WorldLocationData(0x1DF4, 1) } SoraLevels = { # LocationName.Lvl1: WorldLocationData(0xFFFF,1), @@ -743,6 +816,15 @@ FinalLevels = { LocationName.Finallvl6: WorldLocationData(0x33D6, 6), LocationName.Finallvl7: WorldLocationData(0x33D6, 7), +} +SummonLevels = { + LocationName.Summonlvl2: WorldLocationData(0x3526, 2), + LocationName.Summonlvl3: WorldLocationData(0x3526, 3), + LocationName.Summonlvl4: WorldLocationData(0x3526, 4), + LocationName.Summonlvl5: WorldLocationData(0x3526, 5), + LocationName.Summonlvl6: WorldLocationData(0x3526, 6), + LocationName.Summonlvl7: WorldLocationData(0x3526, 7), + } weaponSlots = { LocationName.AdamantShield: WorldLocationData(0x35E6, 1), @@ -817,7 +899,6 @@ tornPageLocks = { all_world_locations = { **TWTNW_Checks, **TT_Checks, - **TT_Checks, **HB_Checks, **BC_Checks, **Oc_Checks, @@ -828,11 +909,9 @@ all_world_locations = { **DC_Checks, **TR_Checks, **HT_Checks, - **HB_Checks, **PR_Checks, **SP_Checks, - **TWTNW_Checks, - **HB_Checks, + **Atlantica_Checks, } levels_locations = { diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 23075a2084..69f844f45a 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -1,15 +1,25 @@ -from BaseClasses import Tutorial, ItemClassification import logging +from typing import List +from BaseClasses import Tutorial, ItemClassification +from Fill import fill_restrictive +from worlds.LauncherComponents import Component, components, Type, launch_subprocess +from worlds.AutoWorld import World, WebWorld from .Items import * -from .Locations import all_locations, setup_locations, exclusion_table, AllWeaponSlot -from .Names import ItemName, LocationName +from .Locations import * +from .Names import ItemName, LocationName, RegionName from .OpenKH import patch_kh2 -from .Options import KH2_Options +from .Options import KingdomHearts2Options from .Regions import create_regions, connect_regions -from .Rules import set_rules -from ..AutoWorld import World, WebWorld -from .logic import KH2Logic +from .Rules import * + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="KH2Client") + + +components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT)) class KingdomHearts2Web(WebWorld): @@ -23,99 +33,119 @@ class KingdomHearts2Web(WebWorld): )] -# noinspection PyUnresolvedReferences class KH2World(World): """ Kingdom Hearts II is an action role-playing game developed and published by Square Enix and released in 2005. It is the sequel to Kingdom Hearts and Kingdom Hearts: Chain of Memories, and like the two previous games, focuses on Sora and his friends' continued battle against the Darkness. """ - game: str = "Kingdom Hearts 2" + game = "Kingdom Hearts 2" web = KingdomHearts2Web() - data_version = 1 - required_client_version = (0, 4, 0) - option_definitions = KH2_Options - item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()} - location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code} + + required_client_version = (0, 4, 4) + options_dataclass = KingdomHearts2Options + options: KingdomHearts2Options + item_name_to_id = {item: item_id + for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)} + location_name_to_id = {item: location + for location, item in enumerate(all_locations.keys(), 0x130000)} item_name_groups = item_groups + visitlocking_dict: Dict[str, int] + plando_locations: Dict[str, str] + lucky_emblem_amount: int + lucky_emblem_required: int + bounties_required: int + bounties_amount: int + filler_items: List[str] + item_quantity_dict: Dict[str, int] + local_items: Dict[int, int] + sora_ability_dict: Dict[str, int] + goofy_ability_dict: Dict[str, int] + donald_ability_dict: Dict[str, int] + total_locations: int + + # growth_list: list[str] + def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) - self.valid_abilities = None - self.visitlocking_dict = None - self.plando_locations = None - self.luckyemblemamount = None - self.luckyemblemrequired = None - self.BountiesRequired = None - self.BountiesAmount = None - self.hitlist = None - self.LocalItems = {} - self.RandomSuperBoss = list() - self.filler_items = list() - self.item_quantity_dict = {} - self.donald_ability_pool = list() - self.goofy_ability_pool = list() - self.sora_keyblade_ability_pool = list() - self.keyblade_slot_copy = list(Locations.Keyblade_Slots.keys()) - self.keyblade_slot_copy.remove(LocationName.KingdomKeySlot) - self.totalLocations = len(all_locations.items()) + # random_super_boss_list List[str] + # has to be in __init__ or else other players affect each other's bounties + self.random_super_boss_list = list() self.growth_list = list() - for x in range(4): - self.growth_list.extend(Movement_Table.keys()) - self.slotDataDuping = set() - self.localItems = dict() + # lists of KH2Item + self.keyblade_ability_pool = list() + + self.goofy_get_bonus_abilities = list() + self.goofy_weapon_abilities = list() + self.donald_get_bonus_abilities = list() + self.donald_weapon_abilities = list() + + self.slot_data_goofy_weapon = dict() + self.slot_data_sora_weapon = dict() + self.slot_data_donald_weapon = dict() def fill_slot_data(self) -> dict: - for values in CheckDupingItems.values(): - if isinstance(values, set): - self.slotDataDuping = self.slotDataDuping.union(values) - else: - for inner_values in values.values(): - self.slotDataDuping = self.slotDataDuping.union(inner_values) - self.LocalItems = {location.address: item_dictionary_table[location.item.name].code - for location in self.multiworld.get_filled_locations(self.player) - if location.item.player == self.player - and location.item.name in self.slotDataDuping - and location.name not in AllWeaponSlot} + for ability in self.slot_data_sora_weapon: + if ability in self.sora_ability_dict and self.sora_ability_dict[ability] >= 1: + self.sora_ability_dict[ability] -= 1 + self.donald_ability_dict = {k: v.quantity for k, v in DonaldAbility_Table.items()} + for ability in self.slot_data_donald_weapon: + if ability in self.donald_ability_dict and self.donald_ability_dict[ability] >= 1: + self.donald_ability_dict[ability] -= 1 + self.goofy_ability_dict = {k: v.quantity for k, v in GoofyAbility_Table.items()} + for ability in self.slot_data_goofy_weapon: + if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: + self.goofy_ability_dict[ability] -= 1 - return {"hitlist": self.hitlist, - "LocalItems": self.LocalItems, - "Goal": self.multiworld.Goal[self.player].value, - "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, - "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, - "BountyRequired": self.multiworld.BountyRequired[self.player].value} + slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data.update({ + "hitlist": [], # remove this after next update + "PoptrackerVersionCheck": 4.3, + "KeybladeAbilities": self.sora_ability_dict, + "StaffAbilities": self.donald_ability_dict, + "ShieldAbilities": self.goofy_ability_dict, + }) + return slot_data - def create_item(self, name: str, ) -> Item: - data = item_dictionary_table[name] - if name in Progression_Dicts["Progression"]: + def create_item(self, name: str) -> Item: + """ + Returns created KH2Item + """ + # data = item_dictionary_table[name] + if name in progression_set: item_classification = ItemClassification.progression + elif name in useful_set: + item_classification = ItemClassification.useful else: item_classification = ItemClassification.filler - created_item = KH2Item(name, item_classification, data.code, self.player) + created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player) return created_item def create_items(self) -> None: - self.visitlocking_dict = Progression_Dicts["AllVisitLocking"].copy() - if self.multiworld.Schmovement[self.player] != "level_0": - for _ in range(self.multiworld.Schmovement[self.player].value): - for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge, - ItemName.Glide}: + """ + Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking. + """ + self.visitlocking_dict = visit_locking_dict["AllVisitLocking"].copy() + if self.options.Schmovement != "level_0": + for _ in range(self.options.Schmovement.value): + for name in Movement_Table.keys(): self.item_quantity_dict[name] -= 1 self.growth_list.remove(name) self.multiworld.push_precollected(self.create_item(name)) - if self.multiworld.RandomGrowth[self.player] != 0: - max_growth = min(self.multiworld.RandomGrowth[self.player].value, len(self.growth_list)) + if self.options.RandomGrowth: + max_growth = min(self.options.RandomGrowth.value, len(self.growth_list)) for _ in range(max_growth): - random_growth = self.multiworld.per_slot_randoms[self.player].choice(self.growth_list) + random_growth = self.random.choice(self.growth_list) self.item_quantity_dict[random_growth] -= 1 self.growth_list.remove(random_growth) self.multiworld.push_precollected(self.create_item(random_growth)) - if self.multiworld.Visitlocking[self.player] == "no_visit_locking": - for item, amount in Progression_Dicts["AllVisitLocking"].items(): + if self.options.Visitlocking == "no_visit_locking": + for item, amount in visit_locking_dict["AllVisitLocking"].items(): for _ in range(amount): self.multiworld.push_precollected(self.create_item(item)) self.item_quantity_dict[item] -= 1 @@ -123,19 +153,19 @@ class KH2World(World): if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) - elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": - for item in Progression_Dicts["2VisitLocking"]: + elif self.options.Visitlocking == "second_visit_locking": + for item in visit_locking_dict["2VisitLocking"]: self.item_quantity_dict[item] -= 1 self.visitlocking_dict[item] -= 1 if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - for _ in range(self.multiworld.RandomVisitLockingItem[self.player].value): + for _ in range(self.options.RandomVisitLockingItem.value): if sum(self.visitlocking_dict.values()) <= 0: break visitlocking_set = list(self.visitlocking_dict.keys()) - item = self.multiworld.per_slot_randoms[self.player].choice(visitlocking_set) + item = self.random.choice(visitlocking_set) self.item_quantity_dict[item] -= 1 self.visitlocking_dict[item] -= 1 if self.visitlocking_dict[item] == 0: @@ -145,175 +175,258 @@ class KH2World(World): itempool = [self.create_item(item) for item, data in self.item_quantity_dict.items() for _ in range(data)] # Creating filler for unfilled locations - itempool += [self.create_filler() - for _ in range(self.totalLocations - len(itempool))] + itempool += [self.create_filler() for _ in range(self.total_locations - len(itempool))] + self.multiworld.itempool += itempool def generate_early(self) -> None: - # Item Quantity dict because Abilities can be a problem for KH2's Software. + """ + Determines the quantity of items and maps plando locations to items. + """ + # Item: Quantity Map + # Example. Quick Run: 4 + self.total_locations = len(all_locations.keys()) + for x in range(4): + self.growth_list.extend(Movement_Table.keys()) + self.item_quantity_dict = {item: data.quantity for item, data in item_dictionary_table.items()} + self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in + dic.items()} # Dictionary to mark locations with their plandoed item # Example. Final Xemnas: Victory + # 3 random support abilities because there are left over slots + support_abilities = list(SupportAbility_Table.keys()) + for _ in range(6): + random_support_ability = self.random.choice(support_abilities) + self.item_quantity_dict[random_support_ability] += 1 + self.sora_ability_dict[random_support_ability] += 1 + self.plando_locations = dict() - self.hitlist = [] self.starting_invo_verify() + + for k, v in self.options.CustomItemPoolQuantity.value.items(): + # kh2's items cannot hold more than a byte + if 255 > v > self.item_quantity_dict[k] and k in default_itempool_option.keys(): + self.item_quantity_dict[k] = v + elif 255 <= v: + logging.warning( + f"{self.player} has too many {k} in their CustomItemPool setting. Setting to default quantity") # Option to turn off Promise Charm Item - if not self.multiworld.Promise_Charm[self.player]: - self.item_quantity_dict[ItemName.PromiseCharm] = 0 + if not self.options.Promise_Charm: + del self.item_quantity_dict[ItemName.PromiseCharm] + + if not self.options.AntiForm: + del self.item_quantity_dict[ItemName.AntiForm] self.set_excluded_locations() - if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": - self.luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value - self.luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value + if self.options.Goal not in ["hitlist", "three_proofs"]: + self.lucky_emblem_amount = self.options.LuckyEmblemsAmount.value + self.lucky_emblem_required = self.options.LuckyEmblemsRequired.value self.emblem_verify() # hitlist - elif self.multiworld.Goal[self.player] == "hitlist": - self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) - self.BountiesAmount = self.multiworld.BountyAmount[self.player].value - self.BountiesRequired = self.multiworld.BountyRequired[self.player].value + if self.options.Goal not in ["lucky_emblem_hunt", "three_proofs"]: + self.random_super_boss_list.extend(exclusion_table["Hitlist"]) + self.bounties_amount = self.options.BountyAmount.value + self.bounties_required = self.options.BountyRequired.value self.hitlist_verify() - for bounty in range(self.BountiesAmount): - randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) - self.plando_locations[randomBoss] = ItemName.Bounty - self.hitlist.append(self.location_name_to_id[randomBoss]) - self.RandomSuperBoss.remove(randomBoss) - self.totalLocations -= 1 + prio_hitlist = [location for location in self.multiworld.priority_locations[self.player].value if + location in self.random_super_boss_list] + for bounty in range(self.options.BountyAmount.value): + if prio_hitlist: + random_boss = self.random.choice(prio_hitlist) + prio_hitlist.remove(random_boss) + else: + random_boss = self.random.choice(self.random_super_boss_list) + self.plando_locations[random_boss] = ItemName.Bounty + self.random_super_boss_list.remove(random_boss) + self.total_locations -= 1 - self.donald_fill() - self.goofy_fill() - self.keyblade_fill() + self.donald_gen_early() + self.goofy_gen_early() + self.keyblade_gen_early() if self.multiworld.FinalXemnas[self.player]: self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory else: self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name + self.total_locations -= 1 - # same item placed because you can only get one of these 2 locations - # they are both under the same flag so the player gets both locations just one of the two items - random_stt_item = self.create_filler().name - for location in {LocationName.JunkMedal, LocationName.JunkMedal}: - self.plando_locations[location] = random_stt_item - self.level_subtraction() - # subtraction from final xemnas and stt - self.totalLocations -= 3 + if self.options.WeaponSlotStartHint: + for location in all_weapon_slot: + self.multiworld.start_location_hints[self.player].value.add(location) + + if self.options.FillerItemsLocal: + for item in filler_items: + self.multiworld.local_items[self.player].value.add(item) + # By imitating remote this doesn't have to be plandoded filler anymore + # for location in {LocationName.JunkMedal, LocationName.JunkMedal}: + # self.plando_locations[location] = random_stt_item + if not self.options.SummonLevelLocationToggle: + self.total_locations -= 6 + + self.total_locations -= self.level_subtraction() def pre_fill(self): + """ + Plandoing Events and Fill_Restrictive for donald,goofy and sora + """ + self.donald_pre_fill() + self.goofy_pre_fill() + self.keyblade_pre_fill() + for location, item in self.plando_locations.items(): self.multiworld.get_location(location, self.player).place_locked_item( self.create_item(item)) def create_regions(self): - location_table = setup_locations() - create_regions(self.multiworld, self.player, location_table) - connect_regions(self.multiworld, self.player) + """ + Creates the Regions and Connects them. + """ + create_regions(self) + connect_regions(self) def set_rules(self): - set_rules(self.multiworld, self.player) + """ + Sets the Logic for the Regions and Locations. + """ + universal_logic = Rules.KH2WorldRules(self) + form_logic = Rules.KH2FormRules(self) + fight_rules = Rules.KH2FightRules(self) + fight_rules.set_kh2_fight_rules() + universal_logic.set_kh2_rules() + form_logic.set_kh2_form_rules() def generate_output(self, output_directory: str): + """ + Generates the .zip for OpenKH (The KH Mod Manager) + """ patch_kh2(self, output_directory) - def donald_fill(self): - for item in DonaldAbility_Table: - data = self.item_quantity_dict[item] - for _ in range(data): - self.donald_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of donald abilities - while len(self.donald_ability_pool) < 32: - self.donald_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) - # Placing Donald Abilities on donald locations - for donaldLocation in Locations.Donald_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) - self.plando_locations[donaldLocation] = random_ability - self.totalLocations -= 1 - self.donald_ability_pool.remove(random_ability) - - def goofy_fill(self): - for item in GoofyAbility_Table.keys(): - data = self.item_quantity_dict[item] - for _ in range(data): - self.goofy_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of goofy abilities - while len(self.goofy_ability_pool) < 33: - self.goofy_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) - # Placing Goofy Abilities on goofy locations - for goofyLocation in Locations.Goofy_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) - self.plando_locations[goofyLocation] = random_ability - self.totalLocations -= 1 - self.goofy_ability_pool.remove(random_ability) - - def keyblade_fill(self): - if self.multiworld.KeybladeAbilities[self.player] == "support": - self.sora_keyblade_ability_pool = { - **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, - ItemName.FinishingPlus: 1}} - - elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if - item in ActionAbility_Table} - # there are too little action abilities so 2 random support abilities are placed - for _ in range(3): - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) - while randomSupportAbility in self.sora_keyblade_ability_pool: - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) - self.sora_keyblade_ability_pool[randomSupportAbility] = 1 - else: - # both action and support on keyblades. - # TODO: make option to just exclude scom - self.sora_keyblade_ability_pool = { - **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, - **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, - ItemName.FinishingPlus: 1}} - - for ability in self.multiworld.BlacklistKeyblade[self.player].value: - if ability in self.sora_keyblade_ability_pool: - self.sora_keyblade_ability_pool.pop(ability) - - # magic number for amount of keyblades - if sum(self.sora_keyblade_ability_pool.values()) < 28: - raise Exception( - f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") - - self.valid_abilities = list(self.sora_keyblade_ability_pool.keys()) - # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - while random_ability == ItemName.NoExperience: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - self.plando_locations[LocationName.KingdomKeySlot] = random_ability - self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool[random_ability] -= 1 - if self.sora_keyblade_ability_pool[random_ability] == 0: - self.valid_abilities.remove(random_ability) - self.sora_keyblade_ability_pool.pop(random_ability) - - # plando keyblades because they can only have abilities - for keyblade in self.keyblade_slot_copy: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) - self.plando_locations[keyblade] = random_ability + def donald_gen_early(self): + random_prog_ability = self.random.choice([ItemName.Fantasia, ItemName.FlareForce]) + donald_master_ability = [donald_ability for donald_ability in DonaldAbility_Table.keys() for _ in + range(self.item_quantity_dict[donald_ability]) if + donald_ability != random_prog_ability] + self.donald_weapon_abilities = [] + self.donald_get_bonus_abilities = [] + # fill goofy weapons first + for _ in range(15): + random_ability = self.random.choice(donald_master_ability) + donald_master_ability.remove(random_ability) + self.donald_weapon_abilities += [self.create_item(random_ability)] self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool[random_ability] -= 1 - if self.sora_keyblade_ability_pool[random_ability] == 0: - self.valid_abilities.remove(random_ability) - self.sora_keyblade_ability_pool.pop(random_ability) - self.totalLocations -= 1 + self.total_locations -= 1 + self.slot_data_donald_weapon = [item_name.name for item_name in self.donald_weapon_abilities] + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # pre plando donald get bonuses + self.donald_get_bonus_abilities += [self.create_item(random_prog_ability)] + self.total_locations -= 1 + for item_name in donald_master_ability: + self.donald_get_bonus_abilities += [self.create_item(item_name)] + self.item_quantity_dict[item_name] -= 1 + self.total_locations -= 1 + + def goofy_gen_early(self): + random_prog_ability = self.random.choice([ItemName.Teamwork, ItemName.TornadoFusion]) + goofy_master_ability = [goofy_ability for goofy_ability in GoofyAbility_Table.keys() for _ in + range(self.item_quantity_dict[goofy_ability]) if goofy_ability != random_prog_ability] + self.goofy_weapon_abilities = [] + self.goofy_get_bonus_abilities = [] + # fill goofy weapons first + for _ in range(15): + random_ability = self.random.choice(goofy_master_ability) + goofy_master_ability.remove(random_ability) + self.goofy_weapon_abilities += [self.create_item(random_ability)] + self.item_quantity_dict[random_ability] -= 1 + self.total_locations -= 1 + + self.slot_data_goofy_weapon = [item_name.name for item_name in self.goofy_weapon_abilities] + + if not self.options.DonaldGoofyStatsanity: + # pre plando goofy get bonuses + self.goofy_get_bonus_abilities += [self.create_item(random_prog_ability)] + self.total_locations -= 1 + for item_name in goofy_master_ability: + self.goofy_get_bonus_abilities += [self.create_item(item_name)] + self.item_quantity_dict[item_name] -= 1 + self.total_locations -= 1 + + def keyblade_gen_early(self): + keyblade_master_ability = [ability for ability in SupportAbility_Table.keys() if ability not in progression_set + for _ in range(self.item_quantity_dict[ability])] + self.keyblade_ability_pool = [] + + for _ in range(len(Keyblade_Slots)): + random_ability = self.random.choice(keyblade_master_ability) + keyblade_master_ability.remove(random_ability) + self.keyblade_ability_pool += [self.create_item(random_ability)] + self.item_quantity_dict[random_ability] -= 1 + self.total_locations -= 1 + self.slot_data_sora_weapon = [item_name.name for item_name in self.keyblade_ability_pool] + + def goofy_pre_fill(self): + """ + Removes donald locations from the location pool maps random donald items to be plandoded. + """ + goofy_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in + Goofy_Checks.keys() if Goofy_Checks[location].yml == "Keyblade"] + # take one of the 2 out + # randomize the list with only + for location in goofy_weapon_location_list: + random_ability = self.random.choice(self.goofy_weapon_abilities) + location.place_locked_item(random_ability) + self.goofy_weapon_abilities.remove(random_ability) + + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # plando goofy get bonuses + goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in + Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"] + for location in goofy_get_bonus_location_pool: + self.random.choice(self.goofy_get_bonus_abilities) + random_ability = self.random.choice(self.goofy_get_bonus_abilities) + location.place_locked_item(random_ability) + self.goofy_get_bonus_abilities.remove(random_ability) + + def donald_pre_fill(self): + donald_weapon_location_list = [self.multiworld.get_location(location, self.player) for location in + Donald_Checks.keys() if Donald_Checks[location].yml == "Keyblade"] + + # take one of the 2 out + # randomize the list with only + for location in donald_weapon_location_list: + random_ability = self.random.choice(self.donald_weapon_abilities) + location.place_locked_item(random_ability) + self.donald_weapon_abilities.remove(random_ability) + + if not self.multiworld.DonaldGoofyStatsanity[self.player]: + # plando goofy get bonuses + donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in + Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"] + for location in donald_get_bonus_location_pool: + random_ability = self.random.choice(self.donald_get_bonus_abilities) + location.place_locked_item(random_ability) + self.donald_get_bonus_abilities.remove(random_ability) + + def keyblade_pre_fill(self): + """ + Fills keyblade slots with abilities determined on player's setting + """ + keyblade_locations = [self.multiworld.get_location(location, self.player) for location in Keyblade_Slots.keys()] + state = self.multiworld.get_all_state(False) + keyblade_ability_pool_copy = self.keyblade_ability_pool.copy() + fill_restrictive(self.multiworld, state, keyblade_locations, keyblade_ability_pool_copy, True, True) def starting_invo_verify(self): + """ + Making sure the player doesn't put too many abilities in their starting inventory. + """ for item, value in self.multiworld.start_inventory[self.player].value.items(): if item in ActionAbility_Table \ - or item in SupportAbility_Table or exclusionItem_table["StatUps"] \ + or item in SupportAbility_Table or exclusion_item_table["StatUps"] \ or item in DonaldAbility_Table or item in GoofyAbility_Table: # cannot have more than the quantity for abilties if value > item_dictionary_table[item].quantity: @@ -324,78 +437,100 @@ class KH2World(World): self.item_quantity_dict[item] -= value def emblem_verify(self): - if self.luckyemblemamount < self.luckyemblemrequired: + """ + Making sure lucky emblems have amount>=required. + """ + if self.lucky_emblem_amount < self.lucky_emblem_required: logging.info( - f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " - f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") - luckyemblemamount = max(self.luckyemblemamount, self.luckyemblemrequired) - self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount + f"Lucky Emblem Amount {self.options.LuckyEmblemsAmount.value} is less than required " + f"{self.options.LuckyEmblemsRequired.value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.options.LuckyEmblemsRequired.value}") + luckyemblemamount = max(self.lucky_emblem_amount, self.lucky_emblem_required) + self.options.LuckyEmblemsAmount.value = luckyemblemamount - self.item_quantity_dict[ItemName.LuckyEmblem] = self.multiworld.LuckyEmblemsAmount[self.player].value + self.item_quantity_dict[ItemName.LuckyEmblem] = self.options.LuckyEmblemsAmount.value # give this proof to unlock the final door once the player has the amount of lucky emblem required - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + if ItemName.ProofofNonexistence in self.item_quantity_dict: + del self.item_quantity_dict[ItemName.ProofofNonexistence] def hitlist_verify(self): + """ + Making sure hitlist have amount>=required. + """ for location in self.multiworld.exclude_locations[self.player].value: - if location in self.RandomSuperBoss: - self.RandomSuperBoss.remove(location) + if location in self.random_super_boss_list: + self.random_super_boss_list.remove(location) + + if not self.options.SummonLevelLocationToggle: + self.random_super_boss_list.remove(LocationName.Summonlvl7) # Testing if the player has the right amount of Bounties for Completion. - if len(self.RandomSuperBoss) < self.BountiesAmount: + if len(self.random_super_boss_list) < self.bounties_amount: logging.info( f"{self.multiworld.get_file_safe_player_name(self.player)} has more bounties than bosses." - f" Setting total bounties to {len(self.RandomSuperBoss)}") - self.BountiesAmount = len(self.RandomSuperBoss) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + f" Setting total bounties to {len(self.random_super_boss_list)}") + self.bounties_amount = len(self.random_super_boss_list) + self.options.BountyAmount.value = self.bounties_amount - if len(self.RandomSuperBoss) < self.BountiesRequired: + if len(self.random_super_boss_list) < self.bounties_required: logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." - f" Setting required bounties to {len(self.RandomSuperBoss)}") - self.BountiesRequired = len(self.RandomSuperBoss) - self.multiworld.BountyRequired[self.player].value = self.BountiesRequired + f" Setting required bounties to {len(self.random_super_boss_list)}") + self.bounties_required = len(self.random_super_boss_list) + self.options.BountyRequired.value = self.bounties_required - if self.BountiesAmount < self.BountiesRequired: - logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " - f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") - self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + if self.bounties_amount < self.bounties_required: + logging.info( + f"Bounties Amount is less than required for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Swapping Amount and Required") + temp = self.options.BountyRequired.value + self.options.BountyRequired.value = self.options.BountyAmount.value + self.options.BountyAmount.value = temp - self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + if self.options.BountyStartingHintToggle: + self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) + + if ItemName.ProofofNonexistence in self.item_quantity_dict: + del self.item_quantity_dict[ItemName.ProofofNonexistence] def set_excluded_locations(self): + """ + Fills excluded_locations from player's settings. + """ # Option to turn off all superbosses. Can do this individually but its like 20+ checks - if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": - for superboss in exclusion_table["Datas"]: - self.multiworld.exclude_locations[self.player].value.add(superboss) + if not self.options.SuperBosses: for superboss in exclusion_table["SuperBosses"]: self.multiworld.exclude_locations[self.player].value.add(superboss) # Option to turn off Olympus Colosseum Cups. - if self.multiworld.Cups[self.player] == "no_cups": + if self.options.Cups == "no_cups": for cup in exclusion_table["Cups"]: self.multiworld.exclude_locations[self.player].value.add(cup) # exclude only hades paradox. If cups and hades paradox then nothing is excluded - elif self.multiworld.Cups[self.player] == "cups": + elif self.options.Cups == "cups": self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) + if not self.options.AtlanticaToggle: + for loc in exclusion_table["Atlantica"]: + self.multiworld.exclude_locations[self.player].value.add(loc) + def level_subtraction(self): - # there are levels but level 1 is there for the yamls - if self.multiworld.LevelDepth[self.player] == "level_99_sanity": - # level 99 sanity - self.totalLocations -= 1 - elif self.multiworld.LevelDepth[self.player] == "level_50_sanity": + """ + Determine how many locations are on sora's levels. + """ + if self.options.LevelDepth == "level_50_sanity": # level 50 sanity - self.totalLocations -= 50 - elif self.multiworld.LevelDepth[self.player] == "level_1": + return 49 + elif self.options.LevelDepth == "level_1": # level 1. No checks on levels - self.totalLocations -= 99 + return 98 + elif self.options.LevelDepth in ["level_50", "level_99"]: + # could be if leveldepth!= 99 sanity but this reads better imo + return 75 else: - # level 50/99 since they contain the same amount of levels - self.totalLocations -= 76 + return 0 def get_filler_item_name(self) -> str: - return self.multiworld.random.choice( - [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) + """ + Returns random filler item name. + """ + return self.random.choice(filler_items) diff --git a/worlds/kh2/logic.py b/worlds/kh2/logic.py deleted file mode 100644 index 10af4144a7..0000000000 --- a/worlds/kh2/logic.py +++ /dev/null @@ -1,312 +0,0 @@ -from .Names import ItemName -from ..AutoWorld import LogicMixin - - -class KH2Logic(LogicMixin): - def kh_lod_unlocked(self, player, amount): - return self.has(ItemName.SwordoftheAncestor, player, amount) - - def kh_oc_unlocked(self, player, amount): - return self.has(ItemName.BattlefieldsofWar, player, amount) - - def kh_twtnw_unlocked(self, player, amount): - return self.has(ItemName.WaytotheDawn, player, amount) - - def kh_ht_unlocked(self, player, amount): - return self.has(ItemName.BoneFist, player, amount) - - def kh_tt_unlocked(self, player, amount): - return self.has(ItemName.IceCream, player, amount) - - def kh_pr_unlocked(self, player, amount): - return self.has(ItemName.SkillandCrossbones, player, amount) - - def kh_sp_unlocked(self, player, amount): - return self.has(ItemName.IdentityDisk, player, amount) - - def kh_stt_unlocked(self, player: int, amount): - return self.has(ItemName.NamineSketches, player, amount) - - # Using Dummy 13 for this - def kh_dc_unlocked(self, player: int, amount): - return self.has(ItemName.CastleKey, player, amount) - - def kh_hb_unlocked(self, player, amount): - return self.has(ItemName.MembershipCard, player, amount) - - def kh_pl_unlocked(self, player, amount): - return self.has(ItemName.ProudFang, player, amount) - - def kh_ag_unlocked(self, player, amount): - return self.has(ItemName.Scimitar, player, amount) - - def kh_bc_unlocked(self, player, amount): - return self.has(ItemName.BeastsClaw, player, amount) - - def kh_amount_of_forms(self, player, amount, requiredform="None"): - level = 0 - formList = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, - ItemName.FinalForm] - # required form is in the logic for region connections - if requiredform != "None": - formList.remove(requiredform) - for form in formList: - if self.has(form, player): - level += 1 - return level >= amount - - def kh_visit_locking_amount(self, player, amount): - visit = 0 - # torn pages are not added since you cannot get exp from that world - for item in {ItemName.CastleKey, ItemName.BattlefieldsofWar, ItemName.SwordoftheAncestor, ItemName.BeastsClaw, - ItemName.BoneFist, ItemName.ProudFang, ItemName.SkillandCrossbones, ItemName.Scimitar, - ItemName.MembershipCard, - ItemName.IceCream, ItemName.WaytotheDawn, - ItemName.IdentityDisk, ItemName.NamineSketches}: - visit += self.count(item, player) - return visit >= amount - - def kh_three_proof_unlocked(self, player): - return self.has(ItemName.ProofofConnection, player, 1) \ - and self.has(ItemName.ProofofNonexistence, player, 1) \ - and self.has(ItemName.ProofofPeace, player, 1) - - def kh_hitlist(self, player, amount): - return self.has(ItemName.Bounty, player, amount) - - def kh_lucky_emblem_unlocked(self, player, amount): - return self.has(ItemName.LuckyEmblem, player, amount) - - def kh_victory(self, player): - return self.has(ItemName.Victory, player, 1) - - def kh_summon(self, player, amount): - summonlevel = 0 - for summon in {ItemName.Genie, ItemName.ChickenLittle, ItemName.Stitch, ItemName.PeterPan}: - if self.has(summon, player): - summonlevel += 1 - return summonlevel >= amount - - # magic progression - def kh_fire(self, player): - return self.has(ItemName.FireElement, player, 1) - - def kh_fira(self, player): - return self.has(ItemName.FireElement, player, 2) - - def kh_firaga(self, player): - return self.has(ItemName.FireElement, player, 3) - - def kh_blizzard(self, player): - return self.has(ItemName.BlizzardElement, player, 1) - - def kh_blizzara(self, player): - return self.has(ItemName.BlizzardElement, player, 2) - - def kh_blizzaga(self, player): - return self.has(ItemName.BlizzardElement, player, 3) - - def kh_thunder(self, player): - return self.has(ItemName.ThunderElement, player, 1) - - def kh_thundara(self, player): - return self.has(ItemName.ThunderElement, player, 2) - - def kh_thundaga(self, player): - return self.has(ItemName.ThunderElement, player, 3) - - def kh_magnet(self, player): - return self.has(ItemName.MagnetElement, player, 1) - - def kh_magnera(self, player): - return self.has(ItemName.MagnetElement, player, 2) - - def kh_magnega(self, player): - return self.has(ItemName.MagnetElement, player, 3) - - def kh_reflect(self, player): - return self.has(ItemName.ReflectElement, player, 1) - - def kh_reflera(self, player): - return self.has(ItemName.ReflectElement, player, 2) - - def kh_reflega(self, player): - return self.has(ItemName.ReflectElement, player, 3) - - def kh_highjump(self, player, amount): - return self.has(ItemName.HighJump, player, amount) - - def kh_quickrun(self, player, amount): - return self.has(ItemName.QuickRun, player, amount) - - def kh_dodgeroll(self, player, amount): - return self.has(ItemName.DodgeRoll, player, amount) - - def kh_aerialdodge(self, player, amount): - return self.has(ItemName.AerialDodge, player, amount) - - def kh_glide(self, player, amount): - return self.has(ItemName.Glide, player, amount) - - def kh_comboplus(self, player, amount): - return self.has(ItemName.ComboPlus, player, amount) - - def kh_aircomboplus(self, player, amount): - return self.has(ItemName.AirComboPlus, player, amount) - - def kh_valorgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.ValorForm, player) - - def kh_wisdomgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.WisdomForm, player) - - def kh_mastergenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.MasterForm, player) - - def kh_finalgenie(self, player): - return self.has(ItemName.Genie, player) and self.has(ItemName.FinalForm, player) - - def kh_rsr(self, player): - return self.has(ItemName.Slapshot, player, 1) and self.has(ItemName.ComboMaster, player) and self.kh_reflect( - player) - - def kh_gapcloser(self, player): - return self.has(ItemName.FlashStep, player, 1) or self.has(ItemName.SlideDash, player) - - # Crowd Control and Berserk Hori will be used when I add hard logic. - - def kh_crowdcontrol(self, player): - return self.kh_magnera(player) and self.has(ItemName.ChickenLittle, player) \ - or self.kh_magnega(player) and self.kh_mastergenie(player) - - def kh_berserkhori(self, player): - return self.has(ItemName.HorizontalSlash, player, 1) and self.has(ItemName.BerserkCharge, player) - - def kh_donaldlimit(self, player): - return self.has(ItemName.FlareForce, player, 1) or self.has(ItemName.Fantasia, player) - - def kh_goofylimit(self, player): - return self.has(ItemName.TornadoFusion, player, 1) or self.has(ItemName.Teamwork, player) - - def kh_basetools(self, player): - # TODO: if option is easy then add reflect,gap closer and second chance&once more. #option east scom option normal adds gap closer or combo master #hard is what is right now - return self.has(ItemName.Guard, player, 1) and self.has(ItemName.AerialRecovery, player, 1) \ - and self.has(ItemName.FinishingPlus, player, 1) - - def kh_roxastools(self, player): - return self.kh_basetools(player) and ( - self.has(ItemName.QuickRun, player) or self.has(ItemName.NegativeCombo, player, 2)) - - def kh_painandpanic(self, player): - return (self.kh_goofylimit(player) or self.kh_donaldlimit(player)) and self.kh_dc_unlocked(player, 2) - - def kh_cerberuscup(self, player): - return self.kh_amount_of_forms(player, 2) and self.kh_thundara(player) \ - and self.kh_ag_unlocked(player, 1) and self.kh_ht_unlocked(player, 1) \ - and self.kh_pl_unlocked(player, 1) - - def kh_titan(self, player: int): - return self.kh_summon(player, 2) and (self.kh_thundara(player) or self.kh_magnera(player)) \ - and self.kh_oc_unlocked(player, 2) - - def kh_gof(self, player): - return self.kh_titan(player) and self.kh_cerberuscup(player) \ - and self.kh_painandpanic(player) and self.kh_twtnw_unlocked(player, 1) - - def kh_dataroxas(self, player): - return self.kh_basetools(player) and \ - ((self.has(ItemName.LimitForm, player) and self.kh_amount_of_forms(player, 3) and self.has( - ItemName.TrinityLimit, player) and self.kh_gapcloser(player)) - or (self.has(ItemName.NegativeCombo, player, 2) or self.kh_quickrun(player, 2))) - - def kh_datamarluxia(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.kh_amount_of_forms(player, 3) and self.has(ItemName.FinalForm, player) and self.kh_fira( - player)) or self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) - - def kh_datademyx(self, player): - return self.kh_basetools(player) and self.kh_amount_of_forms(player, 5) and self.kh_firaga(player) \ - and (self.kh_donaldlimit(player) or self.kh_blizzard(player)) - - def kh_datalexaeus(self, player): - return self.kh_basetools(player) and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) \ - and (self.has(ItemName.NegativeCombo, player, 2) or self.kh_donaldlimit(player)) - - def kh_datasaix(self, player): - return self.kh_basetools(player) and (self.kh_thunder(player) or self.kh_blizzard(player)) \ - and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 2) and self.kh_amount_of_forms(player, 3) \ - and (self.kh_rsr(player) or self.has(ItemName.NegativeCombo, player, 2) or self.has(ItemName.PeterPan, - player)) - - def kh_dataxaldin(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.kh_goofylimit(player) \ - and self.kh_highjump(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, - 2) and self.kh_magnet( - player) - # and (self.kh_form_level_unlocked(player, 3) or self.kh_berserkhori(player)) - - def kh_dataxemnas(self, player): - return self.kh_basetools(player) and self.kh_rsr(player) and self.kh_gapcloser(player) \ - and (self.has(ItemName.LimitForm, player) or self.has(ItemName.TrinityLimit, player)) - - def kh_dataxigbar(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 3) and self.kh_reflera(player) - - def kh_datavexen(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 4) and self.kh_reflera(player) and self.kh_fira(player) - - def kh_datazexion(self, player): - return self.kh_basetools(player) and self.kh_donaldlimit(player) and self.has(ItemName.FinalForm, player) \ - and self.kh_amount_of_forms(player, 3) \ - and self.kh_reflera(player) and self.kh_fira(player) - - def kh_dataaxel(self, player): - return self.kh_basetools(player) \ - and ((self.kh_reflera(player) and self.kh_blizzara(player)) or self.has(ItemName.NegativeCombo, player, 2)) - - def kh_dataluxord(self, player): - return self.kh_basetools(player) and self.kh_reflect(player) - - def kh_datalarxene(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fire( - player)) - or (self.kh_donaldlimit(player) and self.kh_amount_of_forms(player, 2))) - - def kh_sephi(self, player): - return self.kh_dataxemnas(player) - - def kh_onek(self, player): - return self.kh_reflect(player) or self.has(ItemName.Guard, player) - - def kh_terra(self, player): - return self.has(ItemName.ProofofConnection, player) and self.kh_basetools(player) \ - and self.kh_dodgeroll(player, 2) and self.kh_aerialdodge(player, 2) and self.kh_glide(player, 3) \ - and ((self.kh_comboplus(player, 2) and self.has(ItemName.Explosion, player)) or self.has( - ItemName.NegativeCombo, player, 2)) - - def kh_cor(self, player): - return self.kh_reflect(player) \ - and self.kh_highjump(player, 2) and self.kh_quickrun(player, 2) and self.kh_aerialdodge(player, 2) \ - and (self.has(ItemName.MasterForm, player) and self.kh_fire(player) - or (self.has(ItemName.ChickenLittle, player) and self.kh_donaldlimit(player) and self.kh_glide(player, - 2))) - - def kh_transport(self, player): - return self.kh_basetools(player) and self.kh_reflera(player) \ - and ((self.kh_mastergenie(player) and self.kh_magnera(player) and self.kh_donaldlimit(player)) - or (self.has(ItemName.FinalForm, player) and self.kh_amount_of_forms(player, 4) and self.kh_fira( - player))) - - def kh_gr2(self, player): - return (self.has(ItemName.MasterForm, player) or self.has(ItemName.Stitch, player)) \ - and (self.kh_fire(player) or self.kh_blizzard(player) or self.kh_thunder(player)) - - def kh_xaldin(self, player): - return self.kh_basetools(player) and (self.kh_donaldlimit(player) or self.kh_amount_of_forms(player, 1)) - - def kh_mcp(self, player): - return self.kh_reflect(player) and ( - self.has(ItemName.MasterForm, player) or self.has(ItemName.FinalForm, player)) diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml deleted file mode 100644 index 4246132c26..0000000000 --- a/worlds/kh2/mod_template/mod.yml +++ /dev/null @@ -1,38 +0,0 @@ -assets: -- method: binarc - name: 00battle.bin - source: - - method: listpatch - name: fmlv - source: - - name: FmlvList.yml - type: fmlv - type: List - - method: listpatch - name: lvup - source: - - name: LvupList.yml - type: lvup - type: List - - method: listpatch - name: bons - source: - - name: BonsList.yml - type: bons - type: List -- method: binarc - name: 03system.bin - source: - - method: listpatch - name: trsr - source: - - name: TrsrList.yml - type: trsr - type: List - - method: listpatch - name: item - source: - - name: ItemList.yml - type: item - type: List -title: Randomizer Seed diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py deleted file mode 100644 index 97874da2d0..0000000000 --- a/worlds/kh2/test/TestGoal.py +++ /dev/null @@ -1,30 +0,0 @@ -from . import KH2TestBase -from ..Names import ItemName - - -class TestDefault(KH2TestBase): - options = {} - - def testEverything(self): - self.collect_all_but([ItemName.Victory]) - self.assertBeatable(True) - - -class TestLuckyEmblem(KH2TestBase): - options = { - "Goal": 1, - } - - def testEverything(self): - self.collect_all_but([ItemName.LuckyEmblem]) - self.assertBeatable(True) - - -class TestHitList(KH2TestBase): - options = { - "Goal": 2, - } - - def testEverything(self): - self.collect_all_but([ItemName.Bounty]) - self.assertBeatable(True) diff --git a/worlds/kh2/test/TestSlotData.py b/worlds/kh2/test/TestSlotData.py deleted file mode 100644 index 656cd48d5a..0000000000 --- a/worlds/kh2/test/TestSlotData.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest - -from test.general import setup_solo_multiworld -from . import KH2TestBase -from .. import KH2World, all_locations, item_dictionary_table, CheckDupingItems, AllWeaponSlot, KH2Item -from ..Names import ItemName -from ... import AutoWorldRegister -from ...AutoWorld import call_all - - -class TestLocalItems(KH2TestBase): - - def testSlotData(self): - gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") - multiworld = setup_solo_multiworld(KH2World, gen_steps) - for location in multiworld.get_locations(): - if location.item is None: - location.place_locked_item(multiworld.worlds[1].create_item(ItemName.NoExperience)) - call_all(multiworld, "fill_slot_data") - slotdata = multiworld.worlds[1].fill_slot_data() - assert len(slotdata["LocalItems"]) > 0, f"{slotdata['LocalItems']} is empty" diff --git a/worlds/kh2/test/__init__.py b/worlds/kh2/test/__init__.py index dfef227627..6cefe6e791 100644 --- a/worlds/kh2/test/__init__.py +++ b/worlds/kh2/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class KH2TestBase(WorldTestBase): diff --git a/worlds/kh2/test/test_fight_logic.py b/worlds/kh2/test/test_fight_logic.py new file mode 100644 index 0000000000..0c47d132f0 --- /dev/null +++ b/worlds/kh2/test/test_fight_logic.py @@ -0,0 +1,19 @@ +from . import KH2TestBase + + +class TestEasy(KH2TestBase): + options = { + "FightLogic": 0 + } + + +class TestNormal(KH2TestBase): + options = { + "FightLogic": 1 + } + + +class TestHard(KH2TestBase): + options = { + "FightLogic": 2 + } diff --git a/worlds/kh2/test/test_form_logic.py b/worlds/kh2/test/test_form_logic.py new file mode 100644 index 0000000000..1cd850a985 --- /dev/null +++ b/worlds/kh2/test/test_form_logic.py @@ -0,0 +1,214 @@ +from . import KH2TestBase +from ..Names import ItemName, LocationName + +global_all_possible_forms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm] + [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal] + + +class KH2TestFormBase(KH2TestBase): + allForms = [ItemName.ValorForm, ItemName.WisdomForm, ItemName.LimitForm, ItemName.MasterForm, ItemName.FinalForm] + autoForms = [ItemName.AutoValor, ItemName.AutoWisdom, ItemName.AutoLimit, ItemName.AutoMaster, ItemName.AutoFinal] + allLevel2 = [LocationName.Valorlvl2, LocationName.Wisdomlvl2, LocationName.Limitlvl2, LocationName.Masterlvl2, + LocationName.Finallvl2] + allLevel3 = [LocationName.Valorlvl3, LocationName.Wisdomlvl3, LocationName.Limitlvl3, LocationName.Masterlvl3, + LocationName.Finallvl3] + allLevel4 = [LocationName.Valorlvl4, LocationName.Wisdomlvl4, LocationName.Limitlvl4, LocationName.Masterlvl4, + LocationName.Finallvl4] + allLevel5 = [LocationName.Valorlvl5, LocationName.Wisdomlvl5, LocationName.Limitlvl5, LocationName.Masterlvl5, + LocationName.Finallvl5] + allLevel6 = [LocationName.Valorlvl6, LocationName.Wisdomlvl6, LocationName.Limitlvl6, LocationName.Masterlvl6, + LocationName.Finallvl6] + allLevel7 = [LocationName.Valorlvl7, LocationName.Wisdomlvl7, LocationName.Limitlvl7, LocationName.Masterlvl7, + LocationName.Finallvl7] + driveToAuto = { + ItemName.FinalForm: ItemName.AutoFinal, + ItemName.MasterForm: ItemName.AutoMaster, + ItemName.LimitForm: ItemName.AutoLimit, + ItemName.WisdomForm: ItemName.AutoWisdom, + ItemName.ValorForm: ItemName.AutoValor, + } + AutoToDrive = {Auto: Drive for Drive, Auto in driveToAuto.items()} + driveFormMap = { + ItemName.ValorForm: [LocationName.Valorlvl2, + LocationName.Valorlvl3, + LocationName.Valorlvl4, + LocationName.Valorlvl5, + LocationName.Valorlvl6, + LocationName.Valorlvl7], + ItemName.WisdomForm: [LocationName.Wisdomlvl2, + LocationName.Wisdomlvl3, + LocationName.Wisdomlvl4, + LocationName.Wisdomlvl5, + LocationName.Wisdomlvl6, + LocationName.Wisdomlvl7], + ItemName.LimitForm: [LocationName.Limitlvl2, + LocationName.Limitlvl3, + LocationName.Limitlvl4, + LocationName.Limitlvl5, + LocationName.Limitlvl6, + LocationName.Limitlvl7], + ItemName.MasterForm: [LocationName.Masterlvl2, + LocationName.Masterlvl3, + LocationName.Masterlvl4, + LocationName.Masterlvl5, + LocationName.Masterlvl6, + LocationName.Masterlvl7], + ItemName.FinalForm: [LocationName.Finallvl2, + LocationName.Finallvl3, + LocationName.Finallvl4, + LocationName.Finallvl5, + LocationName.Finallvl6, + LocationName.Finallvl7], + } + # global_all_possible_forms = allForms + autoForms + + +class TestDefaultForms(KH2TestFormBase): + """ + Test default form access rules. + """ + options = { + "AutoFormLogic": False, + "FinalFormLogic": "light_and_darkness" + } + + def test_default_Auto_Form_Logic(self): + allPossibleForms = global_all_possible_forms + # this tests with a light and darkness in the inventory. + self.collect_all_but(allPossibleForms) + for form in self.allForms: + self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form) + self.collect(self.get_item_by_name(self.driveToAuto[form])) + self.assertFalse((self.can_reach_location(self.driveFormMap[form][0])), form) + + def test_default_Final_Form(self): + allPossibleForms = global_all_possible_forms + self.collect_all_but(allPossibleForms) + self.collect_by_name(ItemName.FinalForm) + self.assertTrue((self.can_reach_location(LocationName.Finallvl2))) + self.assertTrue((self.can_reach_location(LocationName.Finallvl3))) + self.assertFalse((self.can_reach_location(LocationName.Finallvl4))) + + def test_default_without_LnD(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + for form, levels in self.driveFormMap.items(): + # final form is unique and breaks using this test. Tested above. + if levels[0] == LocationName.Finallvl2: + continue + for driveForm in self.allForms: + if self.count(driveForm) >= 1: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.collect(self.get_item_by_name(form)) + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + self.assertTrue((self.can_reach_location(levels[0])), levels[0]) + self.assertTrue((self.can_reach_location(levels[1])), levels[1]) + self.assertFalse((self.can_reach_location(levels[2])), levels[2]) + for i in range(3): + self.collect(self.get_item_by_name(allFormsCopy[i])) + # for some reason after collecting a form it can pick up light and darkness + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + + self.assertTrue((self.can_reach_location(levels[2 + i]))) + if i < 2: + self.assertFalse((self.can_reach_location(levels[3 + i]))) + else: + self.collect(self.get_item_by_name(allFormsCopy[i + 1])) + for _ in range(self.count(ItemName.LightDarkness)): + self.remove(self.get_item_by_name(ItemName.LightDarkness)) + self.assertTrue((self.can_reach_location(levels[3 + i]))) + + def test_default_with_lnd(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + for form, levels in self.driveFormMap.items(): + if form != ItemName.FinalForm: + for driveForm in self.allForms: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.collect(self.get_item_by_name(ItemName.LightDarkness)) + self.assertFalse((self.can_reach_location(levels[0]))) + self.collect(self.get_item_by_name(form)) + + self.assertTrue((self.can_reach_location(levels[0]))) + self.assertTrue((self.can_reach_location(levels[1]))) + self.assertTrue((self.can_reach_location(levels[2]))) + self.assertFalse((self.can_reach_location(levels[3]))) + for i in range(2): + self.collect(self.get_item_by_name(allFormsCopy[i])) + self.assertTrue((self.can_reach_location(levels[i + 3]))) + if i <= 2: + self.assertFalse((self.can_reach_location(levels[i + 4]))) + + +class TestJustAForm(KH2TestFormBase): + # this test checks if you can unlock final form with just a form. + options = { + "AutoFormLogic": False, + "FinalFormLogic": "just_a_form" + } + + def test_just_a_form_connections(self): + allPossibleForms = self.allForms + self.collect_all_but(allPossibleForms) + allPossibleForms.remove(ItemName.FinalForm) + for form, levels in self.driveFormMap.items(): + for driveForm in self.allForms: + for _ in range(self.count(driveForm)): + self.remove(self.get_item_by_name(driveForm)) + if form != ItemName.FinalForm: + # reset the forms + allFormsCopy = self.allForms.copy() + allFormsCopy.remove(form) + self.assertFalse((self.can_reach_location(levels[0]))) + self.collect(self.get_item_by_name(form)) + self.assertTrue((self.can_reach_location(levels[0]))) + self.assertTrue((self.can_reach_location(levels[1]))) + self.assertTrue((self.can_reach_location(levels[2]))) + + # level 4 of a form. This tests if the player can unlock final form. + self.assertFalse((self.can_reach_location(levels[3]))) + # amount of forms left in the pool are 3. 1 already collected and one is final form. + for i in range(3): + allFormsCopy.remove(allFormsCopy[0]) + # so we don't accidentally collect another form like light and darkness in the above tests. + self.collect_all_but(allFormsCopy) + self.assertTrue((self.can_reach_location(levels[3 + i])), levels[3 + i]) + if i < 2: + self.assertFalse((self.can_reach_location(levels[4 + i])), levels[4 + i]) + + +class TestAutoForms(KH2TestFormBase): + options = { + "AutoFormLogic": True, + "FinalFormLogic": "light_and_darkness" + } + + def test_Nothing(self): + KH2TestBase() + + def test_auto_forms_level_progression(self): + allPossibleForms = self.allForms + [ItemName.LightDarkness] + # state has all auto forms + self.collect_all_but(allPossibleForms) + allPossibleFormsCopy = allPossibleForms.copy() + collectedDrives = [] + i = 0 + for form in allPossibleForms: + currentDriveForm = form + collectedDrives += [currentDriveForm] + allPossibleFormsCopy.remove(currentDriveForm) + self.collect_all_but(allPossibleFormsCopy) + for driveForm in self.allForms: + # +1 every iteration. + self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i])), driveForm) + # making sure having the form still gives an extra drive level to its own form. + if driveForm in collectedDrives and i < 5: + self.assertTrue((self.can_reach_location(self.driveFormMap[driveForm][i + 1])), driveForm) + i += 1 diff --git a/worlds/kh2/test/test_goal.py b/worlds/kh2/test/test_goal.py new file mode 100644 index 0000000000..1a481ad3d9 --- /dev/null +++ b/worlds/kh2/test/test_goal.py @@ -0,0 +1,59 @@ +from . import KH2TestBase +from ..Names import ItemName + + +class TestDefault(KH2TestBase): + options = {} + + +class TestThreeProofs(KH2TestBase): + options = { + "Goal": 0, + } + + +class TestLuckyEmblem(KH2TestBase): + options = { + "Goal": 1, + } + + +class TestHitList(KH2TestBase): + options = { + "Goal": 2, + } + + +class TestLuckyEmblemHitlist(KH2TestBase): + options = { + "Goal": 3, + } + + +class TestThreeProofsNoXemnas(KH2TestBase): + options = { + "Goal": 0, + "FinalXemnas": False, + } + + +class TestLuckyEmblemNoXemnas(KH2TestBase): + options = { + "Goal": 1, + "FinalXemnas": False, + } + + +class TestHitListNoXemnas(KH2TestBase): + options = { + "Goal": 2, + "FinalXemnas": False, + } + + +class TestLuckyEmblemHitlistNoXemnas(KH2TestBase): + options = { + "Goal": 3, + "FinalXemnas": False, + } + From d46e68cb5fdeb68674e4ffcb2fdf591067d456e8 Mon Sep 17 00:00:00 2001 From: Dinopony Date: Sat, 25 Nov 2023 16:00:15 +0100 Subject: [PATCH 232/327] Landstalker: implement new game (#1808) Co-authored-by: Anthony Demarcy Co-authored-by: Phar --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/landstalker/Hints.py | 140 ++ worlds/landstalker/Items.py | 105 + worlds/landstalker/Locations.py | 53 + worlds/landstalker/Options.py | 228 ++ worlds/landstalker/Regions.py | 118 + worlds/landstalker/Rules.py | 134 ++ worlds/landstalker/__init__.py | 262 +++ worlds/landstalker/data/hint_source.py | 1989 ++++++++++++++++ worlds/landstalker/data/item_source.py | 2017 +++++++++++++++++ worlds/landstalker/data/world_node.py | 411 ++++ worlds/landstalker/data/world_path.py | 446 ++++ worlds/landstalker/data/world_region.py | 299 +++ .../landstalker/data/world_teleport_tree.py | 62 + ...andstalker - The Treasures of King Nole.md | 60 + .../landstalker/docs/landstalker_setup_en.md | 119 + worlds/landstalker/docs/ls_guide_ap.png | Bin 0 -> 2283 bytes worlds/landstalker/docs/ls_guide_client.png | Bin 0 -> 86096 bytes worlds/landstalker/docs/ls_guide_emu.png | Bin 0 -> 2598 bytes worlds/landstalker/docs/ls_guide_rom.png | Bin 0 -> 3951 bytes 21 files changed, 6447 insertions(+) create mode 100644 worlds/landstalker/Hints.py create mode 100644 worlds/landstalker/Items.py create mode 100644 worlds/landstalker/Locations.py create mode 100644 worlds/landstalker/Options.py create mode 100644 worlds/landstalker/Regions.py create mode 100644 worlds/landstalker/Rules.py create mode 100644 worlds/landstalker/__init__.py create mode 100644 worlds/landstalker/data/hint_source.py create mode 100644 worlds/landstalker/data/item_source.py create mode 100644 worlds/landstalker/data/world_node.py create mode 100644 worlds/landstalker/data/world_path.py create mode 100644 worlds/landstalker/data/world_region.py create mode 100644 worlds/landstalker/data/world_teleport_tree.py create mode 100644 worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md create mode 100644 worlds/landstalker/docs/landstalker_setup_en.md create mode 100644 worlds/landstalker/docs/ls_guide_ap.png create mode 100644 worlds/landstalker/docs/ls_guide_client.png create mode 100644 worlds/landstalker/docs/ls_guide_emu.png create mode 100644 worlds/landstalker/docs/ls_guide_rom.png diff --git a/README.md b/README.md index b51fe00f9a..3508dd1609 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Currently, the following games are supported: * DOOM II * Shivers * Heretic +* Landstalker: The Treasures of King Nole For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index c589b1333c..0764fa9274 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -67,6 +67,9 @@ # Kingdom Hearts 2 /worlds/kh2/ @JaredWeakStrike +# Landstalker: The Treasures of King Nole +/worlds/landstalker/ @Dinopony + # Lingo /worlds/lingo/ @hatkirby diff --git a/worlds/landstalker/Hints.py b/worlds/landstalker/Hints.py new file mode 100644 index 0000000000..93274f1d68 --- /dev/null +++ b/worlds/landstalker/Hints.py @@ -0,0 +1,140 @@ +from typing import TYPE_CHECKING + +from BaseClasses import Location +from .data.hint_source import HINT_SOURCES_JSON + +if TYPE_CHECKING: + from random import Random + from . import LandstalkerWorld + + +def generate_blurry_location_hint(location: Location, random: "Random"): + cleaned_location_name = location.hint_text.lower().translate({ord(c): None for c in "(),:"}) + cleaned_location_name.replace("-", " ") + cleaned_location_name.replace("/", " ") + cleaned_location_name.replace(".", " ") + location_name_words = [w for w in cleaned_location_name.split(" ") if len(w) > 3] + + random_word_1 = "mysterious" + random_word_2 = "place" + if location_name_words: + random_word_1 = random.choice(location_name_words) + location_name_words.remove(random_word_1) + if location_name_words: + random_word_2 = random.choice(location_name_words) + return [random_word_1, random_word_2] + + +def generate_lithograph_hint(world: "LandstalkerWorld"): + hint_text = "It's barely readable:\n" + jewel_items = world.jewel_items + + for item in jewel_items: + # Jewel hints are composed of 4 'words' shuffled randomly: + # - the name of the player whose world contains said jewel (if not ours) + # - the color of the jewel (if relevant) + # - two random words from the location name + words = generate_blurry_location_hint(item.location, world.random) + words[0] = words[0].upper() + words[1] = words[1].upper() + if len(jewel_items) < 6: + # Add jewel color if we are not using generic jewels because jewel count is 6 or more + words.append(item.name.split(" ")[0].upper()) + if item.location.player != world.player: + # Add player name if it's not in our own world + player_name = world.multiworld.get_player_name(world.player) + words.append(player_name.upper()) + world.random.shuffle(words) + hint_text += " ".join(words) + "\n" + return hint_text.rstrip("\n") + + +def generate_random_hints(world: "LandstalkerWorld"): + hints = {} + hint_texts = [] + random = world.random + multiworld = world.multiworld + this_player = world.player + + # Exclude Life Stock from the hints as some of them are considered as progression for Fahl, but isn't really + # exciting when hinted + excluded_items = ["Life Stock", "EkeEke"] + + progression_items = [item for item in multiworld.itempool if item.advancement and + item.name not in excluded_items] + + local_own_progression_items = [item for item in progression_items if item.player == this_player + and item.location.player == this_player] + remote_own_progression_items = [item for item in progression_items if item.player == this_player + and item.location.player != this_player] + local_unowned_progression_items = [item for item in progression_items if item.player != this_player + and item.location.player == this_player] + remote_unowned_progression_items = [item for item in progression_items if item.player != this_player + and item.location.player != this_player] + + # Hint-type #1: Own progression item in own world + for item in local_own_progression_items: + region_hint = item.location.parent_region.hint_text + hint_texts.append(f"I can sense {item.name} {region_hint}.") + + # Hint-type #2: Remote progression item in own world + for item in local_unowned_progression_items: + other_player = multiworld.get_player_name(item.player) + own_local_region = item.location.parent_region.hint_text + hint_texts.append(f"You might find something useful for {other_player} {own_local_region}. " + f"It is a {item.name}, to be precise.") + + # Hint-type #3: Own progression item in remote location + for item in remote_own_progression_items: + other_player = multiworld.get_player_name(item.location.player) + if item.location.game == "Landstalker - The Treasures of King Nole": + region_hint_name = item.location.parent_region.hint_text + hint_texts.append(f"If you need {item.name}, tell {other_player} to look {region_hint_name}.") + else: + [word_1, word_2] = generate_blurry_location_hint(item.location, random) + if word_1 == "mysterious" and word_2 == "place": + continue + hint_texts.append(f"Looking for {item.name}? I read something about {other_player}'s world... " + f"Does \"{word_1} {word_2}\" remind you anything?") + + # Hint-type #4: Remote progression item in remote location + for item in remote_unowned_progression_items: + owner_name = multiworld.get_player_name(item.player) + if item.location.player == item.player: + world_name = "their own world" + else: + world_name = f"{multiworld.get_player_name(item.location.player)}'s world" + [word_1, word_2] = generate_blurry_location_hint(item.location, random) + if word_1 == "mysterious" and word_2 == "place": + continue + hint_texts.append(f"I once found {owner_name}'s {item.name} in {world_name}. " + f"I remember \"{word_1} {word_2}\"... Does that make any sense?") + + # Hint-type #5: Jokes + other_player_names = [multiworld.get_player_name(player) for player in multiworld.player_ids if + player != this_player] + if other_player_names: + random_player_name = random.choice(other_player_names) + hint_texts.append(f"{random_player_name}'s world is objectively better than yours.") + + hint_texts.append(f"Have you found all of the {len(multiworld.itempool)} items in this universe?") + + local_progression_item_count = len(local_own_progression_items) + len(local_unowned_progression_items) + remote_progression_item_count = len(remote_own_progression_items) + len(remote_unowned_progression_items) + percent = (local_progression_item_count / (local_progression_item_count + remote_progression_item_count)) * 100 + hint_texts.append(f"Did you know that your world contains {int(percent)} percent of all progression items?") + + # Shuffle hint texts and hint source names, and pair the two of those together + hint_texts = list(set(hint_texts)) + random.shuffle(hint_texts) + + hint_count = world.options.hint_count.value + del hint_texts[hint_count:] + + hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if + source["description"].startswith("Foxy")] + random.shuffle(hint_source_names) + + for i in range(hint_count): + hints[hint_source_names[i]] = hint_texts[i] + return hints diff --git a/worlds/landstalker/Items.py b/worlds/landstalker/Items.py new file mode 100644 index 0000000000..ad7efa1cb2 --- /dev/null +++ b/worlds/landstalker/Items.py @@ -0,0 +1,105 @@ +from typing import Dict, List, NamedTuple + +from BaseClasses import Item, ItemClassification + +BASE_ITEM_ID = 4000 + + +class LandstalkerItem(Item): + game: str = "Landstalker - The Treasures of King Nole" + price_in_shops: int + + +class LandstalkerItemData(NamedTuple): + id: int + classification: ItemClassification + price_in_shops: int + quantity: int = 1 + + +item_table: Dict[str, LandstalkerItemData] = { + "EkeEke": LandstalkerItemData(0, ItemClassification.filler, 20, 0), # Variable amount + "Magic Sword": LandstalkerItemData(1, ItemClassification.useful, 300), + "Sword of Ice": LandstalkerItemData(2, ItemClassification.useful, 300), + "Thunder Sword": LandstalkerItemData(3, ItemClassification.useful, 500), + "Sword of Gaia": LandstalkerItemData(4, ItemClassification.progression, 300), + "Fireproof": LandstalkerItemData(5, ItemClassification.progression, 150), + "Iron Boots": LandstalkerItemData(6, ItemClassification.progression, 150), + "Healing Boots": LandstalkerItemData(7, ItemClassification.useful, 300), + "Snow Spikes": LandstalkerItemData(8, ItemClassification.progression, 400), + "Steel Breast": LandstalkerItemData(9, ItemClassification.useful, 200), + "Chrome Breast": LandstalkerItemData(10, ItemClassification.useful, 350), + "Shell Breast": LandstalkerItemData(11, ItemClassification.useful, 500), + "Hyper Breast": LandstalkerItemData(12, ItemClassification.useful, 700), + "Mars Stone": LandstalkerItemData(13, ItemClassification.useful, 150), + "Moon Stone": LandstalkerItemData(14, ItemClassification.useful, 150), + "Saturn Stone": LandstalkerItemData(15, ItemClassification.useful, 200), + "Venus Stone": LandstalkerItemData(16, ItemClassification.useful, 300), + # Awakening Book: 17 + "Detox Grass": LandstalkerItemData(18, ItemClassification.filler, 25, 9), + "Statue of Gaia": LandstalkerItemData(19, ItemClassification.filler, 75, 12), + "Golden Statue": LandstalkerItemData(20, ItemClassification.filler, 150, 10), + "Mind Repair": LandstalkerItemData(21, ItemClassification.filler, 25, 7), + "Casino Ticket": LandstalkerItemData(22, ItemClassification.progression, 50), + "Axe Magic": LandstalkerItemData(23, ItemClassification.progression, 400), + "Blue Ribbon": LandstalkerItemData(24, ItemClassification.filler, 50), + "Buyer Card": LandstalkerItemData(25, ItemClassification.progression, 150), + "Lantern": LandstalkerItemData(26, ItemClassification.progression, 200), + "Garlic": LandstalkerItemData(27, ItemClassification.progression, 150, 2), + "Anti Paralyze": LandstalkerItemData(28, ItemClassification.filler, 20, 7), + "Statue of Jypta": LandstalkerItemData(29, ItemClassification.useful, 250), + "Sun Stone": LandstalkerItemData(30, ItemClassification.progression, 300), + "Armlet": LandstalkerItemData(31, ItemClassification.progression, 300), + "Einstein Whistle": LandstalkerItemData(32, ItemClassification.progression, 200), + "Blue Jewel": LandstalkerItemData(33, ItemClassification.progression, 500, 0), # Detox Book in base game + "Yellow Jewel": LandstalkerItemData(34, ItemClassification.progression, 500, 0), # AntiCurse Book in base game + # Record Book: 35 + # Spell Book: 36 + # Hotel Register: 37 + # Island Map: 38 + "Lithograph": LandstalkerItemData(39, ItemClassification.progression, 250), + "Red Jewel": LandstalkerItemData(40, ItemClassification.progression, 500, 0), + "Pawn Ticket": LandstalkerItemData(41, ItemClassification.useful, 200, 4), + "Purple Jewel": LandstalkerItemData(42, ItemClassification.progression, 500, 0), + "Gola's Eye": LandstalkerItemData(43, ItemClassification.progression, 400), + "Death Statue": LandstalkerItemData(44, ItemClassification.filler, 150), + "Dahl": LandstalkerItemData(45, ItemClassification.filler, 100, 18), + "Restoration": LandstalkerItemData(46, ItemClassification.filler, 40, 9), + "Logs": LandstalkerItemData(47, ItemClassification.progression, 100, 2), + "Oracle Stone": LandstalkerItemData(48, ItemClassification.progression, 250), + "Idol Stone": LandstalkerItemData(49, ItemClassification.progression, 200), + "Key": LandstalkerItemData(50, ItemClassification.progression, 150), + "Safety Pass": LandstalkerItemData(51, ItemClassification.progression, 250), + "Green Jewel": LandstalkerItemData(52, ItemClassification.progression, 500, 0), # No52 in base game + "Bell": LandstalkerItemData(53, ItemClassification.useful, 200), + "Short Cake": LandstalkerItemData(54, ItemClassification.useful, 250), + "Gola's Nail": LandstalkerItemData(55, ItemClassification.progression, 800), + "Gola's Horn": LandstalkerItemData(56, ItemClassification.progression, 800), + "Gola's Fang": LandstalkerItemData(57, ItemClassification.progression, 800), + # Broad Sword: 58 + # Leather Breast: 59 + # Leather Boots: 60 + # No Ring: 61 + "Life Stock": LandstalkerItemData(62, ItemClassification.filler, 250, 0), # Variable amount + "No Item": LandstalkerItemData(63, ItemClassification.filler, 0, 0), + "1 Gold": LandstalkerItemData(64, ItemClassification.filler, 1), + "20 Golds": LandstalkerItemData(65, ItemClassification.filler, 20, 15), + "50 Golds": LandstalkerItemData(66, ItemClassification.filler, 50, 7), + "100 Golds": LandstalkerItemData(67, ItemClassification.filler, 100, 5), + "200 Golds": LandstalkerItemData(68, ItemClassification.useful, 200, 2), + + "Progressive Armor": LandstalkerItemData(69, ItemClassification.useful, 250, 0), + "Kazalt Jewel": LandstalkerItemData(70, ItemClassification.progression, 500, 0) +} + + +def get_weighted_filler_item_names(): + weighted_item_names: List[str] = [] + for name, data in item_table.items(): + if data.classification == ItemClassification.filler: + weighted_item_names += [name for _ in range(data.quantity)] + return weighted_item_names + + +def build_item_name_to_id_table(): + return {name: data.id + BASE_ITEM_ID for name, data in item_table.items()} diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py new file mode 100644 index 0000000000..5e42fbecda --- /dev/null +++ b/worlds/landstalker/Locations.py @@ -0,0 +1,53 @@ +from typing import Dict, Optional + +from BaseClasses import Location +from .Regions import LandstalkerRegion +from .data.item_source import ITEM_SOURCES_JSON + +BASE_LOCATION_ID = 4000 +BASE_GROUND_LOCATION_ID = BASE_LOCATION_ID + 256 +BASE_SHOP_LOCATION_ID = BASE_GROUND_LOCATION_ID + 30 +BASE_REWARD_LOCATION_ID = BASE_SHOP_LOCATION_ID + 50 + + +class LandstalkerLocation(Location): + game: str = "Landstalker - The Treasures of King Nole" + type_string: str + price: int = 0 + + def __init__(self, player: int, name: str, location_id: Optional[int], region: LandstalkerRegion, type_string: str): + super().__init__(player, name, location_id, region) + self.type_string = type_string + + +def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], name_to_id_table: Dict[str, int]): + # Create real locations from the data inside the corresponding JSON file + for data in ITEM_SOURCES_JSON: + region_id = data["nodeId"] + region = regions_table[region_id] + new_location = LandstalkerLocation(player, data["name"], name_to_id_table[data["name"]], region, data["type"]) + region.locations.append(new_location) + + # Create a specific end location that will contain a fake win-condition item + end_location = LandstalkerLocation(player, "End", None, regions_table["end"], "reward") + regions_table["end"].locations.append(end_location) + + +def build_location_name_to_id_table(): + location_name_to_id_table = {} + + for data in ITEM_SOURCES_JSON: + if data["type"] == "chest": + location_id = BASE_LOCATION_ID + int(data["chestId"]) + elif data["type"] == "ground": + location_id = BASE_GROUND_LOCATION_ID + int(data["groundItemId"]) + elif data["type"] == "shop": + location_id = BASE_SHOP_LOCATION_ID + int(data["shopItemId"]) + else: # if data["type"] == "reward": + location_id = BASE_REWARD_LOCATION_ID + int(data["rewardId"]) + location_name_to_id_table[data["name"]] = location_id + + # Win condition location ID + location_name_to_id_table["Gola"] = BASE_REWARD_LOCATION_ID + 10 + + return location_name_to_id_table diff --git a/worlds/landstalker/Options.py b/worlds/landstalker/Options.py new file mode 100644 index 0000000000..65ffd2c1f3 --- /dev/null +++ b/worlds/landstalker/Options.py @@ -0,0 +1,228 @@ +from dataclasses import dataclass + +from Options import Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, Range, Toggle + + +class LandstalkerGoal(Choice): + """ + The goal to accomplish in order to complete the seed. + - Beat Gola: beat the usual final boss (same as vanilla) + - Reach Kazalt: find the jewels and take the teleporter to Kazalt + - Beat Dark Nole: the door to King Nole's fight brings you into a final dungeon with an absurdly hard boss you have + to beat to win the game + """ + display_name = "Goal" + + option_beat_gola = 0 + option_reach_kazalt = 1 + option_beat_dark_nole = 2 + + default = 0 + + +class JewelCount(Range): + """ + Determines the number of jewels to find to be able to reach Kazalt. + """ + display_name = "Jewel Count" + range_start = 0 + range_end = 9 + default = 5 + + +class ProgressiveArmors(DefaultOnToggle): + """ + When obtaining an armor, you get the next armor tier instead of getting the specific armor tier that was + placed here by randomization. Enabling this provides a smoother progression. + """ + display_name = "Progressive Armors" + + +class UseRecordBook(DefaultOnToggle): + """ + Gives a Record Book item in starting inventory, allowing to save the game anywhere. + This makes the game significantly less frustrating and enables interesting save-scumming strategies in some places. + """ + display_name = "Use Record Book" + + +class UseSpellBook(DefaultOnToggle): + """ + Gives a Spell Book item in starting inventory, allowing to warp back to the starting location at any time. + This prevents any kind of softlock and makes the world easier to explore. + """ + display_name = "Use Spell Book" + + +class EnsureEkeEkeInShops(DefaultOnToggle): + """ + Ensures an EkeEke will always be for sale in one shop per region in the game. + Disabling this can lead to frustrating situations where you cannot refill your health items and might get locked. + """ + display_name = "Ensure EkeEke in Shops" + + +class RemoveGumiBoulder(Toggle): + """ + Removes the boulder between Gumi and Ryuma, which is usually a one-way path. + This makes the vanilla early game (Massan, Gumi...) more easily accessible when starting outside it. + """ + display_name = "Remove Boulder After Gumi" + + +class EnemyJumpingInLogic(Toggle): + """ + Adds jumping on enemies' heads as a logical rule. + This gives access to Mountainous Area from Lake Shrine sector and to the cliff chest behind a magic tree near Mir Tower. + These tricks not being easy, you should leave this disabled until practiced. + """ + display_name = "Enemy Jumping in Logic" + + +class TreeCuttingGlitchInLogic(Toggle): + """ + Adds tree-cutting glitch as a logical rule, enabling access to both chests behind magic trees in Mir Tower Sector + without having Axe Magic. + """ + display_name = "Tree-cutting Glitch in Logic" + + +class DamageBoostingInLogic(Toggle): + """ + Adds damage boosting as a logical rule, removing any requirements involving Iron Boots or Fireproof Boots. + Who doesn't like walking on spikes and lava? + """ + display_name = "Damage Boosting in Logic" + + +class WhistleUsageBehindTrees(DefaultOnToggle): + """ + In Greenmaze, Einstein Whistle can only be used to call Cutter from the intended side by default. + Enabling this allows using Einstein Whistle from both sides of the magic trees. + This is only useful in seeds starting in the "waterfall" spawn region or where teleportation trees are made open from the start. + """ + display_name = "Allow Using Einstein Whistle Behind Trees" + + +class SpawnRegion(Choice): + """ + List of spawn locations that can be picked by the randomizer. + It is advised to keep Massan as your spawn location for your first few seeds. + Picking a late-game location can make the seed significantly harder, both for logic and combat. + """ + display_name = "Starting Region" + + option_massan = 0 + option_gumi = 1 + option_kado = 2 + option_waterfall = 3 + option_ryuma = 4 + option_mercator = 5 + option_verla = 6 + option_greenmaze = 7 + option_destel = 8 + + default = 0 + + +class TeleportTreeRequirements(Choice): + """ + Determines the requirements to be able to use a teleport tree pair. + - None: All teleport trees are available right from the start + - Clear Tibor: Tibor needs to be cleared before unlocking any tree + - Visit Trees: Both trees from a tree pair need to be visited to teleport between them + Vanilla behavior is "Clear Tibor And Visit Trees" + """ + display_name = "Teleportation Trees Requirements" + + option_none = 0 + option_clear_tibor = 1 + option_visit_trees = 2 + option_clear_tibor_and_visit_trees = 3 + + default = 3 + + +class ShuffleTrees(Toggle): + """ + If enabled, all teleportation trees will be shuffled into new pairs. + """ + display_name = "Shuffle Teleportation Trees" + + +class ReviveUsingEkeeke(DefaultOnToggle): + """ + In the vanilla game, when you die, you are automatically revived by Friday using an EkeEke. + This setting allows disabling this feature, making the game extremely harder. + USE WITH CAUTION! + """ + display_name = "Revive Using EkeEke" + + +class ShopPricesFactor(Range): + """ + Applies a percentage factor on all prices in shops. Having higher prices can lead to a bit of gold farming, which + can make seeds longer but also sometimes more frustrating. + """ + display_name = "Shop Prices Factor (%)" + range_start = 50 + range_end = 200 + default = 100 + + +class CombatDifficulty(Choice): + """ + Determines the overall combat difficulty in the game by modifying both monsters HP & damage. + - Peaceful: 50% HP & damage + - Easy: 75% HP & damage + - Normal: 100% HP & damage + - Hard: 140% HP & damage + - Insane: 200% HP & damage + """ + display_name = "Combat Difficulty" + + option_peaceful = 0 + option_easy = 1 + option_normal = 2 + option_hard = 3 + option_insane = 4 + + default = 2 + + +class HintCount(Range): + """ + Determines the number of Foxy NPCs that will be scattered across the world, giving various types of hints + """ + display_name = "Hint Count" + range_start = 0 + range_end = 25 + default = 12 + + +@dataclass +class LandstalkerOptions(PerGameCommonOptions): + goal: LandstalkerGoal + spawn_region: SpawnRegion + jewel_count: JewelCount + progressive_armors: ProgressiveArmors + use_record_book: UseRecordBook + use_spell_book: UseSpellBook + + shop_prices_factor: ShopPricesFactor + combat_difficulty: CombatDifficulty + + teleport_tree_requirements: TeleportTreeRequirements + shuffle_trees: ShuffleTrees + + ensure_ekeeke_in_shops: EnsureEkeEkeInShops + remove_gumi_boulder: RemoveGumiBoulder + allow_whistle_usage_behind_trees: WhistleUsageBehindTrees + handle_damage_boosting_in_logic: DamageBoostingInLogic + handle_enemy_jumping_in_logic: EnemyJumpingInLogic + handle_tree_cutting_glitch_in_logic: TreeCuttingGlitchInLogic + + hint_count: HintCount + + revive_using_ekeeke: ReviveUsingEkeeke + death_link: DeathLink diff --git a/worlds/landstalker/Regions.py b/worlds/landstalker/Regions.py new file mode 100644 index 0000000000..21704194f1 --- /dev/null +++ b/worlds/landstalker/Regions.py @@ -0,0 +1,118 @@ +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING + +from BaseClasses import MultiWorld, Region +from .data.world_node import WORLD_NODES_JSON +from .data.world_path import WORLD_PATHS_JSON +from .data.world_region import WORLD_REGIONS_JSON +from .data.world_teleport_tree import WORLD_TELEPORT_TREES_JSON + +if TYPE_CHECKING: + from . import LandstalkerWorld + + +class LandstalkerRegion(Region): + code: str + + def __init__(self, code: str, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): + super().__init__(name, player, multiworld, hint) + self.code = code + + +class LandstalkerRegionData(NamedTuple): + locations: Optional[List[str]] + region_exits: Optional[List[str]] + + +def create_regions(world: "LandstalkerWorld"): + regions_table: Dict[str, LandstalkerRegion] = {} + multiworld = world.multiworld + player = world.player + + # Create the hardcoded starting "Menu" region + menu_region = LandstalkerRegion("menu", "Menu", player, multiworld) + regions_table["menu"] = menu_region + multiworld.regions.append(menu_region) + + # Create regions from world_nodes + for code, region_data in WORLD_NODES_JSON.items(): + random_hint_name = None + if "hints" in region_data: + random_hint_name = multiworld.random.choice(region_data["hints"]) + region = LandstalkerRegion(code, region_data["name"], player, multiworld, random_hint_name) + regions_table[code] = region + multiworld.regions.append(region) + + # Create exits/entrances from world_paths + for data in WORLD_PATHS_JSON: + two_way = data["twoWay"] if "twoWay" in data else False + create_entrance(data["fromId"], data["toId"], two_way, regions_table) + + # Create a path between the fake Menu location and the starting location + starting_region = get_starting_region(world, regions_table) + menu_region.connect(starting_region, f"menu -> {starting_region.code}") + + add_specific_paths(world, regions_table) + + return regions_table + + +def add_specific_paths(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): + # If Gumi boulder is removed, add a path from "route_gumi_ryuma" to "gumi" + if world.options.remove_gumi_boulder == 1: + create_entrance("route_gumi_ryuma", "gumi", False, regions_table) + + # If enemy jumping is in logic, Mountainous Area can be reached from route to Lake Shrine by doing a "ghost jump" + # at crossroads map + if world.options.handle_enemy_jumping_in_logic == 1: + create_entrance("route_lake_shrine", "route_lake_shrine_cliff", False, regions_table) + + # If using Einstein Whistle behind trees is allowed, add a new logic path there to reflect that change + if world.options.allow_whistle_usage_behind_trees == 1: + create_entrance("greenmaze_post_whistle", "greenmaze_pre_whistle", False, regions_table) + + +def create_entrance(from_id: str, to_id: str, two_way: bool, regions_table: Dict[str, LandstalkerRegion]): + created_entrances = [] + + name = from_id + " -> " + to_id + from_region = regions_table[from_id] + to_region = regions_table[to_id] + + created_entrances.append(from_region.connect(to_region, name)) + + if two_way: + reverse_name = to_id + " -> " + from_id + created_entrances.append(to_region.connect(from_region, reverse_name)) + + return created_entrances + + +def get_starting_region(world: "LandstalkerWorld", regions_table: Dict[str, LandstalkerRegion]): + # Most spawn locations have the same name as the region they are bound to, but a few vary. + spawn_id = world.options.spawn_region.current_key + if spawn_id == "waterfall": + return regions_table["greenmaze_post_whistle"] + elif spawn_id == "kado": + return regions_table["route_gumi_ryuma"] + elif spawn_id == "greenmaze": + return regions_table["greenmaze_pre_whistle"] + return regions_table[spawn_id] + + +def get_darkenable_regions(): + return {data["name"]: data["nodeIds"] for data in WORLD_REGIONS_JSON if "darkMapIds" in data} + + +def load_teleport_trees(): + pairs = [] + for pair in WORLD_TELEPORT_TREES_JSON: + first_tree = { + 'name': pair[0]["name"], + 'region': pair[0]["nodeId"] + } + second_tree = { + 'name': pair[1]["name"], + 'region': pair[1]["nodeId"] + } + pairs.append([first_tree, second_tree]) + return pairs diff --git a/worlds/landstalker/Rules.py b/worlds/landstalker/Rules.py new file mode 100644 index 0000000000..51357c9480 --- /dev/null +++ b/worlds/landstalker/Rules.py @@ -0,0 +1,134 @@ +from typing import List, TYPE_CHECKING + +from BaseClasses import CollectionState +from .data.world_path import WORLD_PATHS_JSON +from .Locations import LandstalkerLocation +from .Regions import LandstalkerRegion + +if TYPE_CHECKING: + from . import LandstalkerWorld + + +def _landstalker_has_visited_regions(state: CollectionState, player: int, regions): + return all([state.can_reach(region, None, player) for region in regions]) + + +def _landstalker_has_health(state: CollectionState, player: int, health): + return state.has("Life Stock", player, health) + + +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], dark_region_ids: List[str] +def create_rules(world: "LandstalkerWorld"): + # Item & exploration requirements to take paths + add_path_requirements(world) + add_specific_path_requirements(world) + + # Location rules to forbid some item types depending on location types + add_location_rules(world) + + # Win condition + world.multiworld.completion_condition[world.player] = lambda state: state.has("King Nole's Treasure", world.player) + + +# multiworld: MultiWorld, player: int, regions_table: Dict[str, Region], +# dark_region_ids: List[str] +def add_path_requirements(world: "LandstalkerWorld"): + for data in WORLD_PATHS_JSON: + name = data["fromId"] + " -> " + data["toId"] + + # Determine required items to reach this region + required_items = data["requiredItems"] if "requiredItems" in data else [] + if "itemsPlacedWhenCrossing" in data: + required_items += data["itemsPlacedWhenCrossing"] + + if data["toId"] in world.dark_region_ids: + # Make Lantern required to reach the randomly selected dark regions + required_items.append("Lantern") + if world.options.handle_damage_boosting_in_logic: + # If damage boosting is handled in logic, remove all iron boots & fireproof requirements + required_items = [item for item in required_items if item != "Iron Boots" and item != "Fireproof"] + + # Determine required other visited regions to reach this region + required_region_ids = data["requiredNodes"] if "requiredNodes" in data else [] + required_regions = [world.regions_table[region_id] for region_id in required_region_ids] + + if not (required_items or required_regions): + continue + + # Create the rule lambda using those requirements + access_rule = make_path_requirement_lambda(world.player, required_items, required_regions) + world.multiworld.get_entrance(name, world.player).access_rule = access_rule + + # If two-way, also apply the rule to the opposite path + if "twoWay" in data and data["twoWay"] is True: + reverse_name = data["toId"] + " -> " + data["fromId"] + world.multiworld.get_entrance(reverse_name, world.player).access_rule = access_rule + + +def add_specific_path_requirements(world: "LandstalkerWorld"): + multiworld = world.multiworld + player = world.player + + # Make the jewels required to reach Kazalt + jewel_count = world.options.jewel_count.value + path_to_kazalt = multiworld.get_entrance("king_nole_cave -> kazalt", player) + if jewel_count < 6: + # 5- jewels => the player needs to find as many uniquely named jewel items + required_jewels = ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"] + del required_jewels[jewel_count:] + path_to_kazalt.access_rule = make_path_requirement_lambda(player, required_jewels, []) + else: + # 6+ jewels => the player needs to find as many "Kazalt Jewel" items + path_to_kazalt.access_rule = lambda state: state.has("Kazalt Jewel", player, jewel_count) + + # If enemy jumping is enabled, Mir Tower sector first tree can be bypassed to reach the elevated ledge + if world.options.handle_enemy_jumping_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") + + # Both trees in Mir Tower sector can be abused using tree cutting glitch + if world.options.handle_tree_cutting_glitch_in_logic == 1: + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_ledge") + remove_requirements_for(world, "mir_tower_sector -> mir_tower_sector_tree_coast") + + # If Whistle can be used from behind the trees, it adds a new path that requires the whistle as well + if world.options.allow_whistle_usage_behind_trees == 1: + entrance = multiworld.get_entrance("greenmaze_post_whistle -> greenmaze_pre_whistle", player) + entrance.access_rule = make_path_requirement_lambda(player, ["Einstein Whistle"], []) + + +def make_path_requirement_lambda(player: int, required_items: List[str], required_regions: List[LandstalkerRegion]): + """ + Lambdas are created in a for loop, so values need to be captured + """ + return lambda state: \ + state.has_all(set(required_items), player) and _landstalker_has_visited_regions(state, player, required_regions) + + +def make_shop_location_requirement_lambda(player: int, location: LandstalkerLocation): + """ + Lambdas are created in a for loop, so values need to be captured + """ + # Prevent local golds in shops, as well as duplicates + other_locations_in_shop = [loc for loc in location.parent_region.locations if loc != location] + return lambda item: \ + item.player != player \ + or (" Gold" not in item.name + and item.name not in [loc.item.name for loc in other_locations_in_shop if loc.item is not None]) + + +def remove_requirements_for(world: "LandstalkerWorld", entrance_name: str): + entrance = world.multiworld.get_entrance(entrance_name, world.player) + entrance.access_rule = lambda state: True + + +def add_location_rules(world: "LandstalkerWorld"): + location: LandstalkerLocation + for location in world.multiworld.get_locations(world.player): + if location.type_string == "ground": + location.item_rule = lambda item: not (item.player == world.player and " Gold" in item.name) + elif location.type_string == "shop": + location.item_rule = make_shop_location_requirement_lambda(world.player, location) + + # Add a special rule for Fahl + fahl_location = world.multiworld.get_location("Mercator: Fahl's dojo challenge reward", world.player) + fahl_location.access_rule = lambda state: _landstalker_has_health(state, world.player, 15) diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py new file mode 100644 index 0000000000..baa1deb620 --- /dev/null +++ b/worlds/landstalker/__init__.py @@ -0,0 +1,262 @@ +from typing import ClassVar, Set + +from BaseClasses import LocationProgressType, Tutorial +from worlds.AutoWorld import WebWorld, World +from .Hints import * +from .Items import * +from .Locations import * +from .Options import JewelCount, LandstalkerGoal, LandstalkerOptions, ProgressiveArmors, TeleportTreeRequirements +from .Regions import * +from .Rules import * + + +class LandstalkerWeb(WebWorld): + theme = "grass" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Landstalker Randomizer software on your computer.", + "English", + "landstalker_setup_en.md", + "landstalker_setup/en", + ["Dinopony"] + )] + + +class LandstalkerWorld(World): + """ + Landstalker: The Treasures of King Nole is a classic Action-RPG with an isometric view (also known as "2.5D"). + You play Nigel, a treasure hunter exploring the island of Mercator trying to find the legendary treasure. + Roam freely on the island, get stronger to beat dungeons and gather the required key items in order to reach the + hidden palace and claim the treasure. + """ + game = "Landstalker - The Treasures of King Nole" + options_dataclass = LandstalkerOptions + options: LandstalkerOptions + required_client_version = (0, 4, 4) + web = LandstalkerWeb() + + item_name_to_id = build_item_name_to_id_table() + location_name_to_id = build_location_name_to_id_table() + + cached_spheres: ClassVar[List[Set[Location]]] + + def __init__(self, multiworld, player): + super().__init__(multiworld, player) + self.regions_table: Dict[str, LandstalkerRegion] = {} + self.dark_dungeon_id = "None" + self.dark_region_ids = [] + self.teleport_tree_pairs = [] + self.jewel_items = [] + + def fill_slot_data(self) -> dict: + # Generate hints. + self.adjust_shop_prices() + hints = Hints.generate_random_hints(self) + hints["Lithograph"] = Hints.generate_lithograph_hint(self) + hints["Oracle Stone"] = f"It shows {self.dark_dungeon_id}\nenshrouded in darkness." + + # Put options, locations' contents and some additional data inside slot data + options = [ + "goal", "jewel_count", "progressive_armors", "use_record_book", "use_spell_book", "shop_prices_factor", + "combat_difficulty", "teleport_tree_requirements", "shuffle_trees", "ensure_ekeeke_in_shops", + "remove_gumi_boulder", "allow_whistle_usage_behind_trees", "handle_damage_boosting_in_logic", + "handle_enemy_jumping_in_logic", "handle_tree_cutting_glitch_in_logic", "hint_count", "death_link", + "revive_using_ekeeke", + ] + + slot_data = self.options.as_dict(*options) + slot_data["spawn_region"] = self.options.spawn_region.current_key + slot_data["seed"] = self.random.randint(0, 2 ** 32 - 1) + slot_data["dark_region"] = self.dark_dungeon_id + slot_data["hints"] = hints + slot_data["teleport_tree_pairs"] = [[pair[0]["name"], pair[1]["name"]] for pair in self.teleport_tree_pairs] + + # Type hinting for location. + location: LandstalkerLocation + slot_data["location_prices"] = { + location.name: location.price for location in self.multiworld.get_locations(self.player) if location.price} + + return slot_data + + def generate_early(self): + # Randomly pick a set of dark regions where Lantern is needed + darkenable_regions = get_darkenable_regions() + self.dark_dungeon_id = self.random.choice(list(darkenable_regions)) + self.dark_region_ids = darkenable_regions[self.dark_dungeon_id] + + def create_regions(self): + self.regions_table = Regions.create_regions(self) + Locations.create_locations(self.player, self.regions_table, self.location_name_to_id) + self.create_teleportation_trees() + + def create_item(self, name: str, classification_override: Optional[ItemClassification] = None) -> LandstalkerItem: + data = item_table[name] + classification = classification_override or data.classification + item = LandstalkerItem(name, classification, BASE_ITEM_ID + data.id, self.player) + item.price_in_shops = data.price_in_shops + return item + + def create_event(self, name: str) -> LandstalkerItem: + return LandstalkerItem(name, ItemClassification.progression, None, self.player) + + def get_filler_item_name(self) -> str: + return "EkeEke" + + def create_items(self): + item_pool: List[LandstalkerItem] = [] + for name, data in item_table.items(): + # If item is an armor and progressive armors are enabled, transform it into a progressive armor item + if self.options.progressive_armors and "Breast" in name: + name = "Progressive Armor" + item_pool += [self.create_item(name) for _ in range(data.quantity)] + + # If the appropriate setting is on, place one EkeEke in one shop in every town in the game + if self.options.ensure_ekeeke_in_shops: + shops_to_fill = [ + "Massan: Shop item #1", + "Gumi: Inn item #1", + "Ryuma: Inn item", + "Mercator: Shop item #1", + "Verla: Shop item #1", + "Destel: Inn item", + "Route to Lake Shrine: Greedly's shop item #1", + "Kazalt: Shop item #1" + ] + for location_name in shops_to_fill: + self.multiworld.get_location(location_name, self.player).place_locked_item(self.create_item("EkeEke")) + + # Add a fixed amount of progression Life Stock for a specific requirement (Fahl) + fahl_lifestock_req = 15 + item_pool += [self.create_item("Life Stock", ItemClassification.progression) for _ in range(fahl_lifestock_req)] + # Add a unique progression EkeEke for a specific requirement (Cutter) + item_pool.append(self.create_item("EkeEke", ItemClassification.progression)) + + # Add a variable amount of "useful" Life Stock to the pool, depending on the amount of starting Life Stock + # (i.e. on the starting location) + starting_lifestocks = self.get_starting_health() - 4 + lifestock_count = 80 - starting_lifestocks - fahl_lifestock_req + item_pool += [self.create_item("Life Stock") for _ in range(lifestock_count)] + + # Add jewels to the item pool depending on the number of jewels set in generation settings + self.jewel_items = [self.create_item(name) for name in self.get_jewel_names(self.options.jewel_count)] + item_pool += self.jewel_items + + # Add a pre-placed fake win condition item + self.multiworld.get_location("End", self.player).place_locked_item(self.create_event("King Nole's Treasure")) + + # Fill the rest of the item pool with EkeEke + remaining_items = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) + item_pool += [self.create_item(self.get_filler_item_name()) for _ in range(remaining_items)] + + self.multiworld.itempool += item_pool + + def create_teleportation_trees(self): + self.teleport_tree_pairs = load_teleport_trees() + + def pairwise(iterable): + """Yields pairs of elements from the given list -> [0,1], [2,3]...""" + a = iter(iterable) + return zip(a, a) + + # Shuffle teleport tree pairs if the matching setting is on + if self.options.shuffle_trees: + all_trees = [item for pair in self.teleport_tree_pairs for item in pair] + self.random.shuffle(all_trees) + self.teleport_tree_pairs = [[x, y] for x, y in pairwise(all_trees)] + + # If a specific setting is set, teleport trees are potentially active without visiting both sides. + # This means we need to add those as explorable paths for the generation algorithm. + teleport_trees_mode = self.options.teleport_tree_requirements.value + created_entrances = [] + if teleport_trees_mode in [TeleportTreeRequirements.option_none, TeleportTreeRequirements.option_clear_tibor]: + for pair in self.teleport_tree_pairs: + entrances = create_entrance(pair[0]["region"], pair[1]["region"], True, self.regions_table) + created_entrances += entrances + + # Teleport trees are open but require access to Tibor to work + if teleport_trees_mode == TeleportTreeRequirements.option_clear_tibor: + for entrance in created_entrances: + entrance.access_rule = make_path_requirement_lambda(self.player, [], [self.regions_table["tibor"]]) + + def set_rules(self): + Rules.create_rules(self) + + # In "Reach Kazalt" goal, player doesn't have access to Kazalt, King Nole's Labyrinth & King Nole's Palace. + # As a consequence, all locations inside those regions must be excluded, and the teleporter from + # King Nole's Cave to Kazalt must go to the end region instead. + if self.options.goal == LandstalkerGoal.option_reach_kazalt: + kazalt_tp = self.multiworld.get_entrance("king_nole_cave -> kazalt", self.player) + kazalt_tp.connected_region = self.regions_table["end"] + + excluded_regions = [ + "kazalt", + "king_nole_labyrinth_pre_door", + "king_nole_labyrinth_post_door", + "king_nole_labyrinth_exterior", + "king_nole_labyrinth_fall_from_exterior", + "king_nole_labyrinth_raft_entrance", + "king_nole_labyrinth_raft", + "king_nole_labyrinth_sacred_tree", + "king_nole_labyrinth_path_to_palace", + "king_nole_palace" + ] + + for location in self.multiworld.get_locations(self.player): + if location.parent_region.name in excluded_regions: + location.progress_type = LocationProgressType.EXCLUDED + + def get_starting_health(self): + spawn_id = self.options.spawn_region.current_key + if spawn_id == "destel": + return 20 + elif spawn_id == "verla": + return 16 + elif spawn_id in ["waterfall", "mercator", "greenmaze"]: + return 10 + else: + return 4 + + @classmethod + def stage_post_fill(cls, multiworld): + # Cache spheres for hint calculation after fill completes. + cls.cached_spheres = list(multiworld.get_spheres()) + + @classmethod + def stage_modify_multidata(cls, *_): + # Clean up all references in cached spheres after generation completes. + del cls.cached_spheres + + def adjust_shop_prices(self): + # Calculate prices for items in shops once all items have their final position + unknown_items_price = 250 + earlygame_price_factor = 1.0 + endgame_price_factor = 2.0 + factor_diff = endgame_price_factor - earlygame_price_factor + + global_price_factor = self.options.shop_prices_factor / 100.0 + + spheres = self.cached_spheres + sphere_count = len(spheres) + for sphere_id, sphere in enumerate(spheres): + location: LandstalkerLocation # after conditional, we guarantee it's this kind of location. + for location in sphere: + if location.player != self.player or location.type_string != "shop": + continue + + current_playthrough_progression = sphere_id / sphere_count + progression_price_factor = earlygame_price_factor + (current_playthrough_progression * factor_diff) + + price = location.item.price_in_shops \ + if location.item.game == "Landstalker - The Treasures of King Nole" else unknown_items_price + price *= progression_price_factor + price *= global_price_factor + price -= price % 5 + price = max(price, 5) + location.price = int(price) + + @staticmethod + def get_jewel_names(count: JewelCount): + if count < 6: + return ["Red Jewel", "Purple Jewel", "Green Jewel", "Blue Jewel", "Yellow Jewel"][:count] + + return ["Kazalt Jewel"] * count diff --git a/worlds/landstalker/data/hint_source.py b/worlds/landstalker/data/hint_source.py new file mode 100644 index 0000000000..4f22cac4bd --- /dev/null +++ b/worlds/landstalker/data/hint_source.py @@ -0,0 +1,1989 @@ +HINT_SOURCES_JSON = [ + { + "description": "Lithograph", + "smallTextbox": True + }, + { + "description": "Oracle Stone", + "smallTextbox": True + }, + { + "description": "Mercator fortune teller", + "textIds": [ + 654 + ] + }, + { + "description": "King Nole's Cave sign", + "textIds": [ + 253 + ] + }, + { + "description": "Foxy (next to Ryuma's mayor house)", + "entity": { + "mapId": 611, + "position": { + "x": 47, + "y": 25, + "z": 3 + }, + "orientation": "sw" + }, + "nodeId": "ryuma" + }, + { + "description": "Foxy (behind trees in Gumi)", + "entity": { + "mapId": [602, 603], + "position": { + "x": 24, + "y": 35, + "z": 6 + }, + "orientation": "sw" + }, + "nodeId": "gumi" + }, + { + "description": "Foxy (next to Mercator gates)", + "entity": { + "mapId": 454, + "position": { + "x": 18, + "y": 46, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (near basin behind Mercator)", + "entity": { + "mapId": 636, + "position": { + "x": 18, + "y": 27, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (near cabin on Verla Shore)", + "entity": { + "mapId": 468, + "position": { + "x": 24, + "y": 45, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (outside Verla Mines entrance)", + "entity": { + "mapId": 470, + "position": { + "x": 24, + "y": 29, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (room below Thieves Hideout summit)", + "entity": { + "mapId": 221, + "position": { + "x": 29, + "y": 19, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (near waterfall in Mountainous Area)", + "entity": { + "mapId": 485, + "position": { + "x": 42, + "y": 62, + "z": 2 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Mercator Castle left court)", + "entity": { + "mapId": 32, + "position": { + "x": 36, + "y": 38, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on Mercator inn balcony)", + "entity": { + "mapId": 632, + "position": { + "x": 19, + "y": 35, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on a beach between Ryuma and Mercator)", + "entity": { + "mapId": 450, + "position": { + "x": 18, + "y": 28, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (atop Ryuma's lighthouse)", + "entity": { + "mapId": [628, 629], + "position": { + "x": 26, + "y": 21, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "ryuma" + }, + { + "description": "Foxy (looking at dead man in Thieves Hideout)", + "entity": { + "mapId": 210, + "position": { + "x": 25, + "y": 20, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (contemplating water near goddess statue in Thieves Hideout)", + "entity": { + "mapId": [219, 220], + "position": { + "x": 36, + "y": 31, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (after timed trial in Thieves Hideout)", + "entity": { + "mapId": 196, + "position": { + "x": 49, + "y": 24, + "z": 10 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (inside Mercator Castle armory tower)", + "entity": { + "mapId": 106, + "position": { + "x": 31, + "y": 30, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (near Mercator Castle kitchen)", + "entity": { + "mapId": 71, + "position": { + "x": 15, + "y": 19, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (in Mercator Castle library)", + "entity": { + "mapId": 73, + "position": { + "x": 18, + "y": 29, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (in Mercator Dungeon main room)", + "entity": { + "mapId": 38, + "position": { + "x": 24, + "y": 35, + "z": 3 + }, + "orientation": "se" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (in hallway before tower in Mercator Dungeon)", + "entity": { + "mapId": 46, + "position": { + "x": 24, + "y": 13, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (atop Mercator Dungeon tower)", + "entity": { + "mapId": 35, + "position": { + "x": 31, + "y": 31, + "z": 12 + }, + "orientation": "nw" + }, + "nodeId": "mercator_dungeon" + }, + { + "description": "Foxy (inside Mercator Crypt)", + "entity": { + "mapId": 647, + "position": { + "x": 30, + "y": 21, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "crypt" + }, + { + "description": "Foxy (on Verla beach)", + "entity": { + "mapId": 474, + "position": { + "x": 43, + "y": 30, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "verla_shore" + }, + { + "description": "Foxy (spying on house in Verla)", + "entity": { + "mapId": [711, 712], + "position": { + "x": 48, + "y": 29, + "z": 5 + }, + "orientation": "nw" + }, + "nodeId": "verla" + }, + { + "description": "Foxy (on upper Verla shore, reachable from Dex exit)", + "entity": { + "mapId": 530, + "position": { + "x": 18, + "y": 29, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines jar staircase room)", + "entity": { + "mapId": 235, + "position": { + "x": 42, + "y": 22, + "z": 6 + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lizards and crates room)", + "entity": { + "mapId": 239, + "position": { + "x": 32, + "y": 31, + "z": 3, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lava room in Slasher sector)", + "entity": { + "mapId": 252, + "position": { + "x": 16, + "y": 13, + "z": 1, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines room behind lava)", + "entity": { + "mapId": 265, + "position": { + "x": 13, + "y": 16, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (in Verla Mines lava room in Marley sector)", + "entity": { + "mapId": 264, + "position": { + "x": 18, + "y": 19, + "z": 6, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (on small rocky ledge in elevator map near Kelketo shop)", + "entity": { + "mapId": 473, + "position": { + "x": 35, + "y": 25, + "z": 8 + }, + "orientation": "se" + }, + "nodeId": "route_verla_destel" + }, + { + "description": "Foxy (contemplating fast currents below Kelketo shop)", + "entity": { + "mapId": 481, + "position": { + "x": 40, + "y": 48, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "route_verla_destel" + }, + { + "description": "Foxy (in Destel)", + "entity": { + "mapId": 726, + "position": { + "x": 48, + "y": 55, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "destel" + }, + { + "description": "Foxy (contemplating water near boatmaker house in route after Destel)", + "entity": { + "mapId": 489, + "position": { + "x": 23, + "y": 20, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (looking at Lake Shrine from elevated viewpoint)", + "entity": { + "mapId": 525, + "position": { + "x": 53, + "y": 45, + "z": 5 + }, + "orientation": "ne" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (on small floating block in Destel Well)", + "entity": { + "mapId": 275, + "position": { + "x": 27, + "y": 36, + "z": 5 + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (in Destel Well watery hub room)", + "entity": { + "mapId": 283, + "position": { + "x": 34, + "y": 41, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (in Destel Well watery room before boss)", + "entity": { + "mapId": 287, + "position": { + "x": 50, + "y": 46, + "z": 8, + "halfX": True, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "destel_well" + }, + { + "description": "Foxy (at Destel Well exit on Lake Shrine side)", + "entity": { + "mapId": 545, + "position": { + "x": 58, + "y": 18, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (at crossroads on route to Lake Shrine)", + "entity": { + "mapId": 515, + "position": { + "x": 30, + "y": 20, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (on mountainous path to Lake Shrine)", + "entity": { + "mapId": 514, + "position": { + "x": 57, + "y": 24, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (in volcano to Lake Shrine)", + "entity": { + "mapId": 522, + "position": { + "x": 50, + "y": 39, + "z": 6, + "halfX": True, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (next to Lake Shrine door)", + "entity": { + "mapId": 524, + "position": { + "x": 24, + "y": 51, + "z": 2, + "halfX": True + }, + "orientation": "nw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (above Greedly's shop)", + "entity": { + "mapId": 503, + "position": { + "x": 23, + "y": 35, + "z": 8 + }, + "orientation": "se" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (contemplating water near Greedly's teleport tree)", + "entity": { + "mapId": 501, + "position": { + "x": 30, + "y": 26, + "z": 5 + }, + "orientation": "sw" + }, + "nodeId": "route_lake_shrine" + }, + { + "description": "Foxy (in room after golem hops riddle in Lake Shrine)", + "entity": { + "mapId": 298, + "position": { + "x": 21, + "y": 19, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room next to green golem roundabout in Lake Shrine)", + "entity": { + "mapId": 293, + "position": { + "x": 19, + "y": 18, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in Lake Shrine 'throne room')", + "entity": { + "mapId": 327, + "position": { + "x": 31, + "y": 31, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room next to golden golems roundabout in Lake Shrine)", + "entity": { + "mapId": 353, + "position": { + "x": 31, + "y": 20, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in room near white golems roundabout in Lake Shrine)", + "entity": { + "mapId": 329, + "position": { + "x": 25, + "y": 25, + "z": 2, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (next to Mir Tower)", + "entity": { + "mapId": 475, + "position": { + "x": 34, + "y": 17, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (on the way to Mir Tower)", + "entity": { + "mapId": 464, + "position": { + "x": 22, + "y": 40, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (near Twinkle Village)", + "entity": { + "mapId": 461, + "position": { + "x": 20, + "y": 21, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (inside Tibor)", + "entity": { + "mapId": 813, + "position": { + "x": 19, + "y": 32, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "tibor" + }, + { + "description": "Foxy (inside Tibor spikeballs room)", + "entity": { + "mapId": 810, + "position": { + "x": 21, + "y": 33, + "z": 2, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "tibor" + }, + { + "description": "Foxy (near Kado's house)", + "entity": { + "mapId": 430, + "position": { + "x": 24, + "y": 27, + "z": 11 + }, + "orientation": "se" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (in Gumi boulder map)", + "entity": { + "mapId": 449, + "position": { + "x": 48, + "y": 20, + "z": 1, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (at Waterfall Shrine crossroads)", + "entity": { + "mapId": 425, + "position": { + "x": 22, + "y": 56, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (in upstairs room inside Waterfall Shrine)", + "entity": { + "mapId": 182, + "position": { + "x": 29, + "y": 19, + "z": 4 + }, + "orientation": "nw" + }, + "nodeId": "waterfall_shrine" + }, + { + "description": "Foxy (inside Waterfall Shrine pit)", + "entity": { + "mapId": 174, + "position": { + "x": 32, + "y": 29, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "waterfall_shrine" + }, + { + "description": "Foxy (in Massan)", + "entity": { + "mapId": 592, + "position": { + "x": 24, + "y": 46, + "z": 0, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "massan" + }, + { + "description": "Foxy (in room at the bottom of ladders in Massan Cave)", + "entity": { + "mapId": 805, + "position": { + "x": 34, + "y": 30, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (in treasure room of Massan Cave)", + "entity": { + "mapId": 807, + "position": { + "x": 28, + "y": 22, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (bathing in the swamp next to Swamp Shrine entrance)", + "entity": { + "mapId": 433, + "position": { + "x": 39, + "y": 20, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "massan_cave" + }, + { + "description": "Foxy (in side room of Swamp Shrine accessible without Idol Stone)", + "entity": { + "mapId": 10, + "position": { + "x": 25, + "y": 27, + "z": 2, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (in wooden room with falling EkeEke chest in Swamp Shrine)", + "entity": { + "mapId": 7, + "position": { + "x": 29, + "y": 25, + "z": 1, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine carpet room)", + "entity": { + "mapId": 2, + "position": { + "x": 19, + "y": 33, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine spikeball storage room)", + "entity": { + "mapId": 16, + "position": { + "x": 25, + "y": 24, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Swamp Shrine spiked floor room)", + "entity": { + "mapId": 21, + "position": { + "x": 27, + "y": 17, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Mercator Castle backdoor court)", + "entity": { + "mapId": 639, + "position": { + "x": 23, + "y": 15, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "mercator" + }, + { + "description": "Foxy (on Greenmaze / Mountainous Area crossroad)", + "entity": { + "mapId": 460, + "position": { + "x": 16, + "y": 27, + "z": 4 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (below Mountainous Area bridge)", + "entity": { + "mapId": 486, + "position": { + "x": 52, + "y": 45, + "z": 5 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Mountainous Area isolated cave)", + "entity": { + "mapId": 553, + "position": { + "x": 23, + "y": 21, + "z": 3 + }, + "orientation": "ne" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in access to Zak arena inside Mountainous Area)", + "entity": { + "mapId": 487, + "position": { + "x": 44, + "y": 51, + "z": 3 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in Zak arena inside Mountainous Area)", + "entity": { + "mapId": 492, + "position": { + "x": 27, + "y": 55, + "z": 9 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in empty secret room inside Mountainous Area cave)", + "entity": { + "mapId": 552, + "position": { + "x": 24, + "y": 27, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in empty visible room inside Mountainous Area cave)", + "entity": { + "mapId": 547, + "position": { + "x": 23, + "y": 23, + "z": 0, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in waterfall entrance of Mountainous Area cave)", + "entity": { + "mapId": 549, + "position": { + "x": 27, + "y": 40, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on Mir Tower sector crossroads)", + "entity": { + "mapId": 458, + "position": { + "x": 21, + "y": 21, + "z": 1 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "mir_tower_sector" + }, + { + "description": "Foxy (near Mountainous Area teleport tree)", + "entity": { + "mapId": 484, + "position": { + "x": 38, + "y": 57, + "z": 0 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on route to Mountainous Area, in rocky arch map)", + "entity": { + "mapId": 500, + "position": { + "x": 19, + "y": 19, + "z": 7 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (on route to Mountainous Area, in L-shaped turn map)", + "entity": { + "mapId": 540, + "position": { + "x": 16, + "y": 23, + "z": 3 + }, + "orientation": "se", + "halfY": True, + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in map next to Mountainous Area goddess statue)", + "entity": { + "mapId": 518, + "position": { + "x": 38, + "y": 33, + "z": 12 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "mountainous_area" + }, + { + "description": "Foxy (in King Nole's Cave isolated chest room)", + "entity": { + "mapId": 156, + "position": { + "x": 21, + "y": 27, + "z": 0, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in King Nole's Cave crate stairway room)", + "entity": { + "mapId": 158, + "position": { + "x": 29, + "y": 26, + "z": 6 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in room before boulder hallway inside King Nole's Cave)", + "entity": { + "mapId": 147, + "position": { + "x": 26, + "y": 23, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in empty isolated room inside King Nole's Cave)", + "entity": { + "mapId": 162, + "position": { + "x": 26, + "y": 17, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (looking at the waterfall in King Nole's Cave)", + "entity": { + "mapId": 164, + "position": { + "x": 22, + "y": 48, + "z": 1 + }, + "orientation": "sw", + "highPalette": True + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in King Nole's Cave teleporter to Kazalt)", + "entity": { + "mapId": 170, + "position": { + "x": 22, + "y": 27, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "king_nole_cave" + }, + { + "description": "Foxy (in access to Kazalt)", + "entity": { + "mapId": 739, + "position": { + "x": 17, + "y": 28, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "kazalt" + }, + { + "description": "Foxy (on Kazalt bridge)", + "entity": { + "mapId": 737, + "position": { + "x": 46, + "y": 34, + "z": 7 + }, + "orientation": "se" + }, + "nodeId": "kazalt" + }, + { + "description": "Foxy (in Mir Tower 0F isolated chest room)", + "entity": { + "mapId": 757, + "position": { + "x": 19, + "y": 24, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Mir Tower activatable bridge room)", + "entity": { + "mapId": [752, 753], + "position": { + "x": 29, + "y": 34, + "z": 3, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Garlic trial room inside Mir Tower)", + "entity": { + "mapId": 750, + "position": { + "x": 22, + "y": 21, + "z": 4 + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_pre_garlic" + }, + { + "description": "Foxy (in Mir Tower library)", + "entity": { + "mapId": 759, + "position": { + "x": 38, + "y": 29, + "z": 4 + }, + "orientation": "ne" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in Mir Tower priest room)", + "entity": { + "mapId": 775, + "position": { + "x": 23, + "y": 22, + "z": 1, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (right after making Miro flee with Garlic in Mir Tower)", + "entity": { + "mapId": 758, + "position": { + "x": 14, + "y": 34, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in falling spikeballs room inside Mir Tower)", + "entity": { + "mapId": 761, + "position": { + "x": 14, + "y": 24, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in first room of Mir Tower teleporter maze)", + "entity": { + "mapId": 767, + "position": { + "x": 18, + "y": 18, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in small spikeballs room of Mir Tower teleporter maze)", + "entity": { + "mapId": 771, + "position": { + "x": 18, + "y": 18, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in wooden elevators room after Mir Tower teleporter maze)", + "entity": { + "mapId": 779, + "position": { + "x": 32, + "y": 20, + "z": 7, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in room before Mir Tower boss room)", + "entity": { + "mapId": 783, + "position": { + "x": 32, + "y": 19, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (in Mir Tower treasure room)", + "entity": { + "mapId": 781, + "position": { + "x": 53, + "y": 26, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "mir_tower_post_garlic" + }, + { + "description": "Foxy (next to Waterfall Shrine entrance)", + "entity": { + "mapId": 426, + "position": { + "x": 46, + "y": 31, + "z": 0, + "halfX": True, + "halfY": True + }, + "orientation": "sw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (looking at river next to Massan teleport tree)", + "entity": { + "mapId": 424, + "position": { + "x": 44, + "y": 35, + "z": 0 + }, + "orientation": "nw" + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (looking at bush at Swamp Shrine crossroads)", + "entity": { + "mapId": 440, + "position": { + "x": 25, + "y": 42, + "z": 4 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "route_massan_gumi" + }, + { + "description": "Foxy (at Helga's Hut crossroads)", + "entity": { + "mapId": 447, + "position": { + "x": 24, + "y": 17, + "z": 1 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (near Helga's Hut)", + "entity": { + "mapId": 444, + "position": { + "x": 25, + "y": 26, + "z": 7 + }, + "orientation": "sw" + }, + "nodeId": "route_gumi_ryuma" + }, + { + "description": "Foxy (in reapers room at Greenmaze entrance)", + "entity": { + "mapId": 571, + "position": { + "x": 31, + "y": 20, + "z": 6 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (near Greenmaze swamp)", + "entity": { + "mapId": 566, + "position": { + "x": 53, + "y": 51, + "z": 1 + }, + "orientation": "ne" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (spying on Cutter in Greenmaze)", + "entity": { + "mapId": 560, + "position": { + "x": 31, + "y": 52, + "z": 9 + }, + "orientation": "nw" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in sector with red orcs making an elevator appear in Greenmaze)", + "entity": { + "mapId": 565, + "position": { + "x": 50, + "y": 30, + "z": 1 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in center of Greenmaze)", + "entity": { + "mapId": 576, + "position": { + "x": 32, + "y": 38, + "z": 5, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in waterfall sector of Greenmaze)", + "entity": { + "mapId": 568, + "position": { + "x": 29, + "y": 41, + "z": 7, + "halfX": True + }, + "orientation": "ne", + "highPalette": True + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in ropes sector of Greenmaze)", + "entity": { + "mapId": 567, + "position": { + "x": 38, + "y": 28, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in Sun Stone sector of Greenmaze)", + "entity": { + "mapId": 564, + "position": { + "x": 30, + "y": 35, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "greenmaze_pre_whistle" + }, + { + "description": "Foxy (in first chest map of Greenmaze after cutting trees)", + "entity": { + "mapId": 570, + "position": { + "x": 26, + "y": 15, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "greenmaze_post_whistle" + }, + { + "description": "Foxy (near shortcut cavern entrance in Greenmaze after cutting trees)", + "entity": { + "mapId": 569, + "position": { + "x": 20, + "y": 24, + "z": 6, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "greenmaze_post_whistle" + }, + { + "description": "Foxy (in room next to spiked floor and keydoor room in King Nole's Labyrinth)", + "entity": { + "mapId": 380, + "position": { + "x": 17, + "y": 18, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in ice shortcut room in King Nole's Labyrinth)", + "entity": { + "mapId": 390, + "position": { + "x": 19, + "y": 41, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in exterior room of King Nole's Labyrinth)", + "entity": { + "mapId": 362, + "position": { + "x": 35, + "y": 21, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_pre_door" + }, + { + "description": "Foxy (in room above Iron Boots in King Nole's Labyrinth)", + "entity": { + "mapId": 373, + "position": { + "x": 26, + "y": 30, + "z": 2 + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (next to raft starting point in King Nole's Labyrinth)", + "entity": { + "mapId": 406, + "position": { + "x": 46, + "y": 40, + "z": 7 + }, + "orientation": "nw", + "highPalette": True + }, + "nodeId": "king_nole_labyrinth_raft_entrance" + }, + { + "description": "Foxy (in fast boulder room in King Nole's Labyrinth)", + "entity": { + "mapId": 382, + "position": { + "x": 30, + "y": 30, + "z": 7, + "halfX": True + }, + "orientation": "ne" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in first maze room inside King Nole's Labyrinth)", + "entity": { + "mapId": 367, + "position": { + "x": 43, + "y": 38, + "z": 1 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in lava sector of King Nole's Labyrinth)", + "entity": { + "mapId": 399, + "position": { + "x": 23, + "y": 19, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (in hands room inside King Nole's Labyrinth)", + "entity": { + "mapId": 418, + "position": { + "x": 41, + "y": 31, + "z": 7 + }, + "orientation": "sw" + }, + "nodeId": "king_nole_labyrinth_post_door" + }, + { + "description": "Foxy (next to King Nole's Palace entrance)", + "entity": { + "mapId": 422, + "position": { + "x": 27, + "y": 25, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_labyrinth_path_to_palace" + }, + { + "description": "Foxy (in King Nole's Palace entrance room)", + "entity": { + "mapId": 122, + "position": { + "x": 30, + "y": 35, + "z": 8 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in King Nole's Palace jar and moving platforms room)", + "entity": { + "mapId": 126, + "position": { + "x": 27, + "y": 37, + "z": 6 + }, + "orientation": "se" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in King Nole's Palace last chest room)", + "entity": { + "mapId": 125, + "position": { + "x": 25, + "y": 39, + "z": 2 + }, + "orientation": "ne" + }, + "nodeId": "king_nole_palace" + }, + { + "description": "Foxy (in Mercator casino)", + "entity": { + "mapId": 663, + "position": { + "x": 16, + "y": 58, + "z": 0, + "halfX": True, + "halfY": True + }, + "orientation": "ne" + }, + "nodeId": "mercator_casino" + }, + { + "description": "Foxy (in Helga's hut basement)", + "entity": { + "mapId": 479, + "position": { + "x": 20, + "y": 33, + "z": 0, + "halfX": True + }, + "orientation": "sw" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Helga's hut dungeon deepest room)", + "entity": { + "mapId": 802, + "position": { + "x": 28, + "y": 19, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Helga's hut dungeon topmost room)", + "entity": { + "mapId": 786, + "position": { + "x": 25, + "y": 23, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "helga_hut" + }, + { + "description": "Foxy (in Swamp Shrine right aisle room)", + "entity": { + "mapId": 1, + "position": { + "x": 34, + "y": 20, + "z": 2 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (upstairs in Swamp Shrine main hall)", + "entity": { + "mapId": [5, 15], + "position": { + "x": 45, + "y": 24, + "z": 8, + "halfY": True + }, + "orientation": "nw" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in room before boss inside Swamp Shrine)", + "entity": { + "mapId": 30, + "position": { + "x": 19, + "y": 25, + "z": 2, + "halfY": True + }, + "orientation": "se" + }, + "nodeId": "swamp_shrine" + }, + { + "description": "Foxy (in Thieves Hideout entrance room)", + "entity": { + "mapId": [185, 186], + "position": { + "x": 40, + "y": 35, + "z": 2 + }, + "orientation": "se", + "highPalette": True + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in Thieves Hideout room with hidden door behind waterfall)", + "entity": { + "mapId": [192, 193], + "position": { + "x": 30, + "y": 34, + "z": 1 + }, + "orientation": "nw" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in Thieves Hideout double chest room before goddess statue)", + "entity": { + "mapId": 215, + "position": { + "x": 17, + "y": 17, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_pre_key" + }, + { + "description": "Foxy (in hub room after Thieves Hideout keydoor)", + "entity": { + "mapId": 199, + "position": { + "x": 24, + "y": 52, + "z": 2 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (in reward room after Thieves Hideout moving balls riddle)", + "entity": { + "mapId": 205, + "position": { + "x": 32, + "y": 24, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "thieves_hideout_post_key" + }, + { + "description": "Foxy (in Lake Shrine main hallway)", + "entity": { + "mapId": 302, + "position": { + "x": 20, + "y": 19, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "lake_shrine" + }, + { + "description": "Foxy (in triple chest room in Slasher sector of Verla Mines)", + "entity": { + "mapId": 256, + "position": { + "x": 23, + "y": 23, + "z": 0 + }, + "orientation": "sw" + }, + "nodeId": "verla_mines" + }, + { + "description": "Foxy (near teleport tree after Destel)", + "entity": { + "mapId": 488, + "position": { + "x": 28, + "y": 53, + "z": 0 + }, + "orientation": "se" + }, + "nodeId": "route_after_destel" + }, + { + "description": "Foxy (in lower half of mimics room in King Nole's Labyrinth)", + "entity": { + "mapId": 383, + "position": { + "x": 26, + "y": 26, + "z": 2 + }, + "orientation": "nw" + }, + "nodeId": "king_nole_labyrinth_pre_door" + } +] diff --git a/worlds/landstalker/data/item_source.py b/worlds/landstalker/data/item_source.py new file mode 100644 index 0000000000..e0a2d701f4 --- /dev/null +++ b/worlds/landstalker/data/item_source.py @@ -0,0 +1,2017 @@ +ITEM_SOURCES_JSON = [ + { + "name": "Swamp Shrine (0F): chest in room to the right", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 0 + }, + { + "name": "Swamp Shrine (0F): chest in carpet room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 1 + }, + { + "name": "Swamp Shrine (0F): chest in left hallway (accessed by falling from upstairs)", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 2 + }, + { + "name": "Swamp Shrine (0F): falling chest after beating orc", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 3 + }, + { + "name": "Swamp Shrine (0F): chest in room visible from second entrance", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 4 + }, + { + "name": "Swamp Shrine (1F): lower chest in wooden bridges room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 5 + }, + { + "name": "Swamp Shrine (2F): upper chest in wooden bridges room", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 6 + }, + { + "name": "Swamp Shrine (2F): chest on spiked floor room balcony", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 7 + }, + { + "name": "Swamp Shrine (3F): chest in boss arena", + "type": "chest", + "nodeId": "swamp_shrine", + "chestId": 8 + }, + { + "name": "Mercator Dungeon (-1F): chest on elevated path near entrance", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 9 + }, + { + "name": "Mercator Dungeon (-1F): chest in Moralis's cell", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 10 + }, + { + "name": "Mercator Dungeon (-1F): left chest in undeground double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 11 + }, + { + "name": "Mercator Dungeon (-1F): right chest in undeground double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "hidden in the depths of Mercator" + ], + "chestId": 12 + }, + { + "name": "Mercator: castle kitchen chest", + "type": "chest", + "nodeId": "mercator", + "chestId": 13 + }, + { + "name": "Mercator: chest in special shop backroom", + "type": "chest", + "nodeId": "mercator", + "chestId": 14 + }, + { + "name": "Mercator Dungeon (1F): left chest in tower double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 15 + }, + { + "name": "Mercator Dungeon (1F): right chest in tower double chest room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 16 + }, + { + "name": "Mercator: chest in castle tower (ladder revealed by slashing armor)", + "type": "chest", + "nodeId": "mercator", + "hints": [ + "inside a tower" + ], + "chestId": 17 + }, + { + "name": "Mercator Dungeon (4F): chest in topmost tower room", + "type": "chest", + "nodeId": "mercator_dungeon", + "hints": [ + "inside a tower" + ], + "chestId": 18 + }, + { + "name": "King Nole's Palace: chest at entrance", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 19 + }, + { + "name": "King Nole's Palace: chest along central pit", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 20 + }, + { + "name": "King Nole's Palace: chest in floating button room", + "type": "chest", + "nodeId": "king_nole_palace", + "chestId": 21 + }, + { + "name": "King Nole's Cave: chest in second room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 22 + }, + { + "name": "King Nole's Cave: first chest in third room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 24 + }, + { + "name": "King Nole's Cave: second chest in third room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 25 + }, + { + "name": "King Nole's Cave: chest in isolated room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 28 + }, + { + "name": "King Nole's Cave: chest in crate room", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 29 + }, + { + "name": "King Nole's Cave: boulder chase hallway chest", + "type": "chest", + "nodeId": "king_nole_cave", + "chestId": 31 + }, + { + "name": "Waterfall Shrine: chest under entrance hallway", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 33 + }, + { + "name": "Waterfall Shrine: chest near Prospero", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 34 + }, + { + "name": "Waterfall Shrine: chest on right branch of biggest room", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 35 + }, + { + "name": "Waterfall Shrine: upstairs chest", + "type": "chest", + "nodeId": "waterfall_shrine", + "chestId": 36 + }, + { + "name": "Thieves Hideout: chest under water in entrance room", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 38 + }, + { + "name": "Thieves Hideout (back): right chest after teal knight mini-boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 41 + }, + { + "name": "Thieves Hideout (back): left chest after teal knight mini-boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 42 + }, + { + "name": "Thieves Hideout: left chest in Pockets cell", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 43 + }, + { + "name": "Thieves Hideout: right chest in Pockets cell", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 44 + }, + { + "name": "Thieves Hideout (back): second chest in hallway after quick climb trial", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 45 + }, + { + "name": "Thieves Hideout (back): first chest in hallway after quick climb trial", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 46 + }, + { + "name": "Thieves Hideout (back): chest in moving platforms room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 47 + }, + { + "name": "Thieves Hideout (back): chest in falling platforms room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 48 + }, + { + "name": "Thieves Hideout (back): reward chest after moving balls room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 49 + }, + { + "name": "Thieves Hideout: rolling boulder chest near entrance", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 50 + }, + { + "name": "Thieves Hideout: left chest in room on the way to goddess statue", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 52 + }, + { + "name": "Thieves Hideout: right chest in room on the way to goddess statue", + "type": "chest", + "nodeId": "thieves_hideout_pre_key", + "chestId": 53 + }, + { + "name": "Thieves Hideout (back): left chest in room before boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 54 + }, + { + "name": "Thieves Hideout (back): right chest in room before boss", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 55 + }, + { + "name": "Thieves Hideout (back): chest #1 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 56 + }, + { + "name": "Thieves Hideout (back): chest #2 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 57 + }, + { + "name": "Thieves Hideout (back): chest #3 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 58 + }, + { + "name": "Thieves Hideout (back): chest #4 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 59 + }, + { + "name": "Thieves Hideout (back): chest #5 in boss reward room", + "type": "chest", + "nodeId": "thieves_hideout_post_key", + "chestId": 60 + }, + { + "name": "Verla Mines: right chest in double chest room near entrance", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 66 + }, + { + "name": "Verla Mines: left chest in double chest room near entrance", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 67 + }, + { + "name": "Verla Mines: chest on jar staircase room balcony", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 68 + }, + { + "name": "Verla Mines: Dex reward chest", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 69 + }, + { + "name": "Verla Mines: Slasher reward chest", + "type": "chest", + "nodeId": "verla_mines", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 70 + }, + { + "name": "Verla Mines: left chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 71 + }, + { + "name": "Verla Mines: middle chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 72 + }, + { + "name": "Verla Mines: right chest in 3-chests room near Slasher", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 73 + }, + { + "name": "Verla Mines: right chest in button room near elevator shaft leading to Marley", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 74 + }, + { + "name": "Verla Mines: left chest in button room near elevator shaft leading to Marley", + "type": "chest", + "nodeId": "verla_mines", + "chestId": 75 + }, + { + "name": "Verla Mines: chest in hidden room accessed by walking on lava", + "type": "chest", + "nodeId": "verla_mines_behind_lava", + "hints": [ + "in a very hot place" + ], + "chestId": 76 + }, + { + "name": "Destel Well (0F): 4 crates puzzle room chest", + "type": "chest", + "nodeId": "destel_well", + "chestId": 77 + }, + { + "name": "Destel Well (1F): chest on small stairs", + "type": "chest", + "nodeId": "destel_well", + "chestId": 78 + }, + { + "name": "Destel Well (1F): chest on narrow floating ground", + "type": "chest", + "nodeId": "destel_well", + "chestId": 79 + }, + { + "name": "Destel Well (1F): chest in spiky hallway", + "type": "chest", + "nodeId": "destel_well", + "chestId": 80 + }, + { + "name": "Destel Well (2F): chest in ghosts room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 81 + }, + { + "name": "Destel Well (2F): chest in falling platforms room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 82 + }, + { + "name": "Destel Well (2F): right chest in Pockets room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 83 + }, + { + "name": "Destel Well (2F): left chest in Pockets room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 84 + }, + { + "name": "Destel Well (3F): chest in first trapped arena", + "type": "chest", + "nodeId": "destel_well", + "chestId": 85 + }, + { + "name": "Destel Well (3F): chest in trapped giants room", + "type": "chest", + "nodeId": "destel_well", + "chestId": 86 + }, + { + "name": "Destel Well (3F): chest in second trapped arena", + "type": "chest", + "nodeId": "destel_well", + "chestId": 87 + }, + { + "name": "Destel Well (4F): top chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 88 + }, + { + "name": "Destel Well (4F): left chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 89 + }, + { + "name": "Destel Well (4F): bottom chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 90 + }, + { + "name": "Destel Well (4F): right chest in room before boss", + "type": "chest", + "nodeId": "destel_well", + "chestId": 91 + }, + { + "name": "Lake Shrine (-1F): chest in crate room near green golem spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 92 + }, + { + "name": "Lake Shrine (-1F): chest in hallway with hole leading downstairs", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 93 + }, + { + "name": "Lake Shrine (-1F): chest in spikeballs hallway near green golem spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 94 + }, + { + "name": "Lake Shrine (-1F): reward chest for golem hopping puzzle", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 95 + }, + { + "name": "Lake Shrine (-2F): chest on room corner accessed by falling from above", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 96 + }, + { + "name": "Lake Shrine (-2F): lower chest in throne room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 97 + }, + { + "name": "Lake Shrine (-2F): upper chest in throne room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 98 + }, + { + "name": "Lake Shrine (-3F): chest on floating platform in white golems room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 99 + }, + { + "name": "Lake Shrine (-3F): chest near Sword of Ice", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 100 + }, + { + "name": "Lake Shrine (-3F): chest in snake trapping puzzle room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 101 + }, + { + "name": "Lake Shrine (-3F): chest on cube accessed by falling from upstairs", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 102 + }, + { + "name": "Lake Shrine (-3F): chest in watery archway room", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 103 + }, + { + "name": "Lake Shrine (-3F): left reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 104 + }, + { + "name": "Lake Shrine (-3F): middle reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 105 + }, + { + "name": "Lake Shrine (-3F): right reward chest in boss room", + "type": "chest", + "nodeId": "lake_shrine", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 106 + }, + { + "name": "Lake Shrine (-3F): chest near golden golems spinner", + "type": "chest", + "nodeId": "lake_shrine", + "chestId": 107 + }, + { + "name": "King Nole's Labyrinth (0F): chest in exterior room", + "type": "chest", + "nodeId": "king_nole_labyrinth_exterior", + "chestId": 108 + }, + { + "name": "King Nole's Labyrinth (0F): left chest in room after key door", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 109 + }, + { + "name": "King Nole's Labyrinth (0F): right chest in room after key door", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 110 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in maze room with healing tile", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 111 + }, + { + "name": "King Nole's Labyrinth (0F): chest in spike balls room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 112 + }, + { + "name": "King Nole's Labyrinth (-1F): right chest in 3-chest dark room (left side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 113 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in 3-chest dark room (right side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 114 + }, + { + "name": "King Nole's Labyrinth (-1F): left chest in 3-chest dark room (left side)", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 115 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in maze room with two buttons", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 116 + }, + { + "name": "King Nole's Labyrinth (-1F): upper chest in lantern room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 117 + }, + { + "name": "King Nole's Labyrinth (-1F): lower chest in lantern room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 118 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in ice shortcut room", + "type": "chest", + "nodeId": "king_nole_labyrinth_pre_door", + "chestId": 119 + }, + { + "name": "King Nole's Labyrinth (-2F): chest in save room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 120 + }, + { + "name": "King Nole's Labyrinth (-1F): chest in room with button and crates stairway", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 121 + }, + { + "name": "King Nole's Labyrinth (-3F): first chest before Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "chestId": 122 + }, + { + "name": "King Nole's Labyrinth (-3F): second chest before Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "chestId": 123 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Firedemon", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "kept by a threatening guardian", + "in a very hot place" + ], + "chestId": 124 + }, + { + "name": "King Nole's Labyrinth (-2F): chest in four buttons room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 125 + }, + { + "name": "King Nole's Labyrinth (-3F): first chest after falling from raft", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 126 + }, + { + "name": "King Nole's Labyrinth (-3F): left chest in room before Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 127 + }, + { + "name": "King Nole's Labyrinth (-3F): right chest in room before Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "chestId": 128 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 129 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in room after Spinner", + "type": "chest", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 130 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in room before Miro", + "type": "chest", + "nodeId": "king_nole_labyrinth_path_to_palace", + "hints": [ + "close to a waterfall" + ], + "chestId": 131 + }, + { + "name": "King Nole's Labyrinth (-3F): reward chest for beating Miro", + "type": "chest", + "nodeId": "king_nole_labyrinth_path_to_palace", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 132 + }, + { + "name": "King Nole's Labyrinth (-3F): chest in hands room", + "type": "chest", + "nodeId": "king_nole_labyrinth_post_door", + "chestId": 133 + }, + { + "name": "Route between Gumi and Ryuma: chest on the way to Swordsman Kado", + "type": "chest", + "nodeId": "route_gumi_ryuma", + "chestId": 134 + }, + { + "name": "Route between Massan and Gumi: chest on cliff", + "type": "chest", + "nodeId": "route_massan_gumi", + "hints": [ + "near a swamp" + ], + "chestId": 135 + }, + { + "name": "Route between Mercator and Verla: chest on cliff next to tree", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 136 + }, + { + "name": "Route between Mercator and Verla: chest on cliff next to blocked cave", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 137 + }, + { + "name": "Route between Mercator and Verla: chest near Twinkle village", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 138 + }, + { + "name": "Verla Shore: chest on corner cliff after Verla tunnel", + "type": "chest", + "nodeId": "verla_shore", + "chestId": 139 + }, + { + "name": "Verla Shore: chest on highest cliff after Verla tunnel (accessible through Verla mines)", + "type": "chest", + "nodeId": "verla_shore_cliff", + "chestId": 140 + }, + { + "name": "Route to Mir Tower: chest on cliff accessed by pressing hidden switch", + "type": "chest", + "nodeId": "mir_tower_sector", + "chestId": 141 + }, + { + "name": "Route to Mir Tower: chest behind first sacred tree", + "type": "chest", + "nodeId": "mir_tower_sector_tree_ledge", + "chestId": 142 + }, + { + "name": "Verla Shore: chest behind cabin", + "type": "chest", + "nodeId": "verla_shore", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 143 + }, + { + "name": "Route to Destel: chest in map right after Verla mines exit", + "type": "chest", + "nodeId": "route_verla_destel", + "chestId": 144 + }, + { + "name": "Route to Destel: chest in small platform elevator map", + "type": "chest", + "nodeId": "route_verla_destel", + "chestId": 145 + }, + { + "name": "Route to Mir Tower: chest behind second sacred tree", + "type": "chest", + "nodeId": "mir_tower_sector_tree_coast", + "chestId": 146 + }, + { + "name": "Route to Destel: hidden chest in map right before Destel", + "type": "chest", + "nodeId": "route_verla_destel", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 147 + }, + { + "name": "Mountainous Area: chest near teleport tree", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 148 + }, + { + "name": "Mountainous Area: chest on right side of map before the bridge", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 149 + }, + { + "name": "Mountainous Area: hidden chest in L-shaped path", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 150 + }, + { + "name": "Mountainous Area: hidden chest in uppermost path", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 151 + }, + { + "name": "Mountainous Area: isolated chest on cliff in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 152 + }, + { + "name": "Mountainous Area: left chest on wall in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 153 + }, + { + "name": "Mountainous Area: right chest on wall in bridge map", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 154 + }, + { + "name": "Mountainous Area: right chest in map before Zak arena", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 155 + }, + { + "name": "Mountainous Area: left chest in map before Zak arena", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 156 + }, + { + "name": "Route after Destel: chest on tiny cliff", + "type": "chest", + "nodeId": "route_after_destel", + "chestId": 157 + }, + { + "name": "Route after Destel: hidden chest in map after seeing Duke raft", + "type": "chest", + "nodeId": "route_after_destel", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 158 + }, + { + "name": "Route after Destel: visible chest in map after seeing Duke raft", + "type": "chest", + "nodeId": "route_after_destel", + "chestId": 159 + }, + { + "name": "Mountainous Area: chest hidden under rocky arch", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 160 + }, + { + "name": "Route to Lake Shrine: chest on long cliff in crossroads map", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 161 + }, + { + "name": "Route to Lake Shrine: chest on middle cliff in crossroads map (reached from Mountainous Area)", + "type": "chest", + "nodeId": "route_lake_shrine_cliff", + "chestId": 162 + }, + { + "name": "Mountainous Area: chest in map in front of bridge statue", + "type": "chest", + "nodeId": "mountainous_area", + "chestId": 163 + }, + { + "name": "Route to Lake Shrine: right chest in volcano", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 164 + }, + { + "name": "Route to Lake Shrine: left chest in volcano", + "type": "chest", + "nodeId": "route_lake_shrine", + "chestId": 165 + }, + { + "name": "Mountainous Area Cave: chest in small hidden room", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a well-hidden chest", + "in a cave in the mountains" + ], + "chestId": 166 + }, + { + "name": "Mountainous Area Cave: chest in small visible room", + "type": "chest", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a cave in the mountains" + ], + "chestId": 167 + }, + { + "name": "Greenmaze: chest on path to Cutter", + "type": "chest", + "nodeId": "greenmaze_cutter", + "chestId": 168 + }, + { + "name": "Greenmaze: chest on cliff near the swamp", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 169 + }, + { + "name": "Greenmaze: chest between Sunstone and Massan shortcut", + "type": "chest", + "nodeId": "greenmaze_post_whistle", + "chestId": 170 + }, + { + "name": "Greenmaze: chest in mages room", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 171 + }, + { + "name": "Greenmaze: left chest in elbow cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 172 + }, + { + "name": "Greenmaze: right chest in elbow cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "chestId": 173 + }, + { + "name": "Greenmaze: chest in waterfall cave", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 174 + }, + { + "name": "Greenmaze: left chest in hidden room behind waterfall", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 175 + }, + { + "name": "Greenmaze: right chest in hidden room behind waterfall", + "type": "chest", + "nodeId": "greenmaze_pre_whistle", + "hints": [ + "close to a waterfall" + ], + "chestId": 176 + }, + { + "name": "Massan: chest triggered by dog statue", + "type": "chest", + "nodeId": "massan", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 177 + }, + { + "name": "Massan: chest in house nearest to elder house", + "type": "chest", + "nodeId": "massan", + "chestId": 178 + }, + { + "name": "Massan: chest in middle house", + "type": "chest", + "nodeId": "massan", + "chestId": 179 + }, + { + "name": "Massan: chest in house farthest from elder house", + "type": "chest", + "nodeId": "massan", + "chestId": 180 + }, + { + "name": "Gumi: chest on top of bed in house", + "type": "chest", + "nodeId": "gumi", + "chestId": 181 + }, + { + "name": "Gumi: chest in elder house after saving Fara", + "type": "chest", + "nodeId": "gumi_after_swamp_shrine", + "chestId": 182 + }, + { + "name": "Ryuma: chest in mayor's house", + "type": "chest", + "nodeId": "ryuma", + "chestId": 183 + }, + { + "name": "Ryuma: chest in repaired lighthouse", + "type": "chest", + "nodeId": "ryuma_lighthouse_repaired", + "chestId": 184 + }, + { + "name": "Crypt: chest in main room", + "type": "chest", + "nodeId": "crypt", + "chestId": 185 + }, + { + "name": "Crypt: reward chest", + "type": "chest", + "nodeId": "crypt", + "chestId": 186 + }, + { + "name": "Mercator: hidden casino chest", + "type": "chest", + "nodeId": "mercator_casino", + "hints": [ + "hidden in the depths of Mercator", + "in a well-hidden chest" + ], + "chestId": 191 + }, + { + "name": "Mercator: chest in Greenpea's house", + "type": "chest", + "nodeId": "mercator", + "chestId": 192 + }, + { + "name": "Mercator: chest in grandma's house (pot shelving trial)", + "type": "chest", + "nodeId": "mercator", + "chestId": 193 + }, + { + "name": "Verla: chest in well after beating Marley", + "type": "chest", + "nodeId": "verla_after_mines", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 194 + }, + { + "name": "Destel: chest in inn next to innkeeper", + "type": "chest", + "nodeId": "destel", + "chestId": 196 + }, + { + "name": "Mir Tower: timed jump trial chest", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 197 + }, + { + "name": "Mir Tower: chest after mimic room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 198 + }, + { + "name": "Mir Tower: mimic room chest #1", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 199 + }, + { + "name": "Mir Tower: mimic room chest #2", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 200 + }, + { + "name": "Mir Tower: mimic room chest #3", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 201 + }, + { + "name": "Mir Tower: mimic room chest #4", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 202 + }, + { + "name": "Mir Tower: chest in mushroom pit room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 203 + }, + { + "name": "Mir Tower: chest in room next to mummy switch room", + "type": "chest", + "nodeId": "mir_tower_pre_garlic", + "chestId": 204 + }, + { + "name": "Mir Tower: chest in library accessible from teleporter maze", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 205 + }, + { + "name": "Mir Tower: hidden chest in room before library", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "in a well-hidden chest" + ], + "chestId": 206 + }, + { + "name": "Mir Tower: chest in falling spikeballs room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 207 + }, + { + "name": "Mir Tower: chest in timed challenge room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 208 + }, + { + "name": "Mir Tower: chest in room where Miro closes the door", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 209 + }, + { + "name": "Mir Tower: chest after room where Miro closes the door", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "chestId": 210 + }, + { + "name": "Mir Tower: reward chest", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 211 + }, + { + "name": "Mir Tower: right chest in reward room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 212 + }, + { + "name": "Mir Tower: left chest in reward room", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 213 + }, + { + "name": "Mir Tower: chest behind wall accessible after beating Mir", + "type": "chest", + "nodeId": "mir_tower_post_garlic", + "hints": [ + "kept by a threatening guardian" + ], + "chestId": 214 + }, + { + "name": "Witch Helga's Hut: end chest", + "type": "chest", + "nodeId": "helga_hut", + "chestId": 215 + }, + { + "name": "Massan Cave: right chest", + "type": "chest", + "nodeId": "massan_cave", + "chestId": 216 + }, + { + "name": "Massan Cave: left chest", + "type": "chest", + "nodeId": "massan_cave", + "chestId": 217 + }, + { + "name": "Tibor: reward chest after boss", + "type": "chest", + "nodeId": "tibor", + "chestId": 218 + }, + { + "name": "Tibor: chest in spike balls room", + "type": "chest", + "nodeId": "tibor", + "chestId": 219 + }, + { + "name": "Tibor: left chest on 2 chest group", + "type": "chest", + "nodeId": "tibor", + "chestId": 220 + }, + { + "name": "Tibor: right chest on 2 chest group", + "type": "chest", + "nodeId": "tibor", + "chestId": 221 + }, + { + "name": "Gumi: item on furniture in elder's house", + "type": "ground", + "nodeId": "gumi", + "entity": {"mapId": 605, "entityId": 2}, + "groundItemId": 1 + }, + { + "name": "Greenmaze: item behind trees requiring Cutter", + "type": "ground", + "nodeId": "greenmaze_post_whistle", + "entity": {"mapId": 564, "entityId": 0}, + "groundItemId": 2 + }, + { + "name": "Verla Mines: item in the corner of lava filled room", + "type": "ground", + "nodeId": "verla_mines", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 263, "entityId": 7}, + "groundItemId": 3 + }, + { + "name": "Lake Shrine (-3F): item on ground at the SE exit of the golden golems roundabout", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 333, "entityId": 0}, + "groundItemId": 4 + }, + { + "name": "King Nole's Labyrinth (-3F): item on ground behind waterfall after beating Spinner", + "type": "ground", + "nodeId": "king_nole_labyrinth_raft", + "hints": [ + "kept by a threatening guardian" + ], + "entity": {"mapId": 411, "entityId": 0}, + "groundItemId": 5 + }, + { + "name": "Destel Well: item on platform revealed after beating Quake", + "type": "ground", + "nodeId": "destel_well", + "hints": [ + "kept by a threatening guardian" + ], + "entity": {"mapId": 288, "entityId": 0}, + "groundItemId": 6 + }, + { + "name": "King Nole's Labyrinth (-1F): item on ground in ninjas room", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "entity": {"mapId": 374, "entityId": 3}, + "groundItemId": 7 + }, + { + "name": "Massan Cave: item on ground in treasure room", + "type": "ground", + "nodeId": "massan_cave", + "entity": {"mapId": 807, "entityId": 4}, + "groundItemId": 8 + }, + { + "name": "King Nole's Labyrinth (-3F): item on floating hands", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "entity": {"mapId": 418, "entityId": 0}, + "groundItemId": 9 + }, + { + "name": "Lake Shrine (-3F): isolated item on ground requiring raised platform to reach", + "type": "ground", + "nodeId": "lake_shrine", + "entities": [ + {"mapId": 344, "entityId": 0}, + {"mapId": 345, "entityId": 0} + ], + "groundItemId": 10 + }, + { + "name": "King Nole's Labyrinth (-2F): item on ground after falling from exterior room", + "type": "ground", + "nodeId": "king_nole_labyrinth_fall_from_exterior", + "entity": {"mapId": 363, "entityId": 0}, + "groundItemId": 11 + }, + { + "name": "Route after Destel: item on ground on the cliff", + "type": "ground", + "nodeId": "route_after_destel", + "entity": {"mapId": 483, "entityId": 0}, + "groundItemId": 12 + }, + { + "name": "Mountainous Area cave: item on ground behind hidden path", + "type": "ground", + "nodeId": "mountainous_area", + "hints": [ + "in a small cave", + "in a cave in the mountains" + ], + "entity": {"mapId": 553, "entityId": 0}, + "groundItemId": 13 + }, + { + "name": "Witch Helga's Hut: item on furniture", + "type": "ground", + "nodeId": "helga_hut", + "entity": {"mapId": 479, "entityId": 1}, + "groundItemId": 14 + }, + { + "name": "King Nole's Labyrinth (-3F): item on ground climbing back from Firedemon", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 399, "entityId": 0}, + "groundItemId": 15 + }, + { + "name": "Mercator: falling item in castle court", + "type": "ground", + "nodeId": "mercator", + "entity": {"mapId": 32, "entityId": 2}, + "groundItemId": 16 + }, + { + "name": "Lake Shrine (-2F): north item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 0}, + "groundItemId": 17 + }, + { + "name": "Lake Shrine (-2F): south item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 1}, + "groundItemId": 18 + }, + { + "name": "Lake Shrine (-2F): west item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 2}, + "groundItemId": 19 + }, + { + "name": "Lake Shrine (-2F): east item on ground in quadruple items room", + "type": "ground", + "nodeId": "lake_shrine", + "entity": {"mapId": 318, "entityId": 3}, + "groundItemId": 20 + }, + { + "name": "Twinkle Village: first item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 5}, + "groundItemId": 21 + }, + { + "name": "Twinkle Village: second item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 4}, + "groundItemId": 22 + }, + { + "name": "Twinkle Village: third item on ground", + "type": "ground", + "nodeId": "twinkle_village", + "entity": {"mapId": 462, "entityId": 3}, + "groundItemId": 23 + }, + { + "name": "Mir Tower: Priest room item #1", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 7}, + "groundItemId": 24 + }, + { + "name": "Mir Tower: Priest room item #2", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 6}, + "groundItemId": 25 + }, + { + "name": "Mir Tower: Priest room item #3", + "type": "ground", + "nodeId": "mir_tower_post_garlic", + "entity": {"mapId": 775, "entityId": 1}, + "groundItemId": 26 + }, + { + "name": "King Nole's Labyrinth (-2F): Left item dropped by sacred tree", + "type": "ground", + "nodeId": "king_nole_labyrinth_sacred_tree", + "entity": {"mapId": 415, "entityId": 2}, + "groundItemId": 27 + }, + { + "name": "King Nole's Labyrinth (-2F): Right item dropped by sacred tree", + "type": "ground", + "nodeId": "king_nole_labyrinth_sacred_tree", + "entity": {"mapId": 415, "entityId": 1}, + "groundItemId": 28 + }, + { + "name": "King Nole's Labyrinth (-3F): First item on ground before Firedemon", + "type": "ground", + "nodeId": "king_nole_labyrinth_post_door", + "hints": [ + "in a very hot place" + ], + "entity": {"mapId": 400, "entityId": 0}, + "groundItemId": 29 + }, + { + "name": "Massan: Shop item #1", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 1}, + "shopItemId": 1 + }, + { + "name": "Massan: Shop item #2", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 2}, + "shopItemId": 2 + }, + { + "name": "Massan: Shop item #3", + "type": "shop", + "nodeId": "massan", + "entity": {"mapId": 596, "entityId": 3}, + "shopItemId": 3 + }, + { + "name": "Gumi: Inn item #1", + "type": "shop", + "nodeId": "gumi", + "entity": {"mapId": 608, "entityId": 4}, + "shopItemId": 4 + }, + { + "name": "Gumi: Inn item #2", + "type": "shop", + "nodeId": "gumi", + "entity": {"mapId": 608, "entityId": 2}, + "shopItemId": 5 + }, + { + "name": "Ryuma: Shop item #1", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 2}, + "shopItemId": 6 + }, + { + "name": "Ryuma: Shop item #2", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 3}, + "shopItemId": 7 + }, + { + "name": "Ryuma: Shop item #3", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 4}, + "shopItemId": 8 + }, + { + "name": "Ryuma: Shop item #4", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 5}, + "shopItemId": 9 + }, + { + "name": "Ryuma: Shop item #5", + "type": "shop", + "nodeId": "ryuma_after_thieves_hideout", + "entity": {"mapId": 615, "entityId": 6}, + "shopItemId": 10 + }, + { + "name": "Ryuma: Inn item", + "type": "shop", + "nodeId": "ryuma", + "entity": {"mapId": 624, "entityId": 3}, + "shopItemId": 11 + }, + { + "name": "Mercator: Shop item #1", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 1}, + "shopItemId": 12 + }, + { + "name": "Mercator: Shop item #2", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 2}, + "shopItemId": 13 + }, + { + "name": "Mercator: Shop item #3", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 3}, + "shopItemId": 14 + }, + { + "name": "Mercator: Shop item #4", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 4}, + "shopItemId": 15 + }, + { + "name": "Mercator: Shop item #5", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 5}, + "shopItemId": 16 + }, + { + "name": "Mercator: Shop item #6", + "type": "shop", + "nodeId": "mercator", + "entity": {"mapId": 679, "entityId": 6}, + "shopItemId": 17 + }, + { + "name": "Mercator: Special shop item #1", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 1}, + "shopItemId": 18 + }, + { + "name": "Mercator: Special shop item #2", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 2}, + "shopItemId": 19 + }, + { + "name": "Mercator: Special shop item #3", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 3}, + "shopItemId": 20 + }, + { + "name": "Mercator: Special shop item #4", + "type": "shop", + "nodeId": "mercator_special_shop", + "entity": {"mapId": 696, "entityId": 4}, + "shopItemId": 21 + }, + { + "name": "Mercator: Docks shop item #1", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 3}, + {"mapId": 643, "entityId": 9} + ], + "shopItemId": 22 + }, + { + "name": "Mercator: Docks shop item #2", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 4}, + {"mapId": 643, "entityId": 10} + ], + "shopItemId": 23 + }, + { + "name": "Mercator: Docks shop item #3", + "type": "shop", + "nodeId": "mercator_repaired_docks", + "entities": [ + {"mapId": 644, "entityId": 5}, + {"mapId": 643, "entityId": 11} + ], + "shopItemId": 24 + }, + { + "name": "Verla: Shop item #1", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 0}, + {"mapId": 720, "entityId": 1} + ], + "shopItemId": 25 + }, + { + "name": "Verla: Shop item #2", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 1}, + {"mapId": 720, "entityId": 2} + ], + "shopItemId": 26 + }, + { + "name": "Verla: Shop item #3", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 2}, + {"mapId": 720, "entityId": 3} + ], + "shopItemId": 27 + }, + { + "name": "Verla: Shop item #4", + "type": "shop", + "nodeId": "verla", + "entities": [ + {"mapId": 719, "entityId": 4}, + {"mapId": 720, "entityId": 4} + ], + "shopItemId": 28 + }, + { + "name": "Verla: Shop item #5 (extra item after saving town)", + "type": "shop", + "nodeId": "verla_after_mines", + "entity": {"mapId": 720, "entityId": 5}, + "shopItemId": 29 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #1", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 1}, + "shopItemId": 30 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #2", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 2}, + "shopItemId": 31 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #3", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 3}, + "shopItemId": 32 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #4", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 4}, + "shopItemId": 33 + }, + { + "name": "Route from Verla to Destel: Kelketo shop item #5", + "type": "shop", + "nodeId": "route_verla_destel", + "entity": {"mapId": 517, "entityId": 5}, + "shopItemId": 34 + }, + { + "name": "Destel: Inn item", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 729, "entityId": 2}, + "shopItemId": 35 + }, + { + "name": "Destel: Shop item #1", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 1}, + "shopItemId": 36 + }, + { + "name": "Destel: Shop item #2", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 2}, + "shopItemId": 37 + }, + { + "name": "Destel: Shop item #3", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 3}, + "shopItemId": 38 + }, + { + "name": "Destel: Shop item #4", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 4}, + "shopItemId": 39 + }, + { + "name": "Destel: Shop item #5", + "type": "shop", + "nodeId": "destel", + "entity": {"mapId": 733, "entityId": 5}, + "shopItemId": 40 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #1", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 0}, + "shopItemId": 41 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #2", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 2}, + "shopItemId": 42 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #3", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 3}, + "shopItemId": 43 + }, + { + "name": "Route to Lake Shrine: Greedly's shop item #4", + "type": "shop", + "nodeId": "route_lake_shrine", + "entity": {"mapId": 526, "entityId": 4}, + "shopItemId": 44 + }, + { + "name": "Kazalt: Shop item #1", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 0}, + "shopItemId": 45 + }, + { + "name": "Kazalt: Shop item #2", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 2}, + "shopItemId": 46 + }, + { + "name": "Kazalt: Shop item #3", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 3}, + "shopItemId": 47 + }, + { + "name": "Kazalt: Shop item #4", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 4}, + "shopItemId": 48 + }, + { + "name": "Kazalt: Shop item #5", + "type": "shop", + "nodeId": "kazalt", + "entity": {"mapId": 747, "entityId": 5}, + "shopItemId": 49 + }, + { + "name": "Massan: Elder reward after freeing Fara in Swamp Shrine", + "type": "reward", + "nodeId": "massan_after_swamp_shrine", + "address": 162337, + "flag": {"byte": "0x1004", "bit": 2}, + "rewardId": 0 + }, + { + "name": "Lake Shrine: Mir reward after beating Duke", + "type": "reward", + "nodeId": "lake_shrine", + "address": 166463, + "flag": {"byte": "0x1003", "bit": 0}, + "rewardId": 1 + }, + { + "name": "Greenmaze: Cutter reward for saving Einstein", + "type": "reward", + "nodeId": "greenmaze_cutter", + "address": 166021, + "flag": {"byte": "0x1024", "bit": 4}, + "rewardId": 2 + }, + { + "name": "Mountainous Area: Zak reward after fighting", + "type": "reward", + "nodeId": "mountainous_area", + "hints": [ + "kept by a threatening guardian" + ], + "address": 166515, + "flag": {"byte": "0x1027", "bit": 0}, + "rewardId": 3 + }, + { + "name": "Route between Gumi and Ryuma: Swordsman Kado reward", + "type": "reward", + "nodeId": "route_gumi_ryuma", + "address": 166219, + "flag": {"byte": "0x101B", "bit": 7}, + "rewardId": 4 + }, + { + "name": "Greenmaze: dwarf hidden in the trees", + "type": "reward", + "nodeId": "greenmaze_pre_whistle", + "address": 166111, + "flag": {"byte": "0x1022", "bit": 7}, + "rewardId": 5 + }, + { + "name": "Mercator: Arthur reward (in castle throne room)", + "type": "reward", + "nodeId": "mercator", + "address": 164191, + "flag": {"byte": "0x101B", "bit": 6}, + "rewardId": 6 + }, + { + "name": "Mercator: Fahl's dojo challenge reward", + "type": "reward", + "nodeId": "mercator", + "address": 165029, + "flag": {"byte": "0x101C", "bit": 4}, + "rewardId": 7 + }, + { + "name": "Ryuma: Mayor's first reward", + "type": "reward", + "nodeId": "ryuma_after_thieves_hideout", + "address": 164731, + "flag": {"byte": "0x1004", "bit": 3}, + "rewardId": 8 + }, + { + "name": "Ryuma: Mayor's second reward", + "type": "reward", + "nodeId": "ryuma_after_thieves_hideout", + "address": 164735, + "flag": {"byte": "0x1004", "bit": 3}, + "rewardId": 9 + } +] diff --git a/worlds/landstalker/data/world_node.py b/worlds/landstalker/data/world_node.py new file mode 100644 index 0000000000..f786f9613f --- /dev/null +++ b/worlds/landstalker/data/world_node.py @@ -0,0 +1,411 @@ +WORLD_NODES_JSON = { + "massan": { + "name": "Massan", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Massan" + ] + }, + "massan_cave": { + "name": "Massan Cave", + "hints": [ + "in a large cave", + "in a region inhabited by bears", + "in Massan cave" + ] + }, + "route_massan_gumi": { + "name": "Route between Massan and Gumi", + "hints": [ + "on a route", + "in a region inhabited by bears", + "between Massan and Gumi" + ] + }, + "waterfall_shrine": { + "name": "Waterfall Shrine", + "hints": [ + "in a shrine", + "close to a waterfall", + "in a region inhabited by bears", + "in Waterfall Shrine" + ] + }, + "swamp_shrine": { + "name": "Swamp Shrine", + "hints": [ + "in a shrine", + "near a swamp", + "in a region inhabited by bears", + "in Swamp Shrine" + ] + }, + "massan_after_swamp_shrine": { + "name": "Massan (after Swamp Shrine)", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Massan" + ] + }, + "gumi_after_swamp_shrine": { + "name": "Gumi (after Swamp Shrine)", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Gumi" + ] + }, + "gumi": { + "name": "Gumi", + "hints": [ + "in a village", + "in a region inhabited by bears", + "in the village of Gumi" + ] + }, + "route_gumi_ryuma": { + "name": "Route from Gumi to Ryuma", + "hints": [ + "on a route", + "in a region inhabited by bears", + "between Gumi and Ryuma" + ] + }, + "tibor": { + "name": "Tibor", + "hints": [ + "among the trees", + "inside the elder tree called Tibor" + ] + }, + "ryuma": { + "name": "Ryuma", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "ryuma_after_thieves_hideout": { + "name": "Ryuma (after Thieves Hideout)", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "ryuma_lighthouse_repaired": { + "name": "Ryuma (repaired lighthouse)", + "hints": [ + "in a town", + "in the town of Ryuma" + ] + }, + "thieves_hideout_pre_key": { + "name": "Thieves Hideout (before keydoor)", + "hints": [ + "close to a waterfall", + "in a large cave", + "in the Thieves' Hideout" + ] + }, + "thieves_hideout_post_key": { + "name": "Thieves Hideout (after keydoor)", + "hints": [ + "close to a waterfall", + "in a large cave", + "in the Thieves' Hideout" + ] + }, + "helga_hut": { + "name": "Witch Helga's Hut", + "hints": [ + "near a swamp", + "in the hut of a witch called Helga" + ] + }, + "mercator": { + "name": "Mercator", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mercator_repaired_docks": { + "name": "Mercator (docks with repaired lighthouse)", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mercator_casino": { + "name": "Mercator casino" + }, + "mercator_dungeon": { + "name": "Mercator Dungeon" + }, + "crypt": { + "name": "Crypt", + "hints": [ + "hidden in the depths of Mercator", + "in Mercator crypt" + ] + }, + "mercator_special_shop": { + "name": "Mercator special shop", + "hints": [ + "in a town", + "in the town of Mercator" + ] + }, + "mir_tower_sector": { + "name": "Mir Tower sector", + "hints": [ + "on a route", + "near Mir Tower" + ] + }, + "mir_tower_sector_tree_ledge": { + "name": "Mir Tower sector (ledge behind sacred tree)", + "hints": [ + "on a route", + "among the trees", + "near Mir Tower" + ] + }, + "mir_tower_sector_tree_coast": { + "name": "Mir Tower sector (coast behind sacred tree)", + "hints": [ + "on a route", + "among the trees", + "near Mir Tower" + ] + }, + "twinkle_village": { + "name": "Twinkle village", + "hints": [ + "in a village", + "in Twinkle village" + ] + }, + "mir_tower_pre_garlic": { + "name": "Mir Tower (pre-garlic)", + "hints": [ + "inside a tower", + "in Mir Tower" + ] + }, + "mir_tower_post_garlic": { + "name": "Mir Tower (post-garlic)", + "hints": [ + "inside a tower", + "in Mir Tower" + ] + }, + "greenmaze_pre_whistle": { + "name": "Greenmaze (pre-whistle)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "greenmaze_cutter": { + "name": "Greenmaze (Cutter hidden sector)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "greenmaze_post_whistle": { + "name": "Greenmaze (post-whistle)", + "hints": [ + "among the trees", + "in the infamous Greenmaze" + ] + }, + "verla_shore": { + "name": "Verla shore", + "hints": [ + "on a route", + "near the town of Verla" + ] + }, + "verla_shore_cliff": { + "name": "Verla shore cliff (accessible from Verla Mines)", + "hints": [ + "on a route", + "near the town of Verla" + ] + }, + "verla": { + "name": "Verla", + "hints": [ + "in a town", + "in the town of Verla" + ] + }, + "verla_after_mines": { + "name": "Verla (after mines)", + "hints": [ + "in a town", + "in the town of Verla" + ] + }, + "verla_mines": { + "name": "Verla Mines", + "hints": [ + "in Verla Mines" + ] + }, + "verla_mines_behind_lava": { + "name": "Verla Mines (behind lava)", + "hints": [ + "in Verla Mines" + ] + }, + "route_verla_destel": { + "name": "Route between Verla and Destel", + "hints": [ + "on a route", + "in Destel region", + "between Verla and Destel" + ] + }, + "destel": { + "name": "Destel", + "hints": [ + "in a village", + "in Destel region", + "in the village of Destel" + ] + }, + "route_after_destel": { + "name": "Route after Destel", + "hints": [ + "on a route", + "near a lake", + "in Destel region", + "on the route to the lake after Destel" + ] + }, + "destel_well": { + "name": "Destel Well", + "hints": [ + "in Destel region", + "in a large cave", + "in Destel Well" + ] + }, + "route_lake_shrine": { + "name": "Route to Lake Shrine", + "hints": [ + "on a route", + "near a lake", + "on the mountainous path to Lake Shrine" + ] + }, + "route_lake_shrine_cliff": { + "name": "Route to Lake Shrine cliff", + "hints": [ + "on a route", + "near a lake", + "on the mountainous path to Lake Shrine" + ] + }, + "lake_shrine": { + "name": "Lake Shrine", + "hints": [ + "in a shrine", + "near a lake", + "in Lake Shrine" + ] + }, + "mountainous_area": { + "name": "Mountainous Area", + "hints": [ + "in a mountainous area" + ] + }, + "king_nole_cave": { + "name": "King Nole's Cave", + "hints": [ + "in a large cave", + "in King Nole's cave" + ] + }, + "kazalt": { + "name": "Kazalt", + "hints": [ + "in King Nole's domain", + "in Kazalt" + ] + }, + "king_nole_labyrinth_pre_door": { + "name": "King Nole's Labyrinth (before door)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_post_door": { + "name": "King Nole's Labyrinth (after door)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_exterior": { + "name": "King Nole's Labyrinth (exterior)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_fall_from_exterior": { + "name": "King Nole's Labyrinth (fall from exterior)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_raft_entrance": { + "name": "King Nole's Labyrinth (raft entrance)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_raft": { + "name": "King Nole's Labyrinth (raft)", + "hints": [ + "close to a waterfall", + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_sacred_tree": { + "name": "King Nole's Labyrinth (sacred tree)", + "hints": [ + "among the trees", + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_labyrinth_path_to_palace": { + "name": "King Nole's Labyrinth (path to palace)", + "hints": [ + "in King Nole's domain", + "in King Nole's labyrinth" + ] + }, + "king_nole_palace": { + "name": "King Nole's Palace", + "hints": [ + "in King Nole's domain", + "in King Nole's palace" + ] + }, + "end": { + "name": "The End" + } +} diff --git a/worlds/landstalker/data/world_path.py b/worlds/landstalker/data/world_path.py new file mode 100644 index 0000000000..f7baba358a --- /dev/null +++ b/worlds/landstalker/data/world_path.py @@ -0,0 +1,446 @@ +WORLD_PATHS_JSON = [ + { + "fromId": "massan", + "toId": "massan_cave", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "massan", + "toId": "massan_after_swamp_shrine", + "requiredNodes": [ + "swamp_shrine" + ] + }, + { + "fromId": "massan", + "toId": "route_massan_gumi", + "twoWay": True + }, + { + "fromId": "route_massan_gumi", + "toId": "waterfall_shrine", + "twoWay": True + }, + { + "fromId": "route_massan_gumi", + "toId": "swamp_shrine", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Idol Stone" + ] + }, + { + "fromId": "route_massan_gumi", + "toId": "gumi", + "twoWay": True + }, + { + "fromId": "gumi", + "toId": "gumi_after_swamp_shrine", + "requiredNodes": [ + "swamp_shrine" + ] + }, + { + "fromId": "gumi", + "toId": "route_gumi_ryuma" + }, + { + "fromId": "route_gumi_ryuma", + "toId": "ryuma", + "twoWay": True + }, + { + "fromId": "ryuma", + "toId": "ryuma_after_thieves_hideout", + "requiredNodes": [ + "thieves_hideout_post_key" + ] + }, + { + "fromId": "ryuma", + "toId": "ryuma_lighthouse_repaired", + "twoWay": True, + "requiredItems": [ + "Sun Stone" + ] + }, + { + "fromId": "ryuma", + "toId": "thieves_hideout_pre_key", + "twoWay": True + }, + { + "fromId": "thieves_hideout_pre_key", + "toId": "thieves_hideout_post_key", + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "thieves_hideout_post_key", + "toId": "thieves_hideout_pre_key" + }, + { + "fromId": "route_gumi_ryuma", + "toId": "tibor", + "twoWay": True + }, + { + "fromId": "route_gumi_ryuma", + "toId": "helga_hut", + "twoWay": True, + "requiredItems": [ + "Einstein Whistle" + ], + "requiredNodes": [ + "massan" + ] + }, + { + "fromId": "route_gumi_ryuma", + "toId": "mercator", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Safety Pass" + ] + }, + { + "fromId": "mercator", + "toId": "mercator_dungeon", + "twoWay": True + }, + { + "fromId": "mercator", + "toId": "crypt", + "twoWay": True + }, + { + "fromId": "mercator", + "toId": "mercator_special_shop", + "twoWay": True, + "requiredItems": [ + "Buyer Card" + ] + }, + { + "fromId": "mercator", + "toId": "mercator_casino", + "twoWay": True, + "requiredItems": [ + "Casino Ticket" + ] + }, + { + "fromId": "mercator", + "toId": "mir_tower_sector", + "twoWay": True + }, + { + "fromId": "mir_tower_sector", + "toId": "twinkle_village", + "twoWay": True + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_sector_tree_ledge", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_sector_tree_coast", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mir_tower_sector", + "toId": "mir_tower_pre_garlic", + "requiredItems": [ + "Armlet" + ] + }, + { + "fromId": "mir_tower_pre_garlic", + "toId": "mir_tower_sector" + }, + { + "fromId": "mir_tower_pre_garlic", + "toId": "mir_tower_post_garlic", + "requiredItems": [ + "Garlic" + ] + }, + { + "fromId": "mir_tower_post_garlic", + "toId": "mir_tower_pre_garlic" + }, + { + "fromId": "mir_tower_post_garlic", + "toId": "mir_tower_sector" + }, + { + "fromId": "mercator", + "toId": "greenmaze_pre_whistle", + "weight": 2, + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "greenmaze_post_whistle", + "requiredItems": [ + "Einstein Whistle" + ] + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "greenmaze_cutter", + "requiredItems": [ + "EkeEke" + ], + "twoWay": True + }, + { + "fromId": "greenmaze_post_whistle", + "toId": "route_massan_gumi" + }, + { + "fromId": "mercator", + "toId": "mercator_repaired_docks", + "requiredNodes": [ + "ryuma_lighthouse_repaired" + ] + }, + { + "fromId": "mercator_repaired_docks", + "toId": "verla_shore" + }, + { + "fromId": "verla_shore", + "toId": "verla", + "twoWay": True + }, + { + "fromId": "verla", + "toId": "verla_after_mines", + "requiredNodes": [ + "verla_mines" + ], + "twoWay": True + }, + { + "fromId": "verla_shore", + "toId": "verla_mines", + "twoWay": True + }, + { + "fromId": "verla_mines", + "toId": "verla_shore_cliff", + "twoWay": True + }, + { + "fromId": "verla_shore_cliff", + "toId": "verla_shore" + }, + { + "fromId": "verla_shore", + "toId": "mir_tower_sector", + "requiredNodes": [ + "verla_mines" + ], + "twoWay": True + }, + { + "fromId": "verla_mines", + "toId": "route_verla_destel" + }, + { + "fromId": "verla_mines", + "toId": "verla_mines_behind_lava", + "twoWay": True, + "requiredItems": [ + "Fireproof" + ] + }, + { + "fromId": "route_verla_destel", + "toId": "destel", + "twoWay": True + }, + { + "fromId": "destel", + "toId": "route_after_destel", + "twoWay": True + }, + { + "fromId": "destel", + "toId": "destel_well", + "twoWay": True + }, + { + "fromId": "destel_well", + "toId": "route_lake_shrine", + "twoWay": True + }, + { + "fromId": "route_lake_shrine", + "toId": "lake_shrine", + "itemsPlacedWhenCrossing": [ + "Sword of Gaia" + ] + }, + { + "fromId": "lake_shrine", + "toId": "route_lake_shrine" + }, + { + "fromId": "lake_shrine", + "toId": "mir_tower_sector" + }, + { + "fromId": "greenmaze_pre_whistle", + "toId": "mountainous_area", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "mountainous_area", + "toId": "route_lake_shrine_cliff", + "twoWay": True, + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "route_lake_shrine_cliff", + "toId": "route_lake_shrine" + }, + { + "fromId": "mountainous_area", + "toId": "king_nole_cave", + "twoWay": True, + "weight": 2, + "requiredItems": [ + "Gola's Eye" + ] + }, + { + "fromId": "king_nole_cave", + "toId": "mercator" + }, + { + "fromId": "king_nole_cave", + "toId": "kazalt", + "itemsPlacedWhenCrossing": [ + "Lithograph" + ] + }, + { + "fromId": "kazalt", + "toId": "king_nole_cave" + }, + { + "fromId": "kazalt", + "toId": "king_nole_labyrinth_pre_door", + "twoWay": True + }, + { + "fromId": "king_nole_labyrinth_pre_door", + "toId": "king_nole_labyrinth_post_door", + "requiredItems": [ + "Key" + ] + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_pre_door" + }, + { + "fromId": "king_nole_labyrinth_pre_door", + "toId": "king_nole_labyrinth_exterior", + "requiredItems": [ + "Iron Boots" + ] + }, + { + "fromId": "king_nole_labyrinth_exterior", + "toId": "king_nole_labyrinth_fall_from_exterior", + "requiredItems": [ + "Axe Magic" + ] + }, + { + "fromId": "king_nole_labyrinth_fall_from_exterior", + "toId": "king_nole_labyrinth_pre_door" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_raft_entrance", + "requiredItems": [ + "Snow Spikes" + ] + }, + { + "fromId": "king_nole_labyrinth_raft_entrance", + "toId": "king_nole_labyrinth_post_door" + }, + { + "fromId": "king_nole_labyrinth_raft_entrance", + "toId": "king_nole_labyrinth_raft", + "requiredItems": [ + "Logs" + ] + }, + { + "fromId": "king_nole_labyrinth_raft", + "toId": "king_nole_labyrinth_raft_entrance" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_path_to_palace", + "requiredItems": [ + "Snow Spikes" + ] + }, + { + "fromId": "king_nole_labyrinth_path_to_palace", + "toId": "king_nole_labyrinth_post_door" + }, + { + "fromId": "king_nole_labyrinth_post_door", + "toId": "king_nole_labyrinth_sacred_tree", + "requiredItems": [ + "Axe Magic" + ], + "requiredNodes": [ + "king_nole_labyrinth_raft_entrance" + ] + }, + { + "fromId": "king_nole_labyrinth_path_to_palace", + "toId": "king_nole_palace", + "twoWay": True + }, + { + "fromId": "king_nole_palace", + "toId": "end", + "requiredItems": [ + "Gola's Fang", + "Gola's Horn", + "Gola's Nail" + ] + } +] \ No newline at end of file diff --git a/worlds/landstalker/data/world_region.py b/worlds/landstalker/data/world_region.py new file mode 100644 index 0000000000..3365a9dfa9 --- /dev/null +++ b/worlds/landstalker/data/world_region.py @@ -0,0 +1,299 @@ +WORLD_REGIONS_JSON = [ + { + "name": "Massan", + "hintName": "in the village of Massan", + "nodeIds": [ + "massan", + "massan_after_swamp_shrine" + ] + }, + { + "name": "Massan Cave", + "hintName": "in the cave near Massan", + "nodeIds": [ + "massan_cave" + ], + "darkMapIds": [ + 803, 804, 805, 806, 807 + ] + }, + { + "name": "Route between Massan and Gumi", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_massan_gumi" + ] + }, + { + "name": "Waterfall Shrine", + "hintName": "in the waterfall shrine", + "nodeIds": [ + "waterfall_shrine" + ], + "darkMapIds": [ + 174, 175, 176, 177, 178, 179, 180, 181, 182 + ] + }, + { + "name": "Swamp Shrine", + "hintName": "in the swamp shrine", + "canBeHintedAsRequired": False, + "nodeIds": [ + "swamp_shrine" + ], + "darkMapIds": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 30 + ] + }, + { + "name": "Gumi", + "hintName": "in the village of Gumi", + "nodeIds": [ + "gumi", + "gumi_after_swamp_shrine" + ] + }, + { + "name": "Route between Gumi and Ryuma", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_gumi_ryuma" + ] + }, + { + "name": "Tibor", + "hintName": "inside Tibor", + "nodeIds": [ + "tibor" + ], + "darkMapIds": [ + 808, 809, 810, 811, 812, 813, 814, 815 + ] + }, + { + "name": "Ryuma", + "hintName": "in the town of Ryuma", + "nodeIds": [ + "ryuma", + "ryuma_after_thieves_hideout", + "ryuma_lighthouse_repaired" + ] + }, + { + "name": "Thieves Hideout", + "hintName": "in the thieves' hideout", + "nodeIds": [ + "thieves_hideout_pre_key", + "thieves_hideout_post_key" + ], + "darkMapIds": [ + 185, 186, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, + 203, 204, 205, 206, 207, 208, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222 + ] + }, + { + "name": "Witch Helga's Hut", + "hintName": "in witch Helga's hut", + "nodeIds": [ + "helga_hut" + ] + }, + { + "name": "Mercator", + "hintName": "in the town of Mercator", + "nodeIds": [ + "mercator", + "mercator_repaired_docks", + "mercator_casino", + "mercator_special_shop" + ] + }, + { + "name": "Crypt", + "hintName": "in the crypt of Mercator", + "nodeIds": [ + "crypt" + ], + "darkMapIds": [ + 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659 + ] + }, + { + "name": "Mercator Dungeon", + "hintName": "in the dungeon of Mercator", + "nodeIds": [ + "mercator_dungeon" + ], + "darkMapIds": [ + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 76, 80, 81, 82, 91, 92 + ] + }, + { + "name": "Mir Tower sector", + "hintName": "near Mir Tower", + "canBeHintedAsRequired": False, + "nodeIds": [ + "mir_tower_sector", + "mir_tower_sector_tree_ledge", + "mir_tower_sector_tree_coast", + "twinkle_village" + ] + }, + { + "name": "Mir Tower", + "hintName": "inside Mir Tower", + "canBeHintedAsRequired": False, + "nodeIds": [ + "mir_tower_pre_garlic", + "mir_tower_post_garlic" + ], + "darkMapIds": [ + 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, + 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784 + ] + }, + { + "name": "Greenmaze", + "hintName": "in Greenmaze", + "nodeIds": [ + "greenmaze_pre_whistle", + "greenmaze_post_whistle" + ] + }, + { + "name": "Verla Shore", + "canBeHintedAsRequired": False, + "nodeIds": [ + "verla_shore", + "verla_shore_cliff" + ] + }, + { + "name": "Verla", + "hintName": "in the town of Verla", + "nodeIds": [ + "verla", + "verla_after_mines" + ] + }, + { + "name": "Verla Mines", + "hintName": "in the mines near Verla", + "nodeIds": [ + "verla_mines", + "verla_mines_behind_lava" + ], + "darkMapIds": [ + 227, 228, 229, 230, 231, 232, 233, 234, 235, 237, 239, 240, 241, 242, 243, 244, 246, + 247, 248, 250, 253, 254, 255, 256, 258, 259, 266, 268, 269, 471 + ] + }, + { + "name": "Route between Verla and Destel", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_verla_destel" + ] + }, + { + "name": "Destel", + "hintName": "in the village of Destel", + "nodeIds": [ + "destel" + ] + }, + { + "name": "Route after Destel", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_after_destel" + ] + }, + { + "name": "Destel Well", + "hintName": "in Destel well", + "nodeIds": [ + "destel_well" + ], + "darkMapIds": [ + 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290 + ] + }, + { + "name": "Route to Lake Shrine", + "canBeHintedAsRequired": False, + "nodeIds": [ + "route_lake_shrine", + "route_lake_shrine_cliff" + ] + }, + { + "name": "Lake Shrine", + "hintName": "in the lake shrine", + "nodeIds": [ + "lake_shrine" + ], + "darkMapIds": [ + 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, + 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, + 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, + 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354 + ] + }, + { + "name": "Mountainous Area", + "hintName": "in the mountainous area", + "nodeIds": [ + "mountainous_area" + ] + }, + { + "name": "King Nole's Cave", + "hintName": "in King Nole's cave", + "nodeIds": [ + "king_nole_cave" + ], + "darkMapIds": [ + 145, 147, 150, 152, 154, 155, 156, 158, 160, 161, 162, 164, 166, 170, 171, 172 + ] + }, + { + "name": "Kazalt", + "hintName": "in the hidden town of Kazalt", + "nodeIds": [ + "kazalt" + ] + }, + { + "name": "King Nole's Labyrinth", + "hintName": "in King Nole's labyrinth", + "nodeIds": [ + "king_nole_labyrinth_pre_door", + "king_nole_labyrinth_post_door", + "king_nole_labyrinth_exterior", + "king_nole_labyrinth_fall_from_exterior", + "king_nole_labyrinth_path_to_palace", + "king_nole_labyrinth_raft_entrance", + "king_nole_labyrinth_raft", + "king_nole_labyrinth_sacred_tree" + ], + "darkMapIds": [ + 355, 356, 357, 358, 359, 360, 361, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, + 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, + 390, 391, 392, 393, 394, 395, 396, 397, 398, 405, 406, 408, 409, 410, 411, 412, 413, + 414, 415, 416, 417, 418, 419, 420, 422, 423 + ] + }, + { + "name": "King Nole's Palace", + "hintName": "in King Nole's palace", + "nodeIds": [ + "king_nole_palace", + "end" + ], + "darkMapIds": [ + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, + 131, 132, 133, 134, 135, 136, 137, 138 + ] + } +] \ No newline at end of file diff --git a/worlds/landstalker/data/world_teleport_tree.py b/worlds/landstalker/data/world_teleport_tree.py new file mode 100644 index 0000000000..830f554720 --- /dev/null +++ b/worlds/landstalker/data/world_teleport_tree.py @@ -0,0 +1,62 @@ +WORLD_TELEPORT_TREES_JSON = [ + [ + { + "name": "Massan tree", + "treeMapId": 512, + "nodeId": "route_massan_gumi" + }, + { + "name": "Tibor tree", + "treeMapId": 534, + "nodeId": "route_gumi_ryuma" + } + ], + [ + { + "name": "Mercator front gate tree", + "treeMapId": 539, + "nodeId": "route_gumi_ryuma" + }, + { + "name": "Verla shore tree", + "treeMapId": 537, + "nodeId": "verla_shore" + } + ], + [ + { + "name": "Destel sector tree", + "treeMapId": 536, + "nodeId": "route_after_destel" + }, + { + "name": "Lake Shrine sector tree", + "treeMapId": 513, + "nodeId": "route_lake_shrine" + } + ], + [ + { + "name": "Mir Tower sector tree", + "treeMapId": 538, + "nodeId": "mir_tower_sector" + }, + { + "name": "Mountainous area tree", + "treeMapId": 535, + "nodeId": "mountainous_area" + } + ], + [ + { + "name": "Greenmaze entrance tree", + "treeMapId": 510, + "nodeId": "greenmaze_pre_whistle" + }, + { + "name": "Greenmaze end tree", + "treeMapId": 511, + "nodeId": "greenmaze_post_whistle" + } + ] +] \ No newline at end of file diff --git a/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md new file mode 100644 index 0000000000..90a79f8bd9 --- /dev/null +++ b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md @@ -0,0 +1,60 @@ +# Landstalker: The Treasures of King Nole + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains most of the options you need to +configure and export a config file. + +## What does randomization do to this game? + +All items are shuffled while keeping a logic to make every seed completable. + +Some key items could be obtained in a very different order compared to the vanilla game, leading to very unusual situations. + +The world is made as open as possible while keeping the original locks behind the same items & triggers as vanilla +when that makes sense logic-wise. This puts the emphasis on exploration and gameplay by removing all the scenario +and story-related triggers, giving a wide open world to explore. + +## What items and locations get shuffled? + +All items and locations are shuffled. This includes **chests**, items on **ground**, in **shops**, and given by **NPCs**. + +It's also worth noting that all of these items are shuffled among all worlds, meaning every item can be sent to you +by other players. + +## What are the main differences compared to the vanilla game? + +The **Key** is now a unique item and can open several doors without being consumed, making it a standard progression item. +All key doors are gone, except three of them : + - the Mercator castle backdoor (giving access to Greenmaze sector) + - Thieves Hideout middle door (cutting the level in half) + - King Nole's Labyrinth door near entrance + +--- + +The secondary shop of Mercator requiring to do the traders sidequest in the original game is now unlocked by having +**Buyer Card** in your inventory. + +You will need as many **jewels** as specified in the settings to use the teleporter to go to Kazalt and the final dungeon. +If you find and use the **Lithograph**, it will tell you in which world are each one of your jewels. + +Each seed, there is a random dungeon which is chosen to be the "dark dungeon" where you won't see anything unless you +have the **Lantern** in your inventory. Unlike vanilla, King Nole's Labyrinth no longer has the few dark rooms the lantern +was originally intended for. + +The **Statue of Jypta** is introduced as a real item (instead of just being an intro gimmick) and gives you gold over +time while you're walking, the same way Healing Boots heal you when you walk. + + +## What do I need to know for my first seed? + +It's advised you keep Massan as your starting region for your first seed, since taking another starting region might +be significantly harder, both combat-wise and logic-wise. + +Having fully open & shuffled teleportation trees is an interesting way to play, but is discouraged for beginners +as well since it can force you to go in late-game zones with few Life Stocks. + +Overall, the default settings are good for a beginner-friendly seed, and if you don't feel too confident, you can also +lower the combat difficulty to make it more forgiving. + +*Have fun on your adventure!* diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md new file mode 100644 index 0000000000..9f453c146d --- /dev/null +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -0,0 +1,119 @@ +# Landstalker Setup Guide + +## Required Software + +- [Landstalker Archipelago Client](https://github.com/Dinopony/randstalker-archipelago/releases) (only available on Windows) +- A compatible emulator to run the game + - [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core + - [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core +- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/)) + +## Installation Instructions + +- Unzip the Landstalker Archipelago Client archive into its own folder +- Put your Landstalker ROM (`LandStalker_USA.SGD` on the Steam release) inside this folder +- To launch the client, launch `randstalker_archipelago.exe` inside that folder + +Be aware that you might get antivirus warnings about the client program because one of its main features is to spy +on another process's memory (your emulator). This is something antiviruses obviously dislike, and sometimes mistake +for malicious software. + +If you're not trusting the program, you can check its [source code](https://github.com/Dinopony/randstalker-archipelago/) +or test it on a service like Virustotal. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings +and export a config file from them. + +## How-to-play + +### Connecting to the Archipelago Server + +Once the game has been created, you need to connect to the server using the Landstalker Archipelago Client. + +To do so, run `randstalker_archipelago.exe` inside the folder you created while installing the software. + +A window will open with a few settings to enter: +- **Host**: Put the server address and port in this field (e.g. `archipelago.gg:12345`) +- **Slot name**: Put the player name you specified in your YAML config file in this field. +- **Password**: If the server has a password, put it there. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_ap.png) + +Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to +the Archipelago server. + +If this didn't work, double-check your credentials. An error message should be displayed on the console log to the +right that might help you find the cause of the issue. + +### ROM Generation + +When you connected to the Archipelago server, the client fetched all the required data from the server to be able to +build a randomized ROM. + +You should see a window with settings to fill: +- **Input ROM file**: This is the path to your original ROM file for the game. If you are using the Steam release ROM + and placed it inside the client's folder as mentioned above, you don't need to change anything. +- **Output ROM directory**: This is where the randomized ROMs will be put. No need to change this unless you want them + to be created in a very specific folder. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_rom.png) + +There also a few cosmetic options you can fill before clicking the `Build ROM` button which should create your +randomized seed if everything went right. + +If it didn't, double-check your `Input ROM file` and `Output ROM path`, then retry building the ROM by clicking +the same button again. + +### Connecting to the emulator + +Now that you're connected to the Archipelago server and have a randomized ROM, all we need is to get the client +connected to the emulator. This way, the client will be able to see what's happening while you play and give you in-game +the items you have received from other players. + +You should see the following window: + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_emu.png) + +As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core. +Be careful to select that core, because any other core (e.g. BlastEm) won't work. + +The easiest way to do so is to: +- open the emu of your choice +- if you're using Retroarch and it's your first time, download the Genesis Plus GX core through Retroarch user interface +- click the `Show ROM file in explorer` button +- drag-and-drop the shown ROM file on the emulator window +- press Start to reach file select screen (to ensure game RAM is properly set-up) + +Then, you can click on the `Connect to emulator` button below and it should work. + +If this didn't work, try the following: +- ensure you have loaded your ROM and reached the save select screen +- ensure you are using Genesis Plus GX and not another core (e.g. BlastEm will not work) +- try launching the client in Administrator Mode (right-click on `randstalker_archipelago.exe`, then + `Run as administrator`) +- if all else fails, try using one of those specific emulator versions: + - RetroArch 1.9.0 and Genesis Plus GX 1.7.4 + - Bizhawk 2.9.1 (x64) + +### Play the game + +If all indicators are green and show "Connected," you're good to go! Play the game and enjoy the wonders of isometric +perspective. + +The client is packaged with both an **automatic item tracker** and an **automatic map tracker** for your comfort. + +If you don't know all checks in the game, don't be afraid: you can click the `Where is it?` button that will show +you a screenshot of where the location actually is. + +![Landstalker Archipelago Client user interface](/static/generated/docs/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/ls_guide_client.png) + +Have fun! \ No newline at end of file diff --git a/worlds/landstalker/docs/ls_guide_ap.png b/worlds/landstalker/docs/ls_guide_ap.png new file mode 100644 index 0000000000000000000000000000000000000000..674938ce6707d4f1384a7d1771b66c4a39e5e428 GIT binary patch literal 2283 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 literal 0 HcmV?d00001 diff --git a/worlds/landstalker/docs/ls_guide_emu.png b/worlds/landstalker/docs/ls_guide_emu.png new file mode 100644 index 0000000000000000000000000000000000000000..ff9218de12bafba18009b57efe85b9003930998a GIT binary patch literal 2598 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 literal 0 HcmV?d00001 diff --git a/worlds/landstalker/docs/ls_guide_rom.png b/worlds/landstalker/docs/ls_guide_rom.png new file mode 100644 index 0000000000000000000000000000000000000000..c57554ab43d875c7ee0110990572daa4ae1e6d6b GIT binary patch literal 3951 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 literal 0 HcmV?d00001 From 5475b04b902bf678927e1c22bf64d9a9c80b309e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 07:27:54 -0800 Subject: [PATCH 233/327] Pokemon Emerald: Bump apworld version number (#2504) --- worlds/pokemon_emerald/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/README.md b/worlds/pokemon_emerald/README.md index 61aee77452..2c1e9e3560 100644 --- a/worlds/pokemon_emerald/README.md +++ b/worlds/pokemon_emerald/README.md @@ -1,6 +1,6 @@ # Pokemon Emerald -Version 1.2.0 +Version 1.2.1 This README contains general info useful for understanding the world. Pretty much all the long lists of locations, regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check From 6718fa4e3b7fd1978921604ac5b6aac35051cb20 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 07:38:12 -0800 Subject: [PATCH 234/327] Installer: Add `_bizhawk.apworld` to installer deleted files (#2477) --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 10d699ad70..be5de320a1 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -80,6 +80,7 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC Type: dirifempty; Name: "{app}" [InstallDelete] +Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld" Type: files; Name: "{app}\ArchipelagoLttPClient.exe" Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" From fe6a70a1de997d094bd70153b9e1a6887d301807 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 25 Nov 2023 10:48:13 -0600 Subject: [PATCH 235/327] Docs: add documentation for options comparison (#2505) --- docs/options api.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/options api.md b/docs/options api.md index 80d0737e3a..48a3f763fa 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -77,7 +77,33 @@ or if I need a boolean object, such as in my slot_data I can access it as: ```python start_with_sword = bool(self.options.starting_sword.value) ``` +All numeric options (i.e. Toggle, Choice, Range) can be compared to integers, strings that match their attributes, +strings that match the option attributes after "option_" is stripped, and the attributes themselves. +```python +# options.py +class Logic(Choice): + option_normal = 0 + option_hard = 1 + option_challenging = 2 + option_extreme = 3 + option_insane = 4 + alias_extra_hard = 2 + crazy = 4 # won't be listed as an option and only exists as an attribute on the class +# __init__.py +from .options import Logic + +if self.options.logic: + do_things_for_all_non_normal_logic() +if self.options.logic == 1: + do_hard_things() +elif self.options.logic == "challenging": + do_challenging_things() +elif self.options.logic == Logic.option_extreme: + do_extreme_things() +elif self.options.logic == "crazy": + do_insane_things() +``` ## Generic Option Classes These options are generically available to every game automatically, but can be overridden for slightly different behavior, if desired. See `worlds/soe/Options.py` for an example. From cfe357eb7197a127f1a5dc62ccc9eaa87e6c2668 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 25 Nov 2023 15:07:02 -0600 Subject: [PATCH 236/327] The Messenger, LADX: use collect and remove as intended (#2093) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- worlds/ladx/__init__.py | 31 ++++++++++++----------------- worlds/ladx/test/testShop.py | 38 ++++++++++++++++++++++++++++++++++++ worlds/messenger/__init__.py | 17 +++++++++------- 3 files changed, 60 insertions(+), 26 deletions(-) create mode 100644 worlds/ladx/test/testShop.py diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index eaaea5be2f..181cc05322 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,32 +1,29 @@ import binascii -import bsdiff4 import os import pkgutil -import settings -import typing import tempfile +import typing +import bsdiff4 +import settings from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World - from .Common import * -from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData, - ladxr_item_to_la_item_name, links_awakening_items, - links_awakening_items_by_name, ItemName) +from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, + ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool +from .LADXR.locations.constants import CHEST_ITEMS +from .LADXR.locations.instrument import Instrument from .LADXR.logic import Logic as LAXDRLogic from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup -from .LADXR.locations.instrument import Instrument -from .LADXR.locations.constants import CHEST_ITEMS from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import links_awakening_options, DungeonItemShuffle - +from .Options import DungeonItemShuffle, links_awakening_options from .Rom import LADXDeltaPatch DEVELOPER_MODE = False @@ -511,16 +508,12 @@ class LinksAwakeningWorld(World): def collect(self, state, item: Item) -> bool: change = super().collect(state, item) - if change: - rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] += rupees - + if change and item.name in self.rupees: + state.prog_items[self.player]["RUPEES"] += self.rupees[item.name] return change def remove(self, state, item: Item) -> bool: change = super().remove(state, item) - if change: - rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] -= rupees - + if change and item.name in self.rupees: + state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change diff --git a/worlds/ladx/test/testShop.py b/worlds/ladx/test/testShop.py new file mode 100644 index 0000000000..91d504d521 --- /dev/null +++ b/worlds/ladx/test/testShop.py @@ -0,0 +1,38 @@ +from typing import Optional + +from Fill import distribute_planned +from test.general import setup_solo_multiworld +from worlds.AutoWorld import call_all +from . import LADXTestBase +from .. import LinksAwakeningWorld + + +class PlandoTest(LADXTestBase): + options = { + "plando_items": [{ + "items": { + "Progressive Sword": 2, + }, + "locations": [ + "Shop 200 Item (Mabe Village)", + "Shop 980 Item (Mabe Village)", + ], + }], + } + + def world_setup(self, seed: Optional[int] = None) -> None: + self.multiworld = setup_solo_multiworld( + LinksAwakeningWorld, + ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") + ) + self.multiworld.plando_items[1] = self.options["plando_items"] + distribute_planned(self.multiworld) + call_all(self.multiworld, "pre_fill") + + def test_planned(self): + """Tests plandoing swords in the shop.""" + location_names = ["Shop 200 Item (Mabe Village)", "Shop 980 Item (Mabe Village)"] + locations = [self.multiworld.get_location(loc, 1) for loc in location_names] + for loc in locations: + self.assertEqual("Progressive Sword", loc.item.name) + self.assertFalse(loc.can_reach(self.multiworld.state)) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index f12687361b..d569dd7542 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -176,11 +176,14 @@ class MessengerWorld(World): self.total_shards += count return MessengerItem(name, self.player, item_id, override_prog, count) - def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]: - if item.advancement and "Time Shard" in item.name: - shard_count = int(item.name.strip("Time Shard ()")) - if remove: - shard_count = -shard_count - state.prog_items[self.player]["Shards"] += shard_count + def collect(self, state: "CollectionState", item: "Item") -> bool: + change = super().collect(state, item) + if change and "Time Shard" in item.name: + state.prog_items[self.player]["Shards"] += int(item.name.strip("Time Shard ()")) + return change - return super().collect_item(state, item, remove) + def remove(self, state: "CollectionState", item: "Item") -> bool: + change = super().remove(state, item) + if change and "Time Shard" in item.name: + state.prog_items[self.player]["Shards"] -= int(item.name.strip("Time Shard ()")) + return change From 7a4620925957b2da909d9510358d6773efab3bf3 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 25 Nov 2023 23:12:38 -0500 Subject: [PATCH 237/327] SA2B: Add AP 0.4.4 Game Chao Names (#2510) --- worlds/sa2b/AestheticData.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/sa2b/AestheticData.py b/worlds/sa2b/AestheticData.py index f3699e81e0..077f35fc01 100644 --- a/worlds/sa2b/AestheticData.py +++ b/worlds/sa2b/AestheticData.py @@ -146,6 +146,10 @@ sample_chao_names = [ "Rin", "Doomguy", "Guide", + "May", + "Hubert", + "Corvus", + "Nigel", ] totally_real_item_names = [ From eec35ab1c3c4b6d51d04b12c812ed6abe8e84f09 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sat, 25 Nov 2023 20:13:08 -0800 Subject: [PATCH 238/327] Pokemon Emerald: Fix tracker flags being reset in menus (#2511) --- worlds/pokemon_emerald/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 5420b15fbe..d8b4b8d587 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -254,7 +254,7 @@ class PokemonEmeraldClient(BizHawkClient): "key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}", "default": 0, "want_reply": False, - "operations": [{"operation": "replace", "value": event_bitfield}] + "operations": [{"operation": "or", "value": event_bitfield}] }]) self.local_set_events = local_set_events @@ -269,7 +269,7 @@ class PokemonEmeraldClient(BizHawkClient): "key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}", "default": 0, "want_reply": False, - "operations": [{"operation": "replace", "value": key_bitfield}] + "operations": [{"operation": "or", "value": key_bitfield}] }]) self.local_found_key_items = local_found_key_items except bizhawk.RequestFailedError: From 65f47be511ace6d6f34495df58ad46d02c450f9d Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sun, 26 Nov 2023 14:13:59 +1000 Subject: [PATCH 239/327] Muse Dash: Presets and Song Updates (#2512) --- worlds/musedash/MuseDashData.txt | 8 +++++++- worlds/musedash/Presets.py | 31 +++++++++++++++++++++++++++++++ worlds/musedash/__init__.py | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 worlds/musedash/Presets.py diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 5b3ef40e54..54a0124474 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -495,4 +495,10 @@ Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10| RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10| Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10| OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10| -Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| \ No newline at end of file +Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| +New York Back Raise|68-0|Gambler's Tricks|True|6|8|10| +slic.hertz|68-1|Gambler's Tricks|True|5|7|9| +Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11 +Swing Edge|68-3|Gambler's Tricks|True|4|8|10| +Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11 +Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10| \ No newline at end of file diff --git a/worlds/musedash/Presets.py b/worlds/musedash/Presets.py new file mode 100644 index 0000000000..6459111802 --- /dev/null +++ b/worlds/musedash/Presets.py @@ -0,0 +1,31 @@ +from typing import Any, Dict + +MuseDashPresets: Dict[str, Dict[str, Any]] = { + # An option to support Short Sync games. 40 songs. + "No DLC - Short": { + "allow_just_as_planned_dlc_songs": False, + "starting_song_count": 5, + "additional_song_count": 34, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, + # An option to support Short Sync games but adds variety. 40 songs. + "DLC - Short": { + "allow_just_as_planned_dlc_songs": True, + "starting_song_count": 5, + "additional_song_count": 34, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, + # An option to support Longer Sync/Async games. 100 songs. + "DLC - Long": { + "allow_just_as_planned_dlc_songs": True, + "starting_song_count": 8, + "additional_song_count": 91, + "additional_item_percentage": 80, + "music_sheet_count_percentage": 20, + "music_sheet_win_count_percentage": 90, + }, +} diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 9a0e473494..a68fd2853d 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -8,6 +8,7 @@ from .Options import MuseDashOptions from .Items import MuseDashSongItem, MuseDashFixedItem from .Locations import MuseDashLocation from .MuseDashCollection import MuseDashCollections +from .Presets import MuseDashPresets class MuseDashWebWorld(WebWorld): @@ -33,6 +34,7 @@ class MuseDashWebWorld(WebWorld): ) tutorials = [setup_en, setup_es] + options_presets = MuseDashPresets class MuseDashWorld(World): From f54f8622bb6edd38324a0eadc42b2daa7261f415 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:17:59 -0500 Subject: [PATCH 240/327] Final Fantasy Mystic Quest: Implement new game (#1909) FFMQR by @wildham0 Uses an API created by wildham for Map Shuffle, Crest Shuffle and Battlefield Reward Shuffle, using a similar method of obtaining data from an external website to Super Metroid's Varia Preset option. Generates a .apmq file which the user must bring to the FFMQR website https://www.ffmqrando.net/Archipelago to patch their rom. It is not an actual patch file but contains item placement and options data for the FFMQR website to generate a patched rom with for AP. Some of the AP options may seem unusual, using Choice instead of Range where it may seem more appropriate, but these are options that are passed to FFMQR and I can only be as flexible as it is. @wildham0 deserves the bulk of the credit for not only creating FFMQR in the first place but all the ASM work on the rom needed to make this possible, work on FFMQR to allow patching with the .apmq files, and creating the API that meant I did not have to recreate his map shuffle from scratch. --- README.md | 1 + WebHostLib/downloads.py | 2 + WebHostLib/templates/macros.html | 3 + docs/CODEOWNERS | 3 + worlds/ffmq/Client.py | 119 + worlds/ffmq/Items.py | 297 ++ worlds/ffmq/LICENSE | 22 + worlds/ffmq/Options.py | 258 ++ worlds/ffmq/Output.py | 113 + worlds/ffmq/Regions.py | 251 + worlds/ffmq/__init__.py | 217 + worlds/ffmq/data/entrances.yaml | 2425 ++++++++++ worlds/ffmq/data/rooms.yaml | 4026 +++++++++++++++++ worlds/ffmq/data/settings.yaml | 140 + .../docs/en_Final Fantasy Mystic Quest.md | 33 + worlds/ffmq/docs/setup_en.md | 162 + 16 files changed, 8072 insertions(+) create mode 100644 worlds/ffmq/Client.py create mode 100644 worlds/ffmq/Items.py create mode 100644 worlds/ffmq/LICENSE create mode 100644 worlds/ffmq/Options.py create mode 100644 worlds/ffmq/Output.py create mode 100644 worlds/ffmq/Regions.py create mode 100644 worlds/ffmq/__init__.py create mode 100644 worlds/ffmq/data/entrances.yaml create mode 100644 worlds/ffmq/data/rooms.yaml create mode 100644 worlds/ffmq/data/settings.yaml create mode 100644 worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md create mode 100644 worlds/ffmq/docs/setup_en.md diff --git a/README.md b/README.md index 3508dd1609..a1e03293d5 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Currently, the following games are supported: * Shivers * Heretic * Landstalker: The Treasures of King Nole +* Final Fantasy Mystic Quest 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/WebHostLib/downloads.py b/WebHostLib/downloads.py index 5cf503be1b..a09ca70171 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -90,6 +90,8 @@ def download_slot_file(room_id, player_id: int): fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" elif slot_data.game == "Kingdom Hearts 2": fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip" + elif slot_data.game == "Final Fantasy Mystic Quest": + fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq" else: return "Game download not supported." return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 746399da74..0722ee3174 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -50,6 +50,9 @@ {% elif patch.game == "Dark Souls III" %} Download JSON File... + {% elif patch.game == "Final Fantasy Mystic Quest" %} + + Download APMQ File... {% else %} No file to download for this game. {% endif %} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0764fa9274..e221371b24 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -55,6 +55,9 @@ # Final Fantasy /worlds/ff1/ @jtoyoda +# Final Fantasy Mystic Quest +/worlds/ffmq/ @Alchav @wildham0 + # Heretic /worlds/heretic/ @Daivuk diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py new file mode 100644 index 0000000000..c53f275017 --- /dev/null +++ b/worlds/ffmq/Client.py @@ -0,0 +1,119 @@ + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient +from .Regions import offset +import logging + +snes_logger = logging.getLogger("SNES") + +ROM_NAME = (0x7FC0, 0x7FD4 + 1 - 0x7FC0) + +READ_DATA_START = 0xF50EA8 +READ_DATA_END = 0xF50FE7 + 1 + +GAME_FLAGS = (0xF50EA8, 64) +COMPLETED_GAME = (0xF50F22, 1) +BATTLEFIELD_DATA = (0xF50FD4, 20) + +RECEIVED_DATA = (0xE01FF0, 3) + +ITEM_CODE_START = 0x420000 + +IN_GAME_FLAG = (4 * 8) + 2 + +NPC_CHECKS = { + 4325676: ((6 * 8) + 4, False), # Old Man Level Forest + 4325677: ((3 * 8) + 6, True), # Kaeli Level Forest + 4325678: ((25 * 8) + 1, True), # Tristam + 4325680: ((26 * 8) + 0, True), # Aquaria Vendor Girl + 4325681: ((29 * 8) + 2, True), # Phoebe Wintry Cave + 4325682: ((25 * 8) + 6, False), # Mysterious Man (Life Temple) + 4325683: ((29 * 8) + 3, True), # Reuben Mine + 4325684: ((29 * 8) + 7, True), # Spencer + 4325685: ((29 * 8) + 6, False), # Venus Chest + 4325686: ((29 * 8) + 1, True), # Fireburg Tristam + 4325687: ((26 * 8) + 1, True), # Fireburg Vendor Girl + 4325688: ((14 * 8) + 4, True), # MegaGrenade Dude + 4325689: ((29 * 8) + 5, False), # Tristam's Chest + 4325690: ((29 * 8) + 4, True), # Arion + 4325691: ((29 * 8) + 0, True), # Windia Kaeli + 4325692: ((26 * 8) + 2, True), # Windia Vendor Girl + +} + + +def get_flag(data, flag): + byte = int(flag / 8) + bit = int(0x80 / (2 ** (flag % 8))) + return (data[byte] & bit) > 0 + + +class FFMQClient(SNIClient): + game = "Final Fantasy Mystic Quest" + + async def validate_rom(self, ctx): + from SNIClient import snes_read + rom_name = await snes_read(ctx, *ROM_NAME) + if rom_name is None: + return False + if rom_name[:2] != b"MQ": + return False + + ctx.rom = rom_name + ctx.game = self.game + ctx.items_handling = 0b001 + return True + + async def game_watcher(self, ctx): + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + check_1 = await snes_read(ctx, 0xF53749, 1) + received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) + data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) + check_2 = await snes_read(ctx, 0xF53749, 1) + if check_1 == b'\x00' or check_2 == b'\x00': + return + + def get_range(data_range): + return data[data_range[0] - READ_DATA_START:data_range[0] + data_range[1] - READ_DATA_START] + completed_game = get_range(COMPLETED_GAME) + battlefield_data = get_range(BATTLEFIELD_DATA) + game_flags = get_range(GAME_FLAGS) + + if game_flags is None: + return + if not get_flag(game_flags, IN_GAME_FLAG): + return + + if not ctx.finished_game: + if completed_game[0] & 0x80 and game_flags[30] & 0x18: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + + old_locations_checked = ctx.locations_checked.copy() + + for container in range(256): + if get_flag(game_flags, (0x20 * 8) + container): + ctx.locations_checked.add(offset["Chest"] + container) + + for location, data in NPC_CHECKS.items(): + if get_flag(game_flags, data[0]) is data[1]: + ctx.locations_checked.add(location) + + for battlefield in range(20): + if battlefield_data[battlefield] == 0: + ctx.locations_checked.add(offset["BattlefieldItem"] + battlefield + 1) + + if old_locations_checked != ctx.locations_checked: + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": ctx.locations_checked}]) + + if received[0] == 0: + received_index = int.from_bytes(received[1:], "big") + if received_index < len(ctx.items_received): + item = ctx.items_received[received_index] + received_index += 1 + code = (item.item - ITEM_CODE_START) + 1 + if code > 256: + code -= 256 + snes_buffered_write(ctx, RECEIVED_DATA[0], bytes([code, *received_index.to_bytes(2, "big")])) + await snes_flush_writes(ctx) diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py new file mode 100644 index 0000000000..7660bd5d52 --- /dev/null +++ b/worlds/ffmq/Items.py @@ -0,0 +1,297 @@ +from BaseClasses import ItemClassification, Item + +fillers = {"Cure Potion": 61, "Heal Potion": 52, "Refresher": 17, "Seed": 2, "Bomb Refill": 19, + "Projectile Refill": 50} + + +class ItemData: + def __init__(self, item_id, classification, groups=(), data_name=None): + self.groups = groups + self.classification = classification + self.id = None + if item_id is not None: + self.id = item_id + 0x420000 + self.data_name = data_name + + +item_table = { + "Elixir": ItemData(0, ItemClassification.progression, ["Key Items"]), + "Tree Wither": ItemData(1, ItemClassification.progression, ["Key Items"]), + "Wakewater": ItemData(2, ItemClassification.progression, ["Key Items"]), + "Venus Key": ItemData(3, ItemClassification.progression, ["Key Items"]), + "Multi Key": ItemData(4, ItemClassification.progression, ["Key Items"]), + "Mask": ItemData(5, ItemClassification.progression, ["Key Items"]), + "Magic Mirror": ItemData(6, ItemClassification.progression, ["Key Items"]), + "Thunder Rock": ItemData(7, ItemClassification.progression, ["Key Items"]), + "Captain's Cap": ItemData(8, ItemClassification.progression_skip_balancing, ["Key Items"]), + "Libra Crest": ItemData(9, ItemClassification.progression, ["Key Items"]), + "Gemini Crest": ItemData(10, ItemClassification.progression, ["Key Items"]), + "Mobius Crest": ItemData(11, ItemClassification.progression, ["Key Items"]), + "Sand Coin": ItemData(12, ItemClassification.progression, ["Key Items", "Coins"]), + "River Coin": ItemData(13, ItemClassification.progression, ["Key Items", "Coins"]), + "Sun Coin": ItemData(14, ItemClassification.progression, ["Key Items", "Coins"]), + "Sky Coin": ItemData(15, ItemClassification.progression_skip_balancing, ["Key Items", "Coins"]), + "Sky Fragment": ItemData(15 + 256, ItemClassification.progression_skip_balancing, ["Key Items"]), + "Cure Potion": ItemData(16, ItemClassification.filler, ["Consumables"]), + "Heal Potion": ItemData(17, ItemClassification.filler, ["Consumables"]), + "Seed": ItemData(18, ItemClassification.filler, ["Consumables"]), + "Refresher": ItemData(19, ItemClassification.filler, ["Consumables"]), + "Exit Book": ItemData(20, ItemClassification.useful, ["Spells"]), + "Cure Book": ItemData(21, ItemClassification.useful, ["Spells"]), + "Heal Book": ItemData(22, ItemClassification.useful, ["Spells"]), + "Life Book": ItemData(23, ItemClassification.useful, ["Spells"]), + "Quake Book": ItemData(24, ItemClassification.useful, ["Spells"]), + "Blizzard Book": ItemData(25, ItemClassification.useful, ["Spells"]), + "Fire Book": ItemData(26, ItemClassification.useful, ["Spells"]), + "Aero Book": ItemData(27, ItemClassification.useful, ["Spells"]), + "Thunder Seal": ItemData(28, ItemClassification.useful, ["Spells"]), + "White Seal": ItemData(29, ItemClassification.useful, ["Spells"]), + "Meteor Seal": ItemData(30, ItemClassification.useful, ["Spells"]), + "Flare Seal": ItemData(31, ItemClassification.useful, ["Spells"]), + "Progressive Sword": ItemData(32 + 256, ItemClassification.progression, ["Weapons", "Swords"]), + "Steel Sword": ItemData(32, ItemClassification.progression, ["Weapons", "Swords"]), + "Knight Sword": ItemData(33, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]), + "Excalibur": ItemData(34, ItemClassification.progression_skip_balancing, ["Weapons", "Swords"]), + "Progressive Axe": ItemData(35 + 256, ItemClassification.progression, ["Weapons", "Axes"]), + "Axe": ItemData(35, ItemClassification.progression, ["Weapons", "Axes"]), + "Battle Axe": ItemData(36, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]), + "Giant's Axe": ItemData(37, ItemClassification.progression_skip_balancing, ["Weapons", "Axes"]), + "Progressive Claw": ItemData(38 + 256, ItemClassification.progression, ["Weapons", "Axes"]), + "Cat Claw": ItemData(38, ItemClassification.progression, ["Weapons", "Claws"]), + "Charm Claw": ItemData(39, ItemClassification.progression_skip_balancing, ["Weapons", "Claws"]), + "Dragon Claw": ItemData(40, ItemClassification.progression, ["Weapons", "Claws"]), + "Progressive Bomb": ItemData(41 + 256, ItemClassification.progression, ["Weapons", "Bombs"]), + "Bomb": ItemData(41, ItemClassification.progression, ["Weapons", "Bombs"]), + "Jumbo Bomb": ItemData(42, ItemClassification.progression_skip_balancing, ["Weapons", "Bombs"]), + "Mega Grenade": ItemData(43, ItemClassification.progression, ["Weapons", "Bombs"]), + # Ally-only equipment does nothing when received, no reason to put them in the datapackage + #"Morning Star": ItemData(44, ItemClassification.progression, ["Weapons"]), + #"Bow Of Grace": ItemData(45, ItemClassification.progression, ["Weapons"]), + #"Ninja Star": ItemData(46, ItemClassification.progression, ["Weapons"]), + + "Progressive Helm": ItemData(47 + 256, ItemClassification.useful, ["Helms"]), + "Steel Helm": ItemData(47, ItemClassification.useful, ["Helms"]), + "Moon Helm": ItemData(48, ItemClassification.useful, ["Helms"]), + "Apollo Helm": ItemData(49, ItemClassification.useful, ["Helms"]), + "Progressive Armor": ItemData(50 + 256, ItemClassification.useful, ["Armors"]), + "Steel Armor": ItemData(50, ItemClassification.useful, ["Armors"]), + "Noble Armor": ItemData(51, ItemClassification.useful, ["Armors"]), + "Gaia's Armor": ItemData(52, ItemClassification.useful, ["Armors"]), + #"Replica Armor": ItemData(53, ItemClassification.progression, ["Armors"]), + #"Mystic Robes": ItemData(54, ItemClassification.progression, ["Armors"]), + #"Flame Armor": ItemData(55, ItemClassification.progression, ["Armors"]), + #"Black Robe": ItemData(56, ItemClassification.progression, ["Armors"]), + "Progressive Shield": ItemData(57 + 256, ItemClassification.useful, ["Shields"]), + "Steel Shield": ItemData(57, ItemClassification.useful, ["Shields"]), + "Venus Shield": ItemData(58, ItemClassification.useful, ["Shields"]), + "Aegis Shield": ItemData(59, ItemClassification.useful, ["Shields"]), + #"Ether Shield": ItemData(60, ItemClassification.progression, ["Shields"]), + "Progressive Accessory": ItemData(61 + 256, ItemClassification.useful, ["Accessories"]), + "Charm": ItemData(61, ItemClassification.useful, ["Accessories"]), + "Magic Ring": ItemData(62, ItemClassification.useful, ["Accessories"]), + "Cupid Locket": ItemData(63, ItemClassification.useful, ["Accessories"]), + + # these are understood by FFMQR and I could place these if I want, but it's easier to just let FFMQR + # place them. I want an option to make shuffle battlefield rewards NOT color-code the battlefields, + # and then I would make the non-item reward battlefields into AP checks and these would be put into those as + # the item for AP. But there is no such option right now. + # "54 XP": ItemData(96, ItemClassification.filler, data_name="Xp54"), + # "99 XP": ItemData(97, ItemClassification.filler, data_name="Xp99"), + # "540 XP": ItemData(98, ItemClassification.filler, data_name="Xp540"), + # "744 XP": ItemData(99, ItemClassification.filler, data_name="Xp744"), + # "816 XP": ItemData(100, ItemClassification.filler, data_name="Xp816"), + # "1068 XP": ItemData(101, ItemClassification.filler, data_name="Xp1068"), + # "1200 XP": ItemData(102, ItemClassification.filler, data_name="Xp1200"), + # "2700 XP": ItemData(103, ItemClassification.filler, data_name="Xp2700"), + # "2808 XP": ItemData(104, ItemClassification.filler, data_name="Xp2808"), + # "150 Gp": ItemData(105, ItemClassification.filler, data_name="Gp150"), + # "300 Gp": ItemData(106, ItemClassification.filler, data_name="Gp300"), + # "600 Gp": ItemData(107, ItemClassification.filler, data_name="Gp600"), + # "900 Gp": ItemData(108, ItemClassification.filler, data_name="Gp900"), + # "1200 Gp": ItemData(109, ItemClassification.filler, data_name="Gp1200"), + + + "Bomb Refill": ItemData(221, ItemClassification.filler, ["Refills"]), + "Projectile Refill": ItemData(222, ItemClassification.filler, ["Refills"]), + #"None": ItemData(255, ItemClassification.progression, []), + + "Kaeli 1": ItemData(None, ItemClassification.progression), + "Kaeli 2": ItemData(None, ItemClassification.progression), + "Tristam": ItemData(None, ItemClassification.progression), + "Phoebe 1": ItemData(None, ItemClassification.progression), + "Reuben 1": ItemData(None, ItemClassification.progression), + "Reuben Dad Saved": ItemData(None, ItemClassification.progression), + "Otto": ItemData(None, ItemClassification.progression), + "Captain Mac": ItemData(None, ItemClassification.progression), + "Ship Steering Wheel": ItemData(None, ItemClassification.progression), + "Minotaur": ItemData(None, ItemClassification.progression), + "Flamerus Rex": ItemData(None, ItemClassification.progression), + "Phanquid": ItemData(None, ItemClassification.progression), + "Freezer Crab": ItemData(None, ItemClassification.progression), + "Ice Golem": ItemData(None, ItemClassification.progression), + "Jinn": ItemData(None, ItemClassification.progression), + "Medusa": ItemData(None, ItemClassification.progression), + "Dualhead Hydra": ItemData(None, ItemClassification.progression), + "Gidrah": ItemData(None, ItemClassification.progression), + "Dullahan": ItemData(None, ItemClassification.progression), + "Pazuzu": ItemData(None, ItemClassification.progression), + "Aquaria Plaza": ItemData(None, ItemClassification.progression), + "Summer Aquaria": ItemData(None, ItemClassification.progression), + "Reuben Mine": ItemData(None, ItemClassification.progression), + "Alive Forest": ItemData(None, ItemClassification.progression), + "Rainbow Bridge": ItemData(None, ItemClassification.progression), + "Collapse Spencer's Cave": ItemData(None, ItemClassification.progression), + "Ship Liberated": ItemData(None, ItemClassification.progression), + "Ship Loaned": ItemData(None, ItemClassification.progression), + "Ship Dock Access": ItemData(None, ItemClassification.progression), + "Stone Golem": ItemData(None, ItemClassification.progression), + "Twinhead Wyvern": ItemData(None, ItemClassification.progression), + "Zuh": ItemData(None, ItemClassification.progression), + + "Libra Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Life Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Aquaria Vendor Crest Tile": ItemData(None, ItemClassification.progression), + "Fireburg Vendor Crest Tile": ItemData(None, ItemClassification.progression), + "Fireburg Grenademan Crest Tile": ItemData(None, ItemClassification.progression), + "Sealed Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Wintry Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Kaidge Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Light Temple Crest Tile": ItemData(None, ItemClassification.progression), + "Windia Kids Crest Tile": ItemData(None, ItemClassification.progression), + "Windia Dock Crest Tile": ItemData(None, ItemClassification.progression), + "Ship Dock Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Libra Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Gemini Crest Tile": ItemData(None, ItemClassification.progression), + "Alive Forest Mobius Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Libra Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Gemini Crest Tile": ItemData(None, ItemClassification.progression), + "Wood House Mobius Crest Tile": ItemData(None, ItemClassification.progression), + "Barrel Pushed": ItemData(None, ItemClassification.progression), + "Long Spine Bombed": ItemData(None, ItemClassification.progression), + "Short Spine Bombed": ItemData(None, ItemClassification.progression), + "Skull 1 Bombed": ItemData(None, ItemClassification.progression), + "Skull 2 Bombed": ItemData(None, ItemClassification.progression), + "Ice Pyramid 1F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 3F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 4F Statue": ItemData(None, ItemClassification.progression), + "Ice Pyramid 5F Statue": ItemData(None, ItemClassification.progression), + "Spencer Cave Libra Block Bombed": ItemData(None, ItemClassification.progression), + "Lava Dome Plate": ItemData(None, ItemClassification.progression), + "Pazuzu 2F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 4F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 6F Lock": ItemData(None, ItemClassification.progression), + "Pazuzu 1F": ItemData(None, ItemClassification.progression), + "Pazuzu 2F": ItemData(None, ItemClassification.progression), + "Pazuzu 3F": ItemData(None, ItemClassification.progression), + "Pazuzu 4F": ItemData(None, ItemClassification.progression), + "Pazuzu 5F": ItemData(None, ItemClassification.progression), + "Pazuzu 6F": ItemData(None, ItemClassification.progression), + "Dark King": ItemData(None, ItemClassification.progression), + #"Barred": ItemData(None, ItemClassification.progression), + +} + +prog_map = { + "Swords": "Progressive Sword", + "Axes": "Progressive Axe", + "Claws": "Progressive Claw", + "Bombs": "Progressive Bomb", + "Shields": "Progressive Shield", + "Armors": "Progressive Armor", + "Helms": "Progressive Helm", + "Accessories": "Progressive Accessory", +} + + +def yaml_item(text): + if text == "CaptainCap": + return "Captain's Cap" + elif text == "WakeWater": + return "Wakewater" + return "".join( + [(" " + c if (c.isupper() or c.isnumeric()) and not (text[i - 1].isnumeric() and c == "F") else c) for + i, c in enumerate(text)]).strip() + + +item_groups = {} +for item, data in item_table.items(): + for group in data.groups: + item_groups[group] = item_groups.get(group, []) + [item] + + +def create_items(self) -> None: + items = [] + starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + if self.multiworld.progressive_gear[self.player]: + for item_group in prog_map: + if starting_weapon in self.item_name_groups[item_group]: + starting_weapon = prog_map[item_group] + break + self.multiworld.push_precollected(self.create_item(starting_weapon)) + self.multiworld.push_precollected(self.create_item("Steel Armor")) + if self.multiworld.sky_coin_mode[self.player] == "start_with": + self.multiworld.push_precollected(self.create_item("Sky Coin")) + + precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} + + def add_item(item_name): + if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: + return + if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + return + if self.multiworld.progressive_gear[self.player]: + for item_group in prog_map: + if item_name in self.item_name_groups[item_group]: + item_name = prog_map[item_group] + break + if item_name == "Sky Coin": + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + for _ in range(40): + items.append(self.create_item("Sky Fragment")) + return + elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + items.append(self.create_filler()) + return + if item_name in precollected_item_names: + items.append(self.create_filler()) + return + i = self.create_item(item_name) + if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + i.classification = ItemClassification.useful + if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + item_name == "Exit Book"): + i.classification = ItemClassification.progression + items.append(i) + + for item_group in ("Key Items", "Spells", "Armors", "Helms", "Shields", "Accessories", "Weapons"): + for item in self.item_name_groups[item_group]: + add_item(item) + + if self.multiworld.brown_boxes[self.player] == "include": + filler_items = [] + for item, count in fillers.items(): + filler_items += [self.create_item(item) for _ in range(count)] + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + self.multiworld.random.shuffle(filler_items) + filler_items = filler_items[39:] + items += filler_items + + self.multiworld.itempool += items + + if len(self.multiworld.player_ids) > 1: + early_choices = ["Sand Coin", "River Coin"] + early_item = self.multiworld.random.choice(early_choices) + self.multiworld.early_items[self.player][early_item] = 1 + + +class FFMQItem(Item): + game = "Final Fantasy Mystic Quest" + type = None + + def __init__(self, name, player: int = None): + item_data = item_table[name] + super(FFMQItem, self).__init__( + name, + item_data.classification, + item_data.id, player + ) \ No newline at end of file diff --git a/worlds/ffmq/LICENSE b/worlds/ffmq/LICENSE new file mode 100644 index 0000000000..46ad1c0074 --- /dev/null +++ b/worlds/ffmq/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Alex "Alchav" Avery +Copyright (c) 2023 wildham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py new file mode 100644 index 0000000000..2746bb1977 --- /dev/null +++ b/worlds/ffmq/Options.py @@ -0,0 +1,258 @@ +from Options import Choice, FreeText, Toggle + + +class Logic(Choice): + """Placement logic sets the rules that will be applied when placing items. Friendly: Required Items to clear a + dungeon will never be placed in that dungeon to avoid the need to revisit it. Also, the Magic Mirror and the Mask + will always be available before Ice Pyramid and Volcano, respectively. Note: If Dungeons are shuffled, Friendly + logic will only ensure the availability of the Mirror and the Mask. Standard: Items are randomly placed and logic + merely verifies that they're all accessible. As for Region access, only the Coins are considered. Expert: Same as + Standard, but Items Placement logic also includes other routes than Coins: the Crests Teleporters, the + Fireburg-Aquaria Lava bridge and the Sealed Temple Exit trick.""" + option_friendly = 0 + option_standard = 1 + option_expert = 2 + default = 1 + display_name = "Logic" + + +class BrownBoxes(Choice): + """Include the 201 brown box locations from the original game. Brown Boxes are all the boxes that contained a + consumable in the original game. If shuffle is chosen, the consumables contained will be shuffled but the brown + boxes will not be Archipelago location checks.""" + option_exclude = 0 + option_include = 1 + option_shuffle = 2 + default = 1 + display_name = "Brown Boxes" + + +class SkyCoinMode(Choice): + """Configure how the Sky Coin is acquired. With standard, the Sky Coin will be placed randomly. With Start With, the + Sky Coin will be in your inventory at the start of the game. With Save The Crystals, the Sky Coin will be acquired + once you save all 4 crystals. With Shattered Sky Coin, the Sky Coin is split in 40 fragments; you can enter Doom + Castle once the required amount is found. Shattered Sky Coin will force brown box locations to be included.""" + option_standard = 0 + option_start_with = 1 + option_save_the_crystals = 2 + option_shattered_sky_coin = 3 + default = 0 + display_name = "Sky Coin Mode" + + +class ShatteredSkyCoinQuantity(Choice): + """Configure the number of the 40 Sky Coin Fragments required to enter the Doom Castle. Only has an effect if + Sky Coin Mode is set to shattered. Low: 16. Mid: 24. High: 32. Random Narrow: random between 16 and 32. + Random Wide: random between 10 and 38.""" + option_low_16 = 0 + option_mid_24 = 1 + option_high_32 = 2 + option_random_narrow = 3 + option_random_wide = 4 + default = 1 + display_name = "Shattered Sky Coin" + + +class StartingWeapon(Choice): + """Choose your starting weapon.""" + display_name = "Starting Weapon" + option_steel_sword = 0 + option_axe = 1 + option_cat_claw = 2 + option_bomb = 3 + default = "random" + + +class ProgressiveGear(Toggle): + """Pieces of gear are always acquired from weakest to strongest in a set.""" + display_name = "Progressive Gear" + + +class EnemiesDensity(Choice): + """Set how many of the original enemies are on each map.""" + display_name = "Enemies Density" + option_all = 0 + option_three_quarter = 1 + option_half = 2 + option_quarter = 3 + option_none = 4 + + +class EnemyScaling(Choice): + """Superclass for enemy scaling options.""" + option_quarter = 0 + option_half = 1 + option_three_quarter = 2 + option_normal = 3 + option_one_and_quarter = 4 + option_one_and_half = 5 + option_double = 6 + option_double_and_half = 7 + option_triple = 8 + + +class EnemiesScalingLower(EnemyScaling): + """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones.""" + display_name = "Enemies Scaling Lower" + default = 0 + + +class EnemiesScalingUpper(EnemyScaling): + """Randomly adjust enemies stats by the selected range percentage. Include mini-bosses' weaker clones.""" + display_name = "Enemies Scaling Upper" + default = 4 + + +class BossesScalingLower(EnemyScaling): + """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and + the Dark King.""" + display_name = "Bosses Scaling Lower" + default = 0 + + +class BossesScalingUpper(EnemyScaling): + """Randomly adjust bosses stats by the selected range percentage. Include Mini-Bosses, Bosses, Bosses' refights and + the Dark King.""" + display_name = "Bosses Scaling Upper" + default = 4 + + +class EnemizerAttacks(Choice): + """Shuffles enemy attacks. Standard: No shuffle. Safe: Randomize every attack but leave out self-destruct and Dark + King attacks. Chaos: Randomize and include self-destruct and Dark King attacks. Self Destruct: Every enemy + self-destructs. Simple Shuffle: Instead of randomizing, shuffle one monster's attacks to another. Dark King is left + vanilla.""" + display_name = "Enemizer Attacks" + option_normal = 0 + option_safe = 1 + option_chaos = 2 + option_self_destruct = 3 + option_simple_shuffle = 4 + default = 0 + + +class ShuffleEnemiesPositions(Toggle): + """Instead of their original position in a given map, enemies are randomly placed.""" + display_name = "Shuffle Enemies' Positions" + default = 1 + + +class ProgressiveFormations(Choice): + """Enemies' formations are selected by regions, with the weakest formations always selected in Foresta and the + strongest in Windia. Disabled: Standard formations are used. Regions Strict: Formations will come exclusively + from the current region, whatever the map is. Regions Keep Type: Formations will keep the original formation type + and match with the nearest power level.""" + display_name = "Progressive Formations" + option_disabled = 0 + option_regions_strict = 1 + option_regions_keep_type = 2 + + +class DoomCastle(Choice): + """Configure how you reach the Dark King. With Standard, you need to defeat all four bosses and their floors to + reach the Dark King. With Boss Rush, only the bosses are blocking your way in the corridor to the Dark King's room. + With Dark King Only, the way to the Dark King is free of any obstacle.""" + display_name = "Doom Castle" + option_standard = 0 + option_boss_rush = 1 + option_dark_king_only = 2 + + +class DoomCastleShortcut(Toggle): + """Create a shortcut granting access from the start to Doom Castle at Focus Tower's entrance. + Also modify the Desert floor, so it can be navigated without the Mega Grenades and the Dragon Claw.""" + display_name = "Doom Castle Shortcut" + + +class TweakFrustratingDungeons(Toggle): + """Make some small changes to a few of the most annoying dungeons. Ice Pyramid: Add 3 shortcuts on the 1st floor. + Giant Tree: Add shortcuts on the 1st and 4th floors and curtail mushrooms population. + Pazuzu's Tower: Staircases are devoid of enemies (regardless of Enemies Density settings).""" + display_name = "Tweak Frustrating Dungeons" + + +class MapShuffle(Choice): + """None: No shuffle. Overworld: Only shuffle the Overworld locations. Dungeons: Only shuffle the dungeons' floors + amongst themselves. Temples and Towns aren't included. Overworld And Dungeons: Shuffle the Overworld and dungeons + at the same time. Everything: Shuffle the Overworld, dungeons, temples and towns all amongst each others. + When dungeons are shuffled, defeating Pazuzu won't teleport you to the 7th floor, you have to get there normally to + save the Crystal and get Pazuzu's Chest.""" + display_name = "Map Shuffle" + option_none = 0 + option_overworld = 1 + option_dungeons = 2 + option_overworld_and_dungeons = 3 + option_everything = 4 + default = 0 + + +class CrestShuffle(Toggle): + """Shuffle the Crest tiles amongst themselves.""" + display_name = "Crest Shuffle" + + +class MapShuffleSeed(FreeText): + """If this is a number, it will be used as a set seed number for Map, Crest, and Battlefield Reward shuffles. + If this is "random" the seed will be chosen randomly. If it is any other text, it will be used as a seed group name. + All players using the same seed group name will get the same shuffle results, as long as their Map Shuffle, + Crest Shuffle, and Shuffle Battlefield Rewards settings are the same.""" + display_name = "Map Shuffle Seed" + default = "random" + + +class LevelingCurve(Choice): + """Adjust the level gain rate.""" + display_name = "Leveling Curve" + option_half = 0 + option_normal = 1 + option_one_and_half = 2 + option_double = 3 + option_double_and_half = 4 + option_triple = 5 + option_quadruple = 6 + default = 4 + + +class ShuffleBattlefieldRewards(Toggle): + """Shuffle the type of reward (Item, XP, GP) given by battlefields and color code them by reward type. + Blue: Give an item. Grey: Give XP. Green: Give GP.""" + display_name = "Shuffle Battlefield Rewards" + + +class BattlefieldsBattlesQuantities(Choice): + """Adjust the number of battles that need to be fought to get a battlefield's reward.""" + display_name = "Battlefields Battles Quantity" + option_ten = 0 + option_seven = 1 + option_five = 2 + option_three = 3 + option_one = 4 + option_random_one_through_five = 5 + option_random_one_through_ten = 6 + + +option_definitions = { + "logic": Logic, + "brown_boxes": BrownBoxes, + "sky_coin_mode": SkyCoinMode, + "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, + "starting_weapon": StartingWeapon, + "progressive_gear": ProgressiveGear, + "enemies_density": EnemiesDensity, + "enemies_scaling_lower": EnemiesScalingLower, + "enemies_scaling_upper": EnemiesScalingUpper, + "bosses_scaling_lower": BossesScalingLower, + "bosses_scaling_upper": BossesScalingUpper, + "enemizer_attacks": EnemizerAttacks, + "shuffle_enemies_position": ShuffleEnemiesPositions, + "progressive_formations": ProgressiveFormations, + "doom_castle_mode": DoomCastle, + "doom_castle_shortcut": DoomCastleShortcut, + "tweak_frustrating_dungeons": TweakFrustratingDungeons, + "map_shuffle": MapShuffle, + "crest_shuffle": CrestShuffle, + "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, + "map_shuffle_seed": MapShuffleSeed, + "leveling_curve": LevelingCurve, + "battlefields_battles_quantities": BattlefieldsBattlesQuantities, +} diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py new file mode 100644 index 0000000000..c4c4605c85 --- /dev/null +++ b/worlds/ffmq/Output.py @@ -0,0 +1,113 @@ +import yaml +import os +import zipfile +from copy import deepcopy +from .Regions import object_id_table +from Main import __version__ +from worlds.Files import APContainer +import pkgutil + +settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) + + +def generate_output(self, output_directory): + def output_item_name(item): + if item.player == self.player: + if item.code > 0x420000 + 256: + item_name = self.item_id_to_name[item.code - 256] + else: + item_name = item.name + item_name = "".join(item_name.split("'")) + item_name = "".join(item_name.split(" ")) + else: + if item.advancement or item.useful or (item.trap and + self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + item_name = "APItem" + else: + item_name = "APItemFiller" + return item_name + + item_placement = [] + for location in self.multiworld.get_locations(self.player): + if location.type != "Trigger": + item_placement.append({"object_id": object_id_table[location.name], "type": location.type, "content": + output_item_name(location.item), "player": self.multiworld.player_name[location.item.player], + "item_name": location.item.name}) + + def cc(option): + return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons") + + def tf(option): + return True if option else False + + options = deepcopy(settings_template) + options["name"] = self.multiworld.player_name[self.player] + + option_writes = { + "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "chests_shuffle": "Include", + "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "npcs_shuffle": "Include", + "battlefields_shuffle": "Include", + "logic_options": cc(self.multiworld.logic[self.player]), + "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), + "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), + "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), + "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), + "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), + "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), + "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), + "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if + self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "RandomLow" if + self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + "RandomHigh", + "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "random_starting_weapon": True, + "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), + "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), + "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), + "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), + "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), + "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "enable_spoilers": False, + "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), + "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), + "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), + } + for option, data in option_writes.items(): + options["Final Fantasy Mystic Quest"][option][data] = 1 + + rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + self.rom_name = bytearray(rom_name, + 'utf8') + self.rom_name_available_event.set() + + setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": + hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + + starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + starting_items.append("SkyCoin") + + file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") + + APMQ = APMQFile(file_path, player=self.player, player_name=self.multiworld.player_name[self.player]) + with zipfile.ZipFile(file_path, mode="w", compression=zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + zf.writestr("itemplacement.yaml", yaml.dump(item_placement)) + zf.writestr("flagset.yaml", yaml.dump(options)) + zf.writestr("startingitems.yaml", yaml.dump(starting_items)) + zf.writestr("setup.yaml", yaml.dump(setup)) + zf.writestr("rooms.yaml", yaml.dump(self.rooms)) + + APMQ.write_contents(zf) + + +class APMQFile(APContainer): + game = "Final Fantasy Mystic Quest" + + def get_manifest(self): + manifest = super().get_manifest() + manifest["patch_file_ending"] = ".apmq" + return manifest \ No newline at end of file diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py new file mode 100644 index 0000000000..aac8289a36 --- /dev/null +++ b/worlds/ffmq/Regions.py @@ -0,0 +1,251 @@ +from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification +from worlds.generic.Rules import add_rule +from .Items import item_groups, yaml_item +import pkgutil +import yaml + +rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) +entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} + +object_id_table = {} +object_type_table = {} +offset = {"Chest": 0x420000, "Box": 0x420000, "NPC": 0x420000 + 300, "BattlefieldItem": 0x420000 + 350} +for room in rooms: + for object in room["game_objects"]: + if "Hero Chest" in object["name"] or object["type"] == "Trigger": + continue + if object["type"] in ("BattlefieldItem", "BattlefieldXp", "BattlefieldGp"): + object_type_table[object["name"]] = "BattlefieldItem" + elif object["type"] in ("Chest", "NPC", "Box"): + object_type_table[object["name"]] = object["type"] + object_id_table[object["name"]] = object["object_id"] + +location_table = {loc_name: offset[object_type_table[loc_name]] + obj_id for loc_name, obj_id in + object_id_table.items()} + +weapons = ("Claw", "Bomb", "Sword", "Axe") +crest_warps = [51, 52, 53, 76, 96, 108, 158, 171, 175, 191, 275, 276, 277, 308, 334, 336, 396, 397] + + +def process_rules(spot, access): + for weapon in weapons: + if weapon in access: + add_rule(spot, lambda state, w=weapon: state.has_any(item_groups[w + "s"], spot.player)) + access = [yaml_item(rule) for rule in access if rule not in weapons] + add_rule(spot, lambda state: state.has_all(access, spot.player)) + + +def create_region(world: MultiWorld, player: int, name: str, room_id=None, locations=None, links=None): + if links is None: + links = [] + ret = Region(name, player, world) + if locations: + for location in locations: + location.parent_region = ret + ret.locations.append(location) + ret.links = links + ret.id = room_id + return ret + + +def get_entrance_to(entrance_to): + for room in rooms: + if room["id"] == entrance_to["target_room"]: + for link in room["links"]: + if link["target_room"] == entrance_to["room"]: + return link + else: + raise Exception(f"Did not find entrance {entrance_to}") + + +def create_regions(self): + + menu_region = create_region(self.multiworld, self.player, "Menu") + self.multiworld.regions.append(menu_region) + + for room in self.rooms: + self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"], + [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in + location_table else None, object["type"], object["access"], + self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for + object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in + ("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or + self.multiworld.brown_boxes[self.player] == "include")], room["links"])) + + dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) + dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", []) + dark_king.parent_region = dark_king_room + dark_king.place_locked_item(self.create_item("Dark King")) + dark_king_room.locations.append(dark_king) + + connection = Entrance(self.player, f"Enter Overworld", menu_region) + connection.connect(self.multiworld.get_region("Overworld", self.player)) + menu_region.exits.append(connection) + + for region in self.multiworld.get_regions(self.player): + for link in region.links: + for connect_room in self.multiworld.get_regions(self.player): + if connect_room.id == link["target_room"]: + connection = Entrance(self.player, entrance_names[link["entrance"]] if "entrance" in link and + link["entrance"] != -1 else f"{region.name} to {connect_room.name}", region) + if "entrance" in link and link["entrance"] != -1: + spoiler = False + if link["entrance"] in crest_warps: + if self.multiworld.crest_shuffle[self.player]: + spoiler = True + elif self.multiworld.map_shuffle[self.player] == "everything": + spoiler = True + elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", + "none"): + spoiler = True + elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", + "overworld"): + spoiler = True + + if spoiler: + self.multiworld.spoiler.set_entrance(entrance_names[link["entrance"]], connect_room.name, + 'both', self.player) + if link["access"]: + process_rules(connection, link["access"]) + region.exits.append(connection) + connection.connect(connect_room) + break + +non_dead_end_crest_rooms = [ + 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', + 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', + 'Windia Kid House Basement', 'Windia Old People House Basement' +] + +non_dead_end_crest_warps = [ + 'Libra Temple - Libra Tile Script', 'Aquaria Gemini Room - Gemini Script', + 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'Fireburg Gemini Room - Gemini Teleporter Script', + 'Sealed Temple - Gemini Tile Script', 'Alive Forest - Libra Teleporter Script', + 'Alive Forest - Gemini Teleporter Script', 'Alive Forest - Mobius Teleporter Script', + 'Kaidge Temple - Mobius Teleporter Script', 'Windia Kid House Basement - Mobius Teleporter', + 'Windia Old People House Basement - Mobius Teleporter Script', +] + + +vendor_locations = ["Aquaria - Vendor", "Fireburg - Vendor", "Windia - Vendor"] + + +def set_rules(self) -> None: + self.multiworld.completion_condition[self.player] = lambda state: state.has("Dark King", self.player) + + def hard_boss_logic(state): + return state.has_all(["River Coin", "Sand Coin"], self.player) + + add_rule(self.multiworld.get_location("Pazuzu 1F", self.player), hard_boss_logic) + add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) + add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) + + if self.multiworld.map_shuffle[self.player]: + for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): + loc = self.multiworld.get_location(boss, self.player) + checked_regions = {loc.parent_region} + + def check_foresta(region): + if region.name == "Subregion Foresta": + add_rule(loc, hard_boss_logic) + return True + elif "Subregion" in region.name: + return True + for entrance in region.entrances: + if entrance.parent_region not in checked_regions: + checked_regions.add(entrance.parent_region) + if check_foresta(entrance.parent_region): + return True + check_foresta(loc.parent_region) + + if self.multiworld.logic[self.player] == "friendly": + process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), + ["MagicMirror"]) + process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), + ["Mask"]) + if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), + ["Bomb"]) + process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), + ["Bomb", "Claw"]) + process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), + ["Bomb", "Claw"]) + process_rules(self.multiworld.get_entrance("Overworld - Mine", self.player), + ["MegaGrenade", "Claw", "Reuben1"]) + process_rules(self.multiworld.get_entrance("Overworld - Lava Dome", self.player), + ["MegaGrenade"]) + process_rules(self.multiworld.get_entrance("Overworld - Giant Tree", self.player), + ["DragonClaw", "Axe"]) + process_rules(self.multiworld.get_entrance("Overworld - Mount Gale", self.player), + ["DragonClaw"]) + process_rules(self.multiworld.get_entrance("Overworld - Pazuzu Tower", self.player), + ["DragonClaw", "Bomb"]) + process_rules(self.multiworld.get_entrance("Overworld - Mac Ship", self.player), + ["DragonClaw", "CaptainCap"]) + process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), + ["DragonClaw", "CaptainCap"]) + + if self.multiworld.logic[self.player] == "expert": + if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) + connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) + connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) + connection.access_rule = lambda state: state.has("Exit Book", self.player) + inner_room.exits.append(connection) + else: + for crest_warp in non_dead_end_crest_warps: + entrance = self.multiworld.get_entrance(crest_warp, self.player) + if entrance.connected_region.name in non_dead_end_crest_rooms: + entrance.access_rule = lambda state: False + + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has("Sky Fragment", self.player, logic_coins) + elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) + elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ + lambda state: state.has("Sky Coin", self.player) + + +def stage_set_rules(multiworld): + # If there's no enemies, there's no repeatable income sources + no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") + if multiworld.enemies_density[player] == "none"] + if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, + ItemClassification.trap)]) > len([player for player in no_enemies_players if + multiworld.accessibility[player] == "minimal"]) * 3): + for player in no_enemies_players: + for location in vendor_locations: + if multiworld.accessibility[player] == "locations": + print("exclude") + multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED + else: + print("unreachable") + multiworld.get_location(location, player).access_rule = lambda state: False + else: + # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing + # advancement items so that useful items can be placed. + print("no advancement") + for player in no_enemies_players: + for location in vendor_locations: + multiworld.get_location(location, player).item_rule = lambda item: not item.advancement + + + + +class FFMQLocation(Location): + game = "Final Fantasy Mystic Quest" + + def __init__(self, player, name, address, loc_type, access=None, event=None): + super(FFMQLocation, self).__init__( + player, name, + address + ) + self.type = loc_type + if access: + process_rules(self, access) + if event: + self.place_locked_item(event) diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py new file mode 100644 index 0000000000..b6f19a77fb --- /dev/null +++ b/worlds/ffmq/__init__.py @@ -0,0 +1,217 @@ +import Utils +import settings +import base64 +import threading +import requests +import yaml +from worlds.AutoWorld import World, WebWorld +from BaseClasses import Tutorial +from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\ + non_dead_end_crest_warps +from .Items import item_table, item_groups, create_items, FFMQItem, fillers +from .Output import generate_output +from .Options import option_definitions +from .Client import FFMQClient + + +# removed until lists are supported +# class FFMQSettings(settings.Group): +# class APIUrls(list): +# """A list of API URLs to get map shuffle, crest shuffle, and battlefield reward shuffle data from.""" +# api_urls: APIUrls = [ +# "https://api.ffmqrando.net/", +# "http://ffmqr.jalchavware.com:5271/" +# ] + + +class FFMQWebWorld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Final Fantasy Mystic Quest with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["Alchav"] + )] + + +class FFMQWorld(World): + """Final Fantasy: Mystic Quest is a simple, humorous RPG for the Super Nintendo. You travel across four continents, + linked in the middle of the world by the Focus Tower, which has been locked by four magical coins. Make your way to + the bottom of the Focus Tower, then straight up through the top!""" + # -Giga Otomia + + game = "Final Fantasy Mystic Quest" + + item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} + location_name_to_id = location_table + option_definitions = option_definitions + + topology_present = True + + item_name_groups = item_groups + + generate_output = generate_output + create_items = create_items + create_regions = create_regions + set_rules = set_rules + stage_set_rules = stage_set_rules + + data_version = 1 + + web = FFMQWebWorld() + # settings: FFMQSettings + + def __init__(self, world, player: int): + self.rom_name_available_event = threading.Event() + self.rom_name = None + self.rooms = None + super().__init__(world, player) + + def generate_early(self): + if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + self.multiworld.brown_boxes[self.player].value = 1 + if self.multiworld.enemies_scaling_lower[self.player].value > \ + self.multiworld.enemies_scaling_upper[self.player].value: + (self.multiworld.enemies_scaling_lower[self.player].value, + self.multiworld.enemies_scaling_upper[self.player].value) =\ + (self.multiworld.enemies_scaling_upper[self.player].value, + self.multiworld.enemies_scaling_lower[self.player].value) + if self.multiworld.bosses_scaling_lower[self.player].value > \ + self.multiworld.bosses_scaling_upper[self.player].value: + (self.multiworld.bosses_scaling_lower[self.player].value, + self.multiworld.bosses_scaling_upper[self.player].value) =\ + (self.multiworld.bosses_scaling_upper[self.player].value, + self.multiworld.bosses_scaling_lower[self.player].value) + + @classmethod + def stage_generate_early(cls, multiworld): + + # api_urls = Utils.get_options()["ffmq_options"].get("api_urls", None) + api_urls = [ + "https://api.ffmqrando.net/", + "http://ffmqr.jalchavware.com:5271/" + ] + + rooms_data = {} + + for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): + if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or + world.multiworld.crest_shuffle[world.player]): + if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): + multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) + elif world.multiworld.map_shuffle_seed[world.player].value != "random": + multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) + + int(world.multiworld.seed)) + + seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() + map_shuffle = multiworld.map_shuffle[world.player].value + crest_shuffle = multiworld.crest_shuffle[world.player].current_key + battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key + + query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}" + + if query in rooms_data: + world.rooms = rooms_data[query] + continue + + if not api_urls: + raise Exception("No FFMQR API URLs specified in host.yaml") + + errors = [] + for api_url in api_urls.copy(): + try: + response = requests.get(f"{api_url}GenerateRooms?{query}") + except (ConnectionError, requests.exceptions.HTTPError, requests.exceptions.ConnectionError, + requests.exceptions.RequestException) as err: + api_urls.remove(api_url) + errors.append([api_url, err]) + else: + if response.ok: + world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader) + break + else: + api_urls.remove(api_url) + errors.append([api_url, response]) + else: + error_text = f"Failed to fetch map shuffle data for FFMQ player {world.player}" + for error in errors: + error_text += f"\n{error[0]} - got error {error[1].status_code} {error[1].reason} {error[1].text}" + raise Exception(error_text) + api_urls.append(api_urls.pop(0)) + else: + world.rooms = rooms + + def create_item(self, name: str): + return FFMQItem(name, self.player) + + def collect_item(self, state, item, remove=False): + if "Progressive" in item.name: + i = item.code - 256 + if state.has(self.item_id_to_name[i], self.player): + if state.has(self.item_id_to_name[i+1], self.player): + return self.item_id_to_name[i+2] + return self.item_id_to_name[i+1] + return self.item_id_to_name[i] + return item.name if item.advancement else None + + def modify_multidata(self, multidata): + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + # we skip in case of error, so that the original error in the output thread is the one that gets raised + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + payload = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][new_name] = payload + + def get_filler_item_name(self): + r = self.multiworld.random.randint(0, 201) + for item, count in fillers.items(): + r -= count + r -= fillers[item] + if r <= 0: + return item + + def extend_hint_information(self, hint_data): + hint_data[self.player] = {} + if self.multiworld.map_shuffle[self.player]: + single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] + for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", + "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", + "Subregion Doom Castle"]: + region = self.multiworld.get_region(subregion, self.player) + for location in region.locations: + if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + + (" Region" if subregion not in + single_location_regions else "")) + for overworld_spot in region.exits: + if ("Subregion" in overworld_spot.connected_region.name or + overworld_spot.name == "Overworld - Mac Ship Doom" or "Focus Tower" in overworld_spot.name + or "Doom Castle" in overworld_spot.name or overworld_spot.name == "Overworld - Giant Tree"): + continue + exits = list(overworld_spot.connected_region.exits) + [overworld_spot] + checked_regions = set() + while exits: + exit_check = exits.pop() + if (exit_check.connected_region not in checked_regions and "Subregion" not in + exit_check.connected_region.name): + checked_regions.add(exit_check.connected_region) + exits.extend(exit_check.connected_region.exits) + for location in exit_check.connected_region.locations: + if location.address: + hint = [] + if self.multiworld.map_shuffle[self.player] != "dungeons": + hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not + in single_location_regions else ""))) + if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ + ("Subregion Mac's Ship", "Subregion Doom Castle"): + hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", + "Pazuzu's")) + hint = " - ".join(hint) + if location.address in hint_data[self.player]: + hint_data[self.player][location.address] += f"/{hint}" + else: + hint_data[self.player][location.address] = hint + diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml new file mode 100644 index 0000000000..15bcd02bf6 --- /dev/null +++ b/worlds/ffmq/data/entrances.yaml @@ -0,0 +1,2425 @@ +- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor + id: 0 + area: 7 + coordinates: [24, 19] + teleporter: [0, 0] +- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor + id: 1 + area: 7 + coordinates: [19, 43] + teleporter: [1, 6] +- name: Doom Castle - Aero Room - Aero Room Entrance + id: 2 + area: 7 + coordinates: [27, 39] + teleporter: [1, 0] +- name: Focus Tower B1 - Main Loop - South Entrance + id: 3 + area: 8 + coordinates: [43, 60] + teleporter: [2, 6] +- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall + id: 4 + area: 8 + coordinates: [37, 41] + teleporter: [4, 0] +- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room + id: 5 + area: 8 + coordinates: [59, 35] + teleporter: [5, 0] +- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest + id: 6 + area: 8 + coordinates: [57, 59] + teleporter: [8, 0] +- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door + id: 7 + area: 8 + coordinates: [51, 49] + teleporter: [6, 0] +- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor + id: 8 + area: 8 + coordinates: [51, 45] + teleporter: [7, 0] +- name: Focus Tower 1F - Focus Tower West Entrance + id: 9 + area: 9 + coordinates: [25, 29] + teleporter: [3, 6] +- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin + id: 10 + area: 9 + coordinates: [16, 4] + teleporter: [10, 0] +- name: Focus Tower 1F - To Focus Tower B1 - Main Hall + id: 11 + area: 9 + coordinates: [4, 23] + teleporter: [11, 0] +- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest + id: 12 + area: 9 + coordinates: [26, 17] + teleporter: [12, 0] +- name: Focus Tower 1F - Sky Door + id: 13 + area: 9 + coordinates: [16, 24] + teleporter: [13, 0] +- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin + id: 14 + area: 9 + coordinates: [16, 10] + teleporter: [14, 0] +- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door + id: 15 + area: 9 + coordinates: [16, 29] + teleporter: [15, 0] +- name: Focus Tower 2F - Sand Coin Passage - North Entrance + id: 16 + area: 10 + coordinates: [49, 30] + teleporter: [4, 6] +- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin + id: 17 + area: 10 + coordinates: [47, 33] + teleporter: [17, 0] +- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin + id: 18 + area: 10 + coordinates: [47, 41] + teleporter: [18, 0] +- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor + id: 19 + area: 10 + coordinates: [38, 40] + teleporter: [20, 0] +- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor + id: 20 + area: 10 + coordinates: [56, 40] + teleporter: [19, 0] +- name: Focus Tower 2F - Venus Chest Room - Pillar Script + id: 21 + area: 10 + coordinates: [48, 53] + teleporter: [13, 8] +- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance + id: 22 + area: 11 + coordinates: [11, 39] + teleporter: [6, 6] +- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar + id: 23 + area: 11 + coordinates: [6, 47] + teleporter: [24, 0] +- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance + id: 24 + area: 11 + coordinates: [21, 38] + teleporter: [5, 6] +- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room + id: 25 + area: 11 + coordinates: [24, 47] + teleporter: [23, 0] +- name: Level Forest - Boulder Script + id: 26 + area: 14 + coordinates: [52, 15] + teleporter: [0, 8] +- name: Level Forest - Rotten Tree Script + id: 27 + area: 14 + coordinates: [47, 6] + teleporter: [2, 8] +- name: Level Forest - Exit Level Forest 1 + id: 28 + area: 14 + coordinates: [46, 25] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 2 + id: 29 + area: 14 + coordinates: [46, 26] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 3 + id: 30 + area: 14 + coordinates: [47, 25] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 4 + id: 31 + area: 14 + coordinates: [47, 26] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 5 + id: 32 + area: 14 + coordinates: [60, 14] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 6 + id: 33 + area: 14 + coordinates: [61, 14] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 7 + id: 34 + area: 14 + coordinates: [46, 4] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 8 + id: 35 + area: 14 + coordinates: [46, 3] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest 9 + id: 36 + area: 14 + coordinates: [47, 4] + teleporter: [25, 0] +- name: Level Forest - Exit Level Forest A + id: 37 + area: 14 + coordinates: [47, 3] + teleporter: [25, 0] +- name: Foresta - Exit Foresta 1 + id: 38 + area: 15 + coordinates: [10, 25] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 2 + id: 39 + area: 15 + coordinates: [10, 26] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 3 + id: 40 + area: 15 + coordinates: [11, 25] + teleporter: [31, 0] +- name: Foresta - Exit Foresta 4 + id: 41 + area: 15 + coordinates: [11, 26] + teleporter: [31, 0] +- name: Foresta - Old Man House - Front Door + id: 42 + area: 15 + coordinates: [25, 17] + teleporter: [32, 4] +- name: Foresta - Old Man House - Back Door + id: 43 + area: 15 + coordinates: [25, 14] + teleporter: [33, 0] +- name: Foresta - Kaeli's House + id: 44 + area: 15 + coordinates: [7, 21] + teleporter: [0, 5] +- name: Foresta - Rest House + id: 45 + area: 15 + coordinates: [23, 23] + teleporter: [1, 5] +- name: Kaeli's House - Kaeli's House Entrance + id: 46 + area: 16 + coordinates: [11, 20] + teleporter: [86, 3] +- name: Foresta Houses - Old Man's House - Old Man Front Exit + id: 47 + area: 17 + coordinates: [35, 44] + teleporter: [34, 0] +- name: Foresta Houses - Old Man's House - Old Man Back Exit + id: 48 + area: 17 + coordinates: [35, 27] + teleporter: [35, 0] +- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script + id: 483 + area: 17 + coordinates: [0x23, 0x1E] + teleporter: [0x0D, 8] +- name: Foresta Houses - Rest House - Bed Script + id: 49 + area: 17 + coordinates: [30, 6] + teleporter: [1, 8] +- name: Foresta Houses - Rest House - Rest House Exit + id: 50 + area: 17 + coordinates: [35, 20] + teleporter: [87, 3] +- name: Foresta Houses - Libra House - Libra House Script + id: 51 + area: 17 + coordinates: [8, 49] + teleporter: [67, 8] +- name: Foresta Houses - Gemini House - Gemini House Script + id: 52 + area: 17 + coordinates: [26, 55] + teleporter: [68, 8] +- name: Foresta Houses - Mobius House - Mobius House Script + id: 53 + area: 17 + coordinates: [14, 33] + teleporter: [69, 8] +- name: Sand Temple - Sand Temple Entrance + id: 54 + area: 18 + coordinates: [56, 27] + teleporter: [36, 0] +- name: Bone Dungeon 1F - Bone Dungeon Entrance + id: 55 + area: 19 + coordinates: [13, 60] + teleporter: [37, 0] +- name: Bone Dungeon 1F - To Bone Dungeon B1 + id: 56 + area: 19 + coordinates: [13, 39] + teleporter: [2, 2] +- name: Bone Dungeon B1 - Waterway - Exit Waterway + id: 57 + area: 20 + coordinates: [27, 39] + teleporter: [3, 2] +- name: Bone Dungeon B1 - Waterway - Tristam's Script + id: 58 + area: 20 + coordinates: [27, 45] + teleporter: [3, 8] +- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F + id: 59 + area: 20 + coordinates: [54, 61] + teleporter: [88, 3] +- name: Bone Dungeon B1 - Checker Room - Exit Checker Room + id: 60 + area: 20 + coordinates: [23, 40] + teleporter: [4, 2] +- name: Bone Dungeon B1 - Checker Room - To Waterway + id: 61 + area: 20 + coordinates: [39, 49] + teleporter: [89, 3] +- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room + id: 62 + area: 20 + coordinates: [5, 33] + teleporter: [91, 3] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage + id: 63 + area: 21 + coordinates: [19, 13] + teleporter: [5, 2] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room + id: 64 + area: 21 + coordinates: [29, 15] + teleporter: [6, 2] +- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room + id: 65 + area: 21 + coordinates: [8, 25] + teleporter: [90, 3] +- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room + id: 66 + area: 21 + coordinates: [59, 12] + teleporter: [93, 3] +- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room + id: 67 + area: 21 + coordinates: [59, 28] + teleporter: [94, 3] +- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room + id: 68 + area: 21 + coordinates: [53, 7] + teleporter: [7, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room + id: 69 + area: 21 + coordinates: [41, 3] + teleporter: [8, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room + id: 70 + area: 21 + coordinates: [47, 57] + teleporter: [9, 2] +- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room + id: 71 + area: 21 + coordinates: [54, 23] + teleporter: [92, 3] +- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script + id: 72 + area: 22 + coordinates: [29, 19] + teleporter: [4, 8] +- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script + id: 73 + area: 22 + coordinates: [29, 23] + teleporter: [75, 8] +- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room + id: 74 + area: 22 + coordinates: [30, 27] + teleporter: [95, 3] +- name: Libra Temple - Entrance + id: 75 + area: 23 + coordinates: [10, 15] + teleporter: [13, 6] +- name: Libra Temple - Libra Tile Script + id: 76 + area: 23 + coordinates: [9, 8] + teleporter: [59, 8] +- name: Aquaria Winter - Winter Entrance 1 + id: 77 + area: 24 + coordinates: [25, 25] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 2 + id: 78 + area: 24 + coordinates: [25, 26] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 3 + id: 79 + area: 24 + coordinates: [26, 25] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Entrance 4 + id: 80 + area: 24 + coordinates: [26, 26] + teleporter: [8, 6] +- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script + id: 81 + area: 24 + coordinates: [8, 19] + teleporter: [10, 5] # original value [5, 8] +- name: Aquaria Winter - Winter Vendor House Entrance + id: 82 + area: 24 + coordinates: [8, 5] + teleporter: [44, 4] +- name: Aquaria Winter - Winter INN Entrance + id: 83 + area: 24 + coordinates: [26, 17] + teleporter: [11, 5] +- name: Aquaria Summer - Summer Entrance 1 + id: 84 + area: 25 + coordinates: [57, 25] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 2 + id: 85 + area: 25 + coordinates: [57, 26] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 3 + id: 86 + area: 25 + coordinates: [58, 25] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Entrance 4 + id: 87 + area: 25 + coordinates: [58, 26] + teleporter: [8, 6] +- name: Aquaria Summer - Summer Phoebe's House Entrance + id: 88 + area: 25 + coordinates: [40, 19] + teleporter: [10, 5] +- name: Aquaria Summer - Spencer's Place Entrance Top + id: 89 + area: 25 + coordinates: [40, 16] + teleporter: [42, 0] +- name: Aquaria Summer - Spencer's Place Entrance Side + id: 90 + area: 25 + coordinates: [41, 18] + teleporter: [43, 0] +- name: Aquaria Summer - Summer Vendor House Entrance + id: 91 + area: 25 + coordinates: [40, 5] + teleporter: [44, 4] +- name: Aquaria Summer - Summer INN Entrance + id: 92 + area: 25 + coordinates: [58, 17] + teleporter: [11, 5] +- name: Phoebe's House - Entrance # Change to a script, same as vendor house + id: 93 + area: 26 + coordinates: [29, 14] + teleporter: [5, 8] # Original Value [11,3] +- name: Aquaria Vendor House - Vendor House Entrance's Script + id: 94 + area: 27 + coordinates: [7, 10] + teleporter: [40, 8] +- name: Aquaria Vendor House - Vendor House Stairs + id: 95 + area: 27 + coordinates: [1, 4] + teleporter: [47, 0] +- name: Aquaria Gemini Room - Gemini Script + id: 96 + area: 27 + coordinates: [2, 40] + teleporter: [72, 8] +- name: Aquaria Gemini Room - Gemini Room Stairs + id: 97 + area: 27 + coordinates: [4, 39] + teleporter: [48, 0] +- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house + id: 98 + area: 27 + coordinates: [51, 46] + teleporter: [75, 8] # Original value [48,3] +- name: Wintry Cave 1F - Main Entrance + id: 99 + area: 28 + coordinates: [50, 58] + teleporter: [49, 0] +- name: Wintry Cave 1F - To 3F Top + id: 100 + area: 28 + coordinates: [40, 25] + teleporter: [14, 2] +- name: Wintry Cave 1F - To 2F + id: 101 + area: 28 + coordinates: [10, 43] + teleporter: [15, 2] +- name: Wintry Cave 1F - Phoebe's Script + id: 102 + area: 28 + coordinates: [44, 37] + teleporter: [6, 8] +- name: Wintry Cave 2F - To 3F Bottom + id: 103 + area: 29 + coordinates: [58, 5] + teleporter: [50, 0] +- name: Wintry Cave 2F - To 1F + id: 104 + area: 29 + coordinates: [38, 18] + teleporter: [97, 3] +- name: Wintry Cave 3F Top - Exit from 3F Top + id: 105 + area: 30 + coordinates: [24, 6] + teleporter: [96, 3] +- name: Wintry Cave 3F Bottom - Exit to 2F + id: 106 + area: 31 + coordinates: [4, 29] + teleporter: [51, 0] +- name: Life Temple - Entrance + id: 107 + area: 32 + coordinates: [9, 60] + teleporter: [14, 6] +- name: Life Temple - Libra Tile Script + id: 108 + area: 32 + coordinates: [3, 55] + teleporter: [60, 8] +- name: Life Temple - Mysterious Man Script + id: 109 + area: 32 + coordinates: [9, 44] + teleporter: [78, 8] +- name: Fall Basin - Back Exit Script + id: 110 + area: 33 + coordinates: [17, 5] + teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) +- name: Fall Basin - Main Exit + id: 111 + area: 33 + coordinates: [15, 26] + teleporter: [53, 0] +- name: Fall Basin - Phoebe's Script + id: 112 + area: 33 + coordinates: [17, 6] + teleporter: [9, 8] +- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room + id: 113 + area: 34 + coordinates: [43, 6] + teleporter: [55, 0] +- name: Ice Pyramid 1F Maze - Main Entrance 1 + id: 114 + area: 35 + coordinates: [18, 36] + teleporter: [56, 0] +- name: Ice Pyramid 1F Maze - Main Entrance 2 + id: 115 + area: 35 + coordinates: [19, 36] + teleporter: [56, 0] +- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room + id: 116 + area: 35 + coordinates: [3, 27] + teleporter: [57, 0] +- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room + id: 117 + area: 35 + coordinates: [11, 15] + teleporter: [58, 0] +- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room + id: 118 + area: 35 + coordinates: [25, 16] + teleporter: [59, 0] +- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room + id: 119 + area: 35 + coordinates: [31, 1] + teleporter: [60, 0] +- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor + id: 120 + area: 35 + coordinates: [34, 9] + teleporter: [61, 0] +- name: Ice Pyramid 1F Maze - Statue's Script + id: 121 + area: 35 + coordinates: [21, 32] + teleporter: [77, 8] +- name: Ice Pyramid 2F South Tiled Room - To 1F + id: 122 + area: 36 + coordinates: [4, 26] + teleporter: [62, 0] +- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room + id: 123 + area: 36 + coordinates: [22, 17] + teleporter: [67, 0] +- name: Ice Pyramid 2F West Room - To 1F + id: 124 + area: 36 + coordinates: [9, 10] + teleporter: [63, 0] +- name: Ice Pyramid 2F Center Room - To 1F + id: 125 + area: 36 + coordinates: [22, 14] + teleporter: [64, 0] +- name: Ice Pyramid 2F Small North Room - To 1F + id: 126 + area: 36 + coordinates: [26, 4] + teleporter: [65, 0] +- name: Ice Pyramid 2F North Corridor - To 1F + id: 127 + area: 36 + coordinates: [32, 8] + teleporter: [66, 0] +- name: Ice Pyramid 2F North Corridor - To 3F Main Loop + id: 128 + area: 36 + coordinates: [12, 7] + teleporter: [68, 0] +- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room + id: 129 + area: 37 + coordinates: [24, 54] + teleporter: [69, 0] +- name: Ice Pyramid 3F Main Loop - To 2F Corridor + id: 130 + area: 37 + coordinates: [16, 45] + teleporter: [70, 0] +- name: Ice Pyramid 3F Main Loop - To 4F + id: 131 + area: 37 + coordinates: [19, 43] + teleporter: [71, 0] +- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop + id: 132 + area: 38 + coordinates: [52, 5] + teleporter: [72, 0] +- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room + id: 133 + area: 38 + coordinates: [62, 19] + teleporter: [73, 0] +- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room + id: 134 + area: 39 + coordinates: [54, 63] + teleporter: [74, 0] +- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate + id: 135 + area: 39 + coordinates: [47, 54] + teleporter: [77, 8] +- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room + id: 136 + area: 39 + coordinates: [39, 43] + teleporter: [75, 0] +- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room + id: 137 + area: 39 + coordinates: [39, 60] + teleporter: [76, 0] +- name: Ice Pyramid - Duplicate Ice Golem Room # not used? + id: 138 + area: 40 + coordinates: [44, 43] + teleporter: [77, 0] +- name: Ice Pyramid Climbing Wall Room - To Taunt Room + id: 139 + area: 41 + coordinates: [4, 59] + teleporter: [78, 0] +- name: Ice Pyramid Climbing Wall Room - To 5F Stairs + id: 140 + area: 41 + coordinates: [4, 45] + teleporter: [79, 0] +- name: Ice Pyramid Ice Golem Room - To 5F Stairs + id: 141 + area: 42 + coordinates: [44, 43] + teleporter: [80, 0] +- name: Ice Pyramid Ice Golem Room - Ice Golem Script + id: 142 + area: 42 + coordinates: [53, 32] + teleporter: [10, 8] +- name: Spencer Waterfall - To Spencer Cave + id: 143 + area: 43 + coordinates: [48, 57] + teleporter: [81, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 1 + id: 144 + area: 43 + coordinates: [40, 5] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 2 + id: 145 + area: 43 + coordinates: [40, 6] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 3 + id: 146 + area: 43 + coordinates: [41, 5] + teleporter: [82, 0] +- name: Spencer Waterfall - Upper Exit to Aquaria 4 + id: 147 + area: 43 + coordinates: [41, 6] + teleporter: [82, 0] +- name: Spencer Waterfall - Right Exit to Aquaria 1 + id: 148 + area: 43 + coordinates: [46, 8] + teleporter: [83, 0] +- name: Spencer Waterfall - Right Exit to Aquaria 2 + id: 149 + area: 43 + coordinates: [47, 8] + teleporter: [83, 0] +- name: Spencer Cave Normal Main - To Waterfall + id: 150 + area: 44 + coordinates: [14, 39] + teleporter: [85, 0] +- name: Spencer Cave Normal From Overworld - Exit to Overworld + id: 151 + area: 44 + coordinates: [15, 57] + teleporter: [7, 6] +- name: Spencer Cave Unplug - Exit to Overworld + id: 152 + area: 45 + coordinates: [40, 29] + teleporter: [7, 6] +- name: Spencer Cave Unplug - Libra Teleporter Start Script + id: 153 + area: 45 + coordinates: [28, 21] + teleporter: [33, 8] +- name: Spencer Cave Unplug - Libra Teleporter End Script + id: 154 + area: 45 + coordinates: [46, 4] + teleporter: [34, 8] +- name: Spencer Cave Unplug - Mobius Teleporter Chest Script + id: 155 + area: 45 + coordinates: [21, 9] + teleporter: [35, 8] +- name: Spencer Cave Unplug - Mobius Teleporter Start Script + id: 156 + area: 45 + coordinates: [29, 28] + teleporter: [36, 8] +- name: Wintry Temple Outer Room - Main Entrance + id: 157 + area: 46 + coordinates: [8, 31] + teleporter: [15, 6] +- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple + id: 158 + area: 46 + coordinates: [9, 24] + teleporter: [62, 8] +- name: Fireburg - To Overworld + id: 159 + area: 47 + coordinates: [4, 13] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 160 + area: 47 + coordinates: [5, 13] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 161 + area: 47 + coordinates: [28, 15] + teleporter: [9, 6] +- name: Fireburg - To Overworld + id: 162 + area: 47 + coordinates: [27, 15] + teleporter: [9, 6] +- name: Fireburg - Vendor House + id: 163 + area: 47 + coordinates: [10, 24] + teleporter: [91, 0] +- name: Fireburg - Reuben House + id: 164 + area: 47 + coordinates: [14, 6] + teleporter: [16, 2] +- name: Fireburg - Hotel + id: 165 + area: 47 + coordinates: [20, 8] + teleporter: [17, 2] +- name: Fireburg - GrenadeMan House Script + id: 166 + area: 47 + coordinates: [12, 18] + teleporter: [11, 8] +- name: Reuben House - Main Entrance + id: 167 + area: 48 + coordinates: [33, 46] + teleporter: [98, 3] +- name: GrenadeMan House - Entrance Script + id: 168 + area: 49 + coordinates: [55, 60] + teleporter: [9, 8] +- name: GrenadeMan House - To Mobius Crest Room + id: 169 + area: 49 + coordinates: [57, 52] + teleporter: [93, 0] +- name: GrenadeMan Mobius Room - Stairs to House + id: 170 + area: 49 + coordinates: [39, 26] + teleporter: [94, 0] +- name: GrenadeMan Mobius Room - Mobius Teleporter Script + id: 171 + area: 49 + coordinates: [39, 23] + teleporter: [54, 8] +- name: Fireburg Vendor House - Entrance Script # No use to be a script + id: 172 + area: 49 + coordinates: [7, 10] + teleporter: [95, 0] # Original value [39, 8] +- name: Fireburg Vendor House - Stairs to Gemini Room + id: 173 + area: 49 + coordinates: [1, 4] + teleporter: [96, 0] +- name: Fireburg Gemini Room - Stairs to Vendor House + id: 174 + area: 49 + coordinates: [4, 39] + teleporter: [97, 0] +- name: Fireburg Gemini Room - Gemini Teleporter Script + id: 175 + area: 49 + coordinates: [2, 40] + teleporter: [45, 8] +- name: Fireburg Hotel Lobby - Stairs to beds + id: 176 + area: 49 + coordinates: [4, 50] + teleporter: [213, 0] +- name: Fireburg Hotel Lobby - Entrance + id: 177 + area: 49 + coordinates: [17, 56] + teleporter: [99, 3] +- name: Fireburg Hotel Beds - Stairs to Hotel Lobby + id: 178 + area: 49 + coordinates: [45, 59] + teleporter: [214, 0] +- name: Mine Exterior - Main Entrance + id: 179 + area: 50 + coordinates: [5, 28] + teleporter: [98, 0] +- name: Mine Exterior - To Cliff + id: 180 + area: 50 + coordinates: [58, 29] + teleporter: [99, 0] +- name: Mine Exterior - To Parallel Room + id: 181 + area: 50 + coordinates: [8, 7] + teleporter: [20, 2] +- name: Mine Exterior - To Crescent Room + id: 182 + area: 50 + coordinates: [26, 15] + teleporter: [21, 2] +- name: Mine Exterior - To Climbing Room + id: 183 + area: 50 + coordinates: [21, 35] + teleporter: [22, 2] +- name: Mine Exterior - Jinn Fight Script + id: 184 + area: 50 + coordinates: [58, 31] + teleporter: [74, 8] +- name: Mine Parallel Room - To Mine Exterior + id: 185 + area: 51 + coordinates: [7, 60] + teleporter: [100, 3] +- name: Mine Crescent Room - To Mine Exterior + id: 186 + area: 51 + coordinates: [22, 61] + teleporter: [101, 3] +- name: Mine Climbing Room - To Mine Exterior + id: 187 + area: 51 + coordinates: [56, 21] + teleporter: [102, 3] +- name: Mine Cliff - Entrance + id: 188 + area: 52 + coordinates: [9, 5] + teleporter: [100, 0] +- name: Mine Cliff - Reuben Grenade Script + id: 189 + area: 52 + coordinates: [15, 7] + teleporter: [12, 8] +- name: Sealed Temple - To Overworld + id: 190 + area: 53 + coordinates: [58, 43] + teleporter: [16, 6] +- name: Sealed Temple - Gemini Tile Script + id: 191 + area: 53 + coordinates: [56, 38] + teleporter: [63, 8] +- name: Volcano Base - Main Entrance 1 + id: 192 + area: 54 + coordinates: [23, 25] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 2 + id: 193 + area: 54 + coordinates: [23, 26] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 3 + id: 194 + area: 54 + coordinates: [24, 25] + teleporter: [103, 0] +- name: Volcano Base - Main Entrance 4 + id: 195 + area: 54 + coordinates: [24, 26] + teleporter: [103, 0] +- name: Volcano Base - Left Stairs Script + id: 196 + area: 54 + coordinates: [20, 5] + teleporter: [31, 8] +- name: Volcano Base - Right Stairs Script + id: 197 + area: 54 + coordinates: [32, 5] + teleporter: [30, 8] +- name: Volcano Top Right - Top Exit + id: 198 + area: 55 + coordinates: [44, 8] + teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up +- name: Volcano Top Left - To Right-Left Path Script + id: 199 + area: 55 + coordinates: [40, 24] + teleporter: [26, 8] +- name: Volcano Top Right - To Left-Right Path Script + id: 200 + area: 55 + coordinates: [52, 24] + teleporter: [79, 8] # Original Value [26, 8] +- name: Volcano Right Path - To Volcano Base Script + id: 201 + area: 56 + coordinates: [48, 42] + teleporter: [15, 8] # Original Value [27, 8] +- name: Volcano Left Path - To Volcano Cross Left-Right + id: 202 + area: 56 + coordinates: [40, 31] + teleporter: [25, 2] +- name: Volcano Left Path - To Volcano Cross Right-Left + id: 203 + area: 56 + coordinates: [52, 29] + teleporter: [26, 2] +- name: Volcano Left Path - To Volcano Base Script + id: 204 + area: 56 + coordinates: [36, 42] + teleporter: [27, 8] +- name: Volcano Cross Left-Right - To Volcano Left Path + id: 205 + area: 56 + coordinates: [10, 42] + teleporter: [103, 3] +- name: Volcano Cross Left-Right - To Volcano Top Right Script + id: 206 + area: 56 + coordinates: [16, 24] + teleporter: [29, 8] +- name: Volcano Cross Right-Left - To Volcano Top Left Script + id: 207 + area: 56 + coordinates: [8, 22] + teleporter: [28, 8] +- name: Volcano Cross Right-Left - To Volcano Left Path + id: 208 + area: 56 + coordinates: [16, 42] + teleporter: [104, 3] +- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 + id: 209 + area: 57 + coordinates: [32, 5] + teleporter: [104, 0] +- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 + id: 210 + area: 57 + coordinates: [33, 5] + teleporter: [104, 0] +- name: Lava Dome Inner Ring Main Loop - To Three Steps Room + id: 211 + area: 57 + coordinates: [14, 5] + teleporter: [105, 0] +- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower + id: 212 + area: 57 + coordinates: [40, 17] + teleporter: [106, 0] +- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left + id: 213 + area: 57 + coordinates: [8, 11] + teleporter: [108, 0] +- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room + id: 214 + area: 57 + coordinates: [11, 19] + teleporter: [111, 0] +- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher + id: 215 + area: 57 + coordinates: [32, 11] + teleporter: [107, 0] +- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor + id: 216 + area: 57 + coordinates: [12, 23] + teleporter: [109, 0] +- name: Lava Dome Inner Ring Plate Ledge - Plate Script + id: 217 + area: 57 + coordinates: [5, 23] + teleporter: [47, 8] +- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room + id: 218 + area: 57 + coordinates: [0, 9] + teleporter: [110, 0] +- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room + id: 219 + area: 57 + coordinates: [0, 15] + teleporter: [112, 0] +- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor + id: 220 + area: 57 + coordinates: [54, 5] + teleporter: [113, 0] +- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II + id: 221 + area: 57 + coordinates: [54, 21] + teleporter: [114, 0] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 + id: 222 + area: 57 + coordinates: [62, 20] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 + id: 223 + area: 57 + coordinates: [63, 20] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 + id: 224 + area: 57 + coordinates: [62, 21] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 + id: 225 + area: 57 + coordinates: [63, 21] + teleporter: [29, 2] +- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor + id: 226 + area: 57 + coordinates: [50, 25] + teleporter: [115, 0] +- name: Lava Dome Jump Maze II - Lower Right Entrance + id: 227 + area: 58 + coordinates: [55, 28] + teleporter: [116, 0] +- name: Lava Dome Jump Maze II - Upper Entrance + id: 228 + area: 58 + coordinates: [35, 3] + teleporter: [119, 0] +- name: Lava Dome Jump Maze II - Lower Left Entrance + id: 229 + area: 58 + coordinates: [34, 27] + teleporter: [120, 0] +- name: Lava Dome Up-Down Corridor - Upper Entrance + id: 230 + area: 58 + coordinates: [29, 8] + teleporter: [117, 0] +- name: Lava Dome Up-Down Corridor - Lower Entrance + id: 231 + area: 58 + coordinates: [28, 25] + teleporter: [118, 0] +- name: Lava Dome Jump Maze I - South Entrance + id: 232 + area: 59 + coordinates: [20, 27] + teleporter: [121, 0] +- name: Lava Dome Jump Maze I - North Entrance + id: 233 + area: 59 + coordinates: [7, 3] + teleporter: [122, 0] +- name: Lava Dome Pointless Room - Entrance + id: 234 + area: 60 + coordinates: [2, 7] + teleporter: [123, 0] +- name: Lava Dome Lower Moon Helm Room - Left Entrance + id: 235 + area: 60 + coordinates: [2, 19] + teleporter: [124, 0] +- name: Lava Dome Lower Moon Helm Room - Right Entrance + id: 236 + area: 60 + coordinates: [11, 21] + teleporter: [125, 0] +- name: Lava Dome Moon Helm Room - Entrance + id: 237 + area: 60 + coordinates: [15, 23] + teleporter: [126, 0] +- name: Lava Dome Three Jumps Room - To Main Loop + id: 238 + area: 61 + coordinates: [58, 15] + teleporter: [127, 0] +- name: Lava Dome Life Chest Room - Lower South Entrance + id: 239 + area: 61 + coordinates: [38, 27] + teleporter: [128, 0] +- name: Lava Dome Life Chest Room - Upper South Entrance + id: 240 + area: 61 + coordinates: [28, 23] + teleporter: [129, 0] +- name: Lava Dome Big Jump Room - Left Entrance + id: 241 + area: 62 + coordinates: [42, 51] + teleporter: [133, 0] +- name: Lava Dome Big Jump Room - North Entrance + id: 242 + area: 62 + coordinates: [30, 29] + teleporter: [131, 0] +- name: Lava Dome Big Jump Room - Lower Right Stairs + id: 243 + area: 62 + coordinates: [61, 59] + teleporter: [132, 0] +- name: Lava Dome Split Corridor - Upper Stairs + id: 244 + area: 62 + coordinates: [30, 43] + teleporter: [130, 0] +- name: Lava Dome Split Corridor - Lower Stairs + id: 245 + area: 62 + coordinates: [36, 61] + teleporter: [134, 0] +- name: Lava Dome Plate Corridor - Right Entrance + id: 246 + area: 63 + coordinates: [19, 29] + teleporter: [135, 0] +- name: Lava Dome Plate Corridor - Left Entrance + id: 247 + area: 63 + coordinates: [60, 21] + teleporter: [137, 0] +- name: Lava Dome Four Boxes Stairs - Upper Entrance + id: 248 + area: 63 + coordinates: [22, 3] + teleporter: [136, 0] +- name: Lava Dome Four Boxes Stairs - Lower Entrance + id: 249 + area: 63 + coordinates: [22, 17] + teleporter: [16, 0] +- name: Lava Dome Hydra Room - South Entrance + id: 250 + area: 64 + coordinates: [14, 59] + teleporter: [105, 3] +- name: Lava Dome Hydra Room - North Exit + id: 251 + area: 64 + coordinates: [25, 31] + teleporter: [138, 0] +- name: Lava Dome Hydra Room - Hydra Script + id: 252 + area: 64 + coordinates: [14, 36] + teleporter: [14, 8] +- name: Lava Dome Escape Corridor - South Entrance + id: 253 + area: 65 + coordinates: [22, 17] + teleporter: [139, 0] +- name: Lava Dome Escape Corridor - North Entrance + id: 254 + area: 65 + coordinates: [22, 3] + teleporter: [9, 0] +- name: Rope Bridge - West Entrance 1 + id: 255 + area: 66 + coordinates: [3, 10] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 2 + id: 256 + area: 66 + coordinates: [3, 11] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 3 + id: 257 + area: 66 + coordinates: [3, 12] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 4 + id: 258 + area: 66 + coordinates: [3, 13] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 5 + id: 259 + area: 66 + coordinates: [4, 10] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 6 + id: 260 + area: 66 + coordinates: [4, 11] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 7 + id: 261 + area: 66 + coordinates: [4, 12] + teleporter: [140, 0] +- name: Rope Bridge - West Entrance 8 + id: 262 + area: 66 + coordinates: [4, 13] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 1 + id: 263 + area: 66 + coordinates: [59, 10] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 2 + id: 264 + area: 66 + coordinates: [59, 11] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 3 + id: 265 + area: 66 + coordinates: [59, 12] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 4 + id: 266 + area: 66 + coordinates: [59, 13] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 5 + id: 267 + area: 66 + coordinates: [60, 10] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 6 + id: 268 + area: 66 + coordinates: [60, 11] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 7 + id: 269 + area: 66 + coordinates: [60, 12] + teleporter: [140, 0] +- name: Rope Bridge - East Entrance 8 + id: 270 + area: 66 + coordinates: [60, 13] + teleporter: [140, 0] +- name: Rope Bridge - Reuben Fall Script + id: 271 + area: 66 + coordinates: [13, 12] + teleporter: [15, 8] +- name: Alive Forest - West Entrance 1 + id: 272 + area: 67 + coordinates: [8, 13] + teleporter: [142, 0] +- name: Alive Forest - West Entrance 2 + id: 273 + area: 67 + coordinates: [9, 13] + teleporter: [142, 0] +- name: Alive Forest - Giant Tree Entrance + id: 274 + area: 67 + coordinates: [42, 42] + teleporter: [143, 0] +- name: Alive Forest - Libra Teleporter Script + id: 275 + area: 67 + coordinates: [8, 52] + teleporter: [64, 8] +- name: Alive Forest - Gemini Teleporter Script + id: 276 + area: 67 + coordinates: [57, 49] + teleporter: [65, 8] +- name: Alive Forest - Mobius Teleporter Script + id: 277 + area: 67 + coordinates: [24, 10] + teleporter: [66, 8] +- name: Giant Tree 1F - Entrance Script 1 + id: 278 + area: 68 + coordinates: [18, 31] + teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] +- name: Giant Tree 1F - Entrance Script 2 + id: 279 + area: 68 + coordinates: [19, 31] + teleporter: [56, 1] # Same [49, 8] +- name: Giant Tree 1F - North Entrance To 2F + id: 280 + area: 68 + coordinates: [16, 1] + teleporter: [144, 0] +- name: Giant Tree 2F Main Lobby - North Entrance to 1F + id: 281 + area: 69 + coordinates: [44, 33] + teleporter: [145, 0] +- name: Giant Tree 2F Main Lobby - Central Entrance to 3F + id: 282 + area: 69 + coordinates: [42, 47] + teleporter: [146, 0] +- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room + id: 283 + area: 69 + coordinates: [58, 49] + teleporter: [149, 0] +- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge + id: 284 + area: 69 + coordinates: [34, 37] + teleporter: [147, 0] +- name: Giant Tree 2F Fall From Vine Script + id: 482 + area: 69 + coordinates: [0x2E, 0x33] + teleporter: [76, 8] +- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room + id: 285 + area: 69 + coordinates: [58, 44] + teleporter: [148, 0] +- name: Giant Tree 2F Mushroom Room - Entrance + id: 286 + area: 70 + coordinates: [55, 18] + teleporter: [150, 0] +- name: Giant Tree 2F Mushroom Room - North Face to Meteor + id: 287 + area: 70 + coordinates: [56, 7] + teleporter: [151, 0] +- name: Giant Tree 3F Central Room - Central Entrance to 2F + id: 288 + area: 71 + coordinates: [46, 53] + teleporter: [152, 0] +- name: Giant Tree 3F Central Room - East Entrance to Worm Room + id: 289 + area: 71 + coordinates: [58, 39] + teleporter: [153, 0] +- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room + id: 290 + area: 71 + coordinates: [45, 39] + teleporter: [154, 0] +- name: Giant Tree 3F West Platform - Lower Entrance + id: 291 + area: 71 + coordinates: [33, 43] + teleporter: [155, 0] +- name: Giant Tree 3F West Platform - Top Entrance + id: 292 + area: 71 + coordinates: [52, 25] + teleporter: [156, 0] +- name: Giant Tree Worm Room - East Entrance + id: 293 + area: 72 + coordinates: [20, 58] + teleporter: [157, 0] +- name: Giant Tree Worm Room - West Entrance + id: 294 + area: 72 + coordinates: [6, 56] + teleporter: [158, 0] +- name: Giant Tree 4F Lower Floor - Entrance + id: 295 + area: 73 + coordinates: [20, 7] + teleporter: [159, 0] +- name: Giant Tree 4F Lower Floor - Lower West Mouth + id: 296 + area: 73 + coordinates: [8, 23] + teleporter: [160, 0] +- name: Giant Tree 4F Lower Floor - Lower Central Mouth + id: 297 + area: 73 + coordinates: [14, 25] + teleporter: [161, 0] +- name: Giant Tree 4F Lower Floor - Lower East Mouth + id: 298 + area: 73 + coordinates: [20, 25] + teleporter: [162, 0] +- name: Giant Tree 4F Upper Floor - Upper West Mouth + id: 299 + area: 73 + coordinates: [8, 19] + teleporter: [163, 0] +- name: Giant Tree 4F Upper Floor - Upper Central Mouth + id: 300 + area: 73 + coordinates: [12, 17] + teleporter: [164, 0] +- name: Giant Tree 4F Slime Room - Exit + id: 301 + area: 74 + coordinates: [47, 10] + teleporter: [165, 0] +- name: Giant Tree 4F Slime Room - West Entrance + id: 302 + area: 74 + coordinates: [45, 24] + teleporter: [166, 0] +- name: Giant Tree 4F Slime Room - Central Entrance + id: 303 + area: 74 + coordinates: [50, 24] + teleporter: [167, 0] +- name: Giant Tree 4F Slime Room - East Entrance + id: 304 + area: 74 + coordinates: [57, 28] + teleporter: [168, 0] +- name: Giant Tree 5F - Entrance + id: 305 + area: 75 + coordinates: [14, 51] + teleporter: [169, 0] +- name: Giant Tree 5F - Giant Tree Face # Unused + id: 306 + area: 75 + coordinates: [14, 37] + teleporter: [170, 0] +- name: Kaidge Temple - Entrance + id: 307 + area: 77 + coordinates: [44, 63] + teleporter: [18, 6] +- name: Kaidge Temple - Mobius Teleporter Script + id: 308 + area: 77 + coordinates: [35, 57] + teleporter: [71, 8] +- name: Windhole Temple - Entrance + id: 309 + area: 78 + coordinates: [10, 29] + teleporter: [173, 0] +- name: Mount Gale - Entrance 1 + id: 310 + area: 79 + coordinates: [1, 45] + teleporter: [174, 0] +- name: Mount Gale - Entrance 2 + id: 311 + area: 79 + coordinates: [2, 45] + teleporter: [174, 0] +- name: Windia - Main Entrance 1 + id: 312 + area: 80 + coordinates: [12, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 2 + id: 313 + area: 80 + coordinates: [13, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 3 + id: 314 + area: 80 + coordinates: [14, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 4 + id: 315 + area: 80 + coordinates: [15, 40] + teleporter: [10, 6] +- name: Windia - Main Entrance 5 + id: 316 + area: 80 + coordinates: [12, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 6 + id: 317 + area: 80 + coordinates: [13, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 7 + id: 318 + area: 80 + coordinates: [14, 41] + teleporter: [10, 6] +- name: Windia - Main Entrance 8 + id: 319 + area: 80 + coordinates: [15, 41] + teleporter: [10, 6] +- name: Windia - Otto's House + id: 320 + area: 80 + coordinates: [21, 39] + teleporter: [30, 5] +- name: Windia - INN's Script # Change to teleporter + id: 321 + area: 80 + coordinates: [18, 34] + teleporter: [31, 2] # Original value [79, 8] +- name: Windia - Vendor House + id: 322 + area: 80 + coordinates: [8, 36] + teleporter: [32, 5] +- name: Windia - Kid House + id: 323 + area: 80 + coordinates: [7, 23] + teleporter: [176, 4] +- name: Windia - Old People House + id: 324 + area: 80 + coordinates: [19, 21] + teleporter: [177, 4] +- name: Windia - Rainbow Bridge Script + id: 325 + area: 80 + coordinates: [21, 9] + teleporter: [10, 6] # Change to entrance, usually a script [41, 8] +- name: Otto's House - Attic Stairs + id: 326 + area: 81 + coordinates: [2, 19] + teleporter: [33, 2] +- name: Otto's House - Entrance + id: 327 + area: 81 + coordinates: [9, 30] + teleporter: [106, 3] +- name: Otto's Attic - Stairs + id: 328 + area: 81 + coordinates: [26, 23] + teleporter: [107, 3] +- name: Windia Kid House - Entrance Script # Change to teleporter + id: 329 + area: 82 + coordinates: [7, 10] + teleporter: [178, 0] # Original value [38, 8] +- name: Windia Kid House - Basement Stairs + id: 330 + area: 82 + coordinates: [1, 4] + teleporter: [180, 0] +- name: Windia Old People House - Entrance + id: 331 + area: 82 + coordinates: [55, 12] + teleporter: [179, 0] +- name: Windia Old People House - Basement Stairs + id: 332 + area: 82 + coordinates: [60, 5] + teleporter: [181, 0] +- name: Windia Kid House Basement - Stairs + id: 333 + area: 82 + coordinates: [43, 8] + teleporter: [182, 0] +- name: Windia Kid House Basement - Mobius Teleporter + id: 334 + area: 82 + coordinates: [41, 9] + teleporter: [44, 8] +- name: Windia Old People House Basement - Stairs + id: 335 + area: 82 + coordinates: [39, 26] + teleporter: [183, 0] +- name: Windia Old People House Basement - Mobius Teleporter Script + id: 336 + area: 82 + coordinates: [39, 23] + teleporter: [43, 8] +- name: Windia Inn Lobby - Stairs to Beds + id: 337 + area: 82 + coordinates: [45, 24] + teleporter: [215, 0] +- name: Windia Inn Lobby - Exit + id: 338 + area: 82 + coordinates: [53, 30] + teleporter: [135, 3] +- name: Windia Inn Beds - Stairs to Lobby + id: 339 + area: 82 + coordinates: [33, 59] + teleporter: [216, 0] +- name: Windia Vendor House - Entrance + id: 340 + area: 82 + coordinates: [29, 14] + teleporter: [108, 3] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 + id: 341 + area: 83 + coordinates: [47, 29] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 + id: 342 + area: 83 + coordinates: [47, 30] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 + id: 343 + area: 83 + coordinates: [48, 29] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 + id: 344 + area: 83 + coordinates: [48, 30] + teleporter: [184, 0] +- name: Pazuzu Tower 1F Main Lobby - East Entrance + id: 345 + area: 83 + coordinates: [55, 12] + teleporter: [185, 0] +- name: Pazuzu Tower 1F Main Lobby - South Stairs + id: 346 + area: 83 + coordinates: [51, 25] + teleporter: [186, 0] +- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 + id: 347 + area: 83 + coordinates: [47, 8] + teleporter: [16, 8] +- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 + id: 348 + area: 83 + coordinates: [48, 8] + teleporter: [16, 8] +- name: Pazuzu Tower 1F Boxes Room - West Stairs + id: 349 + area: 83 + coordinates: [38, 17] + teleporter: [187, 0] +- name: Pazuzu 2F - West Upper Stairs + id: 350 + area: 84 + coordinates: [7, 11] + teleporter: [188, 0] +- name: Pazuzu 2F - South Stairs + id: 351 + area: 84 + coordinates: [20, 24] + teleporter: [189, 0] +- name: Pazuzu 2F - West Lower Stairs + id: 352 + area: 84 + coordinates: [6, 17] + teleporter: [190, 0] +- name: Pazuzu 2F - Central Stairs + id: 353 + area: 84 + coordinates: [15, 15] + teleporter: [191, 0] +- name: Pazuzu 2F - Pazuzu 1 + id: 354 + area: 84 + coordinates: [15, 8] + teleporter: [17, 8] +- name: Pazuzu 2F - Pazuzu 2 + id: 355 + area: 84 + coordinates: [16, 8] + teleporter: [17, 8] +- name: Pazuzu 3F Main Room - North Stairs + id: 356 + area: 85 + coordinates: [23, 11] + teleporter: [192, 0] +- name: Pazuzu 3F Main Room - West Stairs + id: 357 + area: 85 + coordinates: [7, 15] + teleporter: [193, 0] +- name: Pazuzu 3F Main Room - Pazuzu Script 1 + id: 358 + area: 85 + coordinates: [15, 8] + teleporter: [18, 8] +- name: Pazuzu 3F Main Room - Pazuzu Script 2 + id: 359 + area: 85 + coordinates: [16, 8] + teleporter: [18, 8] +- name: Pazuzu 3F Central Island - Central Stairs + id: 360 + area: 85 + coordinates: [15, 14] + teleporter: [194, 0] +- name: Pazuzu 3F Central Island - South Stairs + id: 361 + area: 85 + coordinates: [17, 25] + teleporter: [195, 0] +- name: Pazuzu 4F - Northwest Stairs + id: 362 + area: 86 + coordinates: [39, 12] + teleporter: [196, 0] +- name: Pazuzu 4F - Southwest Stairs + id: 363 + area: 86 + coordinates: [39, 19] + teleporter: [197, 0] +- name: Pazuzu 4F - South Stairs + id: 364 + area: 86 + coordinates: [47, 24] + teleporter: [198, 0] +- name: Pazuzu 4F - Northeast Stairs + id: 365 + area: 86 + coordinates: [54, 9] + teleporter: [199, 0] +- name: Pazuzu 4F - Pazuzu Script 1 + id: 366 + area: 86 + coordinates: [47, 8] + teleporter: [19, 8] +- name: Pazuzu 4F - Pazuzu Script 2 + id: 367 + area: 86 + coordinates: [48, 8] + teleporter: [19, 8] +- name: Pazuzu 5F Pazuzu Loop - West Stairs + id: 368 + area: 87 + coordinates: [9, 49] + teleporter: [200, 0] +- name: Pazuzu 5F Pazuzu Loop - South Stairs + id: 369 + area: 87 + coordinates: [16, 55] + teleporter: [201, 0] +- name: Pazuzu 5F Upper Loop - Northeast Stairs + id: 370 + area: 87 + coordinates: [22, 40] + teleporter: [202, 0] +- name: Pazuzu 5F Upper Loop - Northwest Stairs + id: 371 + area: 87 + coordinates: [9, 40] + teleporter: [203, 0] +- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 + id: 372 + area: 87 + coordinates: [15, 40] + teleporter: [20, 8] +- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 + id: 373 + area: 87 + coordinates: [16, 40] + teleporter: [20, 8] +- name: Pazuzu 6F - West Stairs + id: 374 + area: 88 + coordinates: [41, 47] + teleporter: [204, 0] +- name: Pazuzu 6F - Northwest Stairs + id: 375 + area: 88 + coordinates: [41, 40] + teleporter: [205, 0] +- name: Pazuzu 6F - Northeast Stairs + id: 376 + area: 88 + coordinates: [54, 40] + teleporter: [206, 0] +- name: Pazuzu 6F - South Stairs + id: 377 + area: 88 + coordinates: [52, 56] + teleporter: [207, 0] +- name: Pazuzu 6F - Pazuzu Script 1 + id: 378 + area: 88 + coordinates: [47, 40] + teleporter: [21, 8] +- name: Pazuzu 6F - Pazuzu Script 2 + id: 379 + area: 88 + coordinates: [48, 40] + teleporter: [21, 8] +- name: Pazuzu 7F Main Room - Southwest Stairs + id: 380 + area: 89 + coordinates: [15, 54] + teleporter: [26, 0] +- name: Pazuzu 7F Main Room - Northeast Stairs + id: 381 + area: 89 + coordinates: [21, 40] + teleporter: [27, 0] +- name: Pazuzu 7F Main Room - Southeast Stairs + id: 382 + area: 89 + coordinates: [21, 56] + teleporter: [28, 0] +- name: Pazuzu 7F Main Room - Pazuzu Script 1 + id: 383 + area: 89 + coordinates: [15, 44] + teleporter: [22, 8] +- name: Pazuzu 7F Main Room - Pazuzu Script 2 + id: 384 + area: 89 + coordinates: [16, 44] + teleporter: [22, 8] +- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle + id: 480 + area: 89 + coordinates: [15, 40] + teleporter: [38, 8] +- name: Pazuzu 1F to 3F - South Stairs + id: 385 + area: 90 + coordinates: [43, 60] + teleporter: [29, 0] +- name: Pazuzu 1F to 3F - North Stairs + id: 386 + area: 90 + coordinates: [43, 36] + teleporter: [30, 0] +- name: Pazuzu 3F to 5F - South Stairs + id: 387 + area: 91 + coordinates: [43, 60] + teleporter: [40, 0] +- name: Pazuzu 3F to 5F - North Stairs + id: 388 + area: 91 + coordinates: [43, 36] + teleporter: [41, 0] +- name: Pazuzu 5F to 7F - South Stairs + id: 389 + area: 92 + coordinates: [43, 60] + teleporter: [38, 0] +- name: Pazuzu 5F to 7F - North Stairs + id: 390 + area: 92 + coordinates: [43, 36] + teleporter: [39, 0] +- name: Pazuzu 2F to 4F - South Stairs + id: 391 + area: 93 + coordinates: [43, 60] + teleporter: [21, 0] +- name: Pazuzu 2F to 4F - North Stairs + id: 392 + area: 93 + coordinates: [43, 36] + teleporter: [22, 0] +- name: Pazuzu 4F to 6F - South Stairs + id: 393 + area: 94 + coordinates: [43, 60] + teleporter: [2, 0] +- name: Pazuzu 4F to 6F - North Stairs + id: 394 + area: 94 + coordinates: [43, 36] + teleporter: [3, 0] +- name: Light Temple - Entrance + id: 395 + area: 95 + coordinates: [28, 57] + teleporter: [19, 6] +- name: Light Temple - Mobius Teleporter Script + id: 396 + area: 95 + coordinates: [29, 37] + teleporter: [70, 8] +- name: Ship Dock - Mobius Teleporter Script + id: 397 + area: 96 + coordinates: [15, 18] + teleporter: [61, 8] +- name: Ship Dock - From Overworld + id: 398 + area: 96 + coordinates: [15, 11] + teleporter: [73, 0] +- name: Ship Dock - Entrance + id: 399 + area: 96 + coordinates: [15, 23] + teleporter: [17, 6] +- name: Mac Ship Deck - East Entrance Script + id: 400 + area: 97 + coordinates: [26, 40] + teleporter: [37, 8] +- name: Mac Ship Deck - Central Stairs Script + id: 401 + area: 97 + coordinates: [16, 47] + teleporter: [50, 8] +- name: Mac Ship Deck - West Stairs Script + id: 402 + area: 97 + coordinates: [8, 34] + teleporter: [51, 8] +- name: Mac Ship Deck - East Stairs Script + id: 403 + area: 97 + coordinates: [24, 36] + teleporter: [52, 8] +- name: Mac Ship Deck - North Stairs Script + id: 404 + area: 97 + coordinates: [12, 9] + teleporter: [53, 8] +- name: Mac Ship B1 Outer Ring - South Stairs + id: 405 + area: 98 + coordinates: [16, 45] + teleporter: [208, 0] +- name: Mac Ship B1 Outer Ring - West Stairs + id: 406 + area: 98 + coordinates: [8, 35] + teleporter: [175, 0] +- name: Mac Ship B1 Outer Ring - East Stairs + id: 407 + area: 98 + coordinates: [25, 37] + teleporter: [172, 0] +- name: Mac Ship B1 Outer Ring - Northwest Stairs + id: 408 + area: 98 + coordinates: [10, 23] + teleporter: [88, 0] +- name: Mac Ship B1 Square Room - North Stairs + id: 409 + area: 98 + coordinates: [14, 9] + teleporter: [141, 0] +- name: Mac Ship B1 Square Room - South Stairs + id: 410 + area: 98 + coordinates: [16, 12] + teleporter: [87, 0] +- name: Mac Ship B1 Mac Room - Stairs # Unused? + id: 411 + area: 98 + coordinates: [16, 51] + teleporter: [101, 0] +- name: Mac Ship B1 Central Corridor - South Stairs + id: 412 + area: 98 + coordinates: [16, 38] + teleporter: [102, 0] +- name: Mac Ship B1 Central Corridor - North Stairs + id: 413 + area: 98 + coordinates: [16, 26] + teleporter: [86, 0] +- name: Mac Ship B2 South Corridor - South Stairs + id: 414 + area: 99 + coordinates: [48, 51] + teleporter: [57, 1] +- name: Mac Ship B2 South Corridor - North Stairs Script + id: 415 + area: 99 + coordinates: [48, 38] + teleporter: [55, 8] +- name: Mac Ship B2 North Corridor - South Stairs Script + id: 416 + area: 99 + coordinates: [48, 27] + teleporter: [56, 8] +- name: Mac Ship B2 North Corridor - North Stairs Script + id: 417 + area: 99 + coordinates: [48, 12] + teleporter: [57, 8] +- name: Mac Ship B2 Outer Ring - Northwest Stairs Script + id: 418 + area: 99 + coordinates: [55, 11] + teleporter: [58, 8] +- name: Mac Ship B1 Outer Ring Cleared - South Stairs + id: 419 + area: 100 + coordinates: [16, 45] + teleporter: [208, 0] +- name: Mac Ship B1 Outer Ring Cleared - West Stairs + id: 420 + area: 100 + coordinates: [8, 35] + teleporter: [175, 0] +- name: Mac Ship B1 Outer Ring Cleared - East Stairs + id: 421 + area: 100 + coordinates: [25, 37] + teleporter: [172, 0] +- name: Mac Ship B1 Square Room Cleared - North Stairs + id: 422 + area: 100 + coordinates: [14, 9] + teleporter: [141, 0] +- name: Mac Ship B1 Square Room Cleared - South Stairs + id: 423 + area: 100 + coordinates: [16, 12] + teleporter: [87, 0] +- name: Mac Ship B1 Mac Room Cleared - Main Stairs + id: 424 + area: 100 + coordinates: [16, 51] + teleporter: [101, 0] +- name: Mac Ship B1 Central Corridor Cleared - South Stairs + id: 425 + area: 100 + coordinates: [16, 38] + teleporter: [102, 0] +- name: Mac Ship B1 Central Corridor Cleared - North Stairs + id: 426 + area: 100 + coordinates: [16, 26] + teleporter: [86, 0] +- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs + id: 427 + area: 100 + coordinates: [23, 10] + teleporter: [88, 0] +- name: Doom Castle Corridor of Destiny - South Entrance + id: 428 + area: 101 + coordinates: [59, 29] + teleporter: [84, 0] +- name: Doom Castle Corridor of Destiny - Ice Floor Entrance + id: 429 + area: 101 + coordinates: [59, 21] + teleporter: [35, 2] +- name: Doom Castle Corridor of Destiny - Lava Floor Entrance + id: 430 + area: 101 + coordinates: [59, 13] + teleporter: [209, 0] +- name: Doom Castle Corridor of Destiny - Sky Floor Entrance + id: 431 + area: 101 + coordinates: [59, 5] + teleporter: [211, 0] +- name: Doom Castle Corridor of Destiny - Hero Room Entrance + id: 432 + area: 101 + coordinates: [59, 61] + teleporter: [13, 2] +- name: Doom Castle Ice Floor - Entrance + id: 433 + area: 102 + coordinates: [23, 42] + teleporter: [109, 3] +- name: Doom Castle Lava Floor - Entrance + id: 434 + area: 103 + coordinates: [23, 40] + teleporter: [210, 0] +- name: Doom Castle Sky Floor - Entrance + id: 435 + area: 104 + coordinates: [24, 41] + teleporter: [212, 0] +- name: Doom Castle Hero Room - Dark King Entrance 1 + id: 436 + area: 106 + coordinates: [15, 5] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 2 + id: 437 + area: 106 + coordinates: [16, 5] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 3 + id: 438 + area: 106 + coordinates: [15, 4] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Dark King Entrance 4 + id: 439 + area: 106 + coordinates: [16, 4] + teleporter: [54, 0] +- name: Doom Castle Hero Room - Hero Statue Script + id: 440 + area: 106 + coordinates: [15, 17] + teleporter: [24, 8] +- name: Doom Castle Hero Room - Entrance + id: 441 + area: 106 + coordinates: [15, 24] + teleporter: [110, 3] +- name: Doom Castle Dark King Room - Entrance + id: 442 + area: 107 + coordinates: [14, 26] + teleporter: [52, 0] +- name: Doom Castle Dark King Room - Dark King Script + id: 443 + area: 107 + coordinates: [14, 15] + teleporter: [25, 8] +- name: Doom Castle Dark King Room - Unknown + id: 444 + area: 107 + coordinates: [47, 54] + teleporter: [77, 0] +- name: Overworld - Level Forest + id: 445 + area: 0 + type: "Overworld" + teleporter: [0x2E, 8] +- name: Overworld - Foresta + id: 446 + area: 0 + type: "Overworld" + teleporter: [0x02, 1] +- name: Overworld - Sand Temple + id: 447 + area: 0 + type: "Overworld" + teleporter: [0x03, 1] +- name: Overworld - Bone Dungeon + id: 448 + area: 0 + type: "Overworld" + teleporter: [0x04, 1] +- name: Overworld - Focus Tower Foresta + id: 449 + area: 0 + type: "Overworld" + teleporter: [0x05, 1] +- name: Overworld - Focus Tower Aquaria + id: 450 + area: 0 + type: "Overworld" + teleporter: [0x13, 1] +- name: Overworld - Libra Temple + id: 451 + area: 0 + type: "Overworld" + teleporter: [0x07, 1] +- name: Overworld - Aquaria + id: 452 + area: 0 + type: "Overworld" + teleporter: [0x08, 8] +- name: Overworld - Wintry Cave + id: 453 + area: 0 + type: "Overworld" + teleporter: [0x0A, 1] +- name: Overworld - Life Temple + id: 454 + area: 0 + type: "Overworld" + teleporter: [0x0B, 1] +- name: Overworld - Falls Basin + id: 455 + area: 0 + type: "Overworld" + teleporter: [0x0C, 1] +- name: Overworld - Ice Pyramid + id: 456 + area: 0 + type: "Overworld" + teleporter: [0x0D, 1] # Will be switched to a script +- name: Overworld - Spencer's Place + id: 457 + area: 0 + type: "Overworld" + teleporter: [0x30, 8] +- name: Overworld - Wintry Temple + id: 458 + area: 0 + type: "Overworld" + teleporter: [0x10, 1] +- name: Overworld - Focus Tower Frozen Strip + id: 459 + area: 0 + type: "Overworld" + teleporter: [0x11, 1] +- name: Overworld - Focus Tower Fireburg + id: 460 + area: 0 + type: "Overworld" + teleporter: [0x12, 1] +- name: Overworld - Fireburg + id: 461 + area: 0 + type: "Overworld" + teleporter: [0x14, 1] +- name: Overworld - Mine + id: 462 + area: 0 + type: "Overworld" + teleporter: [0x15, 1] +- name: Overworld - Sealed Temple + id: 463 + area: 0 + type: "Overworld" + teleporter: [0x16, 1] +- name: Overworld - Volcano + id: 464 + area: 0 + type: "Overworld" + teleporter: [0x17, 1] +- name: Overworld - Lava Dome + id: 465 + area: 0 + type: "Overworld" + teleporter: [0x18, 1] +- name: Overworld - Focus Tower Windia + id: 466 + area: 0 + type: "Overworld" + teleporter: [0x06, 1] +- name: Overworld - Rope Bridge + id: 467 + area: 0 + type: "Overworld" + teleporter: [0x19, 1] +- name: Overworld - Alive Forest + id: 468 + area: 0 + type: "Overworld" + teleporter: [0x1A, 1] +- name: Overworld - Giant Tree + id: 469 + area: 0 + type: "Overworld" + teleporter: [0x1B, 1] +- name: Overworld - Kaidge Temple + id: 470 + area: 0 + type: "Overworld" + teleporter: [0x1C, 1] +- name: Overworld - Windia + id: 471 + area: 0 + type: "Overworld" + teleporter: [0x1D, 1] +- name: Overworld - Windhole Temple + id: 472 + area: 0 + type: "Overworld" + teleporter: [0x1E, 1] +- name: Overworld - Mount Gale + id: 473 + area: 0 + type: "Overworld" + teleporter: [0x1F, 1] +- name: Overworld - Pazuzu Tower + id: 474 + area: 0 + type: "Overworld" + teleporter: [0x20, 1] +- name: Overworld - Ship Dock + id: 475 + area: 0 + type: "Overworld" + teleporter: [0x3E, 1] +- name: Overworld - Doom Castle + id: 476 + area: 0 + type: "Overworld" + teleporter: [0x21, 1] +- name: Overworld - Light Temple + id: 477 + area: 0 + type: "Overworld" + teleporter: [0x22, 1] +- name: Overworld - Mac Ship + id: 478 + area: 0 + type: "Overworld" + teleporter: [0x24, 1] +- name: Overworld - Mac Ship Doom + id: 479 + area: 0 + type: "Overworld" + teleporter: [0x24, 1] +- name: Dummy House - Bed Script + id: 480 + area: 17 + coordinates: [0x28, 0x38] + teleporter: [1, 8] +- name: Dummy House - Entrance + id: 481 + area: 17 + coordinates: [0x29, 0x3B] + teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml new file mode 100644 index 0000000000..4343d785eb --- /dev/null +++ b/worlds/ffmq/data/rooms.yaml @@ -0,0 +1,4026 @@ +- name: Overworld + id: 0 + type: "Overworld" + game_objects: [] + links: + - target_room: 220 # To Forest Subregion + access: [] +- name: Subregion Foresta + id: 220 + type: "Subregion" + region: "Foresta" + game_objects: + - name: "Foresta South Battlefield" + object_id: 0x01 + location: "ForestaSouthBattlefield" + location_slot: "ForestaSouthBattlefield" + type: "BattlefieldXp" + access: [] + - name: "Foresta West Battlefield" + object_id: 0x02 + location: "ForestaWestBattlefield" + location_slot: "ForestaWestBattlefield" + type: "BattlefieldItem" + access: [] + - name: "Foresta East Battlefield" + object_id: 0x03 + location: "ForestaEastBattlefield" + location_slot: "ForestaEastBattlefield" + type: "BattlefieldGp" + access: [] + links: + - target_room: 15 # Level Forest + location: "LevelForest" + location_slot: "LevelForest" + entrance: 445 + teleporter: [0x2E, 8] + access: [] + - target_room: 16 # Foresta + location: "Foresta" + location_slot: "Foresta" + entrance: 446 + teleporter: [0x02, 1] + access: [] + - target_room: 24 # Sand Temple + location: "SandTemple" + location_slot: "SandTemple" + entrance: 447 + teleporter: [0x03, 1] + access: [] + - target_room: 25 # Bone Dungeon + location: "BoneDungeon" + location_slot: "BoneDungeon" + entrance: 448 + teleporter: [0x04, 1] + access: [] + - target_room: 3 # Focus Tower Foresta + location: "FocusTowerForesta" + location_slot: "FocusTowerForesta" + entrance: 449 + teleporter: [0x05, 1] + access: [] + - target_room: 221 + access: ["SandCoin"] + - target_room: 224 + access: ["RiverCoin"] + - target_room: 226 + access: ["SunCoin"] +- name: Subregion Aquaria + id: 221 + type: "Subregion" + region: "Aquaria" + game_objects: + - name: "South of Libra Temple Battlefield" + object_id: 0x04 + location: "AquariaBattlefield01" + location_slot: "AquariaBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "East of Libra Temple Battlefield" + object_id: 0x05 + location: "AquariaBattlefield02" + location_slot: "AquariaBattlefield02" + type: "BattlefieldGp" + access: [] + - name: "South of Aquaria Battlefield" + object_id: 0x06 + location: "AquariaBattlefield03" + location_slot: "AquariaBattlefield03" + type: "BattlefieldItem" + access: [] + - name: "South of Wintry Cave Battlefield" + object_id: 0x07 + location: "WintryBattlefield01" + location_slot: "WintryBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "West of Wintry Cave Battlefield" + object_id: 0x08 + location: "WintryBattlefield02" + location_slot: "WintryBattlefield02" + type: "BattlefieldGp" + access: [] + - name: "Ice Pyramid Battlefield" + object_id: 0x09 + location: "PyramidBattlefield01" + location_slot: "PyramidBattlefield01" + type: "BattlefieldXp" + access: [] + links: + - target_room: 10 # Focus Tower Aquaria + location: "FocusTowerAquaria" + location_slot: "FocusTowerAquaria" + entrance: 450 + teleporter: [0x13, 1] + access: [] + - target_room: 39 # Libra Temple + location: "LibraTemple" + location_slot: "LibraTemple" + entrance: 451 + teleporter: [0x07, 1] + access: [] + - target_room: 40 # Aquaria + location: "Aquaria" + location_slot: "Aquaria" + entrance: 452 + teleporter: [0x08, 8] + access: [] + - target_room: 45 # Wintry Cave + location: "WintryCave" + location_slot: "WintryCave" + entrance: 453 + teleporter: [0x0A, 1] + access: [] + - target_room: 52 # Falls Basin + location: "FallsBasin" + location_slot: "FallsBasin" + entrance: 455 + teleporter: [0x0C, 1] + access: [] + - target_room: 54 # Ice Pyramid + location: "IcePyramid" + location_slot: "IcePyramid" + entrance: 456 + teleporter: [0x0D, 1] # Will be switched to a script + access: [] + - target_room: 220 + access: ["SandCoin"] + - target_room: 224 + access: ["SandCoin", "RiverCoin"] + - target_room: 226 + access: ["SandCoin", "SunCoin"] + - target_room: 223 + access: ["SummerAquaria"] +- name: Subregion Life Temple + id: 222 + type: "Subregion" + region: "LifeTemple" + game_objects: [] + links: + - target_room: 51 # Life Temple + location: "LifeTemple" + location_slot: "LifeTemple" + entrance: 454 + teleporter: [0x0B, 1] + access: [] +- name: Subregion Frozen Fields + id: 223 + type: "Subregion" + region: "AquariaFrozenField" + game_objects: + - name: "North of Libra Temple Battlefield" + object_id: 0x0A + location: "LibraBattlefield01" + location_slot: "LibraBattlefield01" + type: "BattlefieldItem" + access: [] + - name: "Aquaria Frozen Field Battlefield" + object_id: 0x0B + location: "LibraBattlefield02" + location_slot: "LibraBattlefield02" + type: "BattlefieldXp" + access: [] + links: + - target_room: 74 # Wintry Temple + location: "WintryTemple" + location_slot: "WintryTemple" + entrance: 458 + teleporter: [0x10, 1] + access: [] + - target_room: 14 # Focus Tower Frozen Strip + location: "FocusTowerFrozen" + location_slot: "FocusTowerFrozen" + entrance: 459 + teleporter: [0x11, 1] + access: [] + - target_room: 221 + access: [] + - target_room: 225 + access: ["SummerAquaria", "DualheadHydra"] +- name: Subregion Fireburg + id: 224 + type: "Subregion" + region: "Fireburg" + game_objects: + - name: "Path to Fireburg Southern Battlefield" + object_id: 0x0C + location: "FireburgBattlefield01" + location_slot: "FireburgBattlefield01" + type: "BattlefieldGp" + access: [] + - name: "Path to Fireburg Central Battlefield" + object_id: 0x0D + location: "FireburgBattlefield02" + location_slot: "FireburgBattlefield02" + type: "BattlefieldItem" + access: [] + - name: "Path to Fireburg Northern Battlefield" + object_id: 0x0E + location: "FireburgBattlefield03" + location_slot: "FireburgBattlefield03" + type: "BattlefieldXp" + access: [] + - name: "Sealed Temple Battlefield" + object_id: 0x0F + location: "MineBattlefield01" + location_slot: "MineBattlefield01" + type: "BattlefieldGp" + access: [] + - name: "Mine Battlefield" + object_id: 0x10 + location: "MineBattlefield02" + location_slot: "MineBattlefield02" + type: "BattlefieldItem" + access: [] + - name: "Boulder Battlefield" + object_id: 0x11 + location: "MineBattlefield03" + location_slot: "MineBattlefield03" + type: "BattlefieldXp" + access: [] + links: + - target_room: 13 # Focus Tower Fireburg + location: "FocusTowerFireburg" + location_slot: "FocusTowerFireburg" + entrance: 460 + teleporter: [0x12, 1] + access: [] + - target_room: 76 # Fireburg + location: "Fireburg" + location_slot: "Fireburg" + entrance: 461 + teleporter: [0x14, 1] + access: [] + - target_room: 84 # Mine + location: "Mine" + location_slot: "Mine" + entrance: 462 + teleporter: [0x15, 1] + access: [] + - target_room: 92 # Sealed Temple + location: "SealedTemple" + location_slot: "SealedTemple" + entrance: 463 + teleporter: [0x16, 1] + access: [] + - target_room: 93 # Volcano + location: "Volcano" + location_slot: "Volcano" + entrance: 464 + teleporter: [0x17, 1] # Also this one / 0x0F, 8 + access: [] + - target_room: 100 # Lava Dome + location: "LavaDome" + location_slot: "LavaDome" + entrance: 465 + teleporter: [0x18, 1] + access: [] + - target_room: 220 + access: ["RiverCoin"] + - target_room: 221 + access: ["SandCoin", "RiverCoin"] + - target_room: 226 + access: ["RiverCoin", "SunCoin"] + - target_room: 225 + access: ["DualheadHydra"] +- name: Subregion Volcano Battlefield + id: 225 + type: "Subregion" + region: "VolcanoBattlefield" + game_objects: + - name: "Volcano Battlefield" + object_id: 0x12 + location: "VolcanoBattlefield01" + location_slot: "VolcanoBattlefield01" + type: "BattlefieldXp" + access: [] + links: + - target_room: 224 + access: ["DualheadHydra"] + - target_room: 223 + access: ["SummerAquaria"] +- name: Subregion Windia + id: 226 + type: "Subregion" + region: "Windia" + game_objects: + - name: "Kaidge Temple Battlefield" + object_id: 0x13 + location: "WindiaBattlefield01" + location_slot: "WindiaBattlefield01" + type: "BattlefieldXp" + access: [] + - name: "South of Windia Battlefield" + object_id: 0x14 + location: "WindiaBattlefield02" + location_slot: "WindiaBattlefield02" + type: "BattlefieldXp" + access: [] + links: + - target_room: 9 # Focus Tower Windia + location: "FocusTowerWindia" + location_slot: "FocusTowerWindia" + entrance: 466 + teleporter: [0x06, 1] + access: [] + - target_room: 123 # Rope Bridge + location: "RopeBridge" + location_slot: "RopeBridge" + entrance: 467 + teleporter: [0x19, 1] + access: [] + - target_room: 124 # Alive Forest + location: "AliveForest" + location_slot: "AliveForest" + entrance: 468 + teleporter: [0x1A, 1] + access: [] + - target_room: 125 # Giant Tree + location: "GiantTree" + location_slot: "GiantTree" + entrance: 469 + teleporter: [0x1B, 1] + access: ["Barred"] + - target_room: 152 # Kaidge Temple + location: "KaidgeTemple" + location_slot: "KaidgeTemple" + entrance: 470 + teleporter: [0x1C, 1] + access: [] + - target_room: 156 # Windia + location: "Windia" + location_slot: "Windia" + entrance: 471 + teleporter: [0x1D, 1] + access: [] + - target_room: 154 # Windhole Temple + location: "WindholeTemple" + location_slot: "WindholeTemple" + entrance: 472 + teleporter: [0x1E, 1] + access: [] + - target_room: 155 # Mount Gale + location: "MountGale" + location_slot: "MountGale" + entrance: 473 + teleporter: [0x1F, 1] + access: [] + - target_room: 166 # Pazuzu Tower + location: "PazuzusTower" + location_slot: "PazuzusTower" + entrance: 474 + teleporter: [0x20, 1] + access: [] + - target_room: 220 + access: ["SunCoin"] + - target_room: 221 + access: ["SandCoin", "SunCoin"] + - target_room: 224 + access: ["RiverCoin", "SunCoin"] + - target_room: 227 + access: ["RainbowBridge"] +- name: Subregion Spencer's Cave + id: 227 + type: "Subregion" + region: "SpencerCave" + game_objects: [] + links: + - target_room: 73 # Spencer's Place + location: "SpencersPlace" + location_slot: "SpencersPlace" + entrance: 457 + teleporter: [0x30, 8] + access: [] + - target_room: 226 + access: ["RainbowBridge"] +- name: Subregion Ship Dock + id: 228 + type: "Subregion" + region: "ShipDock" + game_objects: [] + links: + - target_room: 186 # Ship Dock + location: "ShipDock" + location_slot: "ShipDock" + entrance: 475 + teleporter: [0x3E, 1] + access: [] + - target_room: 229 + access: ["ShipLiberated", "ShipDockAccess"] +- name: Subregion Mac's Ship + id: 229 + type: "Subregion" + region: "MacShip" + game_objects: [] + links: + - target_room: 187 # Mac Ship + location: "MacsShip" + location_slot: "MacsShip" + entrance: 478 + teleporter: [0x24, 1] + access: [] + - target_room: 228 + access: ["ShipLiberated", "ShipDockAccess"] + - target_room: 231 + access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] +- name: Subregion Light Temple + id: 230 + type: "Subregion" + region: "LightTemple" + game_objects: [] + links: + - target_room: 185 # Light Temple + location: "LightTemple" + location_slot: "LightTemple" + entrance: 477 + teleporter: [0x23, 1] + access: [] +- name: Subregion Doom Castle + id: 231 + type: "Subregion" + region: "DoomCastle" + game_objects: [] + links: + - target_room: 1 # Doom Castle + location: "DoomCastle" + location_slot: "DoomCastle" + entrance: 476 + teleporter: [0x21, 1] + access: [] + - target_room: 187 # Mac Ship Doom + location: "MacsShipDoom" + location_slot: "MacsShipDoom" + entrance: 479 + teleporter: [0x24, 1] + access: ["Barred"] + - target_room: 229 + access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] +- name: Doom Castle - Sand Floor + id: 1 + game_objects: + - name: "Doom Castle B2 - Southeast Chest" + object_id: 0x01 + type: "Chest" + access: ["Bomb"] + - name: "Doom Castle B2 - Bone Ledge Box" + object_id: 0x1E + type: "Box" + access: [] + - name: "Doom Castle B2 - Hook Platform Box" + object_id: 0x1F + type: "Box" + access: ["DragonClaw"] + links: + - target_room: 231 + entrance: 1 + teleporter: [1, 6] + access: [] + - target_room: 5 + entrance: 0 + teleporter: [0, 0] + access: ["DragonClaw", "MegaGrenade"] +- name: Doom Castle - Aero Room + id: 2 + game_objects: + - name: "Doom Castle B2 - Sun Door Chest" + object_id: 0x00 + type: "Chest" + access: [] + links: + - target_room: 4 + entrance: 2 + teleporter: [1, 0] + access: [] +- name: Focus Tower B1 - Main Loop + id: 3 + game_objects: [] + links: + - target_room: 220 + entrance: 3 + teleporter: [2, 6] + access: [] + - target_room: 6 + entrance: 4 + teleporter: [4, 0] + access: [] +- name: Focus Tower B1 - Aero Corridor + id: 4 + game_objects: [] + links: + - target_room: 9 + entrance: 5 + teleporter: [5, 0] + access: [] + - target_room: 2 + entrance: 6 + teleporter: [8, 0] + access: [] +- name: Focus Tower B1 - Inner Loop + id: 5 + game_objects: [] + links: + - target_room: 1 + entrance: 8 + teleporter: [7, 0] + access: [] + - target_room: 201 + entrance: 7 + teleporter: [6, 0] + access: [] +- name: Focus Tower 1F Main Lobby + id: 6 + game_objects: + - name: "Focus Tower 1F - Main Lobby Box" + object_id: 0x21 + type: "Box" + access: [] + links: + - target_room: 3 + entrance: 11 + teleporter: [11, 0] + access: [] + - target_room: 7 + access: ["SandCoin"] + - target_room: 8 + access: ["RiverCoin"] + - target_room: 9 + access: ["SunCoin"] +- name: Focus Tower 1F SandCoin Room + id: 7 + game_objects: [] + links: + - target_room: 6 + access: ["SandCoin"] + - target_room: 10 + entrance: 10 + teleporter: [10, 0] + access: [] +- name: Focus Tower 1F RiverCoin Room + id: 8 + game_objects: [] + links: + - target_room: 6 + access: ["RiverCoin"] + - target_room: 11 + entrance: 14 + teleporter: [14, 0] + access: [] +- name: Focus Tower 1F SunCoin Room + id: 9 + game_objects: [] + links: + - target_room: 6 + access: ["SunCoin"] + - target_room: 4 + entrance: 12 + teleporter: [12, 0] + access: [] + - target_room: 226 + entrance: 9 + teleporter: [3, 6] + access: [] +- name: Focus Tower 1F SkyCoin Room + id: 201 + game_objects: [] + links: + - target_room: 195 + entrance: 13 + teleporter: [13, 0] + access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] + - target_room: 5 + entrance: 15 + teleporter: [15, 0] + access: [] +- name: Focus Tower 2F - Sand Coin Passage + id: 10 + game_objects: + - name: "Focus Tower 2F - Sand Door Chest" + object_id: 0x03 + type: "Chest" + access: [] + links: + - target_room: 221 + entrance: 16 + teleporter: [4, 6] + access: [] + - target_room: 7 + entrance: 17 + teleporter: [17, 0] + access: [] +- name: Focus Tower 2F - River Coin Passage + id: 11 + game_objects: [] + links: + - target_room: 8 + entrance: 18 + teleporter: [18, 0] + access: [] + - target_room: 13 + entrance: 19 + teleporter: [20, 0] + access: [] +- name: Focus Tower 2F - Venus Chest Room + id: 12 + game_objects: + - name: "Focus Tower 2F - Back Door Chest" + object_id: 0x02 + type: "Chest" + access: [] + - name: "Focus Tower 2F - Venus Chest" + object_id: 9 + type: "NPC" + access: ["Bomb", "VenusKey"] + links: + - target_room: 14 + entrance: 20 + teleporter: [19, 0] + access: [] +- name: Focus Tower 3F - Lower Floor + id: 13 + game_objects: + - name: "Focus Tower 3F - River Door Box" + object_id: 0x22 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 22 + teleporter: [6, 6] + access: [] + - target_room: 11 + entrance: 23 + teleporter: [24, 0] + access: [] +- name: Focus Tower 3F - Upper Floor + id: 14 + game_objects: [] + links: + - target_room: 223 + entrance: 24 + teleporter: [5, 6] + access: [] + - target_room: 12 + entrance: 25 + teleporter: [23, 0] + access: [] +- name: Level Forest + id: 15 + game_objects: + - name: "Level Forest - Northwest Box" + object_id: 0x28 + type: "Box" + access: ["Axe"] + - name: "Level Forest - Northeast Box" + object_id: 0x29 + type: "Box" + access: ["Axe"] + - name: "Level Forest - Middle Box" + object_id: 0x2A + type: "Box" + access: [] + - name: "Level Forest - Southwest Box" + object_id: 0x2B + type: "Box" + access: ["Axe"] + - name: "Level Forest - Southeast Box" + object_id: 0x2C + type: "Box" + access: ["Axe"] + - name: "Minotaur" + object_id: 0 + type: "Trigger" + on_trigger: ["Minotaur"] + access: ["Kaeli1"] + - name: "Level Forest - Old Man" + object_id: 0 + type: "NPC" + access: [] + - name: "Level Forest - Kaeli" + object_id: 1 + type: "NPC" + access: ["Kaeli1", "Minotaur"] + links: + - target_room: 220 + entrance: 28 + teleporter: [25, 0] + access: [] +- name: Foresta + id: 16 + game_objects: + - name: "Foresta - Outside Box" + object_id: 0x2D + type: "Box" + access: ["Axe"] + links: + - target_room: 220 + entrance: 38 + teleporter: [31, 0] + access: [] + - target_room: 17 + entrance: 44 + teleporter: [0, 5] + access: [] + - target_room: 18 + entrance: 42 + teleporter: [32, 4] + access: [] + - target_room: 19 + entrance: 43 + teleporter: [33, 0] + access: [] + - target_room: 20 + entrance: 45 + teleporter: [1, 5] + access: [] +- name: Kaeli's House + id: 17 + game_objects: + - name: "Foresta - Kaeli's House Box" + object_id: 0x2E + type: "Box" + access: [] + - name: "Kaeli 1" + object_id: 0 + type: "Trigger" + on_trigger: ["Kaeli1"] + access: ["TreeWither"] + - name: "Kaeli 2" + object_id: 0 + type: "Trigger" + on_trigger: ["Kaeli2"] + access: ["Kaeli1", "Minotaur", "Elixir"] + links: + - target_room: 16 + entrance: 46 + teleporter: [86, 3] + access: [] +- name: Foresta Houses - Old Man's House Main + id: 18 + game_objects: [] + links: + - target_room: 19 + access: ["BarrelPushed"] + - target_room: 16 + entrance: 47 + teleporter: [34, 0] + access: [] +- name: Foresta Houses - Old Man's House Back + id: 19 + game_objects: + - name: "Foresta - Old Man House Chest" + object_id: 0x05 + type: "Chest" + access: [] + - name: "Old Man Barrel" + object_id: 0 + type: "Trigger" + on_trigger: ["BarrelPushed"] + access: [] + links: + - target_room: 18 + access: ["BarrelPushed"] + - target_room: 16 + entrance: 48 + teleporter: [35, 0] + access: [] +- name: Foresta Houses - Rest House + id: 20 + game_objects: + - name: "Foresta - Rest House Box" + object_id: 0x2F + type: "Box" + access: [] + links: + - target_room: 16 + entrance: 50 + teleporter: [87, 3] + access: [] +- name: Libra Treehouse + id: 21 + game_objects: + - name: "Alive Forest - Libra Treehouse Box" + object_id: 0x32 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 51 + teleporter: [67, 8] + access: ["LibraCrest"] +- name: Gemini Treehouse + id: 22 + game_objects: + - name: "Alive Forest - Gemini Treehouse Box" + object_id: 0x33 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 52 + teleporter: [68, 8] + access: ["GeminiCrest"] +- name: Mobius Treehouse + id: 23 + game_objects: + - name: "Alive Forest - Mobius Treehouse West Box" + object_id: 0x30 + type: "Box" + access: [] + - name: "Alive Forest - Mobius Treehouse East Box" + object_id: 0x31 + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 53 + teleporter: [69, 8] + access: ["MobiusCrest"] +- name: Sand Temple + id: 24 + game_objects: + - name: "Tristam Sand Temple" + object_id: 0 + type: "Trigger" + on_trigger: ["Tristam"] + access: [] + links: + - target_room: 220 + entrance: 54 + teleporter: [36, 0] + access: [] +- name: Bone Dungeon 1F + id: 25 + game_objects: + - name: "Bone Dungeon 1F - Entrance Room West Box" + object_id: 0x35 + type: "Box" + access: [] + - name: "Bone Dungeon 1F - Entrance Room Middle Box" + object_id: 0x36 + type: "Box" + access: [] + - name: "Bone Dungeon 1F - Entrance Room East Box" + object_id: 0x37 + type: "Box" + access: [] + links: + - target_room: 220 + entrance: 55 + teleporter: [37, 0] + access: [] + - target_room: 26 + entrance: 56 + teleporter: [2, 2] + access: [] +- name: Bone Dungeon B1 - Waterway + id: 26 + game_objects: + - name: "Bone Dungeon B1 - Skull Chest" + object_id: 0x06 + type: "Chest" + access: ["Bomb"] + - name: "Bone Dungeon B1 - Tristam" + object_id: 2 + type: "NPC" + access: ["Tristam"] + links: + - target_room: 25 + entrance: 59 + teleporter: [88, 3] + access: [] + - target_room: 28 + entrance: 57 + teleporter: [3, 2] + access: ["Bomb"] +- name: Bone Dungeon B1 - Checker Room + id: 28 + game_objects: + - name: "Bone Dungeon B1 - Checker Room Box" + object_id: 0x38 + type: "Box" + access: ["Bomb"] + links: + - target_room: 26 + entrance: 61 + teleporter: [89, 3] + access: [] + - target_room: 30 + entrance: 60 + teleporter: [4, 2] + access: [] +- name: Bone Dungeon B1 - Hidden Room + id: 29 + game_objects: + - name: "Bone Dungeon B1 - Ribcage Waterway Box" + object_id: 0x39 + type: "Box" + access: [] + links: + - target_room: 31 + entrance: 62 + teleporter: [91, 3] + access: [] +- name: Bone Dungeon B2 - Exploding Skull Room - First Room + id: 30 + game_objects: + - name: "Bone Dungeon B2 - Spines Room Alcove Box" + object_id: 0x3B + type: "Box" + access: [] + - name: "Long Spine" + object_id: 0 + type: "Trigger" + on_trigger: ["LongSpineBombed"] + access: ["Bomb"] + links: + - target_room: 28 + entrance: 65 + teleporter: [90, 3] + access: [] + - target_room: 31 + access: ["LongSpineBombed"] +- name: Bone Dungeon B2 - Exploding Skull Room - Second Room + id: 31 + game_objects: + - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" + object_id: 0x3A + type: "Box" + access: [] + - name: "Short Spine" + object_id: 0 + type: "Trigger" + on_trigger: ["ShortSpineBombed"] + access: ["Bomb"] + links: + - target_room: 29 + entrance: 63 + teleporter: [5, 2] + access: ["LongSpineBombed"] + - target_room: 32 + access: ["ShortSpineBombed"] + - target_room: 30 + access: ["LongSpineBombed"] +- name: Bone Dungeon B2 - Exploding Skull Room - Third Room + id: 32 + game_objects: [] + links: + - target_room: 35 + entrance: 64 + teleporter: [6, 2] + access: [] + - target_room: 31 + access: ["ShortSpineBombed"] +- name: Bone Dungeon B2 - Box Room + id: 33 + game_objects: + - name: "Bone Dungeon B2 - Lone Room Box" + object_id: 0x3D + type: "Box" + access: [] + links: + - target_room: 36 + entrance: 66 + teleporter: [93, 3] + access: [] +- name: Bone Dungeon B2 - Quake Room + id: 34 + game_objects: + - name: "Bone Dungeon B2 - Penultimate Room Chest" + object_id: 0x07 + type: "Chest" + access: [] + links: + - target_room: 37 + entrance: 67 + teleporter: [94, 3] + access: [] +- name: Bone Dungeon B2 - Two Skulls Room - First Room + id: 35 + game_objects: + - name: "Bone Dungeon B2 - Two Skulls Room Box" + object_id: 0x3C + type: "Box" + access: [] + - name: "Skull 1" + object_id: 0 + type: "Trigger" + on_trigger: ["Skull1Bombed"] + access: ["Bomb"] + links: + - target_room: 32 + entrance: 71 + teleporter: [92, 3] + access: [] + - target_room: 36 + access: ["Skull1Bombed"] +- name: Bone Dungeon B2 - Two Skulls Room - Second Room + id: 36 + game_objects: + - name: "Skull 2" + object_id: 0 + type: "Trigger" + on_trigger: ["Skull2Bombed"] + access: ["Bomb"] + links: + - target_room: 33 + entrance: 68 + teleporter: [7, 2] + access: [] + - target_room: 37 + access: ["Skull2Bombed"] + - target_room: 35 + access: ["Skull1Bombed"] +- name: Bone Dungeon B2 - Two Skulls Room - Third Room + id: 37 + game_objects: [] + links: + - target_room: 34 + entrance: 69 + teleporter: [8, 2] + access: [] + - target_room: 38 + entrance: 70 + teleporter: [9, 2] + access: ["Bomb"] + - target_room: 36 + access: ["Skull2Bombed"] +- name: Bone Dungeon B2 - Boss Room + id: 38 + game_objects: + - name: "Bone Dungeon B2 - North Box" + object_id: 0x3E + type: "Box" + access: [] + - name: "Bone Dungeon B2 - South Box" + object_id: 0x3F + type: "Box" + access: [] + - name: "Bone Dungeon B2 - Flamerus Rex Chest" + object_id: 0x08 + type: "Chest" + access: [] + - name: "Bone Dungeon B2 - Tristam's Treasure Chest" + object_id: 0x04 + type: "Chest" + access: [] + - name: "Flamerus Rex" + object_id: 0 + type: "Trigger" + on_trigger: ["FlamerusRex"] + access: [] + links: + - target_room: 37 + entrance: 74 + teleporter: [95, 3] + access: [] +- name: Libra Temple + id: 39 + game_objects: + - name: "Libra Temple - Box" + object_id: 0x40 + type: "Box" + access: [] + - name: "Phoebe" + object_id: 0 + type: "Trigger" + on_trigger: ["Phoebe1"] + access: [] + links: + - target_room: 221 + entrance: 75 + teleporter: [13, 6] + access: [] + - target_room: 51 + entrance: 76 + teleporter: [59, 8] + access: ["LibraCrest"] +- name: Aquaria + id: 40 + game_objects: + - name: "Summer Aquaria" + object_id: 0 + type: "Trigger" + on_trigger: ["SummerAquaria"] + access: ["WakeWater"] + links: + - target_room: 221 + entrance: 77 + teleporter: [8, 6] + access: [] + - target_room: 41 + entrance: 81 + teleporter: [10, 5] + access: [] + - target_room: 42 + entrance: 82 + teleporter: [44, 4] + access: [] + - target_room: 44 + entrance: 83 + teleporter: [11, 5] + access: [] + - target_room: 71 + entrance: 89 + teleporter: [42, 0] + access: ["SummerAquaria"] + - target_room: 71 + entrance: 90 + teleporter: [43, 0] + access: ["SummerAquaria"] +- name: Phoebe's House + id: 41 + game_objects: + - name: "Aquaria - Phoebe's House Chest" + object_id: 0x41 + type: "Box" + access: [] + links: + - target_room: 40 + entrance: 93 + teleporter: [5, 8] + access: [] +- name: Aquaria Vendor House + id: 42 + game_objects: + - name: "Aquaria - Vendor" + object_id: 4 + type: "NPC" + access: [] + - name: "Aquaria - Vendor House Box" + object_id: 0x42 + type: "Box" + access: [] + links: + - target_room: 40 + entrance: 94 + teleporter: [40, 8] + access: [] + - target_room: 43 + entrance: 95 + teleporter: [47, 0] + access: [] +- name: Aquaria Gemini Room + id: 43 + game_objects: [] + links: + - target_room: 42 + entrance: 97 + teleporter: [48, 0] + access: [] + - target_room: 81 + entrance: 96 + teleporter: [72, 8] + access: ["GeminiCrest"] +- name: Aquaria INN + id: 44 + game_objects: [] + links: + - target_room: 40 + entrance: 98 + teleporter: [75, 8] + access: [] +- name: Wintry Cave 1F - East Ledge + id: 45 + game_objects: + - name: "Wintry Cave 1F - North Box" + object_id: 0x43 + type: "Box" + access: [] + - name: "Wintry Cave 1F - Entrance Box" + object_id: 0x46 + type: "Box" + access: [] + - name: "Wintry Cave 1F - Slippery Cliff Box" + object_id: 0x44 + type: "Box" + access: ["Claw"] + - name: "Wintry Cave 1F - Phoebe" + object_id: 5 + type: "NPC" + access: ["Phoebe1"] + links: + - target_room: 221 + entrance: 99 + teleporter: [49, 0] + access: [] + - target_room: 49 + entrance: 100 + teleporter: [14, 2] + access: ["Bomb"] + - target_room: 46 + access: ["Claw"] +- name: Wintry Cave 1F - Central Space + id: 46 + game_objects: + - name: "Wintry Cave 1F - Scenic Overlook Box" + object_id: 0x45 + type: "Box" + access: ["Claw"] + links: + - target_room: 45 + access: ["Claw"] + - target_room: 47 + access: ["Claw"] +- name: Wintry Cave 1F - West Ledge + id: 47 + game_objects: [] + links: + - target_room: 48 + entrance: 101 + teleporter: [15, 2] + access: ["Bomb"] + - target_room: 46 + access: ["Claw"] +- name: Wintry Cave 2F + id: 48 + game_objects: + - name: "Wintry Cave 2F - West Left Box" + object_id: 0x47 + type: "Box" + access: [] + - name: "Wintry Cave 2F - West Right Box" + object_id: 0x48 + type: "Box" + access: [] + - name: "Wintry Cave 2F - East Left Box" + object_id: 0x49 + type: "Box" + access: [] + - name: "Wintry Cave 2F - East Right Box" + object_id: 0x4A + type: "Box" + access: [] + links: + - target_room: 47 + entrance: 104 + teleporter: [97, 3] + access: [] + - target_room: 50 + entrance: 103 + teleporter: [50, 0] + access: [] +- name: Wintry Cave 3F Top + id: 49 + game_objects: + - name: "Wintry Cave 3F - West Box" + object_id: 0x4B + type: "Box" + access: [] + - name: "Wintry Cave 3F - East Box" + object_id: 0x4C + type: "Box" + access: [] + links: + - target_room: 45 + entrance: 105 + teleporter: [96, 3] + access: [] +- name: Wintry Cave 3F Bottom + id: 50 + game_objects: + - name: "Wintry Cave 3F - Squidite Chest" + object_id: 0x09 + type: "Chest" + access: ["Phanquid"] + - name: "Phanquid" + object_id: 0 + type: "Trigger" + on_trigger: ["Phanquid"] + access: [] + - name: "Wintry Cave 3F - Before Boss Box" + object_id: 0x4D + type: "Box" + access: [] + links: + - target_room: 48 + entrance: 106 + teleporter: [51, 0] + access: [] +- name: Life Temple + id: 51 + game_objects: + - name: "Life Temple - Box" + object_id: 0x4E + type: "Box" + access: [] + - name: "Life Temple - Mysterious Man" + object_id: 6 + type: "NPC" + access: [] + links: + - target_room: 222 + entrance: 107 + teleporter: [14, 6] + access: [] + - target_room: 39 + entrance: 108 + teleporter: [60, 8] + access: ["LibraCrest"] +- name: Fall Basin + id: 52 + game_objects: + - name: "Falls Basin - Snow Crab Chest" + object_id: 0x0A + type: "Chest" + access: ["FreezerCrab"] + - name: "Freezer Crab" + object_id: 0 + type: "Trigger" + on_trigger: ["FreezerCrab"] + access: [] + - name: "Falls Basin - Box" + object_id: 0x4F + type: "Box" + access: [] + links: + - target_room: 221 + entrance: 111 + teleporter: [53, 0] + access: [] +- name: Ice Pyramid B1 Taunt Room + id: 53 + game_objects: + - name: "Ice Pyramid B1 - Chest" + object_id: 0x0B + type: "Chest" + access: [] + - name: "Ice Pyramid B1 - West Box" + object_id: 0x50 + type: "Box" + access: [] + - name: "Ice Pyramid B1 - North Box" + object_id: 0x51 + type: "Box" + access: [] + - name: "Ice Pyramid B1 - East Box" + object_id: 0x52 + type: "Box" + access: [] + links: + - target_room: 68 + entrance: 113 + teleporter: [55, 0] + access: [] +- name: Ice Pyramid 1F Maze Lobby + id: 54 + game_objects: + - name: "Ice Pyramid 1F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid1FStatue"] + access: ["Sword"] + links: + - target_room: 221 + entrance: 114 + teleporter: [56, 0] + access: [] + - target_room: 55 + access: ["IcePyramid1FStatue"] +- name: Ice Pyramid 1F Maze + id: 55 + game_objects: + - name: "Ice Pyramid 1F - East Alcove Chest" + object_id: 0x0D + type: "Chest" + access: [] + - name: "Ice Pyramid 1F - Sandwiched Alcove Box" + object_id: 0x53 + type: "Box" + access: [] + - name: "Ice Pyramid 1F - Southwest Left Box" + object_id: 0x54 + type: "Box" + access: [] + - name: "Ice Pyramid 1F - Southwest Right Box" + object_id: 0x55 + type: "Box" + access: [] + links: + - target_room: 56 + entrance: 116 + teleporter: [57, 0] + access: [] + - target_room: 57 + entrance: 117 + teleporter: [58, 0] + access: [] + - target_room: 58 + entrance: 118 + teleporter: [59, 0] + access: [] + - target_room: 59 + entrance: 119 + teleporter: [60, 0] + access: [] + - target_room: 60 + entrance: 120 + teleporter: [61, 0] + access: [] + - target_room: 54 + access: ["IcePyramid1FStatue"] +- name: Ice Pyramid 2F South Tiled Room + id: 56 + game_objects: + - name: "Ice Pyramid 2F - South Side Glass Door Box" + object_id: 0x57 + type: "Box" + access: ["Sword"] + - name: "Ice Pyramid 2F - South Side East Box" + object_id: 0x5B + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 122 + teleporter: [62, 0] + access: [] + - target_room: 61 + entrance: 123 + teleporter: [67, 0] + access: [] +- name: Ice Pyramid 2F West Room + id: 57 + game_objects: + - name: "Ice Pyramid 2F - Northwest Room Box" + object_id: 0x5A + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 124 + teleporter: [63, 0] + access: [] +- name: Ice Pyramid 2F Center Room + id: 58 + game_objects: + - name: "Ice Pyramid 2F - Center Room Box" + object_id: 0x56 + type: "Box" + access: [] + links: + - target_room: 55 + entrance: 125 + teleporter: [64, 0] + access: [] +- name: Ice Pyramid 2F Small North Room + id: 59 + game_objects: + - name: "Ice Pyramid 2F - North Room Glass Door Box" + object_id: 0x58 + type: "Box" + access: ["Sword"] + links: + - target_room: 55 + entrance: 126 + teleporter: [65, 0] + access: [] +- name: Ice Pyramid 2F North Corridor + id: 60 + game_objects: + - name: "Ice Pyramid 2F - North Corridor Glass Door Box" + object_id: 0x59 + type: "Box" + access: ["Sword"] + links: + - target_room: 55 + entrance: 127 + teleporter: [66, 0] + access: [] + - target_room: 62 + entrance: 128 + teleporter: [68, 0] + access: [] +- name: Ice Pyramid 3F Two Boxes Room + id: 61 + game_objects: + - name: "Ice Pyramid 3F - Staircase Dead End Left Box" + object_id: 0x5E + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Staircase Dead End Right Box" + object_id: 0x5F + type: "Box" + access: [] + links: + - target_room: 56 + entrance: 129 + teleporter: [69, 0] + access: [] +- name: Ice Pyramid 3F Main Loop + id: 62 + game_objects: + - name: "Ice Pyramid 3F - Inner Room North Box" + object_id: 0x5C + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Inner Room South Box" + object_id: 0x5D + type: "Box" + access: [] + - name: "Ice Pyramid 3F - East Alcove Box" + object_id: 0x60 + type: "Box" + access: [] + - name: "Ice Pyramid 3F - Leapfrog Box" + object_id: 0x61 + type: "Box" + access: [] + - name: "Ice Pyramid 3F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid3FStatue"] + access: ["Sword"] + links: + - target_room: 60 + entrance: 130 + teleporter: [70, 0] + access: [] + - target_room: 63 + access: ["IcePyramid3FStatue"] +- name: Ice Pyramid 3F Blocked Room + id: 63 + game_objects: [] + links: + - target_room: 64 + entrance: 131 + teleporter: [71, 0] + access: [] + - target_room: 62 + access: ["IcePyramid3FStatue"] +- name: Ice Pyramid 4F Main Loop + id: 64 + game_objects: [] + links: + - target_room: 66 + entrance: 133 + teleporter: [73, 0] + access: [] + - target_room: 63 + entrance: 132 + teleporter: [72, 0] + access: [] + - target_room: 65 + access: ["IcePyramid4FStatue"] +- name: Ice Pyramid 4F Treasure Room + id: 65 + game_objects: + - name: "Ice Pyramid 4F - Chest" + object_id: 0x0C + type: "Chest" + access: [] + - name: "Ice Pyramid 4F - Northwest Box" + object_id: 0x62 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - West Left Box" + object_id: 0x63 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - West Right Box" + object_id: 0x64 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - South Left Box" + object_id: 0x65 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - South Right Box" + object_id: 0x66 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - East Left Box" + object_id: 0x67 + type: "Box" + access: [] + - name: "Ice Pyramid 4F - East Right Box" + object_id: 0x68 + type: "Box" + access: [] + - name: "Ice Pyramid 4F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid4FStatue"] + access: ["Sword"] + links: + - target_room: 64 + access: ["IcePyramid4FStatue"] +- name: Ice Pyramid 5F Leap of Faith Room + id: 66 + game_objects: + - name: "Ice Pyramid 5F - Glass Door Left Box" + object_id: 0x69 + type: "Box" + access: ["IcePyramid5FStatue"] + - name: "Ice Pyramid 5F - West Ledge Box" + object_id: 0x6A + type: "Box" + access: [] + - name: "Ice Pyramid 5F - South Shelf Box" + object_id: 0x6B + type: "Box" + access: [] + - name: "Ice Pyramid 5F - South Leapfrog Box" + object_id: 0x6C + type: "Box" + access: [] + - name: "Ice Pyramid 5F - Glass Door Right Box" + object_id: 0x6D + type: "Box" + access: ["IcePyramid5FStatue"] + - name: "Ice Pyramid 5F - North Box" + object_id: 0x6E + type: "Box" + access: [] + links: + - target_room: 64 + entrance: 134 + teleporter: [74, 0] + access: [] + - target_room: 65 + access: [] + - target_room: 53 + access: ["Bomb", "Claw", "Sword"] +- name: Ice Pyramid 5F Stairs to Ice Golem + id: 67 + game_objects: + - name: "Ice Pyramid 5F Statue" + object_id: 0 + type: "Trigger" + on_trigger: ["IcePyramid5FStatue"] + access: ["Sword"] + links: + - target_room: 69 + entrance: 137 + teleporter: [76, 0] + access: [] + - target_room: 65 + access: [] + - target_room: 70 + entrance: 136 + teleporter: [75, 0] + access: [] +- name: Ice Pyramid Climbing Wall Room Lower Space + id: 68 + game_objects: [] + links: + - target_room: 53 + entrance: 139 + teleporter: [78, 0] + access: [] + - target_room: 69 + access: ["Claw"] +- name: Ice Pyramid Climbing Wall Room Upper Space + id: 69 + game_objects: [] + links: + - target_room: 67 + entrance: 140 + teleporter: [79, 0] + access: [] + - target_room: 68 + access: ["Claw"] +- name: Ice Pyramid Ice Golem Room + id: 70 + game_objects: + - name: "Ice Pyramid 6F - Ice Golem Chest" + object_id: 0x0E + type: "Chest" + access: ["IceGolem"] + - name: "Ice Golem" + object_id: 0 + type: "Trigger" + on_trigger: ["IceGolem"] + access: [] + links: + - target_room: 67 + entrance: 141 + teleporter: [80, 0] + access: [] + - target_room: 66 + access: [] +- name: Spencer Waterfall + id: 71 + game_objects: [] + links: + - target_room: 72 + entrance: 143 + teleporter: [81, 0] + access: [] + - target_room: 40 + entrance: 145 + teleporter: [82, 0] + access: [] + - target_room: 40 + entrance: 148 + teleporter: [83, 0] + access: [] +- name: Spencer Cave Normal Main + id: 72 + game_objects: + - name: "Spencer's Cave - Box" + object_id: 0x6F + type: "Box" + access: ["Claw"] + - name: "Spencer's Cave - Spencer" + object_id: 8 + type: "NPC" + access: [] + - name: "Spencer's Cave - Locked Chest" + object_id: 13 + type: "NPC" + access: ["VenusKey"] + links: + - target_room: 71 + entrance: 150 + teleporter: [85, 0] + access: [] +- name: Spencer Cave Normal South Ledge + id: 73 + game_objects: + - name: "Collapse Spencer's Cave" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipLiberated"] + access: ["MegaGrenade"] + links: + - target_room: 227 + entrance: 151 + teleporter: [7, 6] + access: [] + - target_room: 203 + access: ["MegaGrenade"] +# - target_room: 72 # access to spencer? +# access: ["MegaGrenade"] +- name: Spencer Cave Caved In Main Loop + id: 203 + game_objects: [] + links: + - target_room: 73 + access: [] + - target_room: 207 + entrance: 156 + teleporter: [36, 8] + access: ["MobiusCrest"] + - target_room: 204 + access: ["Claw"] + - target_room: 205 + access: ["Bomb"] +- name: Spencer Cave Caved In Waters + id: 204 + game_objects: + - name: "Bomb Libra Block" + object_id: 0 + type: "Trigger" + on_trigger: ["SpencerCaveLibraBlockBombed"] + access: ["MegaGrenade", "Claw"] + links: + - target_room: 203 + access: ["Claw"] +- name: Spencer Cave Caved In Libra Nook + id: 205 + game_objects: [] + links: + - target_room: 206 + entrance: 153 + teleporter: [33, 8] + access: ["LibraCrest"] +- name: Spencer Cave Caved In Libra Corridor + id: 206 + game_objects: [] + links: + - target_room: 205 + entrance: 154 + teleporter: [34, 8] + access: ["LibraCrest"] + - target_room: 207 + access: ["SpencerCaveLibraBlockBombed"] +- name: Spencer Cave Caved In Mobius Chest + id: 207 + game_objects: + - name: "Spencer's Cave - Mobius Chest" + object_id: 0x0F + type: "Chest" + access: [] + links: + - target_room: 203 + entrance: 155 + teleporter: [35, 8] + access: ["MobiusCrest"] + - target_room: 206 + access: ["Bomb"] +- name: Wintry Temple Outer Room + id: 74 + game_objects: [] + links: + - target_room: 223 + entrance: 157 + teleporter: [15, 6] + access: [] +- name: Wintry Temple Inner Room + id: 75 + game_objects: + - name: "Wintry Temple - West Box" + object_id: 0x70 + type: "Box" + access: [] + - name: "Wintry Temple - North Box" + object_id: 0x71 + type: "Box" + access: [] + links: + - target_room: 92 + entrance: 158 + teleporter: [62, 8] + access: ["GeminiCrest"] +- name: Fireburg Upper Plaza + id: 76 + game_objects: [] + links: + - target_room: 224 + entrance: 159 + teleporter: [9, 6] + access: [] + - target_room: 80 + entrance: 163 + teleporter: [91, 0] + access: [] + - target_room: 77 + entrance: 164 + teleporter: [16, 2] + access: [] + - target_room: 82 + entrance: 165 + teleporter: [17, 2] + access: [] + - target_room: 208 + access: ["Claw"] +- name: Fireburg Lower Plaza + id: 208 + game_objects: + - name: "Fireburg - Hidden Tunnel Box" + object_id: 0x74 + type: "Box" + access: [] + links: + - target_room: 76 + access: ["Claw"] + - target_room: 78 + entrance: 166 + teleporter: [11, 8] + access: ["MultiKey"] +- name: Reuben's House + id: 77 + game_objects: + - name: "Fireburg - Reuben's House Arion" + object_id: 14 + type: "NPC" + access: ["ReubenDadSaved"] + - name: "Reuben" + object_id: 0 + type: "Trigger" + on_trigger: ["Reuben1"] + access: [] + - name: "Fireburg - Reuben's House Box" + object_id: 0x75 + type: "Box" + access: [] + links: + - target_room: 76 + entrance: 167 + teleporter: [98, 3] + access: [] +- name: GrenadeMan's House + id: 78 + game_objects: + - name: "Fireburg - Locked House Man" + object_id: 12 + type: "NPC" + access: [] + links: + - target_room: 208 + entrance: 168 + teleporter: [9, 8] + access: ["MultiKey"] + - target_room: 79 + entrance: 169 + teleporter: [93, 0] + access: [] +- name: GrenadeMan's Mobius Room + id: 79 + game_objects: [] + links: + - target_room: 78 + entrance: 170 + teleporter: [94, 0] + access: [] + - target_room: 161 + entrance: 171 + teleporter: [54, 8] + access: ["MobiusCrest"] +- name: Fireburg Vendor House + id: 80 + game_objects: + - name: "Fireburg - Vendor" + object_id: 11 + type: "NPC" + access: [] + links: + - target_room: 76 + entrance: 172 + teleporter: [95, 0] + access: [] + - target_room: 81 + entrance: 173 + teleporter: [96, 0] + access: [] +- name: Fireburg Gemini Room + id: 81 + game_objects: [] + links: + - target_room: 80 + entrance: 174 + teleporter: [97, 0] + access: [] + - target_room: 43 + entrance: 175 + teleporter: [45, 8] + access: ["GeminiCrest"] +- name: Fireburg Hotel Lobby + id: 82 + game_objects: + - name: "Fireburg - Tristam" + object_id: 10 + type: "NPC" + access: [] + - name: "Tristam Fireburg" + object_id: 0 + type: "Trigger" + on_trigger: ["Tristam"] + access: [] + links: + - target_room: 76 + entrance: 177 + teleporter: [99, 3] + access: [] + - target_room: 83 + entrance: 176 + teleporter: [213, 0] + access: [] +- name: Fireburg Hotel Beds + id: 83 + game_objects: [] + links: + - target_room: 82 + entrance: 178 + teleporter: [214, 0] + access: [] +- name: Mine Exterior North West Platforms + id: 84 + game_objects: [] + links: + - target_room: 224 + entrance: 179 + teleporter: [98, 0] + access: [] + - target_room: 88 + entrance: 181 + teleporter: [20, 2] + access: ["Bomb"] + - target_room: 85 + access: ["Claw"] + - target_room: 86 + access: ["Claw"] + - target_room: 87 + access: ["Claw"] +- name: Mine Exterior Central Ledge + id: 85 + game_objects: [] + links: + - target_room: 90 + entrance: 183 + teleporter: [22, 2] + access: ["Bomb"] + - target_room: 84 + access: ["Claw"] +- name: Mine Exterior North Ledge + id: 86 + game_objects: [] + links: + - target_room: 89 + entrance: 182 + teleporter: [21, 2] + access: ["Bomb"] + - target_room: 85 + access: ["Claw"] +- name: Mine Exterior South East Platforms + id: 87 + game_objects: + - name: "Jinn" + object_id: 0 + type: "Trigger" + on_trigger: ["Jinn"] + access: [] + links: + - target_room: 91 + entrance: 180 + teleporter: [99, 0] + access: ["Jinn"] + - target_room: 86 + access: [] + - target_room: 85 + access: ["Claw"] +- name: Mine Parallel Room + id: 88 + game_objects: + - name: "Mine - Parallel Room West Box" + object_id: 0x77 + type: "Box" + access: ["Claw"] + - name: "Mine - Parallel Room East Box" + object_id: 0x78 + type: "Box" + access: ["Claw"] + links: + - target_room: 84 + entrance: 185 + teleporter: [100, 3] + access: [] +- name: Mine Crescent Room + id: 89 + game_objects: + - name: "Mine - Crescent Room Chest" + object_id: 0x10 + type: "Chest" + access: [] + links: + - target_room: 86 + entrance: 186 + teleporter: [101, 3] + access: [] +- name: Mine Climbing Room + id: 90 + game_objects: + - name: "Mine - Glitchy Collision Cave Box" + object_id: 0x76 + type: "Box" + access: ["Claw"] + links: + - target_room: 85 + entrance: 187 + teleporter: [102, 3] + access: [] +- name: Mine Cliff + id: 91 + game_objects: + - name: "Mine - Cliff Southwest Box" + object_id: 0x79 + type: "Box" + access: [] + - name: "Mine - Cliff Northwest Box" + object_id: 0x7A + type: "Box" + access: [] + - name: "Mine - Cliff Northeast Box" + object_id: 0x7B + type: "Box" + access: [] + - name: "Mine - Cliff Southeast Box" + object_id: 0x7C + type: "Box" + access: [] + - name: "Mine - Reuben" + object_id: 7 + type: "NPC" + access: ["Reuben1"] + - name: "Reuben's dad Saved" + object_id: 0 + type: "Trigger" + on_trigger: ["ReubenDadSaved"] + access: ["MegaGrenade"] + links: + - target_room: 87 + entrance: 188 + teleporter: [100, 0] + access: [] +- name: Sealed Temple + id: 92 + game_objects: + - name: "Sealed Temple - West Box" + object_id: 0x7D + type: "Box" + access: [] + - name: "Sealed Temple - East Box" + object_id: 0x7E + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 190 + teleporter: [16, 6] + access: [] + - target_room: 75 + entrance: 191 + teleporter: [63, 8] + access: ["GeminiCrest"] +- name: Volcano Base + id: 93 + game_objects: + - name: "Volcano - Base Chest" + object_id: 0x11 + type: "Chest" + access: [] + - name: "Volcano - Base West Box" + object_id: 0x7F + type: "Box" + access: [] + - name: "Volcano - Base East Left Box" + object_id: 0x80 + type: "Box" + access: [] + - name: "Volcano - Base East Right Box" + object_id: 0x81 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 192 + teleporter: [103, 0] + access: [] + - target_room: 98 + entrance: 196 + teleporter: [31, 8] + access: [] + - target_room: 96 + entrance: 197 + teleporter: [30, 8] + access: [] +- name: Volcano Top Left + id: 94 + game_objects: + - name: "Volcano - Medusa Chest" + object_id: 0x12 + type: "Chest" + access: ["Medusa"] + - name: "Medusa" + object_id: 0 + type: "Trigger" + on_trigger: ["Medusa"] + access: [] + - name: "Volcano - Behind Medusa Box" + object_id: 0x82 + type: "Box" + access: [] + links: + - target_room: 209 + entrance: 199 + teleporter: [26, 8] + access: [] +- name: Volcano Top Right + id: 95 + game_objects: + - name: "Volcano - Top of the Volcano Left Box" + object_id: 0x83 + type: "Box" + access: [] + - name: "Volcano - Top of the Volcano Right Box" + object_id: 0x84 + type: "Box" + access: [] + links: + - target_room: 99 + entrance: 200 + teleporter: [79, 8] + access: [] +- name: Volcano Right Path + id: 96 + game_objects: + - name: "Volcano - Right Path Box" + object_id: 0x87 + type: "Box" + access: [] + links: + - target_room: 93 + entrance: 201 + teleporter: [15, 8] + access: [] +- name: Volcano Left Path + id: 98 + game_objects: + - name: "Volcano - Left Path Box" + object_id: 0x86 + type: "Box" + access: [] + links: + - target_room: 93 + entrance: 204 + teleporter: [27, 8] + access: [] + - target_room: 99 + entrance: 202 + teleporter: [25, 2] + access: [] + - target_room: 209 + entrance: 203 + teleporter: [26, 2] + access: [] +- name: Volcano Cross Left-Right + id: 99 + game_objects: [] + links: + - target_room: 95 + entrance: 206 + teleporter: [29, 8] + access: [] + - target_room: 98 + entrance: 205 + teleporter: [103, 3] + access: [] +- name: Volcano Cross Right-Left + id: 209 + game_objects: + - name: "Volcano - Crossover Section Box" + object_id: 0x85 + type: "Box" + access: [] + links: + - target_room: 98 + entrance: 208 + teleporter: [104, 3] + access: [] + - target_room: 94 + entrance: 207 + teleporter: [28, 8] + access: [] +- name: Lava Dome Inner Ring Main Loop + id: 100 + game_objects: + - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" + object_id: 0x88 + type: "Box" + access: [] + - name: "Lava Dome - Exterior South Cliff Box" + object_id: 0x89 + type: "Box" + access: [] + links: + - target_room: 224 + entrance: 209 + teleporter: [104, 0] + access: [] + - target_room: 113 + entrance: 211 + teleporter: [105, 0] + access: [] + - target_room: 114 + entrance: 212 + teleporter: [106, 0] + access: [] + - target_room: 116 + entrance: 213 + teleporter: [108, 0] + access: [] + - target_room: 118 + entrance: 214 + teleporter: [111, 0] + access: [] +- name: Lava Dome Inner Ring Center Ledge + id: 101 + game_objects: + - name: "Lava Dome - Exterior Center Dropoff Ledge Box" + object_id: 0x8A + type: "Box" + access: [] + links: + - target_room: 115 + entrance: 215 + teleporter: [107, 0] + access: [] + - target_room: 100 + access: ["Claw"] +- name: Lava Dome Inner Ring Plate Ledge + id: 102 + game_objects: + - name: "Lava Dome Plate" + object_id: 0 + type: "Trigger" + on_trigger: ["LavaDomePlate"] + access: [] + links: + - target_room: 119 + entrance: 216 + teleporter: [109, 0] + access: [] +- name: Lava Dome Inner Ring Upper Ledge West + id: 103 + game_objects: [] + links: + - target_room: 111 + entrance: 219 + teleporter: [112, 0] + access: [] + - target_room: 108 + entrance: 220 + teleporter: [113, 0] + access: [] + - target_room: 104 + access: ["Claw"] + - target_room: 100 + access: ["Claw"] +- name: Lava Dome Inner Ring Upper Ledge East + id: 104 + game_objects: [] + links: + - target_room: 110 + entrance: 218 + teleporter: [110, 0] + access: [] + - target_room: 103 + access: ["Claw"] +- name: Lava Dome Inner Ring Big Door Ledge + id: 105 + game_objects: [] + links: + - target_room: 107 + entrance: 221 + teleporter: [114, 0] + access: [] + - target_room: 121 + entrance: 222 + teleporter: [29, 2] + access: ["LavaDomePlate"] +- name: Lava Dome Inner Ring Tiny Bottom Ledge + id: 106 + game_objects: + - name: "Lava Dome - Exterior Dead End Caldera Box" + object_id: 0x8B + type: "Box" + access: [] + links: + - target_room: 120 + entrance: 226 + teleporter: [115, 0] + access: [] +- name: Lava Dome Jump Maze II + id: 107 + game_objects: + - name: "Lava Dome - Gold Maze Northwest Box" + object_id: 0x8C + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Southwest Box" + object_id: 0xF6 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Northeast Box" + object_id: 0xF7 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze North Box" + object_id: 0xF8 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Center Box" + object_id: 0xF9 + type: "Box" + access: [] + - name: "Lava Dome - Gold Maze Southeast Box" + object_id: 0xFA + type: "Box" + access: [] + links: + - target_room: 105 + entrance: 227 + teleporter: [116, 0] + access: [] + - target_room: 108 + entrance: 228 + teleporter: [119, 0] + access: [] + - target_room: 120 + entrance: 229 + teleporter: [120, 0] + access: [] +- name: Lava Dome Up-Down Corridor + id: 108 + game_objects: [] + links: + - target_room: 107 + entrance: 231 + teleporter: [118, 0] + access: [] + - target_room: 103 + entrance: 230 + teleporter: [117, 0] + access: [] +- name: Lava Dome Jump Maze I + id: 109 + game_objects: + - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" + object_id: 0x8D + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" + object_id: 0x8E + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Center Box" + object_id: 0x8F + type: "Box" + access: [] + - name: "Lava Dome - Bare Maze Southwest Box" + object_id: 0x90 + type: "Box" + access: [] + links: + - target_room: 118 + entrance: 232 + teleporter: [121, 0] + access: [] + - target_room: 111 + entrance: 233 + teleporter: [122, 0] + access: [] +- name: Lava Dome Pointless Room + id: 110 + game_objects: [] + links: + - target_room: 104 + entrance: 234 + teleporter: [123, 0] + access: [] +- name: Lava Dome Lower Moon Helm Room + id: 111 + game_objects: + - name: "Lava Dome - U-Bend Room North Box" + object_id: 0x92 + type: "Box" + access: [] + - name: "Lava Dome - U-Bend Room South Box" + object_id: 0x93 + type: "Box" + access: [] + links: + - target_room: 103 + entrance: 235 + teleporter: [124, 0] + access: [] + - target_room: 109 + entrance: 236 + teleporter: [125, 0] + access: [] +- name: Lava Dome Moon Helm Room + id: 112 + game_objects: + - name: "Lava Dome - Beyond River Room Chest" + object_id: 0x13 + type: "Chest" + access: [] + - name: "Lava Dome - Beyond River Room Box" + object_id: 0x91 + type: "Box" + access: [] + links: + - target_room: 117 + entrance: 237 + teleporter: [126, 0] + access: [] +- name: Lava Dome Three Jumps Room + id: 113 + game_objects: + - name: "Lava Dome - Three Jumps Room Box" + object_id: 0x96 + type: "Box" + access: [] + links: + - target_room: 100 + entrance: 238 + teleporter: [127, 0] + access: [] +- name: Lava Dome Life Chest Room Lower Ledge + id: 114 + game_objects: + - name: "Lava Dome - Gold Bar Room Boulder Chest" + object_id: 0x1C + type: "Chest" + access: ["MegaGrenade"] + links: + - target_room: 100 + entrance: 239 + teleporter: [128, 0] + access: [] + - target_room: 115 + access: ["Claw"] +- name: Lava Dome Life Chest Room Upper Ledge + id: 115 + game_objects: + - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" + object_id: 0x94 + type: "Box" + access: [] + - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" + object_id: 0x95 + type: "Box" + access: [] + links: + - target_room: 101 + entrance: 240 + teleporter: [129, 0] + access: [] + - target_room: 114 + access: ["Claw"] +- name: Lava Dome Big Jump Room Main Area + id: 116 + game_objects: + - name: "Lava Dome - Lava River Room North Box" + object_id: 0x98 + type: "Box" + access: [] + - name: "Lava Dome - Lava River Room East Box" + object_id: 0x99 + type: "Box" + access: [] + - name: "Lava Dome - Lava River Room South Box" + object_id: 0x9A + type: "Box" + access: [] + links: + - target_room: 100 + entrance: 241 + teleporter: [133, 0] + access: [] + - target_room: 119 + entrance: 243 + teleporter: [132, 0] + access: [] + - target_room: 117 + access: ["MegaGrenade"] +- name: Lava Dome Big Jump Room MegaGrenade Area + id: 117 + game_objects: [] + links: + - target_room: 112 + entrance: 242 + teleporter: [131, 0] + access: [] + - target_room: 116 + access: ["Bomb"] +- name: Lava Dome Split Corridor + id: 118 + game_objects: + - name: "Lava Dome - Split Corridor Box" + object_id: 0x97 + type: "Box" + access: [] + links: + - target_room: 109 + entrance: 244 + teleporter: [130, 0] + access: [] + - target_room: 100 + entrance: 245 + teleporter: [134, 0] + access: [] +- name: Lava Dome Plate Corridor + id: 119 + game_objects: [] + links: + - target_room: 102 + entrance: 246 + teleporter: [135, 0] + access: [] + - target_room: 116 + entrance: 247 + teleporter: [137, 0] + access: [] +- name: Lava Dome Four Boxes Stairs + id: 120 + game_objects: + - name: "Lava Dome - Caldera Stairway West Left Box" + object_id: 0x9B + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway West Right Box" + object_id: 0x9C + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway East Left Box" + object_id: 0x9D + type: "Box" + access: [] + - name: "Lava Dome - Caldera Stairway East Right Box" + object_id: 0x9E + type: "Box" + access: [] + links: + - target_room: 107 + entrance: 248 + teleporter: [136, 0] + access: [] + - target_room: 106 + entrance: 249 + teleporter: [16, 0] + access: [] +- name: Lava Dome Hydra Room + id: 121 + game_objects: + - name: "Lava Dome - Dualhead Hydra Chest" + object_id: 0x14 + type: "Chest" + access: ["DualheadHydra"] + - name: "Dualhead Hydra" + object_id: 0 + type: "Trigger" + on_trigger: ["DualheadHydra"] + access: [] + - name: "Lava Dome - Hydra Room Northwest Box" + object_id: 0x9F + type: "Box" + access: [] + - name: "Lava Dome - Hydra Room Southweast Box" + object_id: 0xA0 + type: "Box" + access: [] + links: + - target_room: 105 + entrance: 250 + teleporter: [105, 3] + access: [] + - target_room: 122 + entrance: 251 + teleporter: [138, 0] + access: ["DualheadHydra"] +- name: Lava Dome Escape Corridor + id: 122 + game_objects: [] + links: + - target_room: 121 + entrance: 253 + teleporter: [139, 0] + access: [] +- name: Rope Bridge + id: 123 + game_objects: + - name: "Rope Bridge - West Box" + object_id: 0xA3 + type: "Box" + access: [] + - name: "Rope Bridge - East Box" + object_id: 0xA4 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 255 + teleporter: [140, 0] + access: [] +- name: Alive Forest + id: 124 + game_objects: + - name: "Alive Forest - Tree Stump Chest" + object_id: 0x15 + type: "Chest" + access: ["Axe"] + - name: "Alive Forest - Near Entrance Box" + object_id: 0xA5 + type: "Box" + access: ["Axe"] + - name: "Alive Forest - After Bridge Box" + object_id: 0xA6 + type: "Box" + access: ["Axe"] + - name: "Alive Forest - Gemini Stump Box" + object_id: 0xA7 + type: "Box" + access: ["Axe"] + links: + - target_room: 226 + entrance: 272 + teleporter: [142, 0] + access: ["Axe"] + - target_room: 21 + entrance: 275 + teleporter: [64, 8] + access: ["LibraCrest", "Axe"] + - target_room: 22 + entrance: 276 + teleporter: [65, 8] + access: ["GeminiCrest", "Axe"] + - target_room: 23 + entrance: 277 + teleporter: [66, 8] + access: ["MobiusCrest", "Axe"] + - target_room: 125 + entrance: 274 + teleporter: [143, 0] + access: ["Axe"] +- name: Giant Tree 1F Main Area + id: 125 + game_objects: + - name: "Giant Tree 1F - Northwest Box" + object_id: 0xA8 + type: "Box" + access: [] + - name: "Giant Tree 1F - Southwest Box" + object_id: 0xA9 + type: "Box" + access: [] + - name: "Giant Tree 1F - Center Box" + object_id: 0xAA + type: "Box" + access: [] + - name: "Giant Tree 1F - East Box" + object_id: 0xAB + type: "Box" + access: [] + links: + - target_room: 124 + entrance: 278 + teleporter: [56, 1] # [49, 8] script restored if no map shuffling + access: [] + - target_room: 202 + access: ["DragonClaw"] +- name: Giant Tree 1F North Island + id: 202 + game_objects: [] + links: + - target_room: 127 + entrance: 280 + teleporter: [144, 0] + access: [] + - target_room: 125 + access: ["DragonClaw"] +- name: Giant Tree 1F Central Island + id: 126 + game_objects: [] + links: + - target_room: 202 + access: ["DragonClaw"] +- name: Giant Tree 2F Main Lobby + id: 127 + game_objects: + - name: "Giant Tree 2F - North Box" + object_id: 0xAC + type: "Box" + access: [] + links: + - target_room: 126 + access: ["DragonClaw"] + - target_room: 125 + entrance: 281 + teleporter: [145, 0] + access: [] + - target_room: 133 + entrance: 283 + teleporter: [149, 0] + access: [] + - target_room: 129 + access: ["DragonClaw"] +- name: Giant Tree 2F West Ledge + id: 128 + game_objects: + - name: "Giant Tree 2F - Dropdown Ledge Box" + object_id: 0xAE + type: "Box" + access: [] + links: + - target_room: 140 + entrance: 284 + teleporter: [147, 0] + access: ["Sword"] + - target_room: 130 + access: ["DragonClaw"] +- name: Giant Tree 2F Lower Area + id: 129 + game_objects: + - name: "Giant Tree 2F - South Box" + object_id: 0xAD + type: "Box" + access: [] + links: + - target_room: 130 + access: ["Claw"] + - target_room: 131 + access: ["Claw"] +- name: Giant Tree 2F Central Island + id: 130 + game_objects: [] + links: + - target_room: 129 + access: ["Claw"] + - target_room: 135 + entrance: 282 + teleporter: [146, 0] + access: ["Sword"] +- name: Giant Tree 2F East Ledge + id: 131 + game_objects: [] + links: + - target_room: 129 + access: ["Claw"] + - target_room: 130 + access: ["DragonClaw"] +- name: Giant Tree 2F Meteor Chest Room + id: 132 + game_objects: + - name: "Giant Tree 2F - Gidrah Chest" + object_id: 0x16 + type: "Chest" + access: [] + links: + - target_room: 133 + entrance: 285 + teleporter: [148, 0] + access: [] +- name: Giant Tree 2F Mushroom Room + id: 133 + game_objects: + - name: "Giant Tree 2F - Mushroom Tunnel West Box" + object_id: 0xAF + type: "Box" + access: ["Axe"] + - name: "Giant Tree 2F - Mushroom Tunnel East Box" + object_id: 0xB0 + type: "Box" + access: ["Axe"] + links: + - target_room: 127 + entrance: 286 + teleporter: [150, 0] + access: ["Axe"] + - target_room: 132 + entrance: 287 + teleporter: [151, 0] + access: ["Axe", "Gidrah"] +- name: Giant Tree 3F Central Island + id: 135 + game_objects: + - name: "Giant Tree 3F - Central Island Box" + object_id: 0xB3 + type: "Box" + access: [] + links: + - target_room: 130 + entrance: 288 + teleporter: [152, 0] + access: [] + - target_room: 136 + access: ["Claw"] + - target_room: 137 + access: ["DragonClaw"] +- name: Giant Tree 3F Central Area + id: 136 + game_objects: + - name: "Giant Tree 3F - Center North Box" + object_id: 0xB1 + type: "Box" + access: [] + - name: "Giant Tree 3F - Center West Box" + object_id: 0xB2 + type: "Box" + access: [] + links: + - target_room: 135 + access: ["Claw"] + - target_room: 127 + access: [] + - target_room: 131 + access: [] +- name: Giant Tree 3F Lower Ledge + id: 137 + game_objects: [] + links: + - target_room: 135 + access: ["DragonClaw"] + - target_room: 142 + entrance: 289 + teleporter: [153, 0] + access: ["Sword"] +- name: Giant Tree 3F West Area + id: 138 + game_objects: + - name: "Giant Tree 3F - West Side Box" + object_id: 0xB4 + type: "Box" + access: [] + links: + - target_room: 128 + access: [] + - target_room: 210 + entrance: 290 + teleporter: [154, 0] + access: [] +- name: Giant Tree 3F Middle Up Island + id: 139 + game_objects: [] + links: + - target_room: 136 + access: ["Claw"] +- name: Giant Tree 3F West Platform + id: 140 + game_objects: [] + links: + - target_room: 139 + access: ["Claw"] + - target_room: 141 + access: ["Claw"] + - target_room: 128 + entrance: 291 + teleporter: [155, 0] + access: [] +- name: Giant Tree 3F North Ledge + id: 141 + game_objects: [] + links: + - target_room: 143 + entrance: 292 + teleporter: [156, 0] + access: ["Sword"] + - target_room: 139 + access: ["Claw"] + - target_room: 136 + access: ["Claw"] +- name: Giant Tree Worm Room Upper Ledge + id: 142 + game_objects: + - name: "Giant Tree 3F - Worm Room North Box" + object_id: 0xB5 + type: "Box" + access: ["Axe"] + - name: "Giant Tree 3F - Worm Room South Box" + object_id: 0xB6 + type: "Box" + access: ["Axe"] + links: + - target_room: 137 + entrance: 293 + teleporter: [157, 0] + access: ["Axe"] + - target_room: 210 + access: ["Axe", "Claw"] +- name: Giant Tree Worm Room Lower Ledge + id: 210 + game_objects: [] + links: + - target_room: 138 + entrance: 294 + teleporter: [158, 0] + access: [] +- name: Giant Tree 4F Lower Floor + id: 143 + game_objects: [] + links: + - target_room: 141 + entrance: 295 + teleporter: [159, 0] + access: [] + - target_room: 148 + entrance: 296 + teleporter: [160, 0] + access: [] + - target_room: 148 + entrance: 297 + teleporter: [161, 0] + access: [] + - target_room: 147 + entrance: 298 + teleporter: [162, 0] + access: ["Sword"] +- name: Giant Tree 4F Middle Floor + id: 144 + game_objects: + - name: "Giant Tree 4F - Highest Platform North Box" + object_id: 0xB7 + type: "Box" + access: [] + - name: "Giant Tree 4F - Highest Platform South Box" + object_id: 0xB8 + type: "Box" + access: [] + links: + - target_room: 149 + entrance: 299 + teleporter: [163, 0] + access: [] + - target_room: 145 + access: ["Claw"] + - target_room: 146 + access: ["DragonClaw"] +- name: Giant Tree 4F Upper Floor + id: 145 + game_objects: [] + links: + - target_room: 150 + entrance: 300 + teleporter: [164, 0] + access: ["Sword"] + - target_room: 144 + access: ["Claw"] +- name: Giant Tree 4F South Ledge + id: 146 + game_objects: + - name: "Giant Tree 4F - Hook Ledge Northeast Box" + object_id: 0xB9 + type: "Box" + access: [] + - name: "Giant Tree 4F - Hook Ledge Southwest Box" + object_id: 0xBA + type: "Box" + access: [] + links: + - target_room: 144 + access: ["DragonClaw"] +- name: Giant Tree 4F Slime Room East Area + id: 147 + game_objects: + - name: "Giant Tree 4F - East Slime Room Box" + object_id: 0xBC + type: "Box" + access: ["Axe"] + links: + - target_room: 143 + entrance: 304 + teleporter: [168, 0] + access: [] +- name: Giant Tree 4F Slime Room West Area + id: 148 + game_objects: [] + links: + - target_room: 143 + entrance: 303 + teleporter: [167, 0] + access: ["Axe"] + - target_room: 143 + entrance: 302 + teleporter: [166, 0] + access: ["Axe"] + - target_room: 149 + access: ["Axe", "Claw"] +- name: Giant Tree 4F Slime Room Platform + id: 149 + game_objects: + - name: "Giant Tree 4F - West Slime Room Box" + object_id: 0xBB + type: "Box" + access: [] + links: + - target_room: 144 + entrance: 301 + teleporter: [165, 0] + access: [] + - target_room: 148 + access: ["Claw"] +- name: Giant Tree 5F Lower Area + id: 150 + game_objects: + - name: "Giant Tree 5F - Northwest Left Box" + object_id: 0xBD + type: "Box" + access: [] + - name: "Giant Tree 5F - Northwest Right Box" + object_id: 0xBE + type: "Box" + access: [] + - name: "Giant Tree 5F - South Left Box" + object_id: 0xBF + type: "Box" + access: [] + - name: "Giant Tree 5F - South Right Box" + object_id: 0xC0 + type: "Box" + access: [] + links: + - target_room: 145 + entrance: 305 + teleporter: [169, 0] + access: [] + - target_room: 151 + access: ["Claw"] + - target_room: 143 + access: [] +- name: Giant Tree 5F Gidrah Platform + id: 151 + game_objects: + - name: "Gidrah" + object_id: 0 + type: "Trigger" + on_trigger: ["Gidrah"] + access: [] + links: + - target_room: 150 + access: ["Claw"] +- name: Kaidge Temple Lower Ledge + id: 152 + game_objects: [] + links: + - target_room: 226 + entrance: 307 + teleporter: [18, 6] + access: [] + - target_room: 153 + access: ["Claw"] +- name: Kaidge Temple Upper Ledge + id: 153 + game_objects: + - name: "Kaidge Temple - Box" + object_id: 0xC1 + type: "Box" + access: [] + links: + - target_room: 185 + entrance: 308 + teleporter: [71, 8] + access: ["MobiusCrest"] + - target_room: 152 + access: ["Claw"] +- name: Windhole Temple + id: 154 + game_objects: + - name: "Windhole Temple - Box" + object_id: 0xC2 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 309 + teleporter: [173, 0] + access: [] +- name: Mount Gale + id: 155 + game_objects: + - name: "Mount Gale - Dullahan Chest" + object_id: 0x17 + type: "Chest" + access: ["DragonClaw", "Dullahan"] + - name: "Dullahan" + object_id: 0 + type: "Trigger" + on_trigger: ["Dullahan"] + access: ["DragonClaw"] + - name: "Mount Gale - East Box" + object_id: 0xC3 + type: "Box" + access: ["DragonClaw"] + - name: "Mount Gale - West Box" + object_id: 0xC4 + type: "Box" + access: [] + links: + - target_room: 226 + entrance: 310 + teleporter: [174, 0] + access: [] +- name: Windia + id: 156 + game_objects: [] + links: + - target_room: 226 + entrance: 312 + teleporter: [10, 6] + access: [] + - target_room: 157 + entrance: 320 + teleporter: [30, 5] + access: [] + - target_room: 163 + entrance: 321 + teleporter: [31, 2] + access: [] + - target_room: 165 + entrance: 322 + teleporter: [32, 5] + access: [] + - target_room: 159 + entrance: 323 + teleporter: [176, 4] + access: [] + - target_room: 160 + entrance: 324 + teleporter: [177, 4] + access: [] +- name: Otto's House + id: 157 + game_objects: + - name: "Otto" + object_id: 0 + type: "Trigger" + on_trigger: ["RainbowBridge"] + access: ["ThunderRock"] + links: + - target_room: 156 + entrance: 327 + teleporter: [106, 3] + access: [] + - target_room: 158 + entrance: 326 + teleporter: [33, 2] + access: [] +- name: Otto's Attic + id: 158 + game_objects: + - name: "Windia - Otto's Attic Box" + object_id: 0xC5 + type: "Box" + access: [] + links: + - target_room: 157 + entrance: 328 + teleporter: [107, 3] + access: [] +- name: Windia Kid House + id: 159 + game_objects: [] + links: + - target_room: 156 + entrance: 329 + teleporter: [178, 0] + access: [] + - target_room: 161 + entrance: 330 + teleporter: [180, 0] + access: [] +- name: Windia Old People House + id: 160 + game_objects: [] + links: + - target_room: 156 + entrance: 331 + teleporter: [179, 0] + access: [] + - target_room: 162 + entrance: 332 + teleporter: [181, 0] + access: [] +- name: Windia Kid House Basement + id: 161 + game_objects: [] + links: + - target_room: 159 + entrance: 333 + teleporter: [182, 0] + access: [] + - target_room: 79 + entrance: 334 + teleporter: [44, 8] + access: ["MobiusCrest"] +- name: Windia Old People House Basement + id: 162 + game_objects: + - name: "Windia - Mobius Basement West Box" + object_id: 0xC8 + type: "Box" + access: [] + - name: "Windia - Mobius Basement East Box" + object_id: 0xC9 + type: "Box" + access: [] + links: + - target_room: 160 + entrance: 335 + teleporter: [183, 0] + access: [] + - target_room: 186 + entrance: 336 + teleporter: [43, 8] + access: ["MobiusCrest"] +- name: Windia Inn Lobby + id: 163 + game_objects: [] + links: + - target_room: 156 + entrance: 338 + teleporter: [135, 3] + access: [] + - target_room: 164 + entrance: 337 + teleporter: [215, 0] + access: [] +- name: Windia Inn Beds + id: 164 + game_objects: + - name: "Windia - Inn Bedroom North Box" + object_id: 0xC6 + type: "Box" + access: [] + - name: "Windia - Inn Bedroom South Box" + object_id: 0xC7 + type: "Box" + access: [] + - name: "Windia - Kaeli" + object_id: 15 + type: "NPC" + access: ["Kaeli2"] + links: + - target_room: 163 + entrance: 339 + teleporter: [216, 0] + access: [] +- name: Windia Vendor House + id: 165 + game_objects: + - name: "Windia - Vendor" + object_id: 16 + type: "NPC" + access: [] + links: + - target_room: 156 + entrance: 340 + teleporter: [108, 3] + access: [] +- name: Pazuzu Tower 1F Main Lobby + id: 166 + game_objects: + - name: "Pazuzu 1F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu1F"] + access: [] + links: + - target_room: 226 + entrance: 341 + teleporter: [184, 0] + access: [] + - target_room: 180 + entrance: 345 + teleporter: [185, 0] + access: [] +- name: Pazuzu Tower 1F Boxes Room + id: 167 + game_objects: + - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" + object_id: 0xCA + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" + object_id: 0xCB + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" + object_id: 0xCC + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 1F - Descent Box" + object_id: 0xCD + type: "Box" + access: [] + links: + - target_room: 169 + entrance: 349 + teleporter: [187, 0] + access: [] +- name: Pazuzu Tower 1F Southern Platform + id: 168 + game_objects: [] + links: + - target_room: 169 + entrance: 346 + teleporter: [186, 0] + access: [] + - target_room: 166 + access: ["DragonClaw"] +- name: Pazuzu 2F + id: 169 + game_objects: + - name: "Pazuzu's Tower 2F - East Room West Box" + object_id: 0xCE + type: "Box" + access: [] + - name: "Pazuzu's Tower 2F - East Room East Box" + object_id: 0xCF + type: "Box" + access: [] + - name: "Pazuzu 2F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu2FLock"] + access: ["Axe"] + - name: "Pazuzu 2F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu2F"] + access: ["Bomb"] + links: + - target_room: 183 + entrance: 350 + teleporter: [188, 0] + access: [] + - target_room: 168 + entrance: 351 + teleporter: [189, 0] + access: [] + - target_room: 167 + entrance: 352 + teleporter: [190, 0] + access: [] + - target_room: 171 + entrance: 353 + teleporter: [191, 0] + access: [] +- name: Pazuzu 3F Main Room + id: 170 + game_objects: + - name: "Pazuzu's Tower 3F - Guest Room West Box" + object_id: 0xD0 + type: "Box" + access: [] + - name: "Pazuzu's Tower 3F - Guest Room East Box" + object_id: 0xD1 + type: "Box" + access: [] + - name: "Pazuzu 3F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu3F"] + access: [] + links: + - target_room: 180 + entrance: 356 + teleporter: [192, 0] + access: [] + - target_room: 181 + entrance: 357 + teleporter: [193, 0] + access: [] +- name: Pazuzu 3F Central Island + id: 171 + game_objects: [] + links: + - target_room: 169 + entrance: 360 + teleporter: [194, 0] + access: [] + - target_room: 170 + access: ["DragonClaw"] + - target_room: 172 + access: ["DragonClaw"] +- name: Pazuzu 3F Southern Island + id: 172 + game_objects: + - name: "Pazuzu's Tower 3F - South Ledge Box" + object_id: 0xD2 + type: "Box" + access: [] + links: + - target_room: 173 + entrance: 361 + teleporter: [195, 0] + access: [] + - target_room: 171 + access: ["DragonClaw"] +- name: Pazuzu 4F + id: 173 + game_objects: + - name: "Pazuzu's Tower 4F - Elevator West Box" + object_id: 0xD3 + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 4F - Elevator East Box" + object_id: 0xD4 + type: "Box" + access: ["Bomb"] + - name: "Pazuzu's Tower 4F - East Storage Room Chest" + object_id: 0x18 + type: "Chest" + access: [] + - name: "Pazuzu 4F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu4FLock"] + access: ["Axe"] + - name: "Pazuzu 4F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu4F"] + access: ["Bomb"] + links: + - target_room: 183 + entrance: 362 + teleporter: [196, 0] + access: [] + - target_room: 184 + entrance: 363 + teleporter: [197, 0] + access: [] + - target_room: 172 + entrance: 364 + teleporter: [198, 0] + access: [] + - target_room: 175 + entrance: 365 + teleporter: [199, 0] + access: [] +- name: Pazuzu 5F Pazuzu Loop + id: 174 + game_objects: + - name: "Pazuzu 5F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu5F"] + access: [] + links: + - target_room: 181 + entrance: 368 + teleporter: [200, 0] + access: [] + - target_room: 182 + entrance: 369 + teleporter: [201, 0] + access: [] +- name: Pazuzu 5F Upper Loop + id: 175 + game_objects: + - name: "Pazuzu's Tower 5F - North Box" + object_id: 0xD5 + type: "Box" + access: [] + - name: "Pazuzu's Tower 5F - South Box" + object_id: 0xD6 + type: "Box" + access: [] + links: + - target_room: 173 + entrance: 370 + teleporter: [202, 0] + access: [] + - target_room: 176 + entrance: 371 + teleporter: [203, 0] + access: [] +- name: Pazuzu 6F + id: 176 + game_objects: + - name: "Pazuzu's Tower 6F - Box" + object_id: 0xD7 + type: "Box" + access: [] + - name: "Pazuzu's Tower 6F - Chest" + object_id: 0x19 + type: "Chest" + access: [] + - name: "Pazuzu 6F Lock" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu6FLock"] + access: ["Bomb", "Axe"] + - name: "Pazuzu 6F" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu6F"] + access: ["Bomb"] + links: + - target_room: 184 + entrance: 374 + teleporter: [204, 0] + access: [] + - target_room: 175 + entrance: 375 + teleporter: [205, 0] + access: [] + - target_room: 178 + entrance: 376 + teleporter: [206, 0] + access: [] + - target_room: 178 + entrance: 377 + teleporter: [207, 0] + access: [] +- name: Pazuzu 7F Southwest Area + id: 177 + game_objects: [] + links: + - target_room: 182 + entrance: 380 + teleporter: [26, 0] + access: [] + - target_room: 178 + access: ["DragonClaw"] +- name: Pazuzu 7F Rest of the Area + id: 178 + game_objects: [] + links: + - target_room: 177 + access: ["DragonClaw"] + - target_room: 176 + entrance: 381 + teleporter: [27, 0] + access: [] + - target_room: 176 + entrance: 382 + teleporter: [28, 0] + access: [] + - target_room: 179 + access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] +- name: Pazuzu 7F Sky Room + id: 179 + game_objects: + - name: "Pazuzu's Tower 7F - Pazuzu Chest" + object_id: 0x1A + type: "Chest" + access: [] + - name: "Pazuzu" + object_id: 0 + type: "Trigger" + on_trigger: ["Pazuzu"] + access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] + links: + - target_room: 178 + access: ["DragonClaw"] +- name: Pazuzu 1F to 3F + id: 180 + game_objects: [] + links: + - target_room: 166 + entrance: 385 + teleporter: [29, 0] + access: [] + - target_room: 170 + entrance: 386 + teleporter: [30, 0] + access: [] +- name: Pazuzu 3F to 5F + id: 181 + game_objects: [] + links: + - target_room: 170 + entrance: 387 + teleporter: [40, 0] + access: [] + - target_room: 174 + entrance: 388 + teleporter: [41, 0] + access: [] +- name: Pazuzu 5F to 7F + id: 182 + game_objects: [] + links: + - target_room: 174 + entrance: 389 + teleporter: [38, 0] + access: [] + - target_room: 177 + entrance: 390 + teleporter: [39, 0] + access: [] +- name: Pazuzu 2F to 4F + id: 183 + game_objects: [] + links: + - target_room: 169 + entrance: 391 + teleporter: [21, 0] + access: [] + - target_room: 173 + entrance: 392 + teleporter: [22, 0] + access: [] +- name: Pazuzu 4F to 6F + id: 184 + game_objects: [] + links: + - target_room: 173 + entrance: 393 + teleporter: [2, 0] + access: [] + - target_room: 176 + entrance: 394 + teleporter: [3, 0] + access: [] +- name: Light Temple + id: 185 + game_objects: + - name: "Light Temple - Box" + object_id: 0xD8 + type: "Box" + access: [] + links: + - target_room: 230 + entrance: 395 + teleporter: [19, 6] + access: [] + - target_room: 153 + entrance: 396 + teleporter: [70, 8] + access: ["MobiusCrest"] +- name: Ship Dock + id: 186 + game_objects: + - name: "Ship Dock Access" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipDockAccess"] + access: [] + links: + - target_room: 228 + entrance: 399 + teleporter: [17, 6] + access: [] + - target_room: 162 + entrance: 397 + teleporter: [61, 8] + access: ["MobiusCrest"] +- name: Mac Ship Deck + id: 187 + game_objects: + - name: "Mac Ship Steering Wheel" + object_id: 00 + type: "Trigger" + on_trigger: ["ShipSteeringWheel"] + access: [] + - name: "Mac's Ship Deck - North Box" + object_id: 0xD9 + type: "Box" + access: [] + - name: "Mac's Ship Deck - Center Box" + object_id: 0xDA + type: "Box" + access: [] + - name: "Mac's Ship Deck - South Box" + object_id: 0xDB + type: "Box" + access: [] + links: + - target_room: 229 + entrance: 400 + teleporter: [37, 8] + access: [] + - target_room: 188 + entrance: 401 + teleporter: [50, 8] + access: [] + - target_room: 188 + entrance: 402 + teleporter: [51, 8] + access: [] + - target_room: 188 + entrance: 403 + teleporter: [52, 8] + access: [] + - target_room: 189 + entrance: 404 + teleporter: [53, 8] + access: [] +- name: Mac Ship B1 Outer Ring + id: 188 + game_objects: + - name: "Mac's Ship B1 - Northwest Hook Platform Box" + object_id: 0xE4 + type: "Box" + access: ["DragonClaw"] + - name: "Mac's Ship B1 - Center Hook Platform Box" + object_id: 0xE5 + type: "Box" + access: ["DragonClaw"] + links: + - target_room: 187 + entrance: 405 + teleporter: [208, 0] + access: [] + - target_room: 187 + entrance: 406 + teleporter: [175, 0] + access: [] + - target_room: 187 + entrance: 407 + teleporter: [172, 0] + access: [] + - target_room: 193 + entrance: 408 + teleporter: [88, 0] + access: [] + - target_room: 193 + access: [] +- name: Mac Ship B1 Square Room + id: 189 + game_objects: [] + links: + - target_room: 187 + entrance: 409 + teleporter: [141, 0] + access: [] + - target_room: 192 + entrance: 410 + teleporter: [87, 0] + access: [] +- name: Mac Ship B1 Central Corridor + id: 190 + game_objects: + - name: "Mac's Ship B1 - Central Corridor Box" + object_id: 0xE6 + type: "Box" + access: [] + links: + - target_room: 192 + entrance: 413 + teleporter: [86, 0] + access: [] + - target_room: 191 + entrance: 412 + teleporter: [102, 0] + access: [] + - target_room: 193 + access: [] +- name: Mac Ship B2 South Corridor + id: 191 + game_objects: [] + links: + - target_room: 190 + entrance: 415 + teleporter: [55, 8] + access: [] + - target_room: 194 + entrance: 414 + teleporter: [57, 1] + access: [] +- name: Mac Ship B2 North Corridor + id: 192 + game_objects: [] + links: + - target_room: 190 + entrance: 416 + teleporter: [56, 8] + access: [] + - target_room: 189 + entrance: 417 + teleporter: [57, 8] + access: [] +- name: Mac Ship B2 Outer Ring + id: 193 + game_objects: + - name: "Mac's Ship B2 - Barrel Room South Box" + object_id: 0xDF + type: "Box" + access: [] + - name: "Mac's Ship B2 - Barrel Room North Box" + object_id: 0xE0 + type: "Box" + access: [] + - name: "Mac's Ship B2 - Southwest Room Box" + object_id: 0xE1 + type: "Box" + access: [] + - name: "Mac's Ship B2 - Southeast Room Box" + object_id: 0xE2 + type: "Box" + access: [] + links: + - target_room: 188 + entrance: 418 + teleporter: [58, 8] + access: [] +- name: Mac Ship B1 Mac Room + id: 194 + game_objects: + - name: "Mac's Ship B1 - Mac Room Chest" + object_id: 0x1B + type: "Chest" + access: [] + - name: "Captain Mac" + object_id: 0 + type: "Trigger" + on_trigger: ["ShipLoaned"] + access: ["CaptainCap"] + links: + - target_room: 191 + entrance: 424 + teleporter: [101, 0] + access: [] +- name: Doom Castle Corridor of Destiny + id: 195 + game_objects: [] + links: + - target_room: 201 + entrance: 428 + teleporter: [84, 0] + access: [] + - target_room: 196 + entrance: 429 + teleporter: [35, 2] + access: [] + - target_room: 197 + entrance: 430 + teleporter: [209, 0] + access: ["StoneGolem"] + - target_room: 198 + entrance: 431 + teleporter: [211, 0] + access: ["StoneGolem", "TwinheadWyvern"] + - target_room: 199 + entrance: 432 + teleporter: [13, 2] + access: ["StoneGolem", "TwinheadWyvern", "Zuh"] +- name: Doom Castle Ice Floor + id: 196 + game_objects: + - name: "Doom Castle 4F - Northwest Room Box" + object_id: 0xE7 + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Doom Castle 4F - Southwest Room Box" + object_id: 0xE8 + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Doom Castle 4F - Northeast Room Box" + object_id: 0xE9 + type: "Box" + access: ["Sword"] + - name: "Doom Castle 4F - Southeast Room Box" + object_id: 0xEA + type: "Box" + access: ["Sword", "DragonClaw"] + - name: "Stone Golem" + object_id: 0 + type: "Trigger" + on_trigger: ["StoneGolem"] + access: ["Sword", "DragonClaw"] + links: + - target_room: 195 + entrance: 433 + teleporter: [109, 3] + access: [] +- name: Doom Castle Lava Floor + id: 197 + game_objects: + - name: "Doom Castle 5F - North Left Box" + object_id: 0xEB + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - North Right Box" + object_id: 0xEC + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - South Left Box" + object_id: 0xED + type: "Box" + access: ["DragonClaw"] + - name: "Doom Castle 5F - South Right Box" + object_id: 0xEE + type: "Box" + access: ["DragonClaw"] + - name: "Twinhead Wyvern" + object_id: 0 + type: "Trigger" + on_trigger: ["TwinheadWyvern"] + access: ["DragonClaw"] + links: + - target_room: 195 + entrance: 434 + teleporter: [210, 0] + access: [] +- name: Doom Castle Sky Floor + id: 198 + game_objects: + - name: "Doom Castle 6F - West Box" + object_id: 0xEF + type: "Box" + access: [] + - name: "Doom Castle 6F - East Box" + object_id: 0xF0 + type: "Box" + access: [] + - name: "Zuh" + object_id: 0 + type: "Trigger" + on_trigger: ["Zuh"] + access: ["DragonClaw"] + links: + - target_room: 195 + entrance: 435 + teleporter: [212, 0] + access: [] + - target_room: 197 + access: [] +- name: Doom Castle Hero Room + id: 199 + game_objects: + - name: "Doom Castle Hero Chest 01" + object_id: 0xF2 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 02" + object_id: 0xF3 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 03" + object_id: 0xF4 + type: "Chest" + access: [] + - name: "Doom Castle Hero Chest 04" + object_id: 0xF5 + type: "Chest" + access: [] + links: + - target_room: 200 + entrance: 436 + teleporter: [54, 0] + access: [] + - target_room: 195 + entrance: 441 + teleporter: [110, 3] + access: [] +- name: Doom Castle Dark King Room + id: 200 + game_objects: [] + links: + - target_room: 199 + entrance: 442 + teleporter: [52, 0] + access: [] diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml new file mode 100644 index 0000000000..aa973ee22b --- /dev/null +++ b/worlds/ffmq/data/settings.yaml @@ -0,0 +1,140 @@ +# YAML Preset file for FFMQR +Final Fantasy Mystic Quest: + enemies_density: + All: 0 + ThreeQuarter: 0 + Half: 0 + Quarter: 0 + None: 0 + chests_shuffle: + Prioritize: 0 + Include: 0 + shuffle_boxes_content: + true: 0 + false: 0 + npcs_shuffle: + Prioritize: 0 + Include: 0 + Exclude: 0 + battlefields_shuffle: + Prioritize: 0 + Include: 0 + Exclude: 0 + logic_options: + Friendly: 0 + Standard: 0 + Expert: 0 + shuffle_enemies_position: + true: 0 + false: 0 + enemies_scaling_lower: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + enemies_scaling_upper: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + bosses_scaling_lower: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + bosses_scaling_upper: + Quarter: 0 + Half: 0 + ThreeQuarter: 0 + Normal: 0 + OneAndQuarter: 0 + OneAndHalf: 0 + Double: 0 + DoubleAndHalf: 0 + Triple: 0 + enemizer_attacks: + Normal: 0 + Safe: 0 + Chaos: 0 + SelfDestruct: 0 + SimpleShuffle: 0 + leveling_curve: + Half: 0 + Normal: 0 + OneAndHalf: 0 + Double: 0 + DoubleHalf: 0 + Triple: 0 + Quadruple: 0 + battles_quantity: + Ten: 0 + Seven: 0 + Five: 0 + Three: 0 + One: 0 + RandomHigh: 0 + RandomLow: 0 + shuffle_battlefield_rewards: + true: 0 + false: 0 + random_starting_weapon: + true: 0 + false: 0 + progressive_gear: + true: 0 + false: 0 + tweaked_dungeons: + true: 0 + false: 0 + doom_castle_mode: + Standard: 0 + BossRush: 0 + DarkKingOnly: 0 + doom_castle_shortcut: + true: 0 + false: 0 + sky_coin_mode: + Standard: 0 + StartWith: 0 + SaveTheCrystals: 0 + ShatteredSkyCoin: 0 + sky_coin_fragments_qty: + Low16: 0 + Mid24: 0 + High32: 0 + RandomNarrow: 0 + RandomWide: 0 + enable_spoilers: + true: 0 + false: 0 + progressive_formations: + Disabled: 0 + RegionsStrict: 0 + RegionsKeepType: 0 + map_shuffling: + None: 0 + Overworld: 0 + Dungeons: 0 + OverworldDungeons: 0 + Everything: 0 + crest_shuffle: + true: 0 + false: 0 +description: Generated by Archipelago +game: Final Fantasy Mystic Quest +name: Player diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md new file mode 100644 index 0000000000..dd4ea354fa --- /dev/null +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -0,0 +1,33 @@ +# Final Fantasy Mystic Quest + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Besides items being shuffled, you have multiple options for shuffling maps, crest warps, and battlefield locations. +There are a number of other options for tweaking the difficulty of the game. + +## What items and locations get shuffled? + +Items received normally through chests, from NPCs, or battlefields are shuffled. Optionally, you may also include +the items from brown boxes. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. + +## What does another world's item look like in Final Fantasy Mystic Quest? + +For locations that are originally boxes or chests, they will appear as a box if the item in it is categorized as a +filler item, and a chest if it contains a useful or advancement item. Trap items may randomly appear as a box or chest. +When opening a chest with an item for another player, you will see the Archipelago icon and it will tell you you've +found an "Archipelago Item" + +## When the player receives an item, what happens? + +A dialogue box will open to show you the item you've received. You will not receive items while you are in battle, +menus, or the overworld (except sometimes when closing the menu). + diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md new file mode 100644 index 0000000000..9d9088dbc2 --- /dev/null +++ b/worlds/ffmq/docs/setup_en.md @@ -0,0 +1,162 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client` + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or, + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware + +- Your legally obtained Final Fantasy Mystic Quest 1.1 ROM file, probably named `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +The Archipelago community cannot supply you with this. + +## Installation Procedures + +### Windows Setup + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this, + or you are on an older version, you may run the installer again to install the SNI Client. +2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your `.apmq` patch file. +5. Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM +and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM. +7. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing +everyone's patch files. Your patch file should have a `.apmq` extension. + +Go to the [FFMQR website](https://ffmqrando.net/Archipelago) and select your Final Fantasy Mystic Quest 1.1 ROM +and the .apmq file you received, choose optional preferences, and click `Generate` to get your patched ROM. + +Manually launch the SNI Client, and run the patched ROM in your chosen software or hardware. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: + `Config --> Cores --> SNES --> BSNES` + Once you have changed the loaded core, you must restart BizHawk. +2. Load your ROM file if it hasn't already been loaded. +3. Click on the Tools menu and click on **Lua Console** +4. Click the Open Folder icon that says `Open Script` via the tooltip on mouse hover, or click the Script Menu then `Open Script...`, or press `Ctrl-O`. +5. Select the `Connector.lua` file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + +##### RetroArch 1.10.1 or newer + +You only have to do these steps once. Note, RetroArch 1.9.x will not work as it is older than 1.10.1. + +1. Enter the RetroArch main menu screen. +2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON. +3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default + Network Command Port at 55355. + +![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +When loading a ROM, be sure to select a **bsnes-mercury** core. These are the only cores that allow external tools to +read ROM data. + +#### With hardware + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) + +1. Close your emulator, which may have auto-launched. +2. Power on your device and load the ROM. + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. From ce2f9312ca18106168870c8cf836dfd545b7488b Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 28 Nov 2023 13:50:12 -0800 Subject: [PATCH 241/327] BizHawkClient: Change `open_connection` to use 127.0.0.1 instead of localhost (#2525) When using localhost on mac, both ipv4 and ipv6 are tried and raise separate errors which are combined by asyncio and difficult/inelegant to handle. Python 3.12 adds the argument all_errors, which would make this easier. --- worlds/_bizhawk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index cddfde4ff3..94a9ce1ddf 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -97,7 +97,7 @@ async def connect(ctx: BizHawkContext) -> bool: for port in ports: try: - ctx.streams = await asyncio.open_connection("localhost", port) + ctx.streams = await asyncio.open_connection("127.0.0.1", port) ctx.connection_status = ConnectionStatus.TENTATIVE ctx._port = port return True From 737686a88d54f8ace38f8b577d54d55f5b6c4250 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 28 Nov 2023 13:56:27 -0800 Subject: [PATCH 242/327] BizHawkClient: Use `local_path` when autolaunching BizHawk with lua script (#2526) * BizHawkClient: Change autolaunch path to lua script to use local_path * BizHawkClient: Remove unnecessary call to os.path.join and linting --- worlds/_bizhawk/context.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 2699b0f5f1..4ee6e24f59 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -208,19 +208,30 @@ async def _run_game(rom: str): if auto_start is True: emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path - subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)], - cwd=Utils.local_path("."), - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.Popen( + [ + emuhawk_path, + f"--lua={Utils.local_path('data', 'lua', 'connector_bizhawk_generic.lua')}", + os.path.realpath(rom), + ], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) elif isinstance(auto_start, str): import shlex - subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)], - cwd=Utils.local_path("."), - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) + subprocess.Popen( + [ + *shlex.split(auto_start), + os.path.realpath(rom) + ], + cwd=Utils.local_path("."), + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) async def _patch_and_run_game(patch_file: str): From 39969abd6ad6aa715d979ef6eece1f242e58e575 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:11:17 +0100 Subject: [PATCH 243/327] WebHostLib: fix NamedRange in options presets (#2528) --- WebHostLib/static/assets/player-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/player-options.js b/WebHostLib/static/assets/player-options.js index 37ba7f98ff..92cd6c43f3 100644 --- a/WebHostLib/static/assets/player-options.js +++ b/WebHostLib/static/assets/player-options.js @@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => { break; } - case 'special_range': { + case 'named_range': { const selectElement = document.querySelector(`select[data-key='${option}']`); const rangeElement = document.querySelector(`input[data-key='${option}']`); const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`); From 6c5f8250fba413dd9188041c58a132b3aa7981bd Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 29 Nov 2023 01:19:42 -0500 Subject: [PATCH 244/327] TLOZ: Use the proper location name lookup (#2529) --- Zelda1Client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Zelda1Client.py b/Zelda1Client.py index db3d3519aa..cd76a0a5ca 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -13,7 +13,6 @@ from typing import List import Utils from Utils import async_start -from worlds import lookup_any_location_id_to_name from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ get_base_parser @@ -153,7 +152,7 @@ def get_payload(ctx: ZeldaContext): def reconcile_shops(ctx: ZeldaContext): - checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations] + checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations] shops = [location for location in checked_location_names if "Shop" in location] left_slots = [shop for shop in shops if "Left" in shop] middle_slots = [shop for shop in shops if "Middle" in shop] @@ -191,7 +190,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone= locations_checked = [] location = None for location in ctx.missing_locations: - location_name = lookup_any_location_id_to_name[location] + location_name = ctx.location_names[location] if location_name in Locations.overworld_locations and zone == "overworld": status = locations_array[Locations.major_location_offsets[location_name]] From a83501a2a077fabd1c7cfe9fa4a66b9db1ce33ba Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 29 Nov 2023 22:57:40 -0500 Subject: [PATCH 245/327] Fix a bug in weighted-settings causing accepted range values to be exclusive of outer range (#2535) --- WebHostLib/static/assets/weighted-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index a2fedb5383..80f8efd1d7 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -576,7 +576,7 @@ class GameSettings { option = parseInt(option, 10); let optionAcceptable = false; - if ((option > setting.min) && (option < setting.max)) { + if ((option >= setting.min) && (option <= setting.max)) { optionAcceptable = true; } if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){ From b9ce2052c5dfeb72d421f3052f9b8c6b23986fe8 Mon Sep 17 00:00:00 2001 From: Brooty Johnson <83629348+Br00ty@users.noreply.github.com> Date: Thu, 30 Nov 2023 03:29:55 -0500 Subject: [PATCH 246/327] DS3: update setup guide to preserve downpatching instructions (#2531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update DS3 setup guide to preserve downpatching instructions we want to preserve this on the AP site as the future of the speedsouls wiki is unknown and may disappear at any time. * Update worlds/dark_souls_3/docs/setup_en.md Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> * Update setup_en.md --------- Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> --- worlds/dark_souls_3/docs/setup_en.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index d9dbb2e547..7a3ca4e9bd 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -21,7 +21,20 @@ This client has only been tested with the Official Steam version of the game at ## Downpatching Dark Souls III -Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333" +To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database. + +1. Launch Steam (in online mode). +2. Press the Windows Key + R. This will open the Run window. +3. Open the Steam console by typing the following string: steam://open/console , Steam should now open in Console Mode. +4. Insert the string of the depot you wish to download. For the AP supported v1.15, you will want to use: download_depot 374320 374321 4471176929659548333. +5. Steam will now download the depot. Note: There is no progress bar of the download in Steam, but it is still downloading in the background. +6. Turn off auto-updates in Steam by right-clicking Dark Souls III in your library > Properties > Updates > set "Automatic Updates" to "Only update this game when I launch it" (or change the value for AutoUpdateBehavior to 1 in "\Steam\steamapps\appmanifest_374320.acf"). +7. Back up your existing game folder in "\Steam\steamapps\common\DARK SOULS III". +8. Return back to Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". Back up this game folder as well. +9. Delete your existing game folder in "\Steam\steamapps\common\DARK SOULS III", then replace it with your game folder in "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". +10. Back up and delete your save file "DS30000.sl2" in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type %appdata% and hit enter or: open File Explorer > View > Hidden Items and follow "C:\Users\your username\AppData\Roaming\DarkSoulsIII\numbers". +11. If you did all these steps correctly, you should be able to confirm your game version in the upper left corner after launching Dark Souls III. + ## Installing the Archipelago mod From 80fed1c6fb444664cba8f7bc73c3a8c557eb6d12 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 30 Nov 2023 03:32:32 -0500 Subject: [PATCH 247/327] Stardew Valley: Fixed potential softlock with walnut purchases if Entrance Randomizer locks access to the field office (#2261) * - Added logic rules for reaching, then completing, the field office in order to be allowed to spend significant amounts of walnuts * - Revert moving a method for some reason --- worlds/stardew_valley/logic.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 5a6244cf37..d4476a3f31 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -1536,6 +1536,7 @@ class StardewLogic: reach_west = self.can_reach_region(Region.island_west) reach_hut = self.can_reach_region(Region.leo_hut) reach_southeast = self.can_reach_region(Region.island_south_east) + reach_field_office = self.can_reach_region(Region.field_office) reach_pirate_cove = self.can_reach_region(Region.pirate_cove) reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut) reach_volcano_regions = [self.can_reach_region(Region.volcano), @@ -1544,12 +1545,12 @@ class StardewLogic: self.can_reach_region(Region.volcano_floor_10)] reach_volcano = Or(reach_volcano_regions) reach_all_volcano = And(reach_volcano_regions) - reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano] + reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] reach_caves = And(self.can_reach_region(Region.qi_walnut_room), self.can_reach_region(Region.dig_site), self.can_reach_region(Region.gourmand_frog_cave), self.can_reach_region(Region.colored_crystals_cave), self.can_reach_region(Region.shipwreck), self.has(Weapon.any_slingshot)) - reach_entire_island = And(reach_outside_areas, reach_all_volcano, + reach_entire_island = And(reach_outside_areas, reach_field_office, reach_all_volcano, reach_caves, reach_southeast, reach_pirate_cove) if number <= 5: return Or(reach_south, reach_north, reach_west, reach_volcano) @@ -1563,7 +1564,8 @@ class StardewLogic: return reach_entire_island gems = [Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz] return reach_entire_island & self.has(Fruit.banana) & self.has(gems) & self.can_mine_perfectly() & \ - self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) + self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ + self.can_complete_field_office() def has_everything(self, all_progression_items: Set[str]) -> StardewRule: all_regions = [region.name for region in vanilla_regions] From c7d4c2f63ccef5ce61f7eca9bad3baf504b9a658 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 1 Dec 2023 03:26:27 -0600 Subject: [PATCH 248/327] Docs: Add documentation on writing and running tests (#2348) * Docs: Add documentation on writing and running tests * review improvements * sliver requests --- docs/contributing.md | 2 +- docs/tests.md | 90 ++++++++++++++++++++++++++++++++++++++++++++ docs/world api.md | 6 ++- 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 docs/tests.md diff --git a/docs/contributing.md b/docs/contributing.md index 6fd80fe86e..9b5f93e198 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -7,7 +7,7 @@ Contributions are welcome. We have a few requests for new contributors: * **Ensure that critical changes are covered by tests.** It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working. -If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/world%20api.md#tests). +If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md). If you wish to contribute to the website, please take a look at [these tests](/test/webhost). * **Do not introduce unit test failures/regressions.** diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 0000000000..7a3531f0f8 --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,90 @@ +# Archipelago Unit Testing API + +This document covers some of the generic tests available using Archipelago's unit testing system, as well as some basic +steps on how to write your own. + +## Generic Tests + +Some generic tests are run on every World to ensure basic functionality with default options. These basic tests can be +found in the [general test directory](/test/general). + +## Defining World Tests + +In order to run tests from your world, you will need to create a `test` package within your world package. This can be +done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base +for your world tests can be created in this file that you can then import into other modules. + +### WorldTestBase + +In order to test basic functionality of varying options, as well as to test specific edge cases or that certain +interactions in the world interact as expected, you will want to use the [WorldTestBase](/test/bases.py). This class +comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying +options combinations. + +Example `/worlds//test/__init__.py`: + +```python +from test.bases import WorldTestBase + + +class MyGameTestBase(WorldTestBase): + game = "My Game" +``` + +The basic tests that WorldTestBase comes with include `test_all_state_can_reach_everything`, +`test_empty_state_can_reach_something`, and `test_fill`. These test that with all collected items everything is +reachable, with no collected items at least something is reachable, and that a valid multiworld can be completed with +all steps being called, respectively. + +### Writing Tests + +#### Using WorldTestBase + +Adding runs for the basic tests for a different option combination is as easy as making a new module in the test +package, creating a class that inherits from your game's TestBase, and defining the options in a dict as a field on the +class. The new module should be named `test_.py` and have at least one class inheriting from the base, or +define its own testing methods. Newly defined test methods should follow standard PEP8 snake_case format and also start +with `test_`. + +Example `/worlds//test/test_chest_access.py`: + +```python +from . import MyGameTestBase + + +class TestChestAccess(MyGameTestBase): + options = { + "difficulty": "easy", + "final_boss_hp": 4000, + } + + def test_sword_chests(self) -> None: + """Test locations that require a sword""" + locations = ["Chest1", "Chest2"] + items = [["Sword"]] + # This tests that the provided locations aren't accessible without the provided items, but can be accessed once + # the items are obtained. + # This will also check that any locations not provided don't have the same dependency requirement. + # Optionally, passing only_check_listed=True to the method will only check the locations provided. + self.assertAccessDependency(locations, items) +``` + +When tests are run, this class will create a multiworld with a single player having the provided options, and run the +generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld +that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be +overridden. For more information on what methods are available to your class, check the +[WorldTestBase definition](/test/bases.py#L104). + +#### Alternatives to WorldTestBase + +Unit tests can also be created using [TestBase](/test/bases.py#L14) or +[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These +may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for +testing portions of your code that can be tested without relying on a multiworld to be created first. + +## Running Tests + +In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`. +If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the +working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat +the steps for the test directory within your world. diff --git a/docs/world api.md b/docs/world api.md index 6393f245ba..0ab06da656 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -870,7 +870,7 @@ TestBase, and can then define options to test in the class body, and run tests i Example `__init__.py` ```python -from test.test_base import WorldTestBase +from test.bases import WorldTestBase class MyGameTestBase(WorldTestBase): @@ -879,7 +879,7 @@ class MyGameTestBase(WorldTestBase): Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. -Example `testChestAccess.py` +Example `test_chest_access.py` ```python from . import MyGameTestBase @@ -899,3 +899,5 @@ class TestChestAccess(MyGameTestBase): # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them. self.assertAccessDependency(locations, items) ``` + +For more information on tests check the [tests doc](tests.md). From 5e5018dd6443b7e3e90ce824d1e1a3b3d2e05047 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 1 Dec 2023 21:19:41 +0100 Subject: [PATCH 249/327] WebHost: flash each message only once (#2547) --- WebHostLib/templates/pageWrapper.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/pageWrapper.html b/WebHostLib/templates/pageWrapper.html index ec7888ac73..c7dda523ef 100644 --- a/WebHostLib/templates/pageWrapper.html +++ b/WebHostLib/templates/pageWrapper.html @@ -16,7 +16,7 @@ {% with messages = get_flashed_messages() %} {% if messages %}

    - {% for message in messages %} + {% for message in messages | unique %}
    {{ message }}
    {% endfor %}
    From 6e38126add3cf47c6a88000ca2ce0a62826e1c78 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 1 Dec 2023 14:20:24 -0600 Subject: [PATCH 250/327] Webhost: fix options page redirects (#2540) --- WebHostLib/templates/supportedGames.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 3252b16ad4..6666323c93 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -53,7 +53,7 @@ {% endif %} {% if world.web.options_page is string %} | - Options Page + Options Page {% elif world.web.options_page %} | Options Page From e8ceb122813c9771a89f538dd042cf160de92485 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 2 Dec 2023 12:40:38 -0500 Subject: [PATCH 251/327] =?UTF-8?q?Pok=C3=A9mon=20RB:=20Fix=20connection?= =?UTF-8?q?=20names=20+=20missing=20connection=20(#2553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/regions.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index f844976548..97e63c0557 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1631,7 +1631,7 @@ def create_regions(self): connect(multiworld, player, "Cerulean City", "Route 24", one_way=True) connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player)) connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True) - connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True) connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player)) connect(multiworld, player, "Cerulean City-Outskirts", "Route 5") connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True) @@ -1707,7 +1707,6 @@ def create_regions(self): connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True) connect(multiworld, player, "Route 12-L", "Lavender Town") connect(multiworld, player, "Route 10-S", "Lavender Town") - connect(multiworld, player, "Route 8-W", "Saffron City") connect(multiworld, player, "Route 8", "Lavender Town") connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player])) connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True) @@ -1831,7 +1830,8 @@ def create_regions(self): connect(multiworld, player, "Silph Co 6F", "Silph Co 6F-SW", lambda state: logic.card_key(state, 6, player)) connect(multiworld, player, "Silph Co 7F", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player)) connect(multiworld, player, "Silph Co 7F-SE", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player)) - connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player)) + connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F to Silph Co 8F-W (Card Key)") + connect(multiworld, player, "Silph Co 8F-W", "Silph Co 8F", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F-W to Silph Co 8F (Card Key)") connect(multiworld, player, "Silph Co 9F", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player)) connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player)) connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player)) @@ -1864,22 +1864,23 @@ def create_regions(self): # access to any part of a city will enable flying to the Pokemon Center connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) + connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True) + connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") + connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") + connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") + connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") + connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") + connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") # drops - connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True) - connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True) - connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True) + connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)") + connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)") + connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)") connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) @@ -1888,7 +1889,7 @@ def create_regions(self): # If you haven't dropped the boulders, you'll go straight to B4F connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True) - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)") connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True) @@ -1944,7 +1945,8 @@ def create_regions(self): connect(multiworld, player, region.name, entrance_data["to"]["map"], lambda state: logic.rock_tunnel(state, player), one_way=True) else: - connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True) + connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True, + name=entrance_data["name"] if "name" in entrance_data else None) forced_connections = set() From a83bf2f61687c55337903d77fd8d3451bb0e4962 Mon Sep 17 00:00:00 2001 From: zig-for Date: Sun, 3 Dec 2023 12:24:35 -0800 Subject: [PATCH 252/327] LADX: Fix bug with Webhost usage (#2556) We were using data created in init when we never called init --- worlds/ladx/Options.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index f1d5c51301..691891c0b3 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -349,18 +349,19 @@ class GfxMod(FreeText, LADXROption): normal = '' default = 'Link' + __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx')) __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) - __spriteDir: str = None extensions = [".bin", ".bdiff", ".png", ".bmp"] + + for file in os.listdir(__spriteDir): + name, extension = os.path.splitext(file) + if extension in extensions: + __spriteFiles[name].append(file) + def __init__(self, value: str): super().__init__(value) - if not GfxMod.__spriteDir: - GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx')) - for file in os.listdir(GfxMod.__spriteDir): - name, extension = os.path.splitext(file) - if extension in self.extensions: - GfxMod.__spriteFiles[name].append(file) + def verify(self, world, player_name: str, plando_options) -> None: if self.value == "Link" or self.value in GfxMod.__spriteFiles: From 39a92e98c6a42070871af9e99447125f7b3e9224 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 3 Dec 2023 18:06:11 -0500 Subject: [PATCH 253/327] Lingo: Default color shuffle to on (#2548) * Lingo: Default color shuffle on * Raise error if no progression in multiworld --- worlds/lingo/__init__.py | 10 ++++++++++ worlds/lingo/options.py | 2 +- worlds/lingo/test/TestDoors.py | 9 ++++++--- worlds/lingo/test/TestProgressive.py | 3 ++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index da8a246e79..a8dac86221 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -1,6 +1,8 @@ """ Archipelago init file for Lingo """ +from logging import warning + from BaseClasses import Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .items import ALL_ITEM_TABLE, LingoItem @@ -49,6 +51,14 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): + if not (self.options.shuffle_doors or self.options.shuffle_colors): + if self.multiworld.players == 1: + warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" + f" items. Please turn on Door Shuffle or Color Shuffle if that doesn't seem right.") + else: + raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" + f" progression items. Please turn on Door Shuffle or Color Shuffle.") + self.player_logic = LingoPlayerLogic(self) def create_regions(self): diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index fc9ddee0e0..c00208621f 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -32,7 +32,7 @@ class LocationChecks(Choice): option_insanity = 2 -class ShuffleColors(Toggle): +class ShuffleColors(DefaultOnToggle): """If on, an item is added to the pool for every puzzle color (besides White). You will need to unlock the requisite colors in order to be able to solve puzzles of that color.""" display_name = "Shuffle Colors" diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index 5dc989af59..f496c5f578 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -3,7 +3,8 @@ from . import LingoTestBase class TestRequiredRoomLogic(LingoTestBase): options = { - "shuffle_doors": "complex" + "shuffle_doors": "complex", + "shuffle_colors": "false", } def test_pilgrim_first(self) -> None: @@ -49,7 +50,8 @@ class TestRequiredRoomLogic(LingoTestBase): class TestRequiredDoorLogic(LingoTestBase): options = { - "shuffle_doors": "complex" + "shuffle_doors": "complex", + "shuffle_colors": "false", } def test_through_rhyme(self) -> None: @@ -76,7 +78,8 @@ class TestRequiredDoorLogic(LingoTestBase): class TestSimpleDoors(LingoTestBase): options = { - "shuffle_doors": "simple" + "shuffle_doors": "simple", + "shuffle_colors": "false", } def test_requirement(self): diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index 026971c45d..917c6e7e89 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -81,7 +81,8 @@ class TestSimpleHallwayRoom(LingoTestBase): class TestProgressiveArtGallery(LingoTestBase): options = { - "shuffle_doors": "complex" + "shuffle_doors": "complex", + "shuffle_colors": "false", } def test_item(self): From b7111eeccc0873d158934537adf3e9eb64044648 Mon Sep 17 00:00:00 2001 From: el-u <109771707+el-u@users.noreply.github.com> Date: Mon, 4 Dec 2023 00:06:52 +0100 Subject: [PATCH 254/327] lufia2ac: fix disappearing Ancient key (#2537) Since the coop update, the Ancient key (which is always the reward for defeating the boss) would disappear when leaving the cave, making it impossible to open the locked door behind the Ancient Cave entrance counter. While this is basically cosmetic and has no adverse effects on the multiworld (as the door does not lead to any multiworld locations and is only accessible after defeating the final boss anyway), players may still want to enter this room as part of a ritual to celebrate their victory. Why does this happen? The game keeps track of two different inventories, one for outside and another one for the cave dive. When entering or leaving the cave, important things such as blue chest items and Iris treasures are automatically copied to the other inventory. However, it turns out that the Ancient key doesn't participate in this mechanism. Instead, the script that runs when exiting the cave checks whether event flag 0xC3 is set, and if it is on, it calls a script action that adds the key item to the outside inventory. (Whether or not the player actually had the key item in their in-cave inventory is not checked at all; only the flag matters.) In the unmodified game, that flag is set by the cutscene script that awards the key. It actually sets two event flags, 0xC3 and 0xD1. The latter is used by the game when trying to display the boss in the cafe basement and is used by AP as the indicator that the boss goal was completed. With the coop update, the event script method that created the key was intercepted and modified to send out a location check instead. That location always has the Ancient key as a fixed item placement; the benefit of handling it as a remote item is that in this way the key essentially serves as a signal that transmits the information of the boss' defeat to all clients cooping on the slot. When receiving the key, however, the custom ASM did only set flag 0xD1. As part of the bugfix, it is now changed to set flag 0xC3 as well. But that alone is still not enough to make it work. The subroutine that is called by the game to create the key when exiting the cave with flag 0xC3 is the same subroutine that gets called in the cutscene that originally tried to award the key. But that's the one that has been rewritten to send the location check instead. So instead of creating the key when leaving the cave, it would just send the same location check again, effectively doing nothing. Therefore, the other part of the bugfix is to only intercept this subroutine if the player is currently on the Ancient Cave Final Floor (where the cutscene takes place), thus making it possible to recreate the key item when exiting. --- worlds/lufia2ac/basepatch/basepatch.asm | 6 ++++++ worlds/lufia2ac/basepatch/basepatch.bsdiff4 | Bin 8638 -> 8652 bytes 2 files changed, 6 insertions(+) diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm index f298a1129d..f9c48a5fec 100644 --- a/worlds/lufia2ac/basepatch/basepatch.asm +++ b/worlds/lufia2ac/basepatch/basepatch.asm @@ -170,6 +170,9 @@ pullpc ScriptTX: STA $7FD4F1 ; (overwritten instruction) + LDA $05AC ; load map number + CMP.b #$F1 ; check if ancient cave final floor + BNE + REP #$20 LDA $7FD4EF ; read script item id CMP.w #$01C2 ; test for ancient key @@ -261,6 +264,9 @@ SpecialItemGet: BRA ++ +: CMP.w #$01C2 ; ancient key BNE + + LDA.w #$0008 + ORA $0796 + STA $0796 ; set ancient key EV flag ($C3) LDA.w #$0200 ORA $0797 STA $0797 ; set boss item EV flag ($D1) diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 index 4ed1815039a04c4f3c1ce8a6c5a28ddfda86f96e..664e197c4a1929f6958c1245b11750716b7a9d7e 100644 GIT binary patch literal 8652 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 literal 8638 zcmZ{pWmFX0x5j6PVJK;&yE_IXM260hZV(uWA*B%zq+_H9iJ@yq0civzr8@;Aq(MRn zDUtiW|GVz`f4a|yv(H-3`S7g0&e|XLPtibGLsb$N+qX;bkctlZ(0K!56b%18kNoP83 z{1lWzN+hCxYL=`Jr5c?VrGY^tNTRs3b(E*3lgCvlfFOkeCG`Z3Ssjfq8bti8^2>Qp zUL0n`DKG5;Jd-G=jT>bEg&Uw)Kj`1DxSQyGzjm?m{_(;b@G3 zy`(ByjinpVtL>^kY2HB0;sqjG6!*!fJ~#o z!-wJq3ZY?f#ZWn}EIPX(AZiY=oItNTjZw`4V{@N+9er`1gNT2BqJ`xBqNudMN}!2ST0F)@rp$9LgUd+$)G$88i9Zi zi@|A$!Z2y^m?>U1CG0<1lE* zFy`Ic(GUoHc^&JSe8UxSqweHUhUVg*)tY5MWfT2x^0n_|q007?>HO=IjcGevEW=38 zS$z_195Hq}_Vjwm;;XP8&E1G~$S3fi|7WKSg{Vxx`=V#fscyQ^pEWiN}r z^ToUnmDsy+iMjp6a(xN?u(9>^z0Sr|tc7I|IExxzw!~yVHe2~UWr_VXj9ErbZ^rpN zweXeVse&{uQC0{7>K2y}K*}OHfDRhKZO1-ujQ6W?El8gaTM$_lC9Jafh{in-rTWU% zgNN(~g_Tw#uwz{Qd}OJa!SSpn)rXsmiI_$Wir_Q~jMf3V;pD=mSd}wCK3CEYreOl< zED9iwsD;K*^24uT_(Joc_`xP0RY72 z&fU!U=&3nrsfmPVD*|mH9918wUG-YORy9?<;*IjCs7j>BP4zS355R>2*)-@1n}^Rz zK0R@Sc@FA%@0(F2LeiBatgcX_s)iw9<8x(Os-I?-UKjB0P{{7c<9QQ^Ix{g{Zjh@aE3&4rkby zSfaG_SLCryKF~n@eZkmAQrnjdfRu4Cn|rU+Fe|5;(!FC5An(gaT`q(qCZ~9&b>}|`1LY;aizO``VFD`lb4+%=EQ~v0dm-|8YU)8mzfqH zLgQ`W`-l|xCiJbOd^|UAw2CDaR!;Ttgw6WiwDj#wCB%752JaE7a^7=V6tl(8h$@2eE?_}$w(ANJmZflrfA^!L8*`uey!IPqEd{LC4VoO+RRy)=? zT67z_jV7BpG=3cQrH2J2a-%LywH3Mk*zTE=wR318FSV#7ZkX18w|e_5d+%ZQ zVtv+_UyIV@dux*&HV&!##kG)EVMVs1>w=DuGJ-PHYFyi0ZF-O1w|PU-#UD!SsNb@x zV`gaANvG$pIer$di8pV3`W*92zCNnM?QEdsEl&2#PWhzBlCQ_lX>yYC6V?$sLM$nT zH#3Q2Ox3J{uP=?EJ)gl?MhEVp28coP#oBRmSlm;>BnXK_DTwXTDEFJ?XjVWJw&D(Dp zsBh4fItkK*)|)b=&UXPaJ8NzCILry=otA43H~SJakmvZMaZETiq(pvHJ%*balrhOO zJi15d_`t|zdfIK*HCIbC8mv$Zu@VG_UoS-)Q#KcktS-elCM3d?=Y$+lS$Qb?dc;sG zk9HOkv~S)e{bpg>@`ws7T+2&cYb0!SHr@ROoNO*kHPE%crlFW+b4%DUfd^BaPov=g zUh!UNM@zlb`P=S)3C@Vn->cS-~%A%@7a(7!AW0$pv{;&vMw7#}Jy`ug5!ltbBn~e%h ztZ}62VW{`L`R8Z@LVm?|tESjbpfKE}HNvPi9c>J9O0e^)=K5swirJhE31bkhO3?fZ zU8cG*O%?lKWVfp>V#35Ac3KngIZ>*(T9>p%f2I@2Djki4mSyvIpcPr7cnKCA<>9yX zEJD6}-3&)A#F1eg8rNnWTG=bZdQ%s<%-%!lCN4?Z=8xdCj;k77r|gysMfkz*XrN#} zoN4BQ9cwjGYkx?!!GAga0gT%#0=WV>mFs-cv+9h+(2xaa3Ny znN@ej!{g!1oVxyzmbhGLOoF5vJSaST>^miGwZ(#>yRRL{*-%!ny})2_Mt^>{wKm_S zsF5Q`<}CPlTq2z+vG08ai-SsSshX{(PkSM(m7>goEqdE|gPC5hwZr-{a1>|Hz$UJ! zE`ZgAG)Jo{o_|GfK1RcCJyMTwYlMqVM?pSzr%C=rpoF0Z+tAJDnC+yGfpWEsEm!Cp zebWytSgR!w15PZNim^yVd^Oq2lgv`Nve>Rh;fuO7s=VO5n~CaPiH=o58tqpRpF2mO zq(JuT3nsSbk4W@>ggSEi?Y%akGHtzj_h<|&hQs}v|GYrSji`a}Vwf4vdDe}8#r50W zGhwOV#Gj8%(XJtX9QM9#y;MwLzIb0Qk~~}}O=KzSP%Y0BlX7_RYVBEX64&(hgZsJ( zV}qGMdf(+uz(j^;>4{^p@Sh7&J-8bqA-5zJ5UU1G!}B=9m1pVpadirls1G%B&vFT% z@JqNX)Aw!TZEmnj4Djl4E`&hx&go6NpLNLJyhGGVstEJE(Op(`vwv~KiA~b}>n^+% zU!e!%kn0`~fq^f5;|?~L?Vfnzee)A+2Xv-pMe|RoBJc8DmM5i(!$h<9oAj@knd6G} z?2iPsfABhWzt^aB4j)i>>dViG& zX!9OF_|2NRv2xTfp(`0bD9Os((?VNU6b29xW**B=H#&w7a&Tutj18uO$QbQTIpOmG znv?Jj*{+n)v`@2zfAG2l#k2GzIc%T_b<#gGyc1Htnee%RHB(P^AW028GQNHZzC9^F zdbsoynwlUd7ee;iG&kuRsbNN3+wT5@1v!ED_+{i^cY)S}HD#&K4ycsrH2*KFbvm3h zy5=k*rp0A7Z6S}+g_1ASG(X4avDC5bH@MSm@rG8vEpt-(0MD9p4@xm6FIRFx$q`E6 zhweKn?jc_m46;1q*7|dGM%)5M%?&qXo(8pCTrn|IVjXcy);1xA$s%qN8~r+V==Dgg z9W{z&b)2+xNw(|Yx&G15=NEIkYX^^uIoNEyEgZtuxnn2@M7k3|GrtpfzI$~2s!rZi zug=X4jPTJCGV%}mV4MFrCvU7VO+!Cb)J*EUBK+6nip{GGSN3_bQ!iaCU4!q!mzQ^j zk1<#vja#AI7V+gZ`yKv{D_@fC0WCnu9X_I8{dd9i5yMGu`cuSMsx|fGT2N8!cJuKp zKx}4H)>p0OBn@7+{z4Kx>qHRz>!5fom4VD2XU?~@fUUdX8RuKHzJQ(*<6kB^(!s?@d2Gg%-ZpfW+Veh^e?`X6W1%akKD73Uhz^yx0pHvyt@z#w z4A+bCR7{nnR=cfDi17sf`Xh_XVBfDRoxqwwg}hL-;eN*sTXj{JGv-sE@b1S~kg#H$ zjDZ#`@;O<}3scPd`sNRa!{uGcWtUXSyX_cRdK6#ry{t;0W%9A|^|uQ7g1S7m$%pO9m-~# zzEfxqivazrvwnZ?@)G{)I5UHFRm+VA!iG*#1Y0ADhmzTMCb3_S3sywK7qRe^=z&x9^`JL&O_9 zX+j3&wsmJ#lo*KU6N-fK&oV^Z+!NISIm_w4%frr_uhN6B>}lV?{qK7$&r=4C=I~6R zxr#bo$w#gZ!Bkn23kKr2N40lt!3TbpP{omKTBXN9X(<)&gSMT1U+Zv`bq_J&6G4hT z%KU%KN~m=*1&&2s_vEpEHSOTQhp(Z=zO3Z=xbu)6pKmApT{%EUI7ZZrA_KW9Dd zy@oI-HML#nJ1p8kuz{k!rrs(MZt#2RCuv*f_H*v~GZ0~wn1hCJEXpCU3~%_e6Em%0 z&e*3;t}}^NF$ga0m>|!wTi^SLl5CDR(P{z9=hT`Ulic>Oz>Ru_s(xS)%+c3&`W1a;2v{aKZGyUeXjxS$7 zZ0LkoeQASYly_^Zd_`%;p7}>H?wB&n)-s1Vw0y1aw@toe!J)b=ySU3e##E*g1J2Q# zC)GluXN)M(o~m}j@;cCqE;rlL#c)1JNJr>>i94x`uGKa(UbiurQ&uqS)5OxA>T z`yi6Tq&SA1-rTE6&gf;Q=e-yQ5~dGG1ow{fri98;?dmjLu7KW%N-t?4YUPW^mgkUm zl8gRsmFfEHSal1f0rIb$wU#E*+}tMH!h5|z&qLK%n1!yilt;u@%VDG8#nP`C3*n$g zLuX>&-mEM5{z(Xbu|?OvR&7nZ*VP_M8r0M@U}aZ>w1s9ja7X<^{s5^PAh%1>>X$^uDnvC+Mq+ zqIOZ_YlDmp8gs<{yI;62e<22O0-gG8RrDcSB?MHu6NOBjI6P<@e;Yot%){=~&s| zuhu$m1!BDjHNscO97UyyLf7}_;^rLGyHPmKqc{_$!SOqziK~AZ6whQAXpjGfHU^=+pn9OR|#I`1{_w@AtLd_2dhJ^uF? zqk)3&i99+fXtHo<)Xi4hM;iXC1Kp>c+5|w9FIIYEN2#roV;m)lhE*zb(sOhWNtP9- z*}S1oiutvriF4YQ?=2pE9k@1=w(W3aHJ{>i#hPX*dXW6`scyRjV@2+5>O^(-PjB+N z5Z@;>O2jeB8+S{`E~e&Zz8R=xXY6@5cOm{&xQ_aJqu0nqJ{9P6VZJhUX%r830Xd68 zi0M{5^z-G-o4r&%5Vm!E|7p|PjImtT$?(ti5rTJbWJ2Hx8Y8w+SDbtHP38q8z>LuLI?d%KlNt{F#i!qu0`On2zjU~_beR=ZEg9UsH zlHq3hu6yJ?T-cx3(IKNPSbKP{(b>}#=0*B~z6M99r5ECwowGT6wN#qpw53~^sm#1EBL*tsUTF>bf>bxz_k z2Ovd-M~o$z`ct2VS&ssJ3ybP$3}ZEs$fF2&1Gb`WY{O_-d}A?RN%C6?;WP1b%@h7( zeiA@YN;?my%=e>fZQ%=NtRQ;_5_|3Ny_&CcDoeiw<&!c|9k2LKaZlAFOy+1Bd)mTz zm-YIls*({%`8?euaNHhoK6dA#JK`_hk_CZ5Qk20e0h%yg!3Bo46oj@R6#T3f7$i#f zBkcN;5dZ+$o1sAvlBj>QPXkkRblUMJ1o?_-bE~Wtamrh(N!RAg1=hm_@nV^AEg01Z z|7vc0nx=&4#bCdm4PR|B#$_*?e2zB>?x#sfX{y+{Xb5PGC$$@_``iC2;OYMdGO04 zQgM@QZcgcw!3QJjk`?z-5gdwkCq)qz1$V5mG<9WWQGf@zyO9c#NG31Cq&dLzM`v{b zEziW0r#ZW}p73WgSv=2i98z6R4AZ-M_P+@z=u(+{T-R?MqxzZ?qTg@HA+@77+rTBK zqPp+r3VMSCkBmjxHqobUH&!yHG(xBen2dNZ5{;V3n-`Q`LxxBCx?yCQ%oyM8$RvnK zbCMTezOKF_84c+Y$Xn$Lvud0^mW~i zp$!I4Uc^UlgW8AyT045?ckxkiz_bWrhWGH;OGQdI`Cnl2UwqOQ>7)xF75QIC;(PUQ z_3)72^!DN5<~C^fVeMh};rgN9`0C+weG2Ci-F~N(V%2|&L%}6eSviCbSXe+ugQizkrqHN- zn~aix6}e*2^vaiVI=KIk3gDp|j6``FaSSN)w^OoMAs59n zfoQ}9cg4O!L0~8y@DSistfTuc2^sqLJpdH?FBSoascXmWqo&000Mv@1{Of<{>@NsHw8R|{1Z!z&WubkG8xD*BcQ=TuFZ!`xV>cbD8L7#I{Pav zUV8O!RYctrw#UJY6D=^~;!hJM`6YinFpvR({A%ri8D9y^oU}w5Uow5E3hz$^a*Pky z(;q33ZZq}-SW}UCl6%u{kXZLb(S)g1w4n)w(B;@%f5Rcl{QSMObO*|zW=+^zCW9U{ zN*m!{*?4*Jhl=3g3bGv?*T0_t0q?M|_$j#ce?JvB5$Ha(n*F5yR_Tp9LH5sB#Hcsm zo5k_NYD(qa`J96s!AzCbHw~zDnFG10&cvrAhF?E-5#&4PlOiYLP@57I2Wd|yyYzQn zFqTE%>c_qmpXU;{LE0$KM*I)w*C1-BQ(D%S`tmnOKSg~75kY$jtr)2+N1oEH+Cky| zaAPwpFcv?f!dQNx#|{4Q&zr(YmEOkD*$?v}`*pUXK~*mjQ8D_bUp064*E-)=MiZc20QbpJ+|&b=9zauZP0paqX%3EG-RwnwO~#1cuwxn}!d1-lWeS@xH^a zeY|<&RjCWHg^3(wIM<~q`;ORWtx*tw$w)gQYEu8MahDsA5fX};qST*E7Q2UUN(eOm zh^>&`o%{l=|FNOm!8^$5DgOsYpbCQEv{#SVINr0=tW z%=9;iu6Om}@v3vs;PN{EK*B0};YTBHLUs^MI;+25TGAhvEdPL~vqDUH3KuniY-h;2He~I=G1(H!*;yQ&cQP`LOw;liy zF|(tZ3r#(hp=|89@jS7yi5nNoYp?w8X2z#m3^@ibbLzvZJGuV9ud4h0Ce&L)P?8LBE>n_WJJ;vuj1|SklLB zNUv7U>z7TdZYy6JQLl`9O#d*Di&s}xfQ8dZskoXSwdiqD=x||DRW{!34VwNC+K&Fr zc>|rb3Q3^45L{q<46pCKeMBf7SZSJ?+A7$8 ztR*>BnlZ=!O|;S;@7HWw!T~rH0cMz^K(YGHi}zCv;=@-cp4M@M&bbj*PMf2nKDF_- ze`+V=S0@Kfp=t6d%Pqe>Hm7!Y5yTnE5N3p#(Pus^uc(y$ zS<@dL8AlzV;mpMUlVm#pldAvw`n|tP5A;eaSe!GA3v_7Y8SQ&o`D4`~%mEcYQ}prlao5zJudM zP8L=SP~PW{>5$~_5?AE)VQ;70FHeh(u7}JD^o!$T`;y0Hyo&SdZ@gYz!}Yt#c%cXj zrz;!B7ML5C63cQI7R%TvPR6L$JO3)#7>@?u&S{= zif=zm3$o?MU#LkPZ)fKDLGOu;Pw*{-rm{?Izp3T;{n%&9-=GJPWA9{l<90#fz|wKG zXdMG7Xv*Ifo-l=oZZy3aba?<6@g$Z$r(R~yx~X*pY6ShNRN(%#f2Hellg=vH2?#$d zTjssz<|8;I>6_{SKe3cBr#RElI>92is+^6HyXcShMaGvx2;Zy$r+W%fop= Date: Mon, 4 Dec 2023 16:26:00 +0100 Subject: [PATCH 255/327] The Witness: Fix various incorrect symbol requirements in Vanilla Puzzles (#2543) * Fix Vanilla First Floor Left * More vanilla logic fixes --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/WitnessLogicVanilla.txt | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 719eae6c4e..8591a30d1f 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -257,7 +257,7 @@ Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - Tr 158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser 158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser 158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser -158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser +158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Full Dots & Eraser 158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots 158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser @@ -307,9 +307,9 @@ Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: 158155 - 0x38663 (Second Barrier Panel) - True - True Door - 0x3865F (Second Barrier) - 0x38663 -158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser +158156 - 0x021B5 (Back First Row 1) - True - Stars & Eraser 158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser -158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser +158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Eraser 158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser 158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser 158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser @@ -427,7 +427,7 @@ Keep Tower (Keep) - Keep - 0x04F8F: 158206 - 0x0361B (Tower Shortcut Panel) - True - True Door - 0x04F8F (Tower Shortcut) - 0x0361B 158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True 159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True @@ -516,13 +516,13 @@ Town Red Rooftop (Town): 158607 - 0x17C71 (Rooftop Discard) - True - Triangles 158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Black/White Squares 158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares -158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares & Dots +158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares 158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Black/White Squares & Dots 158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Black/White Squares & Dots 158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True Town Wooden Rooftop (Town): -158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Shapers & Dots & Eraser & Full Dots Town Church (Town): 158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True @@ -740,7 +740,7 @@ Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underw 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers 158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers -158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers +158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A 159332 - 0x3365F (Boat EP) - 0x09DB8 - True 159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True @@ -859,7 +859,7 @@ Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E6 158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers 158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers 158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Stars + Same Colored Symbol -158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Rotated Shapers +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Negative Shapers & Rotated Shapers 158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers Treehouse Green Bridge Front House (Treehouse): @@ -917,10 +917,10 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - True 158416 - 0x09E78 (Left Row 3) - 0x09E75 - Dots & Shapers 158417 - 0x09E79 (Left Row 4) - 0x09E78 - Shapers & Rotated Shapers 158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares -158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers +158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers & Dots 158420 - 0x09E6B (Left Row 7) - 0x09E6F - Dots 158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry -158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars +158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares 158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots 158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers 158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers @@ -933,7 +933,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots 158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol 158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers -158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry +158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares Door - 0x09FFB (Staircase Near) - 0x09FD8 Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8: @@ -1009,8 +1009,8 @@ Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5: 158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers 158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry 158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers -158472 - 0x32962 (First Floor Left) - True - Rotated Shapers -158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Shapers +158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares 158474 - 0x01A31 (First Floor Middle) - True - Colored Squares 158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser 158478 - 0x288EA (First Wooden Beam) - True - Rotated Shapers From 229a263131370570e29f027df7e3fb5a55a0f834 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:17:27 +0100 Subject: [PATCH 256/327] The Witness: Fix logic error with Symmetry Island Upper in doors: panels (broken seed reported) (#2565) Door entities think they can be solved without any other panels needing to be solved. Usually, this is true, because they no longer need to be "powered on" by a previous panel. However, there are some entities that need another entity to be powered/solved for a different reason. In this case, Symmetry Island Lower Left set opens the latches that block your ability to solve the panel. The panel itself actually starts on. Playing doors: panels does not change this, unlike usually where dependencies like this get removed by playing that mode. In the long term, I want to somehow be able to "mark" dependencies as "environmental" or "power based" so I can distinguish them properly. --- worlds/witness/player_logic.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index cfd36c09be..a4a5b04d89 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -70,15 +70,19 @@ class WitnessPlayerLogic: for items_option in these_items: all_options.add(items_option.union(dependentItem)) - # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved... - if panel_hex != "0x28A0D": - return frozenset(all_options) - # ...except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. + # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, + # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it would be wise to make a distinction between "power dependencies" and other dependencies. - if any("0x28998" in option for option in these_panels): - return frozenset(all_options) + if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): + these_items = all_options - these_items = all_options + # Another dependency that is not power-based: The Symmetry Island Upper Panel latches + elif panel_hex == 0x18269: + these_items = all_options + + # For any other door entity, we just return a set with the item that opens it & disregard power dependencies + else: + return frozenset(all_options) disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"} From 530617c9a7099c94ac7649f260809e942ef296a6 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Wed, 6 Dec 2023 18:19:03 +0100 Subject: [PATCH 257/327] sm64ex: Refactor Regions (#2546) Refactors region code to remove references to course index. There were bugs somewhere, but I dont know where tbh. This fixes them but leaves logic otherwise intact, and much cleaner to look at as there's one list less to take care of. Additionally, this fixes stopping the clock from Big Boos Haunt. --- worlds/sm64ex/Regions.py | 63 +++++++++++++-------- worlds/sm64ex/Rules.py | 115 ++++++++++++++++++++------------------ worlds/sm64ex/__init__.py | 11 ++-- 3 files changed, 107 insertions(+), 82 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index c2e9e2d981..d0e767e7ec 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -5,26 +5,43 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \ locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ - locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table + locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table -# List of all courses, including secrets, without BitS as that one is static -sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt", - "Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land", - "Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride", - "The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap", - "Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"] +# sm64paintings is dict of entrances, format LEVEL | AREA +sm64_level_to_paintings = { + 91: "Bob-omb Battlefield", + 241: "Whomp's Fortress", + 121: "Jolly Roger Bay", + 51: "Cool, Cool Mountain", + 41: "Big Boo's Haunt", + 71: "Hazy Maze Cave", + 221: "Lethal Lava Land", + 81: "Shifting Sand Land", + 231: "Dire, Dire Docks", + 101: "Snowman's Land", + 111: "Wet-Dry World", + 361: "Tall, Tall Mountain", + 132: "Tiny-Huge Island (Tiny)", + 131: "Tiny-Huge Island (Huge)", + 141: "Tick Tock Clock", + 151: "Rainbow Ride" +} +sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } +# sm64secrets is list of secret areas, same format +sm64_level_to_secrets = { + 271: "The Princess's Secret Slide", + 201: "The Secret Aquarium", + 171: "Bowser in the Dark World", + 291: "Tower of the Wing Cap", + 281: "Cavern of the Metal Cap", + 181: "Vanish Cap under the Moat", + 191: "Bowser in the Fire Sea", + 311: "Wing Mario over the Rainbow" +} +sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } -# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below -sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151] -sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"] -# sm64secrets is list of secret areas -sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311] -sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"] - -sm64entrances = sm64paintings + sm64secrets -sm64entrances_s = sm64paintings_s + sm64secrets_s -sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s)) -sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))))) +sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level } +sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets } def create_regions(world: MultiWorld, player: int): regSS = Region("Menu", player, world, "Castle Area") @@ -137,11 +154,13 @@ def create_regions(world: MultiWorld, player: int): regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM)) world.regions.append(regTTM) - regTHI = create_region("Tiny-Huge Island", player, world) - create_default_locs(regTHI, locTHI_table, player) + regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world) + create_default_locs(regTHIT, locTHI_table, player) if (world.EnableCoinStars[player].value): - regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI)) - world.regions.append(regTHI) + regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT)) + world.regions.append(regTHIT) + regTHIH = create_region("Tiny-Huge Island (Huge)", player, world) + world.regions.append(regTHIH) regFloor3 = create_region("Third Floor", player, world) world.regions.append(regFloor3) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 27b5fc8f7e..d21ac30004 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,77 +1,84 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances +from .Regions import connect_regions, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_entrances_to_level, sm64_level_to_entrances -def fix_reg(entrance_ids, reg, invalidspot, swaplist, world): - if entrance_ids.index(reg) == invalidspot: # Unlucky :C - swaplist.remove(invalidspot) - rand = world.random.choice(swaplist) - entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot] - swaplist.append(invalidspot) - swaplist.remove(rand) +def shuffle_dict_keys(world, obj: dict) -> dict: + keys = list(obj.keys()) + values = list(obj.values()) + world.random.shuffle(keys) + return dict(zip(keys,values)) -def set_rules(world, player: int, area_connections): - destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions - secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets))) - course_entrance_ids = list(range(len(sm64paintings))) - if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses - world.random.shuffle(course_entrance_ids) +def fix_reg(entrance_ids, entrance, destination, swapdict, world): + if entrance_ids[entrance] == destination: # Unlucky :C + rand = world.random.choice(swapdict.keys()) + entrance_ids[entrance], entrance_ids[swapdict[rand]] = rand, entrance_ids[entrance] + swapdict[rand] = entrance_ids[entrance] + swapdict.pop(entrance) + +def set_rules(world, player: int, area_connections: dict): + randomized_level_to_paintings = sm64_level_to_paintings.copy() + randomized_level_to_secrets = sm64_level_to_secrets.copy() + if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses + randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well - world.random.shuffle(secret_entrance_ids) - entrance_ids = course_entrance_ids + secret_entrance_ids + randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) + randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool - world.random.shuffle(entrance_ids) + randomized_entrances = shuffle_dict_keys(world,randomized_entrances) # Guarantee first entrance is a course - swaplist = list(range(len(entrance_ids))) - if entrance_ids.index(0) > 15: # Unlucky :C - rand = world.random.randint(0,15) - entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)] - swaplist.remove(entrance_ids.index(0)) + swapdict = { entrance: level for (level,entrance) in randomized_entrances } + if randomized_entrances[91] not in sm64_paintings_to_level.keys(): # Unlucky :C (91 -> BoB Entrance) + rand = world.random.choice(sm64_paintings_to_level.values()) + randomized_entrances[91], randomized_entrances[swapdict[rand]] = rand, randomized_entrances[91] + swapdict[rand] = randomized_entrances[91] + swapdict.pop("Bob-omb Battlefield") # Guarantee COTMC is not mapped to HMC, cuz thats impossible - fix_reg(entrance_ids, 20, 5, swaplist, world) + fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Hazy Maze Cave", swapdict, world) # Guarantee BITFS is not mapped to DDD - fix_reg(entrance_ids, 22, 8, swaplist, world) - if entrance_ids.index(22) == 5: # If BITFS is mapped to HMC... - fix_reg(entrance_ids, 20, 8, swaplist, world) # ... then dont allow COTMC to be mapped to DDD - temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only + fix_reg(randomized_entrances, "Bowser in the Fire Sea", "Dire, Dire Docks", swapdict, world) + if randomized_entrances[191] == "Hazy Maze Cave": # If BITFS is mapped to HMC... + fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Dire, Dire Docks", swapdict, world) # ... then dont allow COTMC to be mapped to DDD # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code - area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)}) + area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) + randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} - connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB - connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF - connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB - connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM - connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH - connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS - connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA - connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC - connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW + connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) + connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1)) + connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12)) + connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1)) + connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3)) + connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10)) + connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1)) - connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC - connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL - connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL - connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD - connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC - connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM - connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS + connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) + connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"]) + connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny - connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge + connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"]) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"]) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"]) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"]) + connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"]) + connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)") + connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)") connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value)) - connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC - connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR - connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR - connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS + connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"]) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"]) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"]) + connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) #Special Rules for some Locations add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player)) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 3cc87708e7..ab7409a324 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -5,7 +5,7 @@ from .Items import item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules -from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid +from .Regions import create_regions, sm64_level_to_entrances from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld @@ -55,8 +55,8 @@ class SM64World(World): # Write area_connections to spoiler log for entrance, destination in self.area_connections.items(): self.multiworld.spoiler.set_entrance( - sm64_internalloc_to_string[entrance] + " Entrance", - sm64_internalloc_to_string[destination], + sm64_level_to_entrances[entrance] + " Entrance", + sm64_level_to_entrances[destination], 'entrance', self.player) def create_item(self, name: str) -> Item: @@ -182,8 +182,7 @@ class SM64World(World): if self.topology_present: er_hint_data = {} for entrance, destination in self.area_connections.items(): - regionid = sm64_internalloc_to_regionid[destination] - region = self.multiworld.get_region(sm64courses[regionid], self.player) + region = self.multiworld.get_region(sm64_level_to_entrances[destination], self.player) for location in region.locations: - er_hint_data[location.address] = sm64_internalloc_to_string[entrance] + er_hint_data[location.address] = sm64_level_to_entrances[entrance] multidata['er_hint_data'][self.player] = er_hint_data From 49e1fd0b79f9efc053929203a0e32c68b2b8c0a0 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 6 Dec 2023 11:20:18 -0600 Subject: [PATCH 258/327] The Messenger: ease rule on key of strength a bit (#2541) Makes the logic for accessing key of strength just a tiny bit easier since a few players said it was really difficult. --- worlds/messenger/rules.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 793de50afb..b13a453f7f 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -63,7 +63,10 @@ class MessengerRules: "Searing Crags Seal - Triple Ball Spinner": self.has_vertical, "Searing Crags - Astral Tea Leaves": lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player), - "Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player), + "Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player) + and (self.has_dart(state) + or (self.has_wingsuit(state) + and self.can_destroy_projectiles(state))), # glacial peak "Glacial Peak Seal - Ice Climbers": self.has_dart, "Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles, From 597f94dc22f37f532a3f5d61b87af888812a4870 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:22:11 +0100 Subject: [PATCH 259/327] The Witness: Add all the Challenge panels to Challenge exclusion list (#2564) Just a small cleanup where right now, the logic still considers the entirety of the challenge "solvable" except for Challenge Vault Box --- .../settings/Postgame/Challenge_Vault_Box.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt index d65900418c..8b431694b3 100644 --- a/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt +++ b/worlds/witness/settings/Postgame/Challenge_Vault_Box.txt @@ -1,3 +1,22 @@ Disabled Locations: 0x0356B (Challenge Vault Box) 0x04D75 (Vault Door) +0x0A332 (Start Timer) +0x0088E (Small Basic) +0x00BAF (Big Basic) +0x00BF3 (Square) +0x00C09 (Maze Map) +0x00CDB (Stars and Dots) +0x0051F (Symmetry) +0x00524 (Stars and Shapers) +0x00CD4 (Big Basic 2) +0x00CB9 (Choice Squares Right) +0x00CA1 (Choice Squares Middle) +0x00C80 (Choice Squares Left) +0x00C68 (Choice Squares 2 Right) +0x00C59 (Choice Squares 2 Middle) +0x00C22 (Choice Squares 2 Left) +0x034F4 (Maze Hidden 1) +0x034EC (Maze Hidden 2) +0x1C31A (Dots Pillar) +0x1C319 (Squares Pillar) From d8004f82ef0ebb340231fcf1c1977981b4f6b443 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 6 Dec 2023 09:23:43 -0800 Subject: [PATCH 260/327] Zillion: some typing fixes (#2534) `colorama` has type stubs when it didn't before `ZillionDeltaPatch.hash` annotated type could be `None` but md5s doesn't allow `None` type of `CollectionState.prog_items` changed `WorldTestBase` moved all of the following are related to this issue: https://github.com/python/typing/discussions/1486 CommonContext for `command_processor` (is invalid without specifying immutable - but I don't need it anyway) ZillionWorld options and settings (is invalid without specifying immutable - but I do need it) --- ZillionClient.py | 6 +++--- worlds/zillion/__init__.py | 7 +++++-- worlds/zillion/logic.py | 2 +- worlds/zillion/test/__init__.py | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/ZillionClient.py b/ZillionClient.py index 7d32a72261..30f4f600a6 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,7 +1,7 @@ import asyncio import base64 import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, gui_enabled, \ @@ -10,7 +10,7 @@ from NetUtils import ClientStatus import Utils from Utils import async_start -import colorama # type: ignore +import colorama from zilliandomizer.zri.memory import Memory from zilliandomizer.zri import events @@ -45,7 +45,7 @@ class SetRoomCallback(Protocol): class ZillionContext(CommonContext): game = "Zillion" - command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor + command_processor = ZillionCommandProcessor items_handling = 1 # receive items from other players known_name: Optional[str] diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index a5e1bfe1ad..3f441d12ab 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -33,6 +33,7 @@ class ZillionSettings(settings.Group): """File name of the Zillion US rom""" description = "Zillion US ROM File" copy_to = "Zillion (UE) [!].sms" + assert ZillionDeltaPatch.hash md5s = [ZillionDeltaPatch.hash] class RomStart(str): @@ -70,9 +71,11 @@ class ZillionWorld(World): web = ZillionWebWorld() options_dataclass = ZillionOptions - options: ZillionOptions + options: ZillionOptions # type: ignore + + settings: typing.ClassVar[ZillionSettings] # type: ignore + # these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486 - settings: typing.ClassVar[ZillionSettings] topology_present = True # indicate if world type has any meaningful layout/pathing # map names to their IDs diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index 12f1875b40..305546c78b 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -41,7 +41,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) -LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]] +LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]] """ { hash: (cs.prog_items, accessible_locations) } """ diff --git a/worlds/zillion/test/__init__.py b/worlds/zillion/test/__init__.py index 3b7edebef8..93c0512fb0 100644 --- a/worlds/zillion/test/__init__.py +++ b/worlds/zillion/test/__init__.py @@ -1,5 +1,5 @@ from typing import cast -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from worlds.zillion import ZillionWorld From 56ac6573f128048bd957e246ecf85bdf502805e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 6 Dec 2023 18:24:13 +0100 Subject: [PATCH 261/327] WebHost: fix room shutdown (#2554) Currently when a room shuts down while clients are connected it instantly spins back up. This fixes that behaviour categorically. I still don't know why or when this problem started, but it's certainly wreaking havok on prod. --- WebHostLib/customserver.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 998fec5e73..fb3b314753 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -205,6 +205,12 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) await ctx.shutdown_task + + # ensure auto launch is on the same page in regard to room activity. + with db_session: + room: Room = Room.get(id=ctx.room_id) + room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60) + logging.info("Shutting down") with Locker(room_id): From 87252c14aae238ead03ec76ed9b7ca71f4920428 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:24:59 -0500 Subject: [PATCH 262/327] FFMQ: Update to FFMQR 1.5 (#2568) FFMQR was just updated to 1.5, adding a number of new options. This brings these updates to AP. --- worlds/ffmq/Items.py | 1 + worlds/ffmq/Options.py | 102 +++++++++++++++++++++++++++++++- worlds/ffmq/Output.py | 80 ++++++++++++++----------- worlds/ffmq/__init__.py | 4 +- worlds/ffmq/data/entrances.yaml | 35 +++++++++-- worlds/ffmq/data/rooms.yaml | 32 +++++----- worlds/ffmq/data/settings.yaml | 43 ++++++++++++++ 7 files changed, 239 insertions(+), 58 deletions(-) diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index 7660bd5d52..3eab5dd532 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -187,6 +187,7 @@ item_table = { "Pazuzu 5F": ItemData(None, ItemClassification.progression), "Pazuzu 6F": ItemData(None, ItemClassification.progression), "Dark King": ItemData(None, ItemClassification.progression), + "Tristam Bone Item Given": ItemData(None, ItemClassification.progression), #"Barred": ItemData(None, ItemClassification.progression), } diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index 2746bb1977..eaf3097494 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, FreeText, Toggle +from Options import Choice, FreeText, Toggle, Range class Logic(Choice): @@ -131,6 +131,21 @@ class EnemizerAttacks(Choice): default = 0 +class EnemizerGroups(Choice): + """Set which enemy groups will be affected by Enemizer.""" + display_name = "Enemizer Groups" + option_mobs_only = 0 + option_mobs_and_bosses = 1 + option_mobs_bosses_and_dark_king = 2 + default = 1 + + +class ShuffleResWeakType(Toggle): + """Resistance and Weakness types are shuffled for all enemies.""" + display_name = "Shuffle Resistance/Weakness Types" + default = 0 + + class ShuffleEnemiesPositions(Toggle): """Instead of their original position in a given map, enemies are randomly placed.""" display_name = "Shuffle Enemies' Positions" @@ -231,6 +246,81 @@ class BattlefieldsBattlesQuantities(Choice): option_random_one_through_ten = 6 +class CompanionLevelingType(Choice): + """Set how companions gain levels. + Quests: Complete each companion's individual quest for them to promote to their second version. + Quests Extended: Each companion has four exclusive quests, leveling each time a quest is completed. + Save the Crystals (All): Each time a Crystal is saved, all companions gain levels. + Save the Crystals (Individual): Each companion will level to their second version when a specific Crystal is saved. + Benjamin Level: Companions' level tracks Benjamin's.""" + option_quests = 0 + option_quests_extended = 1 + option_save_crystals_individual = 2 + option_save_crystals_all = 3 + option_benjamin_level = 4 + option_benjamin_level_plus_5 = 5 + option_benjamin_level_plus_10 = 6 + default = 0 + display_name = "Companion Leveling Type" + + +class CompanionSpellbookType(Choice): + """Update companions' spellbook. + Standard: Original game spellbooks. + Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. + Random Balanced: Randomize the spellbooks with an appropriate mix of spells. + Random Chaos: Randomize the spellbooks in total free-for-all.""" + option_standard = 0 + option_standard_extended = 1 + option_random_balanced = 2 + option_random_chaos = 3 + default = 0 + display_name = "Companion Spellbook Type" + + +class StartingCompanion(Choice): + """Set a companion to start with. + Random Companion: Randomly select one companion. + Random Plus None: Randomly select a companion, with the possibility of none selected.""" + display_name = "Starting Companion" + default = 0 + option_none = 0 + option_kaeli = 1 + option_tristam = 2 + option_phoebe = 3 + option_reuben = 4 + option_random_companion = 5 + option_random_plus_none = 6 + + +class AvailableCompanions(Range): + """Select randomly which companions will join your party. Unavailable companions can still be reached to get their items and complete their quests if needed. + Note: If a Starting Companion is selected, it will always be available, regardless of this setting.""" + display_name = "Available Companions" + default = 4 + range_start = 0 + range_end = 4 + + +class CompanionsLocations(Choice): + """Set the primary location of companions. Their secondary location is always the same. + Standard: Companions will be at the same locations as in the original game. + Shuffled: Companions' locations are shuffled amongst themselves. + Shuffled Extended: Add all the Temples, as well as Phoebe's House and the Rope Bridge as possible locations.""" + display_name = "Companions' Locations" + default = 0 + option_standard = 0 + option_shuffled = 1 + option_shuffled_extended = 2 + + +class KaelisMomFightsMinotaur(Toggle): + """Transfer Kaeli's requirements (Tree Wither, Elixir) and the two items she's giving to her mom. + Kaeli will be available to join the party right away without the Tree Wither.""" + display_name = "Kaeli's Mom Fights Minotaur" + default = 0 + + option_definitions = { "logic": Logic, "brown_boxes": BrownBoxes, @@ -238,12 +328,21 @@ option_definitions = { "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, "starting_weapon": StartingWeapon, "progressive_gear": ProgressiveGear, + "leveling_curve": LevelingCurve, + "starting_companion": StartingCompanion, + "available_companions": AvailableCompanions, + "companions_locations": CompanionsLocations, + "kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur, + "companion_leveling_type": CompanionLevelingType, + "companion_spellbook_type": CompanionSpellbookType, "enemies_density": EnemiesDensity, "enemies_scaling_lower": EnemiesScalingLower, "enemies_scaling_upper": EnemiesScalingUpper, "bosses_scaling_lower": BossesScalingLower, "bosses_scaling_upper": BossesScalingUpper, "enemizer_attacks": EnemizerAttacks, + "enemizer_groups": EnemizerGroups, + "shuffle_res_weak_types": ShuffleResWeakType, "shuffle_enemies_position": ShuffleEnemiesPositions, "progressive_formations": ProgressiveFormations, "doom_castle_mode": DoomCastle, @@ -253,6 +352,5 @@ option_definitions = { "crest_shuffle": CrestShuffle, "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, "map_shuffle_seed": MapShuffleSeed, - "leveling_curve": LevelingCurve, "battlefields_battles_quantities": BattlefieldsBattlesQuantities, } diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index c4c4605c85..98ecd28986 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -35,46 +35,58 @@ def generate_output(self, output_directory): "item_name": location.item.name}) def cc(option): - return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons") + return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", + "OverworldDungeons").replace("MobsAndBosses", "MobsBosses").replace("MobsBossesAndDarkKing", + "MobsBossesDK").replace("BenjaminLevelPlus", "BenPlus").replace("BenjaminLevel", "BenPlus0").replace( + "RandomCompanion", "Random") def tf(option): return True if option else False options = deepcopy(settings_template) options["name"] = self.multiworld.player_name[self.player] - option_writes = { - "enemies_density": cc(self.multiworld.enemies_density[self.player]), - "chests_shuffle": "Include", - "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", - "npcs_shuffle": "Include", - "battlefields_shuffle": "Include", - "logic_options": cc(self.multiworld.logic[self.player]), - "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), - "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), - "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), - "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), - "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), - "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), - "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), - "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if - self.multiworld.battlefields_battles_quantities[self.player].value < 5 else - "RandomLow" if - self.multiworld.battlefields_battles_quantities[self.player].value == 5 else - "RandomHigh", - "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), - "random_starting_weapon": True, - "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), - "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), - "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), - "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), - "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), - "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), - "enable_spoilers": False, - "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), - "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), - "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), - } + "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "chests_shuffle": "Include", + "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "npcs_shuffle": "Include", + "battlefields_shuffle": "Include", + "logic_options": cc(self.multiworld.logic[self.player]), + "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), + "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), + "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), + "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), + "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), + "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), + "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), + "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if + self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "RandomLow" if + self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + "RandomHigh", + "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "random_starting_weapon": True, + "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), + "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), + "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), + "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), + "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), + "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "enable_spoilers": False, + "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), + "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), + "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), + "enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]), + "shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]), + "companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]), + "companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]), + "starting_companion": cc(self.multiworld.starting_companion[self.player]), + "available_companions": ["Zero", "One", "Two", + "Three", "Four"][self.multiworld.available_companions[self.player].value], + "companions_locations": cc(self.multiworld.companions_locations[self.player]), + "kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]), + } + for option, data in option_writes.items(): options["Final Fantasy Mystic Quest"][option][data] = 1 @@ -83,7 +95,7 @@ def generate_output(self, output_directory): 'utf8') self.rom_name_available_event.set() - setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": + setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index b6f19a77fb..b995cc427c 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -108,8 +108,10 @@ class FFMQWorld(World): map_shuffle = multiworld.map_shuffle[world.player].value crest_shuffle = multiworld.crest_shuffle[world.player].current_key battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key + companion_shuffle = multiworld.companions_locations[world.player].value + kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key - query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}" + query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" if query in rooms_data: world.rooms = rooms_data[query] diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml index 15bcd02bf6..1dfef2655c 100644 --- a/worlds/ffmq/data/entrances.yaml +++ b/worlds/ffmq/data/entrances.yaml @@ -827,12 +827,12 @@ id: 164 area: 47 coordinates: [14, 6] - teleporter: [16, 2] + teleporter: [98, 8] # Script for reuben, original value [16, 2] - name: Fireburg - Hotel id: 165 area: 47 coordinates: [20, 8] - teleporter: [17, 2] + teleporter: [96, 8] # It's a script now for tristam, original value [17, 2] - name: Fireburg - GrenadeMan House Script id: 166 area: 47 @@ -1178,6 +1178,16 @@ area: 60 coordinates: [2, 7] teleporter: [123, 0] +- name: Lava Dome Pointless Room - Visit Quest Script 1 + id: 490 + area: 60 + coordinates: [4, 4] + teleporter: [99, 8] +- name: Lava Dome Pointless Room - Visit Quest Script 2 + id: 491 + area: 60 + coordinates: [4, 5] + teleporter: [99, 8] - name: Lava Dome Lower Moon Helm Room - Left Entrance id: 235 area: 60 @@ -1568,6 +1578,11 @@ area: 79 coordinates: [2, 45] teleporter: [174, 0] +- name: Mount Gale - Visit Quest + id: 494 + area: 79 + coordinates: [44, 7] + teleporter: [101, 8] - name: Windia - Main Entrance 1 id: 312 area: 80 @@ -1613,11 +1628,11 @@ area: 80 coordinates: [21, 39] teleporter: [30, 5] -- name: Windia - INN's Script # Change to teleporter +- name: Windia - INN's Script # Change to teleporter / Change back to script! id: 321 area: 80 coordinates: [18, 34] - teleporter: [31, 2] # Original value [79, 8] + teleporter: [97, 8] # Original value [79, 8] > [31, 2] - name: Windia - Vendor House id: 322 area: 80 @@ -1697,7 +1712,7 @@ id: 337 area: 82 coordinates: [45, 24] - teleporter: [215, 0] + teleporter: [102, 8] # Changed to script, original value [215, 0] - name: Windia Inn Lobby - Exit id: 338 area: 82 @@ -1998,6 +2013,16 @@ area: 95 coordinates: [29, 37] teleporter: [70, 8] +- name: Light Temple - Visit Quest Script 1 + id: 492 + area: 95 + coordinates: [34, 39] + teleporter: [100, 8] +- name: Light Temple - Visit Quest Script 2 + id: 493 + area: 95 + coordinates: [35, 39] + teleporter: [100, 8] - name: Ship Dock - Mobius Teleporter Script id: 397 area: 96 diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml index 4343d785eb..e0c2e8d7f9 100644 --- a/worlds/ffmq/data/rooms.yaml +++ b/worlds/ffmq/data/rooms.yaml @@ -309,13 +309,13 @@ location: "WindiaBattlefield01" location_slot: "WindiaBattlefield01" type: "BattlefieldXp" - access: [] + access: ["SandCoin", "RiverCoin"] - name: "South of Windia Battlefield" object_id: 0x14 location: "WindiaBattlefield02" location_slot: "WindiaBattlefield02" type: "BattlefieldXp" - access: [] + access: ["SandCoin", "RiverCoin"] links: - target_room: 9 # Focus Tower Windia location: "FocusTowerWindia" @@ -739,7 +739,7 @@ object_id: 0x2E type: "Box" access: [] - - name: "Kaeli 1" + - name: "Kaeli Companion" object_id: 0 type: "Trigger" on_trigger: ["Kaeli1"] @@ -838,7 +838,7 @@ - name: Sand Temple id: 24 game_objects: - - name: "Tristam Sand Temple" + - name: "Tristam Companion" object_id: 0 type: "Trigger" on_trigger: ["Tristam"] @@ -883,6 +883,11 @@ object_id: 2 type: "NPC" access: ["Tristam"] + - name: "Tristam Bone Dungeon Item Given" + object_id: 0 + type: "Trigger" + on_trigger: ["TristamBoneItemGiven"] + access: ["Tristam"] links: - target_room: 25 entrance: 59 @@ -1080,7 +1085,7 @@ object_id: 0x40 type: "Box" access: [] - - name: "Phoebe" + - name: "Phoebe Companion" object_id: 0 type: "Trigger" on_trigger: ["Phoebe1"] @@ -1846,11 +1851,11 @@ access: [] - target_room: 77 entrance: 164 - teleporter: [16, 2] + teleporter: [98, 8] # original value [16, 2] access: [] - target_room: 82 entrance: 165 - teleporter: [17, 2] + teleporter: [96, 8] # original value [17, 2] access: [] - target_room: 208 access: ["Claw"] @@ -1875,7 +1880,7 @@ object_id: 14 type: "NPC" access: ["ReubenDadSaved"] - - name: "Reuben" + - name: "Reuben Companion" object_id: 0 type: "Trigger" on_trigger: ["Reuben1"] @@ -1951,12 +1956,7 @@ - name: "Fireburg - Tristam" object_id: 10 type: "NPC" - access: [] - - name: "Tristam Fireburg" - object_id: 0 - type: "Trigger" - on_trigger: ["Tristam"] - access: [] + access: ["Tristam", "TristamBoneItemGiven"] links: - target_room: 76 entrance: 177 @@ -3183,7 +3183,7 @@ access: [] - target_room: 163 entrance: 321 - teleporter: [31, 2] + teleporter: [97, 8] access: [] - target_room: 165 entrance: 322 @@ -3292,7 +3292,7 @@ access: [] - target_room: 164 entrance: 337 - teleporter: [215, 0] + teleporter: [102, 8] access: [] - name: Windia Inn Beds id: 164 diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml index aa973ee22b..ff03ed26e6 100644 --- a/worlds/ffmq/data/settings.yaml +++ b/worlds/ffmq/data/settings.yaml @@ -73,6 +73,13 @@ Final Fantasy Mystic Quest: Chaos: 0 SelfDestruct: 0 SimpleShuffle: 0 + enemizer_groups: + MobsOnly: 0 + MobsBosses: 0 + MobsBossesDK: 0 + shuffle_res_weak_type: + true: 0 + false: 0 leveling_curve: Half: 0 Normal: 0 @@ -81,6 +88,42 @@ Final Fantasy Mystic Quest: DoubleHalf: 0 Triple: 0 Quadruple: 0 + companion_leveling_type: + Quests: 0 + QuestsExtended: 0 + SaveCrystalsIndividual: 0 + SaveCrystalsAll: 0 + BenPlus0: 0 + BenPlus5: 0 + BenPlus10: 0 + companion_spellbook_type: + Standard: 0 + StandardExtended: 0 + RandomBalanced: 0 + RandomChaos: 0 + starting_companion: + None: 0 + Kaeli: 0 + Tristam: 0 + Phoebe: 0 + Reuben: 0 + Random: 0 + RandomPlusNone: 0 + available_companions: + Zero: 0 + One: 0 + Two: 0 + Three: 0 + Four: 0 + Random14: 0 + Random04: 0 + companions_locations: + Standard: 0 + Shuffled: 0 + ShuffledExtended: 0 + kaelis_mom_fight_minotaur: + true: 0 + false: 0 battles_quantity: Ten: 0 Seven: 0 From 3fa01a41cde73e47512592b4b513514f21b00513 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 7 Dec 2023 06:36:46 +0100 Subject: [PATCH 263/327] The Witness: Fix unreachable locations on certain settings (Keep PP2 EP, Theater Flowers EP) (#2499) Basically, the function for "checking entrances both ways" only checked one way. This resulted in unreachable locations. This affects Expert seeds with (non-remote doors and specific types of EP Shuffle), as well as seeds with non-remote doors + specific types of disabled panels + specific types of EP Shuffle. Also includes two changes that makes spoiler logs nicer (not creating unnecessary events). --- worlds/witness/player_logic.py | 4 ++-- worlds/witness/regions.py | 2 +- worlds/witness/rules.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index a4a5b04d89..73253efc6e 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -375,7 +375,7 @@ class WitnessPlayerLogic: if lasers: adjustment_linesets_in_order.append(get_laser_shuffle()) - if world.options.shuffle_EPs: + if world.options.shuffle_EPs == "obelisk_sides": ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items() if ep_obj["entityType"] == "EP") @@ -489,7 +489,7 @@ class WitnessPlayerLogic: self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): - if event_hex in self.COMPLETELY_DISABLED_ENTITIES: + if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: continue self.EVENT_PANELS.add(event_hex) diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 2187010bac..e097024805 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -71,7 +71,7 @@ class WitnessRegions: source_region.exits.append(connection) connection.connect(target_region) - self.created_entrances[(source, target)].append(connection) + self.created_entrances[source, target].append(connection) # Register any necessary indirect connections mentioned_regions = { diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 07fea23b14..75c662ac0f 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -66,8 +66,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool: - entrance_forward = regio.created_entrances[(source, target)] - entrance_backward = regio.created_entrances[(source, target)] + entrance_forward = regio.created_entrances[source, target] + entrance_backward = regio.created_entrances[target, source] return ( any(entrance.can_reach(state) for entrance in entrance_forward) From 57001ced0f7ac33241e1ff0b64b7c3c0fe179b0d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 7 Dec 2023 01:22:12 -0600 Subject: [PATCH 264/327] The Messenger: remove old links and update relevant ones (#2542) --- worlds/messenger/docs/en_The Messenger.md | 6 ++---- worlds/messenger/docs/setup_en.md | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 4ffe041830..374753b487 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -1,12 +1,10 @@ # The Messenger ## Quick Links -- [Setup](../../../../tutorial/The%20Messenger/setup/en) -- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Setup](/tutorial/The%20Messenger/setup/en) +- [Options Page](/games/The%20Messenger/player-options) - [Courier Github](https://github.com/Brokemia/Courier) -- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod) - [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) -- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) - [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) ## What does randomization do in this game? diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index d93d13b274..9617baf3e0 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -1,16 +1,15 @@ # The Messenger Randomizer Setup Guide ## Quick Links -- [Game Info](../../../../games/The%20Messenger/info/en) -- [Settings Page](../../../../games/The%20Messenger/player-settings) +- [Game Info](/games/The%20Messenger/info/en) +- [Options Page](/games/The%20Messenger/player-options) - [Courier Github](https://github.com/Brokemia/Courier) - [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP) -- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker) - [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack) ## Installation -1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues +1. Read the [Game Info Page](/games/The%20Messenger/info/en) for how the game works, caveats and known issues 2. Download and install Courier Mod Loader using the instructions on the release page * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) 3. Download and install the randomizer mod From 69ae12823a09f3b407370370c7dc5d04e44d0326 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 7 Dec 2023 01:23:05 -0600 Subject: [PATCH 265/327] The Messenger: bump required client version (#2544) Co-authored-by: Fabian Dill --- worlds/messenger/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index d569dd7542..b0d031905c 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -62,7 +62,7 @@ class MessengerWorld(World): "Money Wrench", ], base_offset)} - required_client_version = (0, 4, 1) + required_client_version = (0, 4, 2) web = MessengerWeb() From 5bd022138bff13347452016e30dd95996b7ea08e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 7 Dec 2023 11:15:38 -0800 Subject: [PATCH 266/327] Pokemon Emerald: Fix missing rule for 2 items on Route 120 (#2570) Two items on Route 120 are on the other side of a pond but were considered accessible in logic without Surf. Creates a new separate region for these two items and adds a rule for being able to Surf to get to this region. Also adds the items to the existing surf test. --- worlds/pokemon_emerald/data/regions/routes.json | 13 +++++++++++-- worlds/pokemon_emerald/rules.py | 4 ++++ worlds/pokemon_emerald/test/test_accessibility.py | 8 +++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_emerald/data/regions/routes.json b/worlds/pokemon_emerald/data/regions/routes.json index 029aa85c3c..f4b8d935c3 100644 --- a/worlds/pokemon_emerald/data/regions/routes.json +++ b/worlds/pokemon_emerald/data/regions/routes.json @@ -1106,21 +1106,30 @@ "parent_map": "MAP_ROUTE120", "locations": [ "ITEM_ROUTE_120_NUGGET", - "ITEM_ROUTE_120_FULL_HEAL", "ITEM_ROUTE_120_REVIVE", "ITEM_ROUTE_120_HYPER_POTION", - "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2", "HIDDEN_ITEM_ROUTE_120_ZINC" ], "events": [], "exits": [ "REGION_ROUTE120/NORTH", + "REGION_ROUTE120/SOUTH_PONDS", "REGION_ROUTE121/WEST" ], "warps": [ "MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0" ] }, + "REGION_ROUTE120/SOUTH_PONDS": { + "parent_map": "MAP_ROUTE120", + "locations": [ + "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2", + "ITEM_ROUTE_120_FULL_HEAL" + ], + "events": [], + "exits": [], + "warps": [] + }, "REGION_ROUTE121/WEST": { "parent_map": "MAP_ROUTE121", "locations": [ diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 97110746fb..564bf5af8d 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -626,6 +626,10 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"), can_surf ) + set_rule( + get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"), + can_surf + ) # Route 121 set_rule( diff --git a/worlds/pokemon_emerald/test/test_accessibility.py b/worlds/pokemon_emerald/test/test_accessibility.py index da3ca058be..853a92ffb8 100644 --- a/worlds/pokemon_emerald/test/test_accessibility.py +++ b/worlds/pokemon_emerald/test/test_accessibility.py @@ -44,13 +44,17 @@ class TestScorchedSlabPond(PokemonEmeraldTestBase): class TestSurf(PokemonEmeraldTestBase): options = { - "npc_gifts": Toggle.option_true + "npc_gifts": Toggle.option_true, + "hidden_items": Toggle.option_true, + "require_itemfinder": Toggle.option_false } def test_inaccessible_with_no_surf(self) -> None: self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER"))) self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL"))) self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL"))) + self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2"))) + self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL"))) self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST")) self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN")) self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0")) @@ -60,6 +64,8 @@ class TestSurf(PokemonEmeraldTestBase): self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER"))) self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL"))) self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL"))) + self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2"))) + self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL"))) self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST")) self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN")) self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0")) From bf801a1efe83cc894acb5e140a24dc962c02a3d9 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:40:44 +0100 Subject: [PATCH 267/327] The Witness: Fix Symmetry Island Upper Panel logic (2nd try) I got lazy and didn't properly test the last fix. Big apologies, I got a bit panicked with all the logic errors that were being found. --- worlds/witness/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 73253efc6e..e1ef1ae431 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -77,7 +77,7 @@ class WitnessPlayerLogic: these_items = all_options # Another dependency that is not power-based: The Symmetry Island Upper Panel latches - elif panel_hex == 0x18269: + elif panel_hex == "0x1C349": these_items = all_options # For any other door entity, we just return a set with the item that opens it & disregard power dependencies From abfc2ddfed783f30d3cf7880d6db8881d1de2432 Mon Sep 17 00:00:00 2001 From: beauxq Date: Thu, 7 Dec 2023 13:17:07 -0800 Subject: [PATCH 268/327] Zillion: fix retrieved packet processing --- ZillionClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZillionClient.py b/ZillionClient.py index 30f4f600a6..5f3cbb943f 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -278,7 +278,7 @@ class ZillionContext(CommonContext): logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") return keys = cast(Dict[str, Optional[str]], args["keys"]) - doors_b64 = keys[f"zillion-{self.auth}-doors"] + doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) if doors_b64: logger.info("received door data from server") doors = base64.b64decode(doors_b64) From 9351fb45caab39a76e2773258a0816ce59aeb9a7 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 8 Dec 2023 01:17:12 -0500 Subject: [PATCH 269/327] SA2B: Fix KeyError on Unexpected Characters in Slot Names (#2571) There were no safeguards on characters being used as keys into a conversion dict. Now there are. --- worlds/sa2b/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 4ee03dce9d..7d77aebc4c 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -619,7 +619,7 @@ class SA2BWorld(World): for name in name_list_base: for char_idx in range(7): if char_idx < len(name): - name_list_s.append(chao_name_conversion[name[char_idx]]) + name_list_s.append(chao_name_conversion.get(name[char_idx], 0x5F)) else: name_list_s.append(0x00) From a9a6c72d2c909a502c76d5b114ca6b0cf353bd6b Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:39:24 -0500 Subject: [PATCH 270/327] KH2: Fix events in datapackage (#2576) --- worlds/kh2/Regions.py | 3 +-- worlds/kh2/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/kh2/Regions.py b/worlds/kh2/Regions.py index aceab97f37..6dd8313107 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -1020,10 +1020,9 @@ def create_regions(self): multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in KH2REGIONS.items()] # fill the event locations with events - multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table}) for location, item in event_location_to_item.items(): multiworld.get_location(location, player).place_locked_item( - multiworld.worlds[player].create_item(item)) + multiworld.worlds[player].create_event_item(item)) def connect_regions(self): diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 69f844f45a..dd57f5e759 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -119,11 +119,15 @@ class KH2World(World): item_classification = ItemClassification.useful else: item_classification = ItemClassification.filler - created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player) return created_item + def create_event_item(self, name: str) -> Item: + item_classification = ItemClassification.progression + created_item = KH2Item(name, item_classification, None, self.player) + return created_item + def create_items(self) -> None: """ Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking. From f10431779b9525644b649c92e67977df03359d80 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:33:51 -0500 Subject: [PATCH 271/327] ALTTP: Ensure all Hyrule Castle keys are local in Standard (#2582) --- worlds/alttp/ItemPool.py | 2 -- worlds/alttp/__init__.py | 17 +++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 88a2d899fc..1c3f3e44f7 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -682,8 +682,6 @@ def get_pool_core(world, player: int): key_location = world.random.choice(key_locations) place_item(key_location, "Small Key (Universal)") pool = pool[:-3] - if world.key_drop_shuffle[player]: - pass # pool.extend([item_to_place] * (len(key_drop_data) - 1)) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 32667249f2..3f380d0037 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -289,12 +289,17 @@ class ALTTPWorld(World): self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options) self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options) - if multiworld.mode[player] == 'standard' \ - and multiworld.smallkey_shuffle[player] \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \ - and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with: - self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + if multiworld.mode[player] == 'standard': + if multiworld.smallkey_shuffle[player]: + if (multiworld.smallkey_shuffle[player] not in + (smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons, + smallkey_shuffle.option_start_with)): + self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1 + self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)") + if multiworld.bigkey_shuffle[player]: + self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)") + self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)") # system for sharing ER layouts self.er_seed = str(multiworld.random.randint(0, 2 ** 64)) From 3214cef6cf64451de1d4d9305f1ac82085f02125 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 9 Dec 2023 22:23:40 -0500 Subject: [PATCH 272/327] TLOZ: Fix starting weapon possibly getting overwritten by triforce fragments (#2578) As discovered by this bug report https://discord.com/channels/731205301247803413/1182522267687731220 it's currently possible to accidentally have the starting weapon of a player overwritten by a triforce fragment if TriforceLocations is set to dungeons and StartingPosition is set to dangerous. This fix makes sure to remove the location of a placed starting weapon if said location is in a dungeon from the pool of possible locations that triforce fragments can be placed in this circumstance. --- worlds/tloz/ItemPool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 1d33336172..7773accd8d 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -117,6 +117,9 @@ def get_pool_core(world): else: possible_level_locations = [location for location in standard_level_locations if location not in level_locations[8]] + for location in placed_items.keys(): + if location in possible_level_locations: + possible_level_locations.remove(location) for level in range(1, 9): if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla: placed_items[f"Level {level} Triforce"] = fragment From c3184e7b19c65a7ae649d5878ea97e068080967b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 06:10:01 +0100 Subject: [PATCH 273/327] Factorio: fix wrong parent class for FactorioStartItems (#2587) --- worlds/factorio/Options.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 18eee67e03..b72d57ad9b 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -2,7 +2,7 @@ from __future__ import annotations import typing import datetime -from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ StartInventoryPool from schema import Schema, Optional, And, Or @@ -207,10 +207,9 @@ class RecipeIngredientsOffset(Range): range_end = 5 -class FactorioStartItems(ItemDict): +class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - verify_item_name = False default = {"burner-mining-drill": 19, "stone-furnace": 19} From b0a09f67f4f38fac15f90f57e4b9bae78e37f357 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 9 Dec 2023 21:43:17 -0800 Subject: [PATCH 274/327] Core: some typing and documentation in BaseClasses.py (#2589) --- BaseClasses.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7965eb8b0d..c0a77708c0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -491,7 +491,7 @@ class MultiWorld(): else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state: Optional[CollectionState] = None): + def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool: if starting_state: if self.has_beaten_game(starting_state): return True @@ -504,7 +504,7 @@ class MultiWorld(): and location.item.advancement and location not in state.locations_checked} while prog_locations: - sphere = set() + sphere: Set[Location] = set() # build up spheres of collection radius. # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in prog_locations: @@ -524,12 +524,19 @@ class MultiWorld(): return False - def get_spheres(self): + def get_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of locations 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(self.get_filled_locations()) while locations: - sphere = set() + sphere: Set[Location] = set() for location in locations: if location.can_reach(state): From 6b0eb7da7989bd7e1516118bcac84b1cc0984452 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 10 Dec 2023 12:58:52 -0500 Subject: [PATCH 275/327] KH2: RC1 Bug Fixes (#2530) Changes the finished_game to new variable so now it only checks the game's memory and if it has sent the finished flag before Fixed ag2 not requiring 1 of each black magic Fix hitlist if you exclude summon level 7 and have summon levels option turned off --- worlds/kh2/Client.py | 5 +++-- worlds/kh2/Rules.py | 2 +- worlds/kh2/__init__.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index be85dc6907..a5be06c7fb 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -29,6 +29,7 @@ class KH2Context(CommonContext): self.kh2_local_items = None self.growthlevel = None self.kh2connected = False + self.kh2_finished_game = False self.serverconneced = False self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} self.location_name_to_data = {name: data for name, data, in all_locations.items()} @@ -833,9 +834,9 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.create_task(ctx.verifyItems()) await asyncio.create_task(ctx.verifyLevel()) message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] - if finishedGame(ctx, message): + if finishedGame(ctx, message) and not ctx.kh2_finished_game: await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) - ctx.finished_game = True + ctx.kh2_finished_game = True await ctx.send_msgs(message) elif not ctx.kh2connected and ctx.serverconneced: logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 18375231a5..41207c6cb3 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules): RegionName.Pl2: lambda state: self.pl_unlocked(state, 2), RegionName.Ag: lambda state: self.ag_unlocked(state, 1), - RegionName.Ag2: lambda state: self.ag_unlocked(state, 2), + RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state), RegionName.Bc: lambda state: self.bc_unlocked(state, 1), RegionName.Bc2: lambda state: self.bc_unlocked(state, 2), diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index dd57f5e759..2bddbd5ec3 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -465,7 +465,7 @@ class KH2World(World): if location in self.random_super_boss_list: self.random_super_boss_list.remove(location) - if not self.options.SummonLevelLocationToggle: + if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list: self.random_super_boss_list.remove(LocationName.Summonlvl7) # Testing if the player has the right amount of Bounties for Completion. From 6cd5abdc117328fb327168bae2d43a379b64e8f5 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:07:56 -0500 Subject: [PATCH 276/327] SMZ3: KeyTH check fix (#2574) --- worlds/smz3/TotalSMZ3/Patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 049b200c46..c137442d9b 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -319,7 +319,7 @@ class Patch: def WriteZ3Locations(self, locations: List[Location]): for location in locations: if (location.Type == LocationType.HeraStandingKey): - self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) + self.patches.append((Snes(0x9E3BB), [0xEB])) elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) if (location.Type == LocationType.Pedestal): From 1312884fa2deb33546a500fe743e371492b67806 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:10:09 -0500 Subject: [PATCH 277/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20Silph=20Co?= =?UTF-8?q?=206F=20Hostage=20(#2524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes an issue where the Silph Co 6F hostage check becomes unavailable if Giovanni has been defeated on 11F. This is due to the NPC having separate scripts depending on whether Giovanni was defeated. The code for the check has been moved to before the branch. --- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45946 -> 45872 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45892 -> 45839 bytes worlds/pokemon_rb/rom_addresses.py | 12 ++++++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index bee5a8d2f4996ee1db54d2b7812938cc66dcb922..5ccf4e9bbaf80f0167471e02dd64384b5bb0e622 100644 GIT binary patch literal 45872 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 AWB>pF literal 45946 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

    ^aKX)H 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 O;_gVN3K9a`DoQ|XZiR;c literal 45892 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 UboM_(lt27k$rRy2K>Qy9z{TtP>;M1& diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index cd57e317bd..ffb89a4dfc 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -168,12 +168,12 @@ rom_addresses = { "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a, "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638, - "Event_SKC6F": 0x1a666, - "Warps_SilphCo6F": 0x1a741, - "Missable_Silph_Co_6F_Item_1": 0x1a791, - "Missable_Silph_Co_6F_Item_2": 0x1a798, - "Path_Pallet_Oak": 0x1a91e, - "Path_Pallet_Player": 0x1a92b, + "Event_SKC6F": 0x1a659, + "Warps_SilphCo6F": 0x1a737, + "Missable_Silph_Co_6F_Item_1": 0x1a787, + "Missable_Silph_Co_6F_Item_2": 0x1a78e, + "Path_Pallet_Oak": 0x1a914, + "Path_Pallet_Player": 0x1a921, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, From 19b8624818212739199a50bb73275649d99c9d9d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 19:11:57 +0100 Subject: [PATCH 278/327] Factorio: remove staging folder for mod assembly (#2519) --- worlds/factorio/Mod.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index c897e72dcd..21a8c684f9 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -5,7 +5,7 @@ import os import shutil import threading import zipfile -from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple +from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union import jinja2 @@ -63,7 +63,7 @@ recipe_time_ranges = { class FactorioModFile(worlds.Files.APContainer): game = "Factorio" compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives - writing_tasks: List[Callable[[], Tuple[str, str]]] + writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]] def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -164,9 +164,7 @@ def generate_mod(world: "Factorio", output_directory: str): template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) - mod_dir = os.path.join(output_directory, versioned_mod_name) - - zf_path = os.path.join(mod_dir + ".zip") + zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) if world.zip_path: @@ -177,7 +175,13 @@ def generate_mod(world: "Factorio", output_directory: str): mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file): (arcpath, content)) else: - shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True) + basepath = os.path.join(os.path.dirname(__file__), "data", "mod") + for dirpath, dirnames, filenames in os.walk(basepath): + base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + for filename in filenames: + mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, + file_path=os.path.join(dirpath, filename): + (arcpath, open(file_path, "rb").read())) mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua", data_template.render(**template_data))) @@ -197,5 +201,3 @@ def generate_mod(world: "Factorio", output_directory: str): # write the mod file mod.write() - # clean up - shutil.rmtree(mod_dir) From 01d0c052595ecd61b61d51b2b3b766b3e4cd22a1 Mon Sep 17 00:00:00 2001 From: Rjosephson Date: Sun, 10 Dec 2023 11:12:46 -0700 Subject: [PATCH 279/327] RoR2: Remove begin with loop (#2518) --- worlds/ror2/__init__.py | 10 ++++------ worlds/ror2/options.py | 9 --------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 8735ce81fd..6574a176dc 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -103,12 +103,10 @@ class RiskOfRainWorld(World): if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} - environments_to_precollect = 5 if self.options.begin_with_loop else 1 - # percollect environments for each stage (or just stage 1) - for i in range(environments_to_precollect): - unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) - self.multiworld.push_precollected(self.create_item(unlock[0])) - environments_pool.pop(unlock[0]) + # percollect starting environment for stage 1 + unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) + self.multiworld.push_precollected(self.create_item(unlock[0])) + environments_pool.pop(unlock[0]) # Generate item pool itempool: List[str] = ["Beads of Fealty", "Radar Scanner"] diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 7daf8a8446..abb8e91da2 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -142,14 +142,6 @@ class FinalStageDeath(Toggle): display_name = "Final Stage Death is Win" -class BeginWithLoop(Toggle): - """ - Enable to precollect a full loop of environments. - Only has an effect with Explore Mode. - """ - display_name = "Begin With Loop" - - class DLC_SOTV(Toggle): """ Enable if you are using SOTV DLC. @@ -385,7 +377,6 @@ class ROR2Options(PerGameCommonOptions): total_revivals: TotalRevivals start_with_revive: StartWithRevive final_stage_death: FinalStageDeath - begin_with_loop: BeginWithLoop dlc_sotv: DLC_SOTV death_link: DeathLink item_pickup_step: ItemPickupStep From d3b09bde1274fae368ebb2965d6c40a1524970ab Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 10 Dec 2023 13:15:42 -0500 Subject: [PATCH 280/327] Lingo: Fix entrance checking being broken on default settings (#2506) The most serious issue this PR addresses is that entrances that use doors without items (a small subset of doors when door shuffle is on, but *every* door when door shuffle is off, which is the default) underestimate the requirements needed to use that entrance. The logic would calculate the panels needed to open the door, but would neglect to keep track of the rooms those panels were in, meaning that doors would be considered openable if you had the colors needed to solve a panel that's in a room you have no access to. Another issue is that, previously, logic would always consider the "ANOTHER TRY" panel accessible for the purposes of the LEVEL 2 panel hunt. This could result in seeds where the player is expected to have exactly the correct number of solves to reach LEVEL 2, but in reality is short by one because ANOTHER TRY itself is not revealed until the panel hunt is complete. This change marks ANOTHER TRY as non-counting, because even though it is technically a counting panel in-game, it can never contribute to the LEVEL 2 panel hunt. This issue could also apply to THE MASTER, since it is the only other counting panel with special access rules, although it is much less likely. This change adds special handling for counting THE MASTER. These issues were possible to manifest whenever the LEVEL 2 panel hunt was enabled, which it is by default. Smaller logic issues also fixed in this PR: * The Orange Tower Basement MASTERY panel was marked as requiring the mastery doors to be opened, when it was actually possible to get it without them by using a painting to get into the room. * The Pilgrim Room painting item was incorrectly being marked as a filler item, despite it being progression. * There has been another update to the game that adds connections between areas that were previously not connected. These changes were additive, which is why they are not critical. * The panel stacks in the rhyme room now require both colours on each panel. --- worlds/lingo/__init__.py | 6 +- worlds/lingo/data/LL1.yaml | 112 ++++++++++++++++++++------ worlds/lingo/data/ids.yaml | 5 +- worlds/lingo/player_logic.py | 31 +++++-- worlds/lingo/regions.py | 15 +--- worlds/lingo/rules.py | 23 ++---- worlds/lingo/utils/validate_config.rb | 2 +- 7 files changed, 131 insertions(+), 63 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index a8dac86221..f22d344c8f 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -104,9 +104,11 @@ class LingoWorld(World): classification = item.classification if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\ and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping - for painting_id in item.painting_ids): + for painting_id in item.painting_ids)\ + and "pilgrim_painting2" not in item.painting_ids: # If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings - # go nowhere, then this item should not be progression. + # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be + # excluded from this. classification = ItemClassification.filler return LingoItem(name, classification, item.code, self.player) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 8a4f831f94..ea5886fea0 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -373,6 +373,7 @@ ANOTHER TRY: id: Entry Room/Panel_advance tag: topwhite + non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt. LEVEL 2: # We will set up special rules for this in code. id: EndPanel/Panel_level_2 @@ -1033,6 +1034,8 @@ Hallway Room (3): True Hallway Room (4): True Hedge Maze: True # through the door to the sectioned-off part of the hedge maze + Cellar: + door: Lookout Entrance panels: MASSACRED: id: Palindrome Room/Panel_massacred_sacred @@ -1168,11 +1171,21 @@ - KEEP - BAILEY - TOWER + Lookout Entrance: + id: Cross Room Doors/Door_missing + location_name: Outside The Agreeable - Lookout Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: panda_painting orientation: south - id: eyes_yellow_painting orientation: east + - id: pencil_painting7 + orientation: north progression: Progressive Hallway Room: - Hallway Door @@ -2043,7 +2056,7 @@ door: Sixth Floor Cellar: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit Welcome Back Area: door: Welcome Back Art Gallery: @@ -2302,9 +2315,6 @@ id: Master Room/Panel_mastery_mastery3 tag: midwhite hunt: True - required_door: - room: Orange Tower Seventh Floor - door: Mastery THE LIBRARY: id: EndPanel/Panel_library check: True @@ -2675,6 +2685,10 @@ Outside The Undeterred: True Outside The Agreeable: True Outside The Wanderer: True + The Observant: True + Art Gallery: True + The Scientific: True + Cellar: True Orange Tower Fifth Floor: room: Orange Tower Fifth Floor door: Welcome Back @@ -2991,8 +3005,7 @@ PATS: id: Rhyme Room/Panel_wrath_path colors: purple - tag: midpurp and rhyme - copy_to_sign: sign15 + tag: forbid KNIGHT: id: Rhyme Room/Panel_knight_write colors: purple @@ -3158,6 +3171,8 @@ door: Painting Shortcut painting: True Room Room: True # trapdoor + Outside The Agreeable: + painting: True panels: UNOPEN: id: Truncate Room/Panel_unopened_open @@ -6299,17 +6314,22 @@ SKELETON: id: Double Room/Panel_bones_syn tag: syn rhyme + colors: purple subtag: bot link: rhyme BONES REPENTANCE: id: Double Room/Panel_sentence_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme SENTENCE WORD: id: Double Room/Panel_sentence_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme SENTENCE @@ -6321,6 +6341,7 @@ link: rhyme DREAM FANTASY: id: Double Room/Panel_dream_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DREAM @@ -6332,6 +6353,7 @@ link: rhyme MYSTERY SECRET: id: Double Room/Panel_mystery_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme MYSTERY @@ -6386,25 +6408,33 @@ door: Nines FERN: id: Double Room/Panel_return_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme RETURN STAY: id: Double Room/Panel_return_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme RETURN FRIEND: id: Double Room/Panel_descend_rhyme - colors: purple + colors: + - purple + - black tag: ant rhyme subtag: top link: rhyme DESCEND RISE: id: Double Room/Panel_descend_ant - colors: black + colors: + - purple + - black tag: ant rhyme subtag: bot link: rhyme DESCEND @@ -6416,6 +6446,7 @@ link: rhyme JUMP BOUNCE: id: Double Room/Panel_jump_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme JUMP @@ -6427,6 +6458,7 @@ link: rhyme FALL PLUNGE: id: Double Room/Panel_fall_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme FALL @@ -6456,13 +6488,17 @@ panels: BIRD: id: Double Room/Panel_word_rhyme - colors: purple + colors: + - purple + - blue tag: whole rhyme subtag: top link: rhyme WORD LETTER: id: Double Room/Panel_word_whole - colors: blue + colors: + - purple + - blue tag: whole rhyme subtag: bot link: rhyme WORD @@ -6474,6 +6510,7 @@ link: rhyme HIDDEN CONCEALED: id: Double Room/Panel_hidden_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme HIDDEN @@ -6485,6 +6522,7 @@ link: rhyme SILENT MUTE: id: Double Room/Panel_silent_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme SILENT @@ -6531,6 +6569,7 @@ link: rhyme BLOCKED OBSTRUCTED: id: Double Room/Panel_blocked_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme BLOCKED @@ -6542,6 +6581,7 @@ link: rhyme RISE SWELL: id: Double Room/Panel_rise_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme RISE @@ -6553,6 +6593,7 @@ link: rhyme ASCEND CLIMB: id: Double Room/Panel_ascend_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme ASCEND @@ -6564,6 +6605,7 @@ link: rhyme DOUBLE DUPLICATE: id: Double Room/Panel_double_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme DOUBLE @@ -6642,6 +6684,7 @@ link: rhyme CHILD KID: id: Double Room/Panel_child_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CHILD @@ -6653,6 +6696,7 @@ link: rhyme CRYSTAL QUARTZ: id: Double Room/Panel_crystal_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CRYSTAL @@ -6664,6 +6708,7 @@ link: rhyme CREATIVE INNOVATIVE (Bottom): id: Double Room/Panel_creative_syn + colors: purple tag: syn rhyme subtag: bot link: rhyme CREATIVE @@ -6882,7 +6927,7 @@ event: True panels: - WALL (1) - Shortcut to Fifth Floor: + Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 @@ -6895,7 +6940,10 @@ door: Excavation Orange Tower Fifth Floor: room: Room Room - door: Shortcut to Fifth Floor + door: Cellar Exit + Outside The Agreeable: + room: Outside The Agreeable + door: Lookout Entrance Outside The Wise: entrances: Orange Tower Sixth Floor: @@ -7319,49 +7367,65 @@ link: change GRAVITY PART: id: Chemistry Room/Panel_physics_2 - colors: blue + colors: + - blue + - red tag: blue mid red bot subtag: mid link: xur PARTICLE MATTER: id: Chemistry Room/Panel_physics_1 - colors: red + colors: + - blue + - red tag: blue mid red bot subtag: bot link: xur PARTICLE ELECTRIC: id: Chemistry Room/Panel_physics_6 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr ELECTRON ATOM (1): id: Chemistry Room/Panel_physics_3 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr ELECTRON NEUTRAL: id: Chemistry Room/Panel_physics_7 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr NEUTRON ATOM (2): id: Chemistry Room/Panel_physics_4 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr NEUTRON PROPEL: id: Chemistry Room/Panel_physics_8 - colors: purple + colors: + - purple + - red tag: purple mid red bot subtag: mid link: xpr PROTON ATOM (3): id: Chemistry Room/Panel_physics_5 - colors: red + colors: + - purple + - red tag: purple mid red bot subtag: bot link: xpr PROTON diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 1a1ceca24a..3239f21854 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1064,6 +1064,9 @@ doors: Hallway Door: item: 444459 location: 445214 + Lookout Entrance: + item: 444579 + location: 445271 Dread Hallway: Tenacious Entrance: item: 444462 @@ -1402,7 +1405,7 @@ doors: item: 444570 location: 445266 Room Room: - Shortcut to Fifth Floor: + Cellar Exit: item: 444571 location: 445076 Outside The Wise: diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index a0b33d1dbe..b046f1cfe3 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -190,6 +190,25 @@ class LingoPlayerLogic: if item.should_include(world): self.real_items.append(name) + # Calculate the requirements for the fake pilgrimage. + fake_pilgrimage = [ + ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], + ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], + ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], + ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], + ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], + ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], + ["Outside The Agreeable", "Tenacious Entrance"] + ] + pilgrimage_reqs = AccessRequirements() + for door in fake_pilgrimage: + door_object = DOORS_BY_ROOM[door[0]][door[1]] + if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world)) + else: + pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1])) + self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs + # Create the paintings mapping, if painting shuffle is on. if painting_shuffle: # Shuffle paintings until we get something workable. @@ -369,11 +388,9 @@ class LingoPlayerLogic: door_object = DOORS_BY_ROOM[room][door] for req_panel in door_object.panels: - if req_panel.room is not None and req_panel.room != room: - access_reqs.rooms.add(req_panel.room) - - sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room, - req_panel.panel, world) + panel_room = room if req_panel.room is None else req_panel.room + access_reqs.rooms.add(panel_room) + sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world) access_reqs.merge(sub_access_reqs) self.door_reqs[room][door] = access_reqs @@ -397,8 +414,8 @@ class LingoPlayerLogic: unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): - # We won't count non-counting panels. - if panel_data.non_counting: + # We won't count non-counting panels. THE MASTER has special access rules and is handled separately. + if panel_data.non_counting or panel_name == "THE MASTER": continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index c24144a160..bdc42f42f5 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -4,7 +4,7 @@ from BaseClasses import Entrance, ItemClassification, Region from .items import LingoItem from .locations import LingoLocation from .player_logic import LingoPlayerLogic -from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda +from .rules import lingo_can_use_entrance, make_location_lambda from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor if TYPE_CHECKING: @@ -25,15 +25,6 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi return new_region -def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: - target_region = regions["Pilgrim Antechamber"] - source_region = regions["Outside The Agreeable"] - source_region.connect( - target_region, - "Pilgrimage", - lambda state: lingo_can_use_pilgrimage(state, world, player_logic)) - - def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str, door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic): connection = Entrance(world.player, description, source_region) @@ -91,7 +82,9 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None: connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world, player_logic) - handle_pilgrim_room(regions, world, player_logic) + # Add the fake pilgrimage. + connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage", + RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic) if early_color_hallways: regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways") diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index ee9dcc4192..481fab18b5 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -17,23 +17,6 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, return _lingo_can_open_door(state, effective_room, door.door, world, player_logic) -def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic): - fake_pilgrimage = [ - ["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"], - ["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"], - ["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"], - ["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"], - ["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"], - ["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"], - ["Outside The Agreeable", "Tenacious Entrance"] - ] - for entrance in fake_pilgrimage: - if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic): - return False - - return True - - def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic): return _lingo_can_satisfy_requirements(state, location.access, world, player_logic) @@ -56,6 +39,12 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True + # THE MASTER has to be handled separately, because it has special access rules. + if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ + and lingo_can_use_mastery_location(state, world, player_logic): + counted_panels += 1 + if counted_panels >= world.options.level_2_requirement.value - 1: + return True return False diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index bed5188e31..3ac49dc220 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -40,7 +40,7 @@ mentioned_panels = Set[] door_groups = {} directives = Set["entrances", "panels", "doors", "paintings", "progression"] -panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"] +panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"] door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] From 3a096773331a6a701c1f57402d9a5c778887760e Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Sun, 10 Dec 2023 20:31:43 +0100 Subject: [PATCH 281/327] sm64ex: Fix generations (#2583) --- worlds/sm64ex/Regions.py | 86 +++++++++++++++++++++++++++------------- worlds/sm64ex/Rules.py | 37 ++++++++--------- 2 files changed, 77 insertions(+), 46 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index d0e767e7ec..d426804c30 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -1,4 +1,7 @@ import typing + +from enum import Enum + from BaseClasses import MultiWorld, Region, Entrance, Location from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \ locBBH_table, \ @@ -7,36 +10,63 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \ locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table -# sm64paintings is dict of entrances, format LEVEL | AREA -sm64_level_to_paintings = { - 91: "Bob-omb Battlefield", - 241: "Whomp's Fortress", - 121: "Jolly Roger Bay", - 51: "Cool, Cool Mountain", - 41: "Big Boo's Haunt", - 71: "Hazy Maze Cave", - 221: "Lethal Lava Land", - 81: "Shifting Sand Land", - 231: "Dire, Dire Docks", - 101: "Snowman's Land", - 111: "Wet-Dry World", - 361: "Tall, Tall Mountain", - 132: "Tiny-Huge Island (Tiny)", - 131: "Tiny-Huge Island (Huge)", - 141: "Tick Tock Clock", - 151: "Rainbow Ride" +class SM64Levels(int, Enum): + BOB_OMB_BATTLEFIELD = 91 + WHOMPS_FORTRESS = 241 + JOLLY_ROGER_BAY = 121 + COOL_COOL_MOUNTAIN = 51 + BIG_BOOS_HAUNT = 41 + HAZY_MAZE_CAVE = 71 + LETHAL_LAVA_LAND = 221 + SHIFTING_SAND_LAND = 81 + DIRE_DIRE_DOCKS = 231 + SNOWMANS_LAND = 101 + WET_DRY_WORLD = 111 + TALL_TALL_MOUNTAIN = 361 + TINY_HUGE_ISLAND_TINY = 132 + TINY_HUGE_ISLAND_HUGE = 131 + TICK_TOCK_CLOCK = 141 + RAINBOW_RIDE = 151 + THE_PRINCESS_SECRET_SLIDE = 271 + THE_SECRET_AQUARIUM = 201 + BOWSER_IN_THE_DARK_WORLD = 171 + TOWER_OF_THE_WING_CAP = 291 + CAVERN_OF_THE_METAL_CAP = 281 + VANISH_CAP_UNDER_THE_MOAT = 181 + BOWSER_IN_THE_FIRE_SEA = 191 + WING_MARIO_OVER_THE_RAINBOW = 311 + +# sm64paintings is a dict of entrances, format LEVEL | AREA +sm64_level_to_paintings: typing.Dict[SM64Levels, str] = { + SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield", + SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress", + SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay", + SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain", + SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt", + SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave", + SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land", + SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land", + SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks", + SM64Levels.SNOWMANS_LAND: "Snowman's Land", + SM64Levels.WET_DRY_WORLD: "Wet-Dry World", + SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain", + SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)", + SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)", + SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock", + SM64Levels.RAINBOW_RIDE: "Rainbow Ride" } sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() } -# sm64secrets is list of secret areas, same format -sm64_level_to_secrets = { - 271: "The Princess's Secret Slide", - 201: "The Secret Aquarium", - 171: "Bowser in the Dark World", - 291: "Tower of the Wing Cap", - 281: "Cavern of the Metal Cap", - 181: "Vanish Cap under the Moat", - 191: "Bowser in the Fire Sea", - 311: "Wing Mario over the Rainbow" + +# sm64secrets is a dict of secret areas, same format as sm64paintings +sm64_level_to_secrets: typing.Dict[SM64Levels, str] = { + SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide", + SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium", + SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World", + SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap", + SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap", + SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat", + SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea", + SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow" } sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() } diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index d21ac30004..5f85bcdafd 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -1,5 +1,5 @@ from ..generic.Rules import add_rule -from .Regions import connect_regions, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_entrances_to_level, sm64_level_to_entrances +from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances def shuffle_dict_keys(world, obj: dict) -> dict: keys = list(obj.keys()) @@ -7,12 +7,16 @@ def shuffle_dict_keys(world, obj: dict) -> dict: world.random.shuffle(keys) return dict(zip(keys,values)) -def fix_reg(entrance_ids, entrance, destination, swapdict, world): - if entrance_ids[entrance] == destination: # Unlucky :C - rand = world.random.choice(swapdict.keys()) - entrance_ids[entrance], entrance_ids[swapdict[rand]] = rand, entrance_ids[entrance] - swapdict[rand] = entrance_ids[entrance] - swapdict.pop(entrance) +def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, + swapdict: dict, world): + if entrance_map[entrance] in invalid_regions: # Unlucky :C + replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items() + if rand_region not in invalid_regions] + rand_region, rand_entrance = world.random.choice(replacement_regions) + old_dest = entrance_map[entrance] + entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest + swapdict[rand_region] = entrance + swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() @@ -24,19 +28,16 @@ def set_rules(world, player: int, area_connections: dict): randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets } if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool randomized_entrances = shuffle_dict_keys(world,randomized_entrances) + swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() } # Guarantee first entrance is a course - swapdict = { entrance: level for (level,entrance) in randomized_entrances } - if randomized_entrances[91] not in sm64_paintings_to_level.keys(): # Unlucky :C (91 -> BoB Entrance) - rand = world.random.choice(sm64_paintings_to_level.values()) - randomized_entrances[91], randomized_entrances[swapdict[rand]] = rand, randomized_entrances[91] - swapdict[rand] = randomized_entrances[91] - swapdict.pop("Bob-omb Battlefield") - # Guarantee COTMC is not mapped to HMC, cuz thats impossible - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Hazy Maze Cave", swapdict, world) + fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world) # Guarantee BITFS is not mapped to DDD - fix_reg(randomized_entrances, "Bowser in the Fire Sea", "Dire, Dire Docks", swapdict, world) - if randomized_entrances[191] == "Hazy Maze Cave": # If BITFS is mapped to HMC... - fix_reg(randomized_entrances, "Cavern of the Metal Cap", "Dire, Dire Docks", swapdict, world) # ... then dont allow COTMC to be mapped to DDD + fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world) + # Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD. + if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave": + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world) + else: + fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) From e2109dba507d97d45fe4d23e3416b894a709ea16 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:35:46 +0100 Subject: [PATCH 282/327] The Witness: Fix Logic Error for Keep Pressure Plates 2 EP in puzzle_randomization: none (#2515) --- worlds/witness/WitnessLogicVanilla.txt | 2 +- worlds/witness/items.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/worlds/witness/WitnessLogicVanilla.txt b/worlds/witness/WitnessLogicVanilla.txt index 8591a30d1f..779ead6bde 100644 --- a/worlds/witness/WitnessLogicVanilla.txt +++ b/worlds/witness/WitnessLogicVanilla.txt @@ -430,7 +430,7 @@ Door - 0x04F8F (Tower Shortcut) - 0x0361B 158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers Laser - 0x014BB (Laser) - 0x0360E | 0x03317 159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True -159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True +159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True 159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True 159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True 159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 15c693b25d..a8c889de93 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -115,6 +115,7 @@ class WitnessPlayerItems: # Adjust item classifications based on game settings. eps_shuffled = self._world.options.shuffle_EPs come_to_you = self._world.options.elevators_come_to_you + difficulty = self._world.options.puzzle_randomization for item_name, item_data in self.item_data.items(): if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", "Monastery Shortcuts", @@ -130,10 +131,12 @@ class WitnessPlayerItems: "Monastery Laser Shortcut (Door)", "Orchard Second Gate (Door)", "Jungle Bamboo Laser Shortcut (Door)", - "Keep Pressure Plates 2 Exit (Door)", "Caves Elevator Controls (Panel)"}: # Downgrade doors that don't gate progress. item_data.classification = ItemClassification.useful + elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled): + # PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary + item_data.classification = ItemClassification.useful # Build the mandatory item list. self._mandatory_items: Dict[str, int] = {} From 81425641561cf2efc0e57d46e46367400c70e3eb Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:36:55 +0100 Subject: [PATCH 283/327] The Witness: Fix non-deterministic hints (#2514) --- worlds/witness/hints.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 1e54ec352c..d238aa4adf 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -161,7 +161,7 @@ joke_hints = [ ] -def get_always_hint_items(world: "WitnessWorld"): +def get_always_hint_items(world: "WitnessWorld") -> List[str]: always = [ "Boat", "Caves Shortcuts", @@ -187,17 +187,17 @@ def get_always_hint_items(world: "WitnessWorld"): return always -def get_always_hint_locations(_: "WitnessWorld"): - return { +def get_always_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Challenge Vault Box", "Mountain Bottom Floor Discard", "Theater Eclipse EP", "Shipwreck Couch EP", "Mountainside Cloud Cycle EP", - } + ] -def get_priority_hint_items(world: "WitnessWorld"): +def get_priority_hint_items(world: "WitnessWorld") -> List[str]: priority = { "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", @@ -246,11 +246,11 @@ def get_priority_hint_items(world: "WitnessWorld"): lasers.append("Desert Laser") priority.update(world.random.sample(lasers, 6)) - return priority + return sorted(priority) -def get_priority_hint_locations(_: "WitnessWorld"): - return { +def get_priority_hint_locations(_: "WitnessWorld") -> List[str]: + return [ "Swamp Purple Underwater", "Shipwreck Vault Box", "Town RGB Room Left", @@ -264,7 +264,7 @@ def get_priority_hint_locations(_: "WitnessWorld"): "Tunnels Theater Flowers EP", "Boat Shipwreck Green EP", "Quarry Stoneworks Control Room Left", - } + ] def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]): @@ -365,8 +365,8 @@ def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item] remaining_hints = hint_amount - len(hints) priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2))) - prog_items_in_this_world = sorted(list(prog_items_in_this_world)) - locations_in_this_world = sorted(list(loc_in_this_world)) + prog_items_in_this_world = sorted(prog_items_in_this_world) + locations_in_this_world = sorted(loc_in_this_world) world.random.shuffle(prog_items_in_this_world) world.random.shuffle(locations_in_this_world) From 1a05bad612040ba734e40f80539ae444afa07cd8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:38:49 +0100 Subject: [PATCH 284/327] Core: update modules (#2551) --- WebHostLib/requirements.txt | 2 +- requirements.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 654104252c..62707d78cf 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -5,5 +5,5 @@ Flask-Caching>=2.1.0 Flask-Compress>=1.14 Flask-Limiter>=3.5.0 bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.2.2; python_version >= '3.9' +bokeh>=3.3.2; python_version >= '3.9' markupsafe>=2.1.3 diff --git a/requirements.txt b/requirements.txt index 7d93928bb5..0db55a8035 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ colorama>=0.4.5 -websockets>=11.0.3 +websockets>=12.0 PyYAML>=6.0.1 jellyfish>=1.0.3 jinja2>=3.1.2 schema>=0.7.5 -kivy>=2.2.0 +kivy>=2.2.1 bsdiff4>=1.2.4 platformdirs>=4.0.0 certifi>=2023.11.17 -cython>=3.0.5 +cython>=3.0.6 cymem>=2.0.8 orjson>=3.9.10 \ No newline at end of file From e8f96dabe80bfd7c3d89195e69aad3c3be906581 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:42:07 +0100 Subject: [PATCH 285/327] Core: faster prog balance (#2586) * Core: rename world to multiworld in balance_multiworld_progression * Core: small optimization to progression balance speed --- Fill.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Fill.py b/Fill.py index 342c155079..525d27d338 100644 --- a/Fill.py +++ b/Fill.py @@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None: break -def balance_multiworld_progression(world: MultiWorld) -> None: +def balance_multiworld_progression(multiworld: MultiWorld) -> None: # A system to reduce situations where players have no checks remaining, popularly known as "BK mode." # Overall progression balancing algorithm: # Gather up all locations in a sphere. @@ -558,28 +558,28 @@ def balance_multiworld_progression(world: MultiWorld) -> None: # If other players are below the threshold value, swap progression in this sphere into earlier spheres, # which gives more locations available by this sphere. balanceable_players: typing.Dict[int, float] = { - player: world.worlds[player].options.progression_balancing / 100 - for player in world.player_ids - if world.worlds[player].options.progression_balancing > 0 + player: multiworld.worlds[player].options.progression_balancing / 100 + for player in multiworld.player_ids + if multiworld.worlds[player].options.progression_balancing > 0 } if not balanceable_players: logging.info('Skipping multiworld progression balancing.') else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') logging.debug(balanceable_players) - state: CollectionState = CollectionState(world) + state: CollectionState = CollectionState(multiworld) checked_locations: typing.Set[Location] = set() - unchecked_locations: typing.Set[Location] = set(world.get_locations()) + unchecked_locations: typing.Set[Location] = set(multiworld.get_locations()) total_locations_count: typing.Counter[int] = Counter( location.player - for location in world.get_locations() + for location in multiworld.get_locations() if not location.locked ) reachable_locations_count: typing.Dict[int, int] = { player: 0 - for player in world.player_ids - if total_locations_count[player] and len(world.get_filled_locations(player)) != 0 + for player in multiworld.player_ids + if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0 } balanceable_players = { player: balanceable_players[player] @@ -658,7 +658,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: balancing_unchecked_locations.remove(location) if not location.locked: balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all( + if multiworld.has_beaten_game(balancing_state) or all( item_percentage(player, reachables) >= threshold_percentages[player] for player, reachables in balancing_reachables.items() if player in threshold_percentages): @@ -675,7 +675,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: locations_to_test = unlocked_locations[player] items_to_test = list(candidate_items[player]) items_to_test.sort() - world.random.shuffle(items_to_test) + multiworld.random.shuffle(items_to_test) while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() @@ -687,8 +687,8 @@ def balance_multiworld_progression(world: MultiWorld) -> None: reducing_state.sweep_for_events(locations=locations_to_test) - if world.has_beaten_game(balancing_state): - if not world.has_beaten_game(reducing_state): + if multiworld.has_beaten_game(balancing_state): + if not multiworld.has_beaten_game(reducing_state): items_to_replace.append(testing) else: reduced_sphere = get_sphere_locations(reducing_state, locations_to_test) @@ -696,33 +696,32 @@ def balance_multiworld_progression(world: MultiWorld) -> None: if p < threshold_percentages[player]: items_to_replace.append(testing) - replaced_items = False + old_moved_item_count = moved_item_count # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) - world.random.shuffle(replacement_locations) + multiworld.random.shuffle(replacement_locations) items_to_replace.sort() - world.random.shuffle(items_to_replace) + multiworld.random.shuffle(items_to_replace) # Start swapping items. Since we swap into earlier spheres, no need for accessibility checks. while replacement_locations and items_to_replace: old_location = items_to_replace.pop() - for new_location in replacement_locations: + for i, new_location in enumerate(replacement_locations): if new_location.can_fill(state, old_location.item, False) and \ old_location.can_fill(state, new_location.item, False): - replacement_locations.remove(new_location) + replacement_locations.pop(i) swap_location_item(old_location, new_location) logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " f"displacing {old_location.item} into {old_location}") moved_item_count += 1 state.collect(new_location.item, True, new_location) - replaced_items = True break else: logging.warning(f"Could not Progression Balance {old_location.item}") - if replaced_items: + if old_moved_item_count < moved_item_count: logging.debug(f"Moved {moved_item_count} items so far\n") unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} for location in get_sphere_locations(state, unlocked): @@ -736,7 +735,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: state.collect(location.item, True, location) checked_locations |= sphere_locations - if world.has_beaten_game(state): + if multiworld.has_beaten_game(state): break elif not sphere_locations: logging.warning("Progression Balancing ran out of paths.") From 13122ab4668681772261195b88bf4c496152b55f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Dec 2023 20:42:41 +0100 Subject: [PATCH 286/327] Core: remove start_inventory_from_pool from early_items (#2579) --- Main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Main.py b/Main.py index b64650478b..8dac8f7d20 100644 --- a/Main.py +++ b/Main.py @@ -117,6 +117,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) + # remove from_pool items also from early items handling, as starting is plenty early. + early = world.early_items[player].get(item_name, 0) + if early: + world.early_items[player][item_name] = max(0, early-count) + remaining_count = count-early + if remaining_count > 0: + local_early = world.early_local_items[player].get(item_name, 0) + if local_early: + world.early_items[player][item_name] = max(0, local_early - remaining_count) + del local_early + del early logger.info('Creating World.') AutoWorld.call_all(world, "create_regions") From d9d282c9259f79abc0e029d27075b45c3a631fbd Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 11 Dec 2023 19:14:44 -0600 Subject: [PATCH 287/327] Tests: test that the datapackage after generation is still valid (#2575) --- test/general/test_ids.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test/general/test_ids.py b/test/general/test_ids.py index 4edfb8d994..98c41b67b1 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -1,5 +1,8 @@ import unittest -from worlds.AutoWorld import AutoWorldRegister + +from Fill import distribute_items_restrictive +from worlds.AutoWorld import AutoWorldRegister, call_all +from . import setup_solo_multiworld class TestIDs(unittest.TestCase): @@ -66,3 +69,34 @@ class TestIDs(unittest.TestCase): for gamename, world_type in AutoWorldRegister.world_types.items(): with self.subTest(game=gamename): self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) + + def test_postgen_datapackage(self): + """Generates a solo multiworld and checks that the datapackage is still valid""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + multiworld = setup_solo_multiworld(world_type) + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + datapackage = world_type.get_data_package_data() + for item_group, item_names in datapackage["item_name_groups"].items(): + self.assertIsInstance(item_group, str, + f"item_name_group names should be strings: {item_group}") + for item_name in item_names: + self.assertIsInstance(item_name, str, + f"{item_name}, in group {item_group} is not a string") + for loc_group, loc_names in datapackage["location_name_groups"].items(): + self.assertIsInstance(loc_group, str, + f"location_name_group names should be strings: {loc_group}") + for loc_name in loc_names: + self.assertIsInstance(loc_name, str, + f"{loc_name}, in group {loc_group} is not a string") + for item_name, item_id in datapackage["item_name_to_id"].items(): + self.assertIsInstance(item_name, str, + f"{item_name} is not a valid item name for item_name_to_id") + self.assertIsInstance(item_id, int, + f"{item_id} for {item_name} should be an int") + for loc_name, loc_id in datapackage["location_name_to_id"].items(): + self.assertIsInstance(loc_name, str, + f"{loc_name} is not a valid item name for location_name_to_id") + self.assertIsInstance(loc_id, int, + f"{loc_id} for {loc_name} should be an int") From e9317d403176723cbf58584b8d597726acb0a6e8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:39:38 -0500 Subject: [PATCH 288/327] FFMQR: Fix Empty Kaeli Companion Event Location and Spellbook option (#2591) --- worlds/ffmq/Regions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index aac8289a36..61f70864c0 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -67,10 +67,10 @@ def create_regions(self): self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"], [FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in location_table else None, object["type"], object["access"], - self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for - object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in - ("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or - self.multiworld.brown_boxes[self.player] == "include")], room["links"])) + self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in + room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", + "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", []) From 45fa9a8f9e6c28368968fced26878ddf02524636 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 11 Dec 2023 20:48:20 -0800 Subject: [PATCH 289/327] BizHawkClient: Add SGB to systems using explicit vblank callback (#2593) --- data/lua/connector_bizhawk_generic.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index c4e729300d..eff400cb03 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -585,7 +585,7 @@ else -- misaligned, so for GB and GBC we explicitly set the callback on -- vblank instead. -- https://github.com/TASEmulators/BizHawk/issues/3711 - if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then + if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then event.onmemoryexecute(tick, 0x40, "tick", "System Bus") else event.onframeend(tick) From db1d195cb07244e0eceb365d2a6c9c5a6cc11c58 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 12 Dec 2023 20:11:10 -0600 Subject: [PATCH 290/327] Hollow Knight: remove unused option check (#2595) --- worlds/hk/__init__.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index f7e7e22e69..8b07b34eb0 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -419,17 +419,16 @@ class HKWorld(World): def set_rules(self): world = self.multiworld player = self.player - if world.logic[player] != 'nologic': - goal = world.Goal[player] - if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) - elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) - elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) - else: - # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + goal = world.Goal[player] + if goal == Goal.option_hollowknight: + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + elif goal == Goal.option_siblings: + world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + elif goal == Goal.option_radiance: + world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + else: + # Any goal + world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) set_rules(self) From 0eefe9e93646ef5f7915a4b8962d50f0fed8e54f Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Tue, 12 Dec 2023 20:12:16 -0600 Subject: [PATCH 291/327] WebHost: Some refactors and additional checks when uploading files. (#2549) --- WebHostLib/check.py | 55 +++++++++++++++++++++-------------------- WebHostLib/upload.py | 58 ++++++++++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 4db2ec2ce3..e739dda02d 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -1,3 +1,4 @@ +import os import zipfile import base64 from typing import Union, Dict, Set, Tuple @@ -6,13 +7,7 @@ from flask import request, flash, redirect, url_for, render_template from markupsafe import Markup from WebHostLib import app - -banned_zip_contents = (".sfc",) - - -def allowed_file(filename): - return filename.endswith(('.txt', ".yaml", ".zip")) - +from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file from Generate import roll_settings, PlandoOptions from Utils import parse_yamls @@ -51,33 +46,41 @@ def mysterycheck(): def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]: options = {} for uploaded_file in files: - # if user does not select file, browser also - # submit an empty part without filename - if uploaded_file.filename == '': - return 'No selected file' + if banned_file(uploaded_file.filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. " + "Your file was deleted.") + # If the user does not select file, the browser will still submit an empty string without a file name. + elif uploaded_file.filename == "": + return "No selected file." elif uploaded_file.filename in options: - return f'Conflicting files named {uploaded_file.filename} submitted' - elif uploaded_file and allowed_file(uploaded_file.filename): + return f"Conflicting files named {uploaded_file.filename} submitted." + elif uploaded_file and allowed_options(uploaded_file.filename): if uploaded_file.filename.endswith(".zip"): + if not zipfile.is_zipfile(uploaded_file): + return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened." - with zipfile.ZipFile(uploaded_file, 'r') as zfile: - infolist = zfile.infolist() + uploaded_file.seek(0) # offset from is_zipfile check + with zipfile.ZipFile(uploaded_file, "r") as zfile: + for file in zfile.infolist(): + # Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS). + base_filename = os.path.basename(file.filename) - if any(file.filename.endswith(".archipelago") for file in infolist): - return Markup("Error: Your .zip file contains an .archipelago file. " - 'Did you mean to host a game?') - - for file in infolist: - if file.filename.endswith(banned_zip_contents): - return ("Uploaded data contained a rom file, " - "which is likely to contain copyrighted material. " - "Your file was deleted.") - elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): + if base_filename.endswith(".archipelago"): + return Markup("Error: Your .zip file contains an .archipelago file. " + 'Did you mean to host a game?') + elif base_filename.endswith(".zip"): + return "Nested .zip files inside a .zip are not supported." + elif banned_file(base_filename): + return ("Uploaded data contained a rom file, which is likely to contain copyrighted " + "material. Your file was deleted.") + # Ignore dot-files. + elif not base_filename.startswith(".") and allowed_options(base_filename): options[file.filename] = zfile.open(file, "r").read() else: options[uploaded_file.filename] = uploaded_file.read() + if not options: - return "Did not find a .yaml file to process." + return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}" return options diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index e7ac033913..8f01294eac 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -19,7 +19,22 @@ from worlds.Files import AutoPatchRegister from . import app from .models import Seed, Room, Slot, GameDataPackage -banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") +banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba") +allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") +allowed_generation_extensions = (".archipelago", ".zip") + + +def allowed_options(filename: str) -> bool: + return filename.endswith(allowed_options_extensions) + + +def allowed_generation(filename: str) -> bool: + return filename.endswith(allowed_generation_extensions) + + +def banned_file(filename: str) -> bool: + return filename.endswith(banned_extensions) + def process_multidata(compressed_multidata, files={}): decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) @@ -61,8 +76,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if not owner: owner = session["_id"] infolist = zfile.infolist() - if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist): - flash(Markup("Error: Your .zip file only contains .yaml files. " + if all(allowed_options(file.filename) or file.is_dir() for file in infolist): + flash(Markup("Error: Your .zip file only contains options files. " 'Did you mean to generate a game?')) return @@ -73,7 +88,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load files. for file in infolist: handler = AutoPatchRegister.get_handler(file.filename) - if file.filename.endswith(banned_zip_contents): + if banned_file(file.filename): return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ "Your file was deleted." @@ -136,35 +151,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flash("No multidata was found in the zip file, which is required.") -@app.route('/uploads', methods=['GET', 'POST']) +@app.route("/uploads", methods=["GET", "POST"]) def uploads(): - if request.method == 'POST': - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') + if request.method == "POST": + # check if the POST request has a file part. + if "file" not in request.files: + flash("No file part in POST request.") else: - file = request.files['file'] - # if user does not select file, browser also - # submit an empty part without filename - if file.filename == '': - flash('No selected file') - elif file and allowed_file(file.filename): - if zipfile.is_zipfile(file): - with zipfile.ZipFile(file, 'r') as zfile: + uploaded_file = request.files["file"] + # If the user does not select file, the browser will still submit an empty string without a file name. + if uploaded_file.filename == "": + flash("No selected file.") + elif uploaded_file and allowed_generation(uploaded_file.filename): + if zipfile.is_zipfile(uploaded_file): + with zipfile.ZipFile(uploaded_file, "r") as zfile: try: res = upload_zip_to_db(zfile) except VersionException: flash(f"Could not load multidata. Wrong Version detected.") else: - if type(res) == str: + if res is str: return res elif res: return redirect(url_for("view_seed", seed=res.id)) else: - file.seek(0) # offset from is_zipfile check + uploaded_file.seek(0) # offset from is_zipfile check # noinspection PyBroadException try: - multidata = file.read() + multidata = uploaded_file.read() slots, multidata = process_multidata(multidata) except Exception as e: flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})") @@ -182,7 +196,3 @@ def user_content(): rooms = select(room for room in Room if room.owner == session["_id"]) seeds = select(seed for seed in Seed if seed.owner == session["_id"]) return render_template("userContent.html", rooms=rooms, seeds=seeds) - - -def allowed_file(filename): - return filename.endswith(('.archipelago', ".zip")) From a3b0476b4baca0092c44ee5b275f0a0ac6eda7af Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:34:36 -0500 Subject: [PATCH 292/327] LTTP: Boss rule fix (#2600) --- worlds/alttp/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 469f4f82ee..8a04f87afa 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -136,7 +136,8 @@ def mirrorless_path_to_castle_courtyard(world, player): def set_defeat_dungeon_boss_rule(location): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used - set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) + def set_always_allow(spot, rule): spot.always_allow = rule From ff556bf4ccd96931b0c7c29becedc2f562931cbe Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Wed, 13 Dec 2023 23:46:46 +0100 Subject: [PATCH 293/327] sm64ex: Fix server (#2599) --- worlds/sm64ex/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 5f85bcdafd..c428f85543 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -40,7 +40,8 @@ def set_rules(world, player: int, area_connections: dict): fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world) # Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code - area_connections.update({entrance_lvl: sm64_entrances_to_level[destination] for (entrance_lvl,destination) in randomized_entrances.items()}) + # Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise + area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) From 3e3af385fa39c21ab62c7fb74a31d2491f9246ee Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:57:14 -0500 Subject: [PATCH 294/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20client=20location?= =?UTF-8?q?s=20import=20(#2596)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index fb29045cf4..7424cc8ddf 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -6,7 +6,7 @@ from NetUtils import ClientStatus from worlds._bizhawk.client import BizHawkClient from worlds._bizhawk import read, write, guarded_write -from worlds.pokemon_rb.locations import location_data +from .locations import location_data logger = logging.getLogger("Client") From 394633558fae17683bb9baaa5f4d4da01fd0b57c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:39:09 -0500 Subject: [PATCH 295/327] ALTTP: Restore allow_excluded (#2607) Restores allow_excluded to the dungeon fill_restrictive call, which was apparently removed by mistake during merge conflict resolution --- worlds/alttp/Dungeons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a68acf7288..b456174f39 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True, name="LttP Dungeon Items") From b500cf600c329b7c6e598f928be3ff173ceea020 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:13 -0500 Subject: [PATCH 296/327] FFMQ: Actually fix the spellbook option (#2594) --- worlds/ffmq/Options.py | 4 ++-- worlds/ffmq/data/settings.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index eaf3097494..4b9f4a4a88 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -267,11 +267,11 @@ class CompanionLevelingType(Choice): class CompanionSpellbookType(Choice): """Update companions' spellbook. Standard: Original game spellbooks. - Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. + Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. Random Balanced: Randomize the spellbooks with an appropriate mix of spells. Random Chaos: Randomize the spellbooks in total free-for-all.""" option_standard = 0 - option_standard_extended = 1 + option_extended = 1 option_random_balanced = 2 option_random_chaos = 3 default = 0 diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml index ff03ed26e6..826a8c744d 100644 --- a/worlds/ffmq/data/settings.yaml +++ b/worlds/ffmq/data/settings.yaml @@ -85,7 +85,7 @@ Final Fantasy Mystic Quest: Normal: 0 OneAndHalf: 0 Double: 0 - DoubleHalf: 0 + DoubleAndHalf: 0 Triple: 0 Quadruple: 0 companion_leveling_type: @@ -98,7 +98,7 @@ Final Fantasy Mystic Quest: BenPlus10: 0 companion_spellbook_type: Standard: 0 - StandardExtended: 0 + Extended: 0 RandomBalanced: 0 RandomChaos: 0 starting_companion: From 6c4fdc985df5d1fabe1bfeb3da594bda1b9baa5d Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:36 -0500 Subject: [PATCH 297/327] SA2B: Fix Weapons Bed - Omochao 2 Logic (#2605) --- worlds/sa2b/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index 6b7ad69cd1..afd0dcb182 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player), lambda state: state.has(ItemName.shadow_air_shoes, player)) add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_large_cannon, player)) add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player), lambda state: state.has(ItemName.tails_booster, player)) From c56cbd047488d09e3055d1247abb124269199502 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:28:54 -0500 Subject: [PATCH 298/327] SM: item link replacement fix (#2597) --- worlds/sm/variaRandomizer/rando/GraphBuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 7bee33ec82..88b539e7f0 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -170,7 +170,8 @@ class GraphBuilder(object): ap = "Landing Site" # dummy value it'll be overwritten at first collection while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)): il = itemLocs.pop(0) - if il.Location.restricted or il.Item.Type == "ArchipelagoItem": + # can happen with item links replacement items that its not in the container's itemPool + if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool: continue self.log.debug("collecting " + getItemLocStr(il)) container.collect(il) From 7dff09dc1ad0d760621684c847169042ed78643f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 16 Dec 2023 15:21:05 -0600 Subject: [PATCH 299/327] Options: set old options api before the world is created (#2378) --- BaseClasses.py | 15 ++++++++++----- Utils.py | 19 +++++++++++++++++++ worlds/AutoWorld.py | 4 ++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c0a77708c0..dddcfc3a6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -252,15 +252,20 @@ class MultiWorld(): range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: + # TODO - remove this section once all worlds use options dataclasses + all_keys: Set[str] = {key for player in self.player_ids for key in + AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} + for option_key in all_keys: + option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. " + f"Please use `self.options.{option_key}` instead.") + option.update(getattr(args, option_key, {})) + setattr(self, option_key, option) + for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] - for option_key in world_type.options_dataclass.type_hints: - option_values = getattr(args, option_key, {}) - setattr(self, option_key, option_values) - # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + options_dataclass: typing.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}) diff --git a/Utils.py b/Utils.py index 5955e92432..f6e4a9ab60 100644 --- a/Utils.py +++ b/Utils.py @@ -779,6 +779,25 @@ def deprecate(message: str): import warnings warnings.warn(message) + +class DeprecateDict(dict): + log_message: str + should_error: bool + + def __init__(self, message, error: bool = False) -> None: + self.log_message = message + self.should_error = error + super().__init__() + + def __getitem__(self, item: Any) -> Any: + if self.should_error: + deprecate(self.log_message) + elif __debug__: + import warnings + warnings.warn(self.log_message) + return super().__getitem__(item) + + def _extend_freeze_support() -> None: """Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn.""" # upstream issue: https://github.com/python/cpython/issues/76327 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d0533e068..f56c39f690 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -77,6 +77,10 @@ class AutoWorldRegister(type): # create missing options_dataclass from legacy option_definitions # TODO - remove this once all worlds use options dataclasses if "options_dataclass" not in dct and "option_definitions" in dct: + # TODO - switch to deprecate after a version + if __debug__: + from warnings import warn + warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) From f958af4067c9f0be0faf5349e19cb5a92b190edf Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:22:51 -0600 Subject: [PATCH 300/327] Adventure: Fix KeyError on Retrieved (#2560) --- AdventureClient.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AdventureClient.py b/AdventureClient.py index d2f4e734ac..06e4d60dad 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -115,11 +115,12 @@ class AdventureContext(CommonContext): msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": - self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] - if self.freeincarnates_used is None: - self.freeincarnates_used = 0 - self.freeincarnates_used += self.freeincarnate_pending - self.send_pending_freeincarnates() + if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() elif cmd == "SetReply": if args["key"] == f"adventure_{self.auth}_freeincarnates_used": self.freeincarnates_used = args["value"] From 4979314825c4498bbaa16409ce1a4275c80bbf4b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 17 Dec 2023 06:08:40 +0100 Subject: [PATCH 301/327] Webhost: open graph support for /room (#2580) * WebHost: add Open Graph metadata to /room * WebHost: Open Graph cleanup --- WebHostLib/templates/hostRoom.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index ba15d64aca..2981c41452 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -3,6 +3,16 @@ {% block head %} Multiworld {{ room.id|suuid }} {% if should_refresh %}{% endif %} + + + + {% if room.seed.slots|length < 2 %} + + {% else %} + + {% endif %} {% endblock %} From a549af8304de03c3a6814c53d589a7b2f4a9c3f4 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sun, 17 Dec 2023 10:11:40 -0600 Subject: [PATCH 302/327] Hollow Knight: Add additional DeathLink option and add ExtraPlatforms option. (#2545) --- worlds/hk/Options.py | 47 ++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index fcc938474d..ef7fbd0dfe 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -2,7 +2,7 @@ import typing from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -402,22 +402,34 @@ class WhitePalace(Choice): default = 0 -class DeathLink(Choice): +class ExtraPlatforms(DefaultOnToggle): + """Places additional platforms to make traveling throughout Hallownest more convenient.""" + + +class DeathLinkShade(Choice): + """Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any. + + vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any. + shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched. + shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless. + + * This option has no effect if DeathLink is disabled. + ** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override + your existing shade, if any. """ - When you die, everyone dies. Of course the reverse is true too. - When enabled, choose how incoming deathlinks are handled: - vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo. - shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched. - shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise. - """ - option_off = 0 - alias_no = 0 - alias_true = 1 - alias_on = 1 - alias_yes = 1 + option_vanilla = 0 option_shadeless = 1 - option_vanilla = 2 - option_shade = 3 + option_shade = 2 + default = 2 + + +class DeathLinkBreaksFragileCharms(Toggle): + """Sets if fragile charms break when you are killed by a DeathLink. + + * This option has no effect if DeathLink is disabled. + ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it + will continue to do so. + """ class StartingGeo(Range): @@ -476,7 +488,8 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, StartingGeo, DeathLink, + StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo, + DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, MinimumEssencePrice, MaximumEssencePrice, @@ -488,7 +501,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { LegEaterShopSlots, GrubfatherRewardSlots, SeerRewardSlots, ExtraShopSlots, SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw, - CostSanity, CostSanityHybridChance, + CostSanity, CostSanityHybridChance ) }, **cost_sanity_weights From c8adadb08bbd23d1d11c114333fdb95477eb4fbf Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:39:04 -0500 Subject: [PATCH 303/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20Flash=20lea?= =?UTF-8?q?rnable=20logic=20(#2615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index d9bd6dde76..ee0f0052e1 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -414,7 +414,7 @@ class PokemonRedBlueWorld(World): > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))): intervene_move = "Cut" elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player] - and (((self.multiworld.accessibility[self.player] != "minimal" and + and (((self.multiworld.accessibility[self.player] != "minimal" or (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or self.multiworld.door_shuffle[self.player]))): intervene_move = "Flash" From 817197c14dabf8a90cf81464ce4c7cc1fb477493 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 18 Dec 2023 10:46:24 -0500 Subject: [PATCH 304/327] Lingo: Tests no longer disable forced good item (#2602) The static class with the "disable forced good item" field is gone. Now, certain tests that want to check for specific access progression can run a method that removes the forced good item and adds it back to the pool. Tests that don't care about this will collect the forced good item like normal. This should prevent the intermittent fill failures on complex doors unit tests, since the forced good item should provide enough locations to fill in. --- worlds/lingo/__init__.py | 1 - worlds/lingo/player_logic.py | 3 +-- worlds/lingo/test/TestDoors.py | 10 ++++++++++ worlds/lingo/test/TestOrangeTower.py | 4 ++++ worlds/lingo/test/TestProgressive.py | 6 ++++++ worlds/lingo/test/__init__.py | 8 ++++++-- worlds/lingo/testing.py | 2 -- 7 files changed, 27 insertions(+), 7 deletions(-) delete mode 100644 worlds/lingo/testing.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index f22d344c8f..0889674450 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -11,7 +11,6 @@ from .options import LingoOptions from .player_logic import LingoPlayerLogic from .regions import create_regions from .static_logic import Room, RoomEntrance -from .testing import LingoTestOptions class LingoWebWorld(WebWorld): diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b046f1cfe3..fa497c59bd 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -6,7 +6,6 @@ from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel -from .testing import LingoTestOptions if TYPE_CHECKING: from . import LingoWorld @@ -224,7 +223,7 @@ class LingoPlayerLogic: "kind of logic error.") if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ - and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: + and not early_color_hallways is False: # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index f496c5f578..49a0f9c490 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -8,6 +8,8 @@ class TestRequiredRoomLogic(LingoTestBase): } def test_pilgrim_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) @@ -28,6 +30,8 @@ class TestRequiredRoomLogic(LingoTestBase): self.assertTrue(self.can_reach_location("The Seeker - Achievement")) def test_hidden_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) self.assertFalse(self.can_reach_location("The Seeker - Achievement")) @@ -55,6 +59,8 @@ class TestRequiredDoorLogic(LingoTestBase): } def test_through_rhyme(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -64,6 +70,8 @@ class TestRequiredDoorLogic(LingoTestBase): self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) def test_through_hidden(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -83,6 +91,8 @@ class TestSimpleDoors(LingoTestBase): } def test_requirement(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py index 7b0c3bb525..9170de108a 100644 --- a/worlds/lingo/test/TestOrangeTower.py +++ b/worlds/lingo/test/TestOrangeTower.py @@ -8,6 +8,8 @@ class TestProgressiveOrangeTower(LingoTestBase): } def test_from_welcome_back(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) @@ -83,6 +85,8 @@ class TestProgressiveOrangeTower(LingoTestBase): self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) def test_from_hub_room(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index 917c6e7e89..8edc7ce6cc 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -7,6 +7,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -58,6 +60,8 @@ class TestSimpleHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -86,6 +90,8 @@ class TestProgressiveArtGallery(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py index ffbf9032b6..7ff456d8fc 100644 --- a/worlds/lingo/test/__init__.py +++ b/worlds/lingo/test/__init__.py @@ -1,7 +1,6 @@ from typing import ClassVar from test.bases import WorldTestBase -from .. import LingoTestOptions class LingoTestBase(WorldTestBase): @@ -9,5 +8,10 @@ class LingoTestBase(WorldTestBase): player: ClassVar[int] = 1 def world_setup(self, *args, **kwargs): - LingoTestOptions.disable_forced_good_item = True super().world_setup(*args, **kwargs) + + def remove_forced_good_item(self): + location = self.multiworld.get_location("Second Room - Good Luck", self.player) + self.remove(location.item) + self.multiworld.itempool.append(location.item) + self.multiworld.state.events.add(location) diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py deleted file mode 100644 index 22fafea0fc..0000000000 --- a/worlds/lingo/testing.py +++ /dev/null @@ -1,2 +0,0 @@ -class LingoTestOptions: - disable_forced_good_item: bool = False From 8842f5d5c722529bab2fb3aa8187c3752dfe0c63 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:11:11 +0100 Subject: [PATCH 305/327] Core: make update_reachable_regions local variables more wordy (#2522) --- BaseClasses.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index dddcfc3a6f..855e69c5d4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -651,34 +651,34 @@ class CollectionState(): def update_reachable_regions(self, player: int): self.stale[player] = False - rrp = self.reachable_regions[player] - bc = self.blocked_connections[player] + reachable_regions = self.reachable_regions[player] + blocked_connections = self.blocked_connections[player] queue = deque(self.blocked_connections[player]) - start = self.multiworld.get_region('Menu', player) + start = self.multiworld.get_region("Menu", player) # init on first call - this can't be done on construction since the regions don't exist yet - if start not in rrp: - rrp.add(start) - bc.update(start.exits) + if start not in reachable_regions: + reachable_regions.add(start) + blocked_connections.update(start.exits) queue.extend(start.exits) # run BFS on all connections, and keep track of those blocked by missing items while queue: connection = queue.popleft() new_region = connection.connected_region - if new_region in rrp: - bc.remove(connection) + 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" - rrp.add(new_region) - bc.remove(connection) - bc.update(new_region.exits) + reachable_regions.add(new_region) + blocked_connections.remove(connection) + blocked_connections.update(new_region.exits) queue.extend(new_region.exits) self.path[new_region] = (new_region.name, self.path.get(connection, None)) # Retry connections if the new region can unblock them for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): - if new_entrance in bc and new_entrance not in queue: + if new_entrance in blocked_connections and new_entrance not in queue: queue.append(new_entrance) def copy(self) -> CollectionState: From 0d929b81e8cfa19297e3acad16d0a220adefa487 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:26:41 +0100 Subject: [PATCH 306/327] Factorio: fix files from mod base directory not being grabbed correctly in non-apworld (#2603) --- worlds/factorio/Mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 21a8c684f9..d7b3d4b1eb 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -177,7 +177,7 @@ def generate_mod(world: "Factorio", output_directory: str): else: basepath = os.path.join(os.path.dirname(__file__), "data", "mod") for dirpath, dirnames, filenames in os.walk(basepath): - base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\") for filename in filenames: mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, file_path=os.path.join(dirpath, filename): From bb0a0f2acaf04ab5919284a88f5cf613576d97d3 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 22 Dec 2023 20:02:49 -0800 Subject: [PATCH 307/327] Factorio: Fix unbeatable seeds where a science pack needs chemical plant (#2613) --- worlds/factorio/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index eb078720c6..17f3163e90 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -246,7 +246,8 @@ class Factorio(World): location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ (ingredient not in technology_table or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients - for technology in required_technologies[sub_ingredient]) + for technology in required_technologies[sub_ingredient]) and \ + all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) From 2512eb750109d83fe1003bf25d18002f09f39763 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:25:41 -0700 Subject: [PATCH 308/327] Hylics 2: Add missing logic (#2638) --- worlds/hylics2/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py index 6c55c8745b..ff9544e0e8 100644 --- a/worlds/hylics2/Rules.py +++ b/worlds/hylics2/Rules.py @@ -444,6 +444,8 @@ def set_rules(hylics2world): lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Alcove Medallion", player), lambda state: paddle(state, player)) + add_rule(world.get_location("Arcade 1: Lava Medallion", player), + lambda state: paddle(state, player)) add_rule(world.get_location("Foglast: Under Lair Medallion", player), lambda state: bridge_key(state, player)) add_rule(world.get_location("Foglast: Mid-Air Medallion", player), From 7c70b87f2971528730254638d2a30d61b9a8f851 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 28 Dec 2023 08:01:48 +0100 Subject: [PATCH 309/327] sm64ex: Fix randomizing Courses and Secrets separately (#2637) Backported from #2569 --- worlds/sm64ex/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index c428f85543..fedd5b7a6e 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -21,7 +21,7 @@ def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() - if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) From b99c7349549b1a9597d13dcc25c264ab7ca3138c Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:14:13 -0500 Subject: [PATCH 310/327] SM: strict rom validation fix (#2632) added a more robust ROM tag validation to free oher games to use tag starting with "SM" followed by another letter (SMW, SMZ3, SMRPG, SMMR,...) --- worlds/sm/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index df73ae47a0..756fd4bf36 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -61,7 +61,7 @@ class SMSNIClient(SNIClient): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE) - if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW": + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[2] not in b"1234567890": return False ctx.game = self.game From 576c7051068b3c99d13ff1b8afd7e7640ae4e51c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:15:48 -0500 Subject: [PATCH 311/327] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Badge=20plando=20?= =?UTF-8?q?fix=20(#2628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only attempt to place badges in badge locations if they are empty. Return unplaced badges to the item pool if fewer than 8 locations are being filled. This should fix errors that occur when items are placed into badge locations via plando, or whatever other worlds may do. --- worlds/pokemon_rb/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index ee0f0052e1..5a94a8b5ff 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -281,18 +281,20 @@ class PokemonRedBlueWorld(World): self.multiworld.itempool.remove(badge) progitempool.remove(badge) for _ in range(5): - badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [ - "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", - "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", - "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", - "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]] + badgelocs = [ + self.multiworld.get_location(loc, self.player) for loc in [ + "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", + "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", + "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", + "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize" + ] if self.multiworld.get_location(loc, self.player).item is None] state = self.multiworld.get_all_state(False) self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) - if badges: + if len(badges) > 8 - len(badgelocs): for location in badgelocs: if location.item: badges.append(location.item) @@ -302,6 +304,7 @@ class PokemonRedBlueWorld(World): for location in badgelocs: if location.item: fill_locations.remove(location) + progitempool += badges break else: raise FillError(f"Failed to place badges for player {self.player}") From 70eb2b58f55c277433d4f453ea2f5daa51462fbf Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:16:38 -0500 Subject: [PATCH 312/327] [TLOZ] Fix bug with item drops in non-expanded item pool (#2623) There was a bug in non-expanded item pool where due to the base patch changes to accommodate more items in dungeons, some items were transformed into glitch items that removed bombs (this also happened in expanded item pool, but the item placement would overwrite the results of this bug so it didn't appear as frequently). Being a Zelda game, losing bombs is bad. This PR fixes the base patch process to avoid this bug, by properly carrying the value of a variable through a procedure. --- worlds/tloz/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 20ab003ead..6e8927c4e7 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -200,15 +200,17 @@ class TLoZWorld(World): for i in range(0, 0x7F): item = rom_data[first_quest_dungeon_items_early + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_early + i] = item if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 item = rom_data[first_quest_dungeon_items_late + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_late + i] = item if item & 0b00011111 == 0b00000011: rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 return rom_data From 04d194db741ee190d87348c282195e299dccfc74 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Dec 2023 04:33:30 -0800 Subject: [PATCH 313/327] Pokemon Emerald: Change "settings" to "options" in docs (#2517) * Pokemon Emerald: Change "settings" to "options" in docs * Pokemon Emerald: Fix two more usages of "setting" instead of "option" * Pokemon Emerald: Minor rephrase in docs Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener --- worlds/pokemon_emerald/docs/en_Pokemon Emerald.md | 10 +++++----- worlds/pokemon_emerald/docs/setup_en.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 5d50c37ea9..8b09b51b38 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -1,15 +1,15 @@ # Pokémon Emerald -## Where is the settings page? +## Where is the options page? -You can read through all the settings and generate a YAML [here](../player-settings). +You can read through all the options and generate a YAML [here](../player-options). ## What does randomization do to this game? This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon, abilities, types, etc… You can even change a percentage of single battles into double battles. Check the -[settings page](../player-settings) for a more comprehensive list of what can be changed. +[options page](../player-options) for a more comprehensive list of what can be changed. ## What items and locations get randomized? @@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones: - You can have both bikes simultaneously - You can run or bike (almost) anywhere - The Wally catching tutorial is skipped -- All text is instant, and with a setting it can be automatically progressed by holding A +- All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another - Many more minor improvements… @@ -44,7 +44,7 @@ your inventory. ## When the player receives an item, what happens? You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages` -setting, the received item will either be silently added to your bag or you will be shown a text box with the item's +option, the received item will either be silently added to your bag or you will be shown a text box with the item's name and the item will be added to your bag while a fanfare plays. ## Can I play offline? diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index 3c5c8c193a..e3f6d3c301 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -26,8 +26,8 @@ clear it. ## Generating and Patching a Game -1. Create your settings file (YAML). You can make one on the -[Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings). +1. Create your options file (YAML). You can make one on the +[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). This will generate an output file for you. Your patch file will have the `.apemerald` file extension. 3. Open `ArchipelagoLauncher.exe` From af1f6e9113a80442dd91fcef0b22935c216633c6 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:43:42 +0100 Subject: [PATCH 314/327] Oot : Update setup fr (#2394) * add new translation * Add translation for OOT Setup in french * Update setup_fr.md * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update setup_fr.md Fix treu to true * Update worlds/oot/docs/setup_fr.md Co-authored-by: Marech * Update OOT Init and Update Minecraft Init * Fix formatting errors * Fix wrong link in stardew valley randomizer setup guide Fix wrong link in stardew valley randomizer setup guide * Add new translation for Adventure and Archipidle in french Add new translation for Adventure and Archipidle in french * Add more store in setup page subnautica for more fairness Add more store in setup page subnautica for more fairness * tweak update merge #1685 for lua file tweak update merge #1685 for lua file * fix text fix text * fix wrong translation fix wrong translation * Yes it's better Yes it's better Co-authored-by: Fabian Dill * Update OOT Setup FR Update OOT Setup FR * Tweak Text Tweak Text --------- Co-authored-by: Ludovic Marechal Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Fabian Dill --- worlds/oot/docs/setup_fr.md | 426 ++++-------------------------------- 1 file changed, 37 insertions(+), 389 deletions(-) diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index 57099cdf2e..f5915e1878 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -1,422 +1,70 @@ -# Guide d'installation Archipelago pour Ocarina of Time +# Guide de configuration pour Ocarina of Time Archipelago ## Important -Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux. +Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux. ## Logiciel requis -- BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory) - - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité. +- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus. + - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) - (sélectionnez `Ocarina of Time Client` lors de l'installation). + (sélectionnez « Ocarina of Time Client » lors de l'installation). - Une ROM Ocarina of Time v1.0. ## Configuration de BizHawk -Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants : +Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants : -- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers - "Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. - **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations** - ** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours ** - **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.** -- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s. - Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur. -- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de - continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. -- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement - désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`. -- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right" - car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place. -- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent. +- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à + "Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. + **REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations** + **des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions** +- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s. + Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur. +- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. +- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ». +- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right". + car ceux-ci interfèrent avec la visée s’ils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique. +- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent. (Le menu N64 n'apparaît qu'après le chargement d'une ROM.) -Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer. -Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez -la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk -et sélectionnez EmuHawk.exe. +Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer. +Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe. -Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés +Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles [ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). -## Configuration de votre fichier YAML +## Créer un fichier de configuration (.yaml) -### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? -Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit -générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter -d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes. +Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel. +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) -### Où puis-je obtenir un fichier YAML ? +### Où puis-je obtenir un fichier de configuration (.yaml) ? -Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce -tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis -la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans -le dossier "Lecteurs". +La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings) -``` yaml -description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers -# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères -name: VotreNom -game: - Ocarina of Time: 1 -requires: - version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu. -# Options partagées prises en charge par tous les jeux : -accessibility: - items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements - locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles - none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets -progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure - 0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique. - 25: 0 - 50: 50 # Faites en sorte que vous ayez probablement des choses à faire. - 99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression. -Ocarina of Time: - logic_rules: # définit la logique utilisée pour le générateur. - glitchless: 50 - glitched: 0 - no_logic: 0 - logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil. - false: 50 - true: 0 - open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo. - open: 50 - closed_deku: 0 - closed: 0 - open_kakariko: # Définit l'état de la porte du village de Kakariko. - open: 50 - zelda: 0 - closed: 0 - open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps. - false: 0 - true: 50 - zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora. - open: 0 - adult: 0 - closed: 50 - gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo. - normal: 0 - fast: 50 - open: 0 - bridge: # Définit les exigences pour le pont arc-en-ciel. - open: 0 - vanilla: 0 - stones: 0 - medallions: 50 - dungeons: 0 - tokens: 0 - trials: # Définit le nombre d'épreuves requises dans le Château de Ganon. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 50 # valeur minimale - 6: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - starting_age: # Choisissez l'âge auquel Link commencera. - child: 50 - adult: 0 - triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu. - false: 50 - true: 0 - triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 1: 0 # valeur minimale - 50: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - 20: 50 - bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling. - false: 50 - true: 0 - bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons. - remove: 0 - startwith: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo. - vanilla: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon. - remove: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - on_lacs: 0 - enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon. - false: 50 - true: 0 - lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps. - vanilla: 50 - stones: 0 - medallions: 0 - dungeons: 0 - tokens: 0 - lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_medallions: # Définissez le nombre de médaillons requis pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_song_items: # Définit où les chansons peuvent apparaître. - song: 50 - dungeon: 0 - any: 0 - shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins. - 0: 0 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random_value: 0 - off: 50 - tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve. - off: 50 - dungeons: 0 - overworld: 0 - all: 0 - shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix. - off: 50 - low: 0 - regular: 0 - random_prices: 0 - shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée. - false: 50 - true: 0 - shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets. - false: 50 - true: 0 - shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets. - false: 50 - true: 0 - shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule. - false: 50 - true: 0 - shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets. - false: 50 - true: 0 - shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies. - false: 50 - true: 0 - shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland. - false: 50 - true: 0 - skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés. - false: 50 - true: 0 - no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon. - false: 50 - true: 0 - no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda. - false: 50 - true: 0 - no_epona_race: # Epona peut toujours être invoquée avec Epona's Song. - false: 50 - true: 0 - skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative. - false: 50 - true: 0 - complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask. - false: 50 - true: 0 - useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins. - false: 50 - true: 0 - fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente. - false: 50 - true: 0 - free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson. - false: 50 - true: 0 - fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask. - false: 50 - true: 0 - chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant. - \# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum - 0: 0 # valeur minimale - 7: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets. - none: 0 - mask: 0 - agony: 0 - always: 50 - hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc. - balanced: 50 - ddr: 0 - league: 0 - mw2: 0 - scrubs: 0 - strong: 0 - tournament: 0 - useless: 0 - very_strong: 0 - text_shuffle: # Randomise le texte dans le jeu pour un effet comique. - none: 50 - except_hints: 0 - complete: 0 - damage_multiplier: # contrôle la quantité de dégâts subis par Link. - half: 0 - normal: 50 - double: 0 - quadruple: 0 - ohko: 0 - no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets. - false: 50 - true: 0 - starting_tod: # Changer l'heure de début de la journée. - default: 50 - sunrise: 0 - morning: 0 - noon: 0 - afternoon: 0 - sunset: 0 - evening: 0 - midnight: 0 - witching_hour: 0 - start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins. - false: 50 - true: 0 - start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille. - false: 50 - true: 0 - item_pool_value: # modifie le nombre d'objets disponibles dans le jeu. - plentiful: 0 - balanced: 50 - scarce: 0 - minimal: 0 - junk_ice_traps: # Ajoute des pièges à glace au pool d'objets. - off: 0 - normal: 50 - on: 0 - mayhem: 0 - onslaught: 0 - ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes. - major_only: 50 - junk_only: 0 - anything: 0 - logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 50 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 0 - logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 0 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 50 +### Vérification de votre fichier de configuration -``` +Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator. +YAML page du validateur : [page de validation YAML](/mysterycheck) -## Rejoindre une partie MultiWorld +## Rejoindre un jeu multimonde -### Obtenez votre fichier de correctif OOT +### Obtenez votre fichier OOT modifié -Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini, -l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun -des dossiers. Votre fichier de données doit avoir une extension `.apz5`. +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ». -Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé -(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension -à l'émulateur comme recommandé). +Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé). ### Connectez-vous au multiserveur -Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils" -menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua. +Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script` 〉 `Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.) -Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`. +Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect : [mot de passe]`) -Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le -le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect : [mot de passe]`) - -Vous êtes maintenant prêt à commencer votre aventure à Hyrule. +Vous êtes maintenant prêt à commencer votre aventure dans Hyrule. \ No newline at end of file From 7af654e619bb39ee45544484fa14046264238e30 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 13:57:41 +0100 Subject: [PATCH 315/327] WebHost: validate uploaded datapackage and calculate own checksum (#2639) --- WebHostLib/upload.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 8f01294eac..af4ed264aa 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -11,11 +11,14 @@ from flask import request, flash, redirect, url_for, session, render_template from markupsafe import Markup from pony.orm import commit, flush, select, rollback from pony.orm.core import TransactionIntegrityError +import schema import MultiServer from NetUtils import SlotType from Utils import VersionException, __version__ +from worlds import GamesPackage from worlds.Files import AutoPatchRegister +from worlds.AutoWorld import data_package_checksum from . import app from .models import Seed, Room, Slot, GameDataPackage @@ -23,6 +26,15 @@ banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gb allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") allowed_generation_extensions = (".archipelago", ".zip") +games_package_schema = schema.Schema({ + "item_name_groups": {str: [str]}, + "item_name_to_id": {str: int}, + "location_name_groups": {str: [str]}, + "location_name_to_id": {str: int}, + schema.Optional("checksum"): str, + schema.Optional("version"): int, +}) + def allowed_options(filename: str) -> bool: return filename.endswith(allowed_options_extensions) @@ -37,6 +49,8 @@ def banned_file(filename: str) -> bool: def process_multidata(compressed_multidata, files={}): + game_data: GamesPackage + decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) slots: typing.Set[Slot] = set() @@ -45,11 +59,19 @@ def process_multidata(compressed_multidata, files={}): game_data_packages: typing.List[GameDataPackage] = [] for game, game_data in decompressed_multidata["datapackage"].items(): if game_data.get("checksum"): + original_checksum = game_data.pop("checksum") + game_data = games_package_schema.validate(game_data) + game_data = {key: value for key, value in sorted(game_data.items())} + game_data["checksum"] = data_package_checksum(game_data) game_data_package = GameDataPackage(checksum=game_data["checksum"], data=pickle.dumps(game_data)) + if original_checksum != game_data["checksum"]: + raise Exception(f"Original checksum {original_checksum} != " + f"calculated checksum {game_data['checksum']} " + f"for game {game}.") decompressed_multidata["datapackage"][game] = { "version": game_data.get("version", 0), - "checksum": game_data["checksum"] + "checksum": game_data["checksum"], } try: commit() # commit game data package @@ -64,14 +86,15 @@ def process_multidata(compressed_multidata, files={}): if slot_info.type == SlotType.group: continue slots.add(Slot(data=files.get(slot, None), - player_name=slot_info.name, - player_id=slot, - game=slot_info.game)) + player_name=slot_info.name, + player_id=slot, + game=slot_info.game)) flush() # commit slots compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) return slots, compressed_multidata + def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None): if not owner: owner = session["_id"] From 8e708f829d143fb32771282c0de57bb2e7955dff Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:12:37 +0100 Subject: [PATCH 316/327] The Witness: Fix an instance of multiworld.random being used (#2630) o_o --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index c2d2311c15..6360c33aef 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -143,7 +143,7 @@ class WitnessWorld(World): # Pick an early item to place on the tutorial gate. early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] if early_items: - random_early_item = self.multiworld.random.choice(early_items) + random_early_item = self.random.choice(early_items) if self.options.puzzle_randomization == 1: # In Expert, only tag the item as early, rather than forcing it onto the gate. self.multiworld.local_early_items[self.player][random_early_item] = 1 From c7617f92dd299f0907872c7ceb167d194b5dec93 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu, 28 Dec 2023 08:17:23 -0500 Subject: [PATCH 317/327] TLOZ: Try accounting for non_local_items with the pool of starting weapons (#2620) It was brought up that if you attempt to non_local any of the starting weapons, there is still a chance for it to get chosen as your starting weapon if you are on a StartingPosition value lower than very_dangerous. This fix will attempt to build the starting weapons list accounting for non_local items, but if all possible weapons have been set to non_local, force one of them to be your starting weapon anyway since the player is still expecting a starting weapon in their world if they have chosen one of the lower StartingPosition values. --- worlds/tloz/ItemPool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 7773accd8d..456598edec 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -93,7 +93,11 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() - starting_weapon = random.choice(starting_weapons) + final_starting_weapons = [weapon for weapon in starting_weapons + if weapon not in world.multiworld.non_local_items[world.player]] + if not final_starting_weapons: + final_starting_weapons = starting_weapons + starting_weapon = random.choice(final_starting_weapons) if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon elif world.multiworld.StartingPosition[world.player] in \ From 901201f675794d76d8f08232e1799246f7c9ea50 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 28 Dec 2023 08:21:54 -0500 Subject: [PATCH 318/327] Noita: Don't allow impossible slot names (#2608) * Noita: Add note about allowable slot names * Update character list * Update init to raise an exception if a yaml has bad characters * Slightly adjust exception message --- worlds/noita/__init__.py | 4 ++++ worlds/noita/docs/setup_en.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 499d202a64..792b90e3f5 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -35,6 +35,10 @@ class NoitaWorld(World): web = NoitaWeb() + def generate_early(self): + if not self.multiworld.get_player_name(self.player).isascii(): + raise Exception("Noita yaml's slot name has invalid character(s).") + # Returned items will be sent over to the client def fill_slot_data(self): return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md index bd6a151432..b67e840bb9 100644 --- a/worlds/noita/docs/setup_en.md +++ b/worlds/noita/docs/setup_en.md @@ -40,6 +40,8 @@ or try restarting your game. ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn about why Archipelago uses YAML files and what they're for. +Please note that Noita only allows you to type certain characters for your slot name. +These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/`` ### Where do I get a YAML? You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to @@ -54,4 +56,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th Click on the "AP" symbol at the top, then enter the desired address, slot name, and password. That's all you need for it. It will provide you with a quick reference to see which checks you've done and -which checks you still have left. \ No newline at end of file +which checks you still have left. From 24ac3de12586ce1cfb47e371b9fa7a91e3f3b933 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 14:30:10 +0100 Subject: [PATCH 319/327] Factorio: "improve" default start items (#2588) Makes it less likely that people kill themselves via pollution and gives them some healing items they may not even know about. --- worlds/factorio/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b72d57ad9b..3429ebbd42 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range): class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - default = {"burner-mining-drill": 19, "stone-furnace": 19} + default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50} class FactorioFreeSampleBlacklist(OptionSet): From d1a17a350d94428ce345da9f23032bb91cc76e3f Mon Sep 17 00:00:00 2001 From: Jarno Date: Thu, 28 Dec 2023 14:41:24 +0100 Subject: [PATCH 320/327] Docs: Add missing Get location_name_groups_* to network protocol (#2550) --- docs/network protocol.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 199f96f481..274b6e3716 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -380,12 +380,13 @@ Additional arguments sent in this package will also be added to the [Retrieved]( Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. -| Name | Type | Notes | -|------------------------------|-------------------------------|---------------------------------------------------| -| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | -| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | -| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | -| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | +| Name | Type | Notes | +|----------------------------------|-------------------------------|-------------------------------------------------------| +| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | +| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | +| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | +| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. From e674e37e08fed77fa724021a29eeaa7d92b25bfa Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:43:16 -0500 Subject: [PATCH 321/327] SMZ3: optimized message queues (#2611) --- worlds/smz3/Client.py | 22 +++++++++++----------- worlds/smz3/__init__.py | 3 ++- worlds/smz3/data/zsm.ips | Bin 1470841 -> 1470841 bytes 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 687a43b00f..859cf234eb 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -69,7 +69,7 @@ class SMZ3SNIClient(SNIClient): ctx.finished_game = True return - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4) if data is None: return @@ -77,14 +77,14 @@ class SMZ3SNIClient(SNIClient): recv_item = data[2] | (data[3] << 8) while (recv_index < recv_item): - item_address = recv_index * 8 - message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8) - is_z3_item = ((message[5] & 0x80) != 0) - masked_part = (message[5] & 0x7F) if is_z3_item else message[5] - item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) + item_address = recv_index * 2 + message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2) + is_z3_item = ((message[1] & 0x80) != 0) + masked_part = (message[1] & 0x7F) if is_z3_item else message[1] + item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) recv_index += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from .TotalSMZ3.Location import locations_start_id from . import convertLocSMZ3IDToAPID @@ -95,7 +95,7 @@ class SMZ3SNIClient(SNIClient): snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4) if data is None: return @@ -106,10 +106,10 @@ class SMZ3SNIClient(SNIClient): item = ctx.items_received[item_out_ptr] item_id = item.item - items_start_id - player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF])) + player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id])) item_out_ptr += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 2cc2ac97d9..39aa42c07a 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -80,7 +80,8 @@ class SMZ3World(World): locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} # first added for 0.2.6 - required_client_version = (0, 2, 6) + # optimized message queues for 0.4.4 + required_client_version = (0, 4, 4) def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index fff36d95d15c96a9cc756c4fd0aa10ccda3e1751..87a4f924f1933fcf59493753c034192ef03a328c 100644 GIT binary patch delta 508 zcmezQDDvl{$O+3CKWtnvO`6Fece8^`77OFb&8ro5T;lp7_~TYgayzHem*(8h?YW;B zftU%1nSq!Eh*`Jier9WyVpQM$k(d2ABcuBCo&4;ojArfk`PqS(1Bf|+m}~oee(u$e z7+t4#f8ss^v}AhZXKq7phulRkUOWh3xbRV<>Q_d#$F22txr?|~&B(}@zaV!Jh$Gbx zB(gm?4jHUB%U#rOmAgm-s1C#hGW&r-K(2)VgJ|9M`=7au80$9(g4CV2$X)cI=#j$u zPm&B5QrA9Vx{$E;J@bX=rLiX&KJcAn`M`Bj@B`aP@efQ5?n?zuvIE(iK(_T#!3*YV z|4Du@vg-XO2~^6|pnFIGWFXh7{R|0}CyxW8(DYbNo@#BF z%ast;9fnzVP(cYC1Q6@4Z2!l}qn!#2XRUmm?OOS~-_#kk+ZCtt0x{oq#p(PkeT+NX h)l>zxtEmcJ?O+sY|Fc#Qh(WY45Q}X8vsSeBFaS*}+?xOZ delta 513 zcmezQDDvl{$O+3CUvFG7O`2&z+GYotEEdKkn^!CBxWx5d@W-u~LNMw6(95PtXkhZ9wC2f%gPzQ($WcCAvfLsd!2GPpxS3h$bG1jjZ1gSgE zl(y(Y?jwcupClPBB&>bHbRl}}d*%yaOJh$meBe9D@`3B5;0Lyo;vbkAoR$ilWCyZ2 zfo!v-f)|X|{*(NmYt{Qt5~!4^LF14D$Uvau_A?|bYOUMP1SD$|PJykF0y&_8btM;2 zHOMPU4AZ;6a&PB$6aJz3pW%bZs{PBR`+nor=axefX#d9T!?t From 3d1be0c468d0717fb9781886b006b498fa7751a4 Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:13:35 -0500 Subject: [PATCH 322/327] FF1: Fix terminated_event access_rule not getting set (#2648) --- worlds/ff1/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 16905cc6da..4ff361c072 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -74,6 +74,7 @@ class FF1World(World): items = get_options(self.multiworld, 'items', self.player) goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]], self.player) + terminated_event.access_rule = goal_rule if "Shard" in items.keys(): def goal_rule_and_shards(state): return goal_rule(state) and state.has("Shard", self.player, 32) From c104e81145d6a5e89a25fb90c402abe2585ce007 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Mon, 1 Jan 2024 11:42:41 -0800 Subject: [PATCH 323/327] Zillion: move client to worlds/zillion (#2649) --- ZillionClient.py | 505 +-------------------------------------- worlds/zillion/client.py | 501 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 500 deletions(-) create mode 100644 worlds/zillion/client.py diff --git a/ZillionClient.py b/ZillionClient.py index 5f3cbb943f..ef96edab04 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,505 +1,10 @@ -import asyncio -import base64 -import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +import ModuleUpdate +ModuleUpdate.update() -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, gui_enabled, \ - ClientCommandProcessor, logger, get_base_parser -from NetUtils import ClientStatus -import Utils -from Utils import async_start - -import colorama - -from zilliandomizer.zri.memory import Memory -from zilliandomizer.zri import events -from zilliandomizer.utils.loc_name_maps import id_to_loc -from zilliandomizer.options import Chars -from zilliandomizer.patch import RescueInfo - -from worlds.zillion.id_maps import make_id_to_others -from worlds.zillion.config import base_id, zillion_map - - -class ZillionCommandProcessor(ClientCommandProcessor): - ctx: "ZillionContext" - - def _cmd_sms(self) -> None: - """ Tell the client that Zillion is running in RetroArch. """ - logger.info("ready to look for game") - self.ctx.look_for_retroarch.set() - - def _cmd_map(self) -> None: - """ Toggle view of the map tracker. """ - self.ctx.ui_toggle_map() - - -class ToggleCallback(Protocol): - def __call__(self) -> None: ... - - -class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... - - -class ZillionContext(CommonContext): - game = "Zillion" - command_processor = ZillionCommandProcessor - items_handling = 1 # receive items from other players - - known_name: Optional[str] - """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ - - from_game: "asyncio.Queue[events.EventFromGame]" - to_game: "asyncio.Queue[events.EventToGame]" - ap_local_count: int - """ local checks watched by server """ - next_item: int - """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] - start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} - got_room_info: asyncio.Event - """ flag for connected to server """ - got_slot_data: asyncio.Event - """ serves as a flag for whether I am logged in to the server """ - - look_for_retroarch: asyncio.Event - """ - There is a bug in Python in Windows - https://github.com/python/cpython/issues/91227 - that makes it so if I look for RetroArch before it's ready, - it breaks the asyncio udp transport system. - - As a workaround, we don't look for RetroArch until this event is set. - """ - - ui_toggle_map: ToggleCallback - ui_set_rooms: SetRoomCallback - """ parameter is y 16 x 8 numbers to show in each room """ - - def __init__(self, - server_address: str, - password: str) -> None: - super().__init__(server_address, password) - self.known_name = None - self.from_game = asyncio.Queue() - self.to_game = asyncio.Queue() - self.got_room_info = asyncio.Event() - self.got_slot_data = asyncio.Event() - self.ui_toggle_map = lambda: None - self.ui_set_rooms = lambda rooms: None - - self.look_for_retroarch = asyncio.Event() - if platform.system() != "Windows": - # asyncio udp bug is only on Windows - self.look_for_retroarch.set() - - self.reset_game_state() - - def reset_game_state(self) -> None: - for _ in range(self.from_game.qsize()): - self.from_game.get_nowait() - for _ in range(self.to_game.qsize()): - self.to_game.get_nowait() - self.got_slot_data.clear() - - self.ap_local_count = 0 - self.next_item = 0 - self.ap_id_to_name = {} - self.ap_id_to_zz_id = {} - self.rescues = {} - self.loc_mem_to_id = {} - - self.locations_checked.clear() - self.missing_locations.clear() - self.checked_locations.clear() - self.finished_game = False - self.items_received.clear() - - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: - self.to_game.put_nowait(events.DeathEventToGame()) - return super().on_deathlink(data) - - # override - async def server_auth(self, password_requested: bool = False) -> None: - if password_requested and not self.password: - await super().server_auth(password_requested) - if not self.auth: - logger.info('waiting for connection to game...') - return - logger.info("logging in to server...") - await self.send_connect() - - # override - def run_gui(self) -> None: - from kvui import GameManager - from kivy.core.text import Label as CoreLabel - from kivy.graphics import Ellipse, Color, Rectangle - from kivy.uix.layout import Layout - from kivy.uix.widget import Widget - - class ZillionManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Zillion Client" - - class MapPanel(Widget): - MAP_WIDTH: ClassVar[int] = 281 - - _number_textures: List[Any] = [] - rooms: List[List[int]] = [] - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.rooms = [[0 for _ in range(8)] for _ in range(16)] - - self._make_numbers() - self.update_map() - - self.bind(pos=self.update_map) - # self.bind(size=self.update_bg) - - def _make_numbers(self) -> None: - self._number_textures = [] - for n in range(10): - label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) - label.refresh() - self._number_textures.append(label.texture) - - def update_map(self, *args: Any) -> None: - self.canvas.clear() - - with self.canvas: - Color(1, 1, 1, 1) - Rectangle(source=zillion_map, - pos=self.pos, - size=(ZillionManager.MapPanel.MAP_WIDTH, - int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image - for y in range(16): - for x in range(8): - num = self.rooms[15 - y][x] - if num > 0: - Color(0, 0, 0, 0.4) - pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] - Ellipse(size=[22, 22], pos=pos) - Color(1, 1, 1, 1) - pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] - num_texture = self._number_textures[num] - Rectangle(texture=num_texture, size=num_texture.size, pos=pos) - - def build(self) -> Layout: - container = super().build() - self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) - self.main_area_container.add_widget(self.map_widget) - return container - - def toggle_map_width(self) -> None: - if self.map_widget.width == 0: - self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH - else: - self.map_widget.width = 0 - self.container.do_layout() - - def set_rooms(self, rooms: List[List[int]]) -> None: - self.map_widget.rooms = rooms - self.map_widget.update_map() - - self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() - self.ui_task = asyncio.create_task(run_co, name="UI") - - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: - self.room_item_numbers_to_ui() - if cmd == "Connected": - logger.info("logged in to Archipelago server") - if "slot_data" not in args: - logger.warn("`Connected` packet missing `slot_data`") - return - slot_data = args["slot_data"] - - if "start_char" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") - return - self.start_char = slot_data['start_char'] - if self.start_char not in {"Apple", "Champ", "JJ"}: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` `start_char` has invalid value: {self.start_char}") - - if "rescues" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") - return - rescues = slot_data["rescues"] - self.rescues = {} - for rescue_id, json_info in rescues.items(): - assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" - # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? - assert json_info["start_char"] == self.start_char, \ - f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' - ri = RescueInfo(json_info["start_char"], - json_info["room_code"], - json_info["mask"]) - self.rescues[0 if rescue_id == "0" else 1] = ri - - if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") - return - loc_mem_to_id = slot_data["loc_mem_to_id"] - self.loc_mem_to_id = {} - for mem_str, id_str in loc_mem_to_id.items(): - mem = int(mem_str) - id_ = int(id_str) - room_i = mem // 256 - assert 0 <= room_i < 74 - assert id_ in id_to_loc - self.loc_mem_to_id[mem] = id_ - - if len(self.loc_mem_to_id) != 394: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") - - self.got_slot_data.set() - - payload = { - "cmd": "Get", - "keys": [f"zillion-{self.auth}-doors"] - } - async_start(self.send_msgs([payload])) - elif cmd == "Retrieved": - if "keys" not in args: - logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") - return - keys = cast(Dict[str, Optional[str]], args["keys"]) - doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) - if doors_b64: - logger.info("received door data from server") - doors = base64.b64decode(doors_b64) - self.to_game.put_nowait(events.DoorEventToGame(doors)) - elif cmd == "RoomInfo": - self.seed_name = args["seed_name"] - self.got_room_info.set() - - def room_item_numbers_to_ui(self) -> None: - rooms = [[0 for _ in range(8)] for _ in range(16)] - for loc_id in self.missing_locations: - loc_id_small = loc_id - base_id - loc_name = id_to_loc[loc_id_small] - y = ord(loc_name[0]) - 65 - x = ord(loc_name[2]) - 49 - if y == 9 and x == 5: - # don't show main computer in numbers - continue - assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" - rooms[y][x] += 1 - # TODO: also add locations with locals lost from loading save state or reset - self.ui_set_rooms(rooms) - - def process_from_game_queue(self) -> None: - if self.from_game.qsize(): - event_from_game = self.from_game.get_nowait() - if isinstance(event_from_game, events.AcquireLocationEventFromGame): - server_id = event_from_game.id + base_id - loc_name = id_to_loc[event_from_game.id] - self.locations_checked.add(server_id) - if server_id in self.missing_locations: - self.ap_local_count += 1 - n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') - async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} - ])) - else: - # This will happen a lot in Zillion, - # because all the key words are local and unwatched by the server. - logger.debug(f"DEBUG: {loc_name} not in missing") - elif isinstance(event_from_game, events.DeathEventFromGame): - async_start(self.send_death()) - elif isinstance(event_from_game, events.WinEventFromGame): - if not self.finished_game: - async_start(self.send_msgs([ - {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} - ])) - self.finished_game = True - elif isinstance(event_from_game, events.DoorEventFromGame): - if self.auth: - doors_b64 = base64.b64encode(event_from_game.doors).decode() - payload = { - "cmd": "Set", - "key": f"zillion-{self.auth}-doors", - "operations": [{"operation": "replace", "value": doors_b64}] - } - async_start(self.send_msgs([payload])) - else: - logger.warning(f"WARNING: unhandled event from game {event_from_game}") - - def process_items_received(self) -> None: - if len(self.items_received) > self.next_item: - zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] - for index in range(self.next_item, len(self.items_received)): - ap_id = self.items_received[index].item - from_name = self.player_names[self.items_received[index].player] - # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') - self.to_game.put_nowait( - events.ItemEventToGame(zz_item_ids) - ) - self.next_item = len(self.items_received) - - -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: - """ returns player name, and end of seed string """ - if len(data) == 0: - # no connection to game - return "", "xxx" - null_index = data.find(b'\x00') - if null_index == -1: - logger.warning(f"invalid game id in rom {repr(data)}") - null_index = len(data) - name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) - if null_index_2 == -1: - null_index_2 = len(data) - seed_name = data[null_index + 1:null_index_2].decode() - - return name, seed_name - - -async def zillion_sync_task(ctx: ZillionContext) -> None: - logger.info("started zillion sync task") - - # to work around the Python bug where we can't check for RetroArch - if not ctx.look_for_retroarch.is_set(): - logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") - await asyncio.wait(( - asyncio.create_task(ctx.look_for_retroarch.wait()), - asyncio.create_task(ctx.exit_event.wait()) - ), return_when=asyncio.FIRST_COMPLETED) - - last_log = "" - - def log_no_spam(msg: str) -> None: - nonlocal last_log - if msg != last_log: - last_log = msg - logger.info(msg) - - # to only show this message once per client run - help_message_shown = False - - with Memory(ctx.from_game, ctx.to_game) as memory: - while not ctx.exit_event.is_set(): - ram = await memory.read() - game_id = memory.get_rom_to_ram_data(ram) - name, seed_end = name_seed_from_ram(game_id) - if len(name): - if name == ctx.known_name: - ctx.auth = name - # this is the name we know - if ctx.server and ctx.server.socket: # type: ignore - if ctx.got_room_info.is_set(): - if ctx.seed_name and ctx.seed_name.endswith(seed_end): - # correct seed - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - async_start(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - asyncio.create_task(ctx.got_slot_data.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets - else: # not correct seed name - log_no_spam("incorrect seed - did you mix up roms?") - else: # no room info - # If we get here, it looks like `RoomInfo` packet got lost - log_no_spam("waiting for room info from server...") - else: # server not connected - log_no_spam("waiting for server connection...") - else: # new game - log_no_spam("connected to new game") - await ctx.disconnect() - ctx.reset_server_state() - ctx.seed_name = None - ctx.got_room_info.clear() - ctx.reset_game_state() - memory.reset_game_state() - - ctx.auth = name - ctx.known_name = name - async_start(ctx.connect()) - await asyncio.wait(( - asyncio.create_task(ctx.got_room_info.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) - else: # no name found in game - if not help_message_shown: - logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') - help_message_shown = True - log_no_spam("looking for connection to game...") - await asyncio.sleep(0.3) - - await asyncio.sleep(0.09375) - logger.info("zillion sync task ending") - - -async def main() -> None: - parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') - # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) - args = parser.parse_args() - print(args) - - if args.diff_file: - import Patch - logger.info("patch file was supplied - creating sms rom...") - meta, rom_file = Patch.create_rom_file(args.diff_file) - if "server" in meta: - args.connect = meta["server"] - logger.info(f"wrote rom file to {rom_file}") - - ctx = ZillionContext(args.connect, args.password) - if ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - sync_task = asyncio.create_task(zillion_sync_task(ctx)) - - await ctx.exit_event.wait() - - ctx.server_address = None - logger.debug("waiting for sync task to end") - await sync_task - logger.debug("sync task ended") - await ctx.shutdown() +import Utils # noqa: E402 +from worlds.zillion.client import launch # noqa: E402 if __name__ == "__main__": Utils.init_logging("ZillionClient", exception_logger="Client") - - colorama.init() - asyncio.run(main()) - colorama.deinit() + launch() diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py new file mode 100644 index 0000000000..ac73f6db50 --- /dev/null +++ b/worlds/zillion/client.py @@ -0,0 +1,501 @@ +import asyncio +import base64 +import platform +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast + +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +from Utils import async_start + +import colorama + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from .id_maps import make_id_to_others +from .config import base_id, zillion_map + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + def _cmd_map(self) -> None: + """ Toggle view of the map tracker. """ + self.ctx.ui_toggle_map() + + +class ToggleCallback(Protocol): + def __call__(self) -> None: ... + + +class SetRoomCallback(Protocol): + def __call__(self, rooms: List[List[int]]) -> None: ... + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + known_name: Optional[str] + """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + ui_toggle_map: ToggleCallback + ui_set_rooms: SetRoomCallback + """ parameter is y 16 x 8 numbers to show in each room """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.known_name = None + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() + self.got_slot_data = asyncio.Event() + self.ui_toggle_map = lambda: None + self.ui_set_rooms = lambda rooms: None + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + from kivy.core.text import Label as CoreLabel + from kivy.graphics import Ellipse, Color, Rectangle + from kivy.uix.layout import Layout + from kivy.uix.widget import Widget + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + class MapPanel(Widget): + MAP_WIDTH: ClassVar[int] = 281 + + _number_textures: List[Any] = [] + rooms: List[List[int]] = [] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.rooms = [[0 for _ in range(8)] for _ in range(16)] + + self._make_numbers() + self.update_map() + + self.bind(pos=self.update_map) + # self.bind(size=self.update_bg) + + def _make_numbers(self) -> None: + self._number_textures = [] + for n in range(10): + label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) + label.refresh() + self._number_textures.append(label.texture) + + def update_map(self, *args: Any) -> None: + self.canvas.clear() + + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(source=zillion_map, + pos=self.pos, + size=(ZillionManager.MapPanel.MAP_WIDTH, + int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image + for y in range(16): + for x in range(8): + num = self.rooms[15 - y][x] + if num > 0: + Color(0, 0, 0, 0.4) + pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] + Ellipse(size=[22, 22], pos=pos) + Color(1, 1, 1, 1) + pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] + num_texture = self._number_textures[num] + Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + + def build(self) -> Layout: + container = super().build() + self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) + self.main_area_container.add_widget(self.map_widget) + return container + + def toggle_map_width(self) -> None: + if self.map_widget.width == 0: + self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH + else: + self.map_widget.width = 0 + self.container.do_layout() + + def set_rooms(self, rooms: List[List[int]]) -> None: + self.map_widget.rooms = rooms + self.map_widget.update_map() + + self.ui = ZillionManager(self) + self.ui_toggle_map = lambda: self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + self.room_item_numbers_to_ui() + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + if len(self.loc_mem_to_id) != 394: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + async_start(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() + + def room_item_numbers_to_ui(self) -> None: + rooms = [[0 for _ in range(8)] for _ in range(16)] + for loc_id in self.missing_locations: + loc_id_small = loc_id - base_id + loc_name = id_to_loc[loc_id_small] + y = ord(loc_name[0]) - 65 + x = ord(loc_name[2]) - 49 + if y == 9 and x == 5: + # don't show main computer in numbers + continue + assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" + rooms[y][x] += 1 + # TODO: also add locations with locals lost from loading save state or reset + self.ui_set_rooms(rooms) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + async_start(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + async_start(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + async_start(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + async_start(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {repr(data)}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) + if len(name): + if name == ctx.known_name: + ctx.auth = name + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + async_start(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + asyncio.create_task(ctx.got_slot_data.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + ctx.known_name = name + async_start(ctx.connect()) + await asyncio.wait(( + asyncio.create_task(ctx.got_room_info.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +def launch() -> None: + colorama.init() + asyncio.run(main()) + colorama.deinit() From 88c7484b3a105cea8adbb8ade5c2afd80fff4c4c Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:16:45 -0700 Subject: [PATCH 324/327] Shivers: Fixes rule logic for location 'puzzle solved three floor elevator' (#2657) Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index fdd260ca91..4e1058fecf 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -157,7 +157,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) and state.has("Key for Office Elevator", player))), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) and state.has("Key for Three Floor Elevator", player))) }, "lightning": { From e5c739ee31c43450dd5845768fa459f98e917dce Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:19:57 -0500 Subject: [PATCH 325/327] KH2: Ability dupe fix and stat increase fix (#2621) Makes the client make sure the player has the correct amount of stat increase instead of letting the goa mod (apcompanion) do it abilities: checks the slot where abilities could dupe unless that slot is being used for an actual abiliity given to the player --- worlds/kh2/Client.py | 95 +++++++++++++++++++++++++++++++++++--------- worlds/kh2/Rules.py | 5 +-- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index a5be06c7fb..544e710741 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -80,11 +80,6 @@ class KH2Context(CommonContext): }, }, } - self.front_of_inventory = { - "Sora": 0x2546, - "Donald": 0x2658, - "Goofy": 0x276C, - } self.kh2seedname = None self.kh2slotdata = None self.itemamount = {} @@ -169,6 +164,14 @@ class KH2Context(CommonContext): self.ability_code_list = None self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + self.base_hp = 20 + self.base_mp = 100 + self.base_drive = 5 + self.base_accessory_slots = 1 + self.base_armor_slots = 1 + self.base_item_slots = 3 + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(KH2Context, self).server_auth(password_requested) @@ -219,6 +222,12 @@ class KH2Context(CommonContext): def kh2_read_byte(self, address): return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + def kh2_read_int(self, address): + return self.kh2.read_int(self.kh2.base_address + address) + + def kh2_write_int(self, address, value): + self.kh2.write_int(self.kh2.base_address + address, value) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -476,7 +485,7 @@ class KH2Context(CommonContext): async def give_item(self, item, location): try: - # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -507,6 +516,8 @@ class KH2Context(CommonContext): ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: @@ -518,11 +529,14 @@ class KH2Context(CommonContext): ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 - elif itemname in self.goofy_ability_set: + else: ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: # if memaddr is in a bitmask location in memory if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: @@ -615,7 +629,7 @@ class KH2Context(CommonContext): master_sell = master_equipment | master_staff | master_shield await asyncio.create_task(self.IsInShop(master_sell)) - + # print(self.kh2_seed_save_cache["AmountInvo"]["Ability"]) for item_name in master_amount: item_data = self.item_name_to_data[item_name] amount_of_items = 0 @@ -673,10 +687,10 @@ class KH2Context(CommonContext): self.kh2_write_short(self.Save + slot, item_data.memaddr) # removes the duped ability if client gave faster than the game. - for charInvo in {"Sora", "Donald", "Goofy"}: - if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: - print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") - self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + for ability in self.front_ability_slots: + if self.kh2_read_short(self.Save + ability) != 0: + print(f"removed {self.Save + ability} from {ability}") + self.kh2_write_short(self.Save + ability, 0) # remove the dummy level 1 growths if they are in these invo slots. for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: @@ -740,15 +754,60 @@ class KH2Context(CommonContext): self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: - item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5: + if item_name == ItemName.MaxHPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 5 + else: # Critical + Bonus = 2 + if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items)) + + elif item_name == ItemName.MaxMPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 10 + else: # Critical + Bonus = 5 + if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items)) + + elif item_name == ItemName.DriveGaugeUp: + current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2) + # change when max drive is changed from 6 to 4 + if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items: + self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items) + + elif item_name == ItemName.AccessorySlotUp: + current_accessory = self.kh2_read_byte(self.Save + 0x2501) + if current_accessory != self.base_accessory_slots + amount_of_items: + if 4 > current_accessory < self.base_accessory_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1) + elif self.base_accessory_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items) + + elif item_name == ItemName.ArmorSlotUp: + current_armor_slots = self.kh2_read_byte(self.Save + 0x2500) + if current_armor_slots != self.base_armor_slots + amount_of_items: + if 4 > current_armor_slots < self.base_armor_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1) + elif self.base_armor_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items) + + elif item_name == ItemName.ItemSlotUp: + current_item_slots = self.kh2_read_byte(self.Save + 0x2502) + if current_item_slots != self.base_item_slots + amount_of_items: + if 8 > current_item_slots < self.base_item_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) + elif self.base_item_slots + amount_of_items < 8: + self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) + + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) - # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ - and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ - self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: - self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 41207c6cb3..7c5551dbd5 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -268,7 +268,6 @@ class KH2WorldRules(KH2Rules): add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) @@ -291,8 +290,8 @@ class KH2WorldRules(KH2Rules): else: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ - state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) if self.multiworld.FinalXemnas[self.player]: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) else: From bf17582c5534d3dc35bdb597bcb2da097228e275 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 2 Jan 2024 03:32:03 -0700 Subject: [PATCH 326/327] BizHawkClient: Add some handling for non-string errors (#2656) --- data/lua/connector_bizhawk_generic.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index eff400cb03..47af6e003d 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -456,6 +456,7 @@ function send_receive () failed_guard_response = response end else + if type(response) ~= "string" then response = "Unknown error" end res[i] = {type = "ERROR", err = response} end end From 0df0955415cd4523531ab091f05090831fb5016d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 2 Jan 2024 08:03:39 -0600 Subject: [PATCH 327/327] Core: check if a location is an event before excluding it (#2653) * Core: check if a location is an event before excluding it * log a warning * put the warning in the right spot --- worlds/generic/Rules.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 520ad22525..ac5e1aa507 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,4 +1,5 @@ import collections +import logging import typing from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance @@ -81,15 +82,18 @@ def locality_rules(world: MultiWorld): i.name not in sending_blockers[i.player] and old_rule(i) -def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: +def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: - location = world.get_location(loc_name, player) + location = multiworld.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if loc_name not in world.worlds[player].location_name_to_id: + if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: - location.progress_type = LocationProgressType.EXCLUDED + if not location.event: + location.progress_type = LocationProgressType.EXCLUDED + else: + logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):

    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 z zI_E{U{ER_H)t#8lm99hP!-9d550Xb@*6h6+a>5k78!kbZSf<*O++XOEeW>` z3`^s0ENI)ls;b@qcsWE9yF;2=kBdXS#p95ZS*9Vs3Um|m=Xco}RCr;_#ETb5R&`t1 zWs#PYUZDl+@VXyaRhy`Hk=bh<4RAK=tE*KAKTgiNvN+!5yY3Uep^DOZoDYjvxofk3O3jGnvqZ^QV zi)a8X|E`J*&>m9rw>rZg?qoSXe}|oEJYNVSzg|NAwZGrl8_l3Kq~Lvs?ETn9;1iOG zGk?e{vIeo|^N))WZ8z=s&umDlCwEDLP={k!ya_P6tWRTHA;-MvdBEifXH_*Fxy`7z%`E*1(STgfj!oie&E=%;8S4V}p1wp~|$*iJ#!cS>Y3*79> zN4p>-7SN)V_Kjf{{xbFLq2k)o@BwP>H6qt*NtzL^rlUs=&d#eNugD*rW$|*iMek&hBh87w}GI@oR<}R`#u_aIMwd0y9xVR=j&c zGe|S(eTCXEFF>7ta=f5~c*EjtXd*DVXTfnycnbBG z#zcM5jB>mS&fO2J%%xdzes^>YX3$+dU}t}#&OOX3)z8Zq_W(KDFqa+Lw9w;nEcMLe zc1zZq=zP)Jd+9Xb!7wN(4*~LxWzfyLE7^G=6!doB9a0R#V`{~g<;T2$)SALqh?+CY zzMhy=Hc8&tn{nMd?)so5n9Lm0(Ne&DP!uAhndA&f?trW~<4qOKRbh+Hhprb}WmQw$ ze!p>L%VdwHBV%!r!>!KJ?rdDd=vv)pm9m!#@B64Mt&tYGtTrqZJp&q}FSPVN7_n-C z7+tT?2*JW=HoD;o3rby$=Lq7O3D~(fUF}u#b*-JG-g)FcmUjHi62^*WYMcGW(>Yix!E3JlzqQAcV#m6fCpTh$DmEh87*Wf63` zz|GEzQl;i$Esbwl`k=FP4Tl9;eyzPQbZ~cjaKCSEvJ{w`e+DYtj#~1bi44Z;To?`+ zTQ8E8zeLMZG^p;@S=d9aOT!31gZKD|hqS^|>V3Px)x5={vVw1Lv@EqNNKs{#I{~pW zw?}?f*P`YePVc!--{_rhPcYFGKl!cBNpY;TN{aFPB7WcAYs%~6szPr=wq^e9UGCY9 zeACoV@AfOLxJ#;3gR47*`OWO(kgl|^Fw!bc3fzieZiK&Ub|M2w5<;8ju)O86c{{tJ z$h#y#(QfXJ*RZ`FFL}1R%16ts7=`(B$E#)hw7_ZPF)zi8l%KPhLs_1;*rDQSPhNC4 zzN`%2zlz#V7-hmftD~qa@j5Ml8UyF16H=9sGw429gbX?l&(B9d|I2q0ms;2zyjBGI z3l^`VFcb4W=go@Lf~JcOmh}31Vhn-b@6sG!&IX>4@l!;3qs-N9rIK$d+E4o z_i~tOkP_Jz;+P-nTKIE&Z*V=qcIpH0aJBD>HCA0NmbtAas*aCidLDGaS@dalE~K~@ za?Z1{F^N%gKDytiz#=O^5GBrYS1sTFxrO$U7ddS+Sr_GOo!YRH#qy z3%|5g4^^a^K8rK3-JpwG7i6#WV%FF-`~`NsA=Eua&%^ER6_9;ggBd764aogkT#|Y@ zo|BZl0(ddh3aL1j8Nm1Vqq;Zew;w6Y=SWR?#jRo+ zE!b756|G{gX)C|i*hC03aqt9Nj8e4IbIrf;qYardh! z+$JN>49~X5tk~5P!eG~GI6&4?zP+RSB_PA8Q?wm=arb?{Nv`)syE1&yS$Y2a(LB3e z37tH)KRchef+ya4Ej_pj(EP6+isM+lZ#{-8polRP6hnhek}SOQ;gSl@ng3^~j4VS1 zpUn_uvdedQ>lMa|bagM|#PQL$W?AcQTRzm9pSzUpyML(oJn4Th(9SwQYy^p+>t=7Q z`nvv(6nl)FvuTn@dZ0@fTdbmb!3{O_?s}6_2qpDq*SOSfz4t?aaTOG}B)+hurvt0o zxA;V*M0?LH-TV$bb=R9hQ4<9!u@0XXh3dw+_}gkDYFupC?3z6u;$tXR6X)E#1SJmI zb~l{|l~daGo?H1&KGZM9gn(9kzix*a+H`k*em-T9Z1#uha52q_lb;83Fq2-FutFdw zHP^g@!b|nE_-rM5VeBbNXXipCAESrQ9UUo6e5i#A^Wcn*P5?xlXxb+Gc;ol=ZBo72 zUaAYad9Ag`)YAnFsj=(r_gPUo1qde5O*%-m&A+yTT!w}p5vLKmvq?2RBudNcEuKA4 zpXjrLtEfLvuGYrZhy_ilW*~tYd8=XxgMr)l&sIu{4SkGKxyEM2*9|+$kQ%$`Ktfq<$V0o*n7=D0|xJD!&Du6pvXLAjYomcI$5~eX79xb)pPEw4U`i{PAjd z8iGRR`mCh9+;3U!N6}Gm*td`LF!r8S&N&uhO7I-+e9j+4qDd?eX%#*idcboGuz~A- zmM{KP;<3)K-YC8VfOJ9P<07JEwaRW-bsieX4W*<<+w-JZpU~LV!NrL=uB)*03u1{Y z$%5jSUlwDpSV5%CLIZ&Lp~F1C({9}HGG+g)$U1Y(i=*oPW42GttlR1gj=*V6vUt}X zD{Pe-Dvn8YEgU5EH2;`zG%R@OV-!2#u&?AOCYa;&i^_TV>rmFbsNAg`OVG1Q-bLuj zA@eKYGtIN*|E?|geVaF^$8PuyqTMLMit3y8}pAE70=h67;=#=s*i(J;h4CaKQ1lxZ(%jC z2;#xFw1nItuifEG8DvHu*IL!>H<7@T)P0gkv&HL4Xm~c{*~Y=t zf0K{ZC9CwR9Y)e|PlZ94!8mcE!OL&Xzjd7DC?&TJx%)0EVK90tg|W$(b+=KjDacHV zr41o=g}gp1dLQ3pIg|pjmF3*9KI@IvbtPif&L-yd3E5UC_qsE6I3qC0VkD*(25zoR;C#*>z_)L}`!c8$J9M4^ej69`rA}$t z$#eBl{&E=ZVOpOqPbCh1i zNfpBUC7wE~m{MmYjTs3ozVC#Icq zkkId8F`2&VU@|up!1kb3M$UjBVo4;(*l~Gqy_HxjZ-oM5n6~Bvl1VTht84C3bVrQ~ ztM$q{H?_oS4J%PspcKjGbDcKu!}xaVw);FaA#qM1P^Ufg13{V`LB8$F&|JZzE7=nU zD&c~F+{M;6l@tzp9=mM(os-5&9WX_RhZ3*vDHTO`KpG_=!&l(7{6~)G-`Sk6b`cfT z)dmZmQGlYx`FCDt2^B+J+a-VD4fP+M>6jJD75)2l{+$*20S3gV)0Fgwy;4U>B@U%$ z*>waw%wK#~x6eP(ULu0R8sDEB`#t`{*I;5!Wc*XE;x9%l|J6UyVQBx64x{|*Bq;Uv z?PlTNMtk~pzWK^Iq(dK6w>{M_C6J>93nOlA&9Z~BJiB=7v}Tyr*>O2G=i=ph4Ms%| zavyD+@Y-~O{=5`#xM)m&qPtMg3Hjd&yMgdpfZ#>ryGGU6u za8X2`Ox@@_>05mzF0S|nZJ3q=$pi=>DHwd;18%W(#p?^oh-utF9deYb|Qr+yl)ybWJIEZ1|S; za+ibd&)t2qV3T3WH7>!EMf;!T)E-?QUx1ZRk_Qb9Hkd%;ljC=t4~7?BBRm{pEq!#* z*b7rc24C@>y$39(ncu&DW5+4DJ%27N-`3}vzuK}M!>Vtx`YaD(^-YB8g&U-au@xD> ztk#*X7uU-`0*zmQ9FgL`&q_1Q-l;g0o=N$U)kgYba+b~K=tvJODwar5>G_&j3XC+` z(6wmAHiF3;)oI2uW%(mlq&%13_#~K0F%Z}i8`;P2<&V1iNFr_SKIDAFJTQF2(5w@W zvvOSVeJ9dGXs6`%8I$w8=^XEPod^7rE23VbPXp?&JM|ZK3IhQlN)P|utMxJx`J zImZ;okHaXuLqO1VAsl?->=~#zfSK0wEnX)?xeO8a zymc+@R`2IU{PV_Q`5i0LA8>GFITD8a%||%wPA1A1JW7+N;~w{0xOPGdwZg+^gFI#V zdJGo60v+uq(-1EbtDv9{I$Z-185uHbxQwwEw|5X<;Ku@cn6pg%$23T~*^q~`EXB3o z1)O~*dC+WEdkwpgk>fY321H`R z!s&z=s>4q!Qbzz~Iv2jlWxB3mJt@MogL_XuS6`wIM>3(}q%3=|Q0u7eHp{4(PzRXG zxKIUYLJs?9-!ci2@1Cw9D=Hx9j(QXyjbz3cCAS6c{ZdbJoiax@AcbG`F;a&Zltq)J z1wtC~Vb%E;PqTjxblx1UZpW-vh-0=^w{kELit$IwInRdTZ}wdZhGWZB2A~h?+hztR z=Pz9Oj_-{%y}HmoS)UWjjCtQ$Aw^HM>)$A}&%QCmuvbvrFymOAZ3CN7zu&IU_B7xq z#rrlDv+;>mL!*{m*wkm2o1l7f4rH*m#Ko-~@zb=(G>B;Vud2)GH2G@?P_1fvY6NR% zjk;QDhRX^RMn3nUcRJ|B(9ewqytjnl8MY-`rYP%o;2ACG7F~4B*$u{1M^8^1J%X?J zo==*#b!V9%q;YNOpr?tm8Rj^O)$~kqM^c>xJD8RN&=%XSN?JlU#gOmkkH4W6v&GJh z9b+7d?3)Lf%D77JSV@1=NTXs>!C%TWB$wT~b|0j>8_FFzNo*#s5wn=^qv@M9u6)7h@p|;xgBV!dF zi=L|4pTucG&B4j!?izDv)Y|-dXFPrcVM@u+C&=fCZEYyB5xnQp@Av9?6f<;4R+LcY zVU8uWwZlKiDk-_GWB)7q)A3OR?QP~H5qJ(gSCtwnXMCkXNyd_#E8{rkr2MpEM0k#z ztkC@*N**7X50+y?(&Z)zOhgWlsUi~b9DTBBS>P)^p1$%-TFFCW&#O79zEpk4L<;ei zNKRc`ahA3uZcPRa`2(do{i4~%F*|tH?+6{8 zyuQ%ME^U#@zO%5+<(ZNWhq|{<8uN%I2c&8%#3{?WZc_J1hq;{3qrl5YDNNZ&8au6H zEx3Kfg+^@({#{(~Vq7)qt1nKAPx75bkRF^njZ|#yBe(V;e)SKEoemr;7tGTXt(HHP zL})G6p)GNmFUlLQwxMRKMPJ)2EnG=EslyjLybE{ZX->codd>38&T+y4aeu)3ueHB_Az5uyfvPyH2T z?!#65_fulp3(9}xnnFU<-+-O--^zjg%g4ZkTR(#fG>-cjbgpga0|3q65La#;XvxK9 z{R5vYg#T-)=il>NYs6`e`UI;4ze2p46W$0QnnVd+(n4{VlJGbNH87y3g`kP~iSmRT zY6XV2EWiLZ%Y>S|b3b4rVAa>~p=ZivtUA+!IVi6~9WHLDVuU8nG+13jV8P^{#tQV6 z9kw=k5|EIzgDT-t&8Oh`p zY&an;ZJ$XtA6uL|H0S)B2n;loonI|2HAeAxqbRU5s8?rtR#t*0Zz2qLEE$2X8{9Y<>(nr-!89uUK~>SZILN z-~)~CoL&TmA>$kTVB!kI7zS=UQqSRG>Uw!;YsC?)L++%UN&E}JrK#hhT24`)D$wh} z!^bGtxERKg2=wFtnZK6+pH`inzVvzuO|rg(>TxhXsd*xq2!gpdU+OItYI83U^uZs~ zDc15)z9`HXj{2g-y~8CuTfXVqKQO?vJpJoe->dap{VU*0@odC(^)2+&V7uX3~|esszBj=J-}<$Pbe8*bQByfDh> z(w!m?0b@O?#&z&eiHpBic7~m~dKG&aWXxTSiI2f7u^%hDydt8&H3&$AJ>h}jKn2xT zT`ewsRpRCWFqY^0^WC^`6?=Tn%NJYKHplqJwrk^r{PR;$#9*R90r{OJ)o8cTE7s2u z>}h%OJ~6K;QFSx>Zq(}fQjCI)S~t;#b1~Z$y16lOj@t~;AjtjtA3xcV(Ml$BJ&>#v z*t~ReEQ0D6wCjGDdCh86m4T--tWtM>cXCw^~i1xGE0(Hd&m7Pc83a1 zUU-s+v7ef+>R7GC*;L$ zdGKVPW>_da3oz8%z*WzstIpA=-dR?C#TBKemU`T-K&YCB!ww6_anR<06qEI{Ia3J* zF@ffFX;k8+M~ok=x;cusmid92?>OH)?QpPula+%50R~cSeY*|!!gBk^DKP}5D95>c z$O-E;NFZXFzSr{oT)$%=7$Wb9XRq!?S1;;>n-k%%4qvZt{g&_h$>2_W%sT}Ana7M9 zmU+>uVWwr*eoY2u9V;D2?2|Muw=(N`}DHzb40rRnBaW_rJqCZ9}>z=)xnId95?Un zxzd5INh`rutJ$a5D* z%)d(iOAC-j?2P=-fmt0`j> z+eS*+l6X&EWTP?-gVw4+-X0K?$@8qE^ERQ5N^^3()oij8C(Up1kFz`UAvSNx@TE`V zN%kEga9Yzngx3)?1J`;Ud_IORd7zibYE2PQNYDSdD)o-7yJ-st{>O@&;<_@Ttz8RM zuvgv9rzyEPCqd2kJ`!LE_@BwHA7NDQ7t8+Ge02PAN2}WP=Jw<(R$9F1Q`^Q3=A=9q zFYhGXPBj&5M3drG%4)K&buD$BPfilEFRL|QIi3pRH}#BV+Ed036erG<)-`;SY_f1l z{<4!P%Rn=AEK;=y>hOv=&kL7e6RbR*yP|P8*8c4o`B&&sUuO8LMYz=3SkigIVhv`m zc5}3Q-41HzBDLymj{9d_c2qVG%S%%Y4o@FQd2oaYbPX=kAY&M`bxL z+|2J+xKklS&>ttV=dZLkl*fR3EQg1W8(2?@MSRbm@v|ZiIq!C_=3<2L?558UOpX5! z^2U%;;ui!}awb&kkX7{LUFKN*i`7dhV#R3ew9t9Z`7yt!>5b<|37G_BM7>)xDy1DM zi%n*|0O!zW5@q2~ z>uN*PP(?iNDvLLBR2ubhTCE`djyw|_hke>6c^ab0^d!gW%gWR&?Ws{7R34;+YNgaX zLA?7woV}nPCSqF$xMd-avuO_>CxeGvR#MAuKe~TCZFpzmdve>ddYyyR-m;ML;3SrjEgWkX8PQ-3No(sWcJN2O%&rbk3|az0_m zu|axF)M5E|(wKa`&Gxq6R#9X&bC(DpsbkKGb%uT)NXQxa;GLoW~qCj+7O zZLVNgN5ed~w}U-G*aEKiv~|yBY5w*!fp^o1x|vUJlP}& zK8G~g=q9~}uit^_R&O*8EQA~-fZHO7riMfU_rbmqpxY{dxvaAAL`tl-5CPtBWUQW$ znCle0a#mWkSr2})NLFv4?o_qWiK$MdocY`kU%xdc{0KdPRjHGw7 zh=L(Nk9=!DnfxO_01y{8q4WQUw4u}SUeOzNW(TUhW9Ijl4Cc#MjFdq$Ab}@6ElatC zV)dn^CmS0Ztk3zv;^Wn%hWd5g+>jIy0ql)SN^0-S%yO`>nEqqBxj0j3^$OqB3G8omZOId5LozlRRn ztxQ*&GH7j;!nW$hwpy$3q*R48m#ee%u%M6HkO7Dx9t=sm(vgsr#bD3Kh)YQ693SVf zl9D!XdJ)W|k@}!wuox4DQVUc2yB2{6-j?`JQ;}yM2OpktCQ7uXOmb345s0X~tWu$< zXPoB13gsx1`}!??Q|{b4Kkok>h*r|NfDuCTG_vUx7sZ14U;DA%QiC$Oikt zpbvzD+>2p7DtK3gEadt;-_)R?&v!|DkF9K20&?M3YCERrP&~HKNL&lRvgalz6 z%7`&ef%@7|JksHNr~spg7U=E{a5%91@Ar!(jl*V8b11g(RJ)$99LMF8Dux z&q1ZM<6&^RPUnq68_uUs5$$wkjV5O{=oZq&^AG=eBCNLE@J2O%&vdhz+=fYyC*CUM zNuOEoYC_7R->myyCPV)hIPEE^{^4=oxggGUg0#9m3{6p=qcxD$rl&!l82&1bcP?kv z#tP83z*aEj{%d z>}o`%RM-SrAiWc)vD+OwTqwX;>i8W?i89Q~$~p*BlmcG?jizH0#lQ|;a6Kmp3JQXG za(6bMqt%ivEl}e{NuGB4+v-eB2hOy9T@7GAhIe+(p8d#j`9u0Lk}QTQ-Nh>vpNc(n zRd%IKk)0M?1;(&G2ul(Bbu=*b^B)&AHGKP)#~8R@+yTF&ZuI_{#!v4PUd44Aa%}4h zMa^mt^tryxKE+-U%9eW~d9Uoh8Ox@fV!y)HV5Om(xj(h)M%&?G9Bz z185DicatIM2~E?7fZ&|+yf4S*C@qgTCm%J3G&l6AD)9_J+NsP*gEhufB{@7CqCPPT zO{5?;iHwX`9*?hP(=zdOI-TLXUhRAJy@@5Fnf#WnSnCP}_?5V+&4&X4tW0=)!3xJU zCiz2aHksz)D8BE|Y98iAV6UwqYh$;*(9@nG&~3fU+S~L-E3@Y9@X4(qQ>s-GNg{dN z^DWiNTXD^&=llMX%_oOvr(Md;OsL&VR7Pl{(7 zyzX4zf^_h=k2Tu%_=Ad?+d&(J1)*>pQ%9dP5Z=shX&QWlNGsfE6lc`ArJ*janU2NA>>-s5^c#BQ9sz@IGPjv#T@cv)*5$vr^B-Z#cg1_lj}>Du=)C`G;l$6&?EpO zG(~F#Ppuuv=S@7dX;M9m468{oe-IWZrT^=WNAs!b3;uluz#gh9~@!ecUy?2@> zYHMaZh8O*(uPhL=&;r*|8(}5cNWN@hi*l(5uUYU5&HpnXUmT5I{4SI|=_V2xu}u;% z-DE}Xmtqa6sTFyP@-&heUu`SRT21GeWX&e(tupZ+Ca?EWLyTcA_CVew|N|Gy2yC#f87 zP6Bvconj?RvtoCiTfS40@u7B_a*#z8G)f#IF!`+SVnUl^uG4|%?gf;=F*eKT=M4cr zo1DnuQmjkoK>33NoAA2`vFuC6wT2nCE{C0}2|KikubY-0YU^oh{|i{2<*?!YC7Udv zSWUJ-P{H$$tz{8*|C2Z0vC-q$zaD@hgx%mF!9OFk&h(LMt4)xQ#yK{TG|eWY|_fB`7|mWQF*vJ2v1#|Zn1Sb`V2v8;ixNf zFRV*yYJg*-T8Mrbp5uTArN(nlFo0)q|D=;S2^0GdU?;|=-35TX!@1!bSl-{>sHTkV z1SUbIfvhjDI-4b;6(gvqkoDA@`<_f4y^uy72^_PRk=$#|D?qI~q1yA$8ARM_&C^)0 z_7E6rP7HUE@eh|)HmWusdXcf<4V(&+Jm8?RRAMgx2UU-R&u+VEjRc_H`~N$F7l4p- z{0+gIHW4)Cs^|Co4ZzbbY6n`?wm!M&EUiVbn@$>j$9d7D!xJcSs`WI*jqvm~Hvnar zW3vUK%6+!IXMXnd5HcAX%z~LqRk|*5B%*UF>Z7kBd)~63w-M2yUtH{TX4eTxwo`s8 zJodFK3kz1AF;cM;CzFHv)WuPsYw+4jU0K35{3d5hjMa6?TdxHI#q;0R{!e(m>a9;U z?~uqoZt1%zi_zYEo#SD}E>O*t>-0YhKh<@-oB4$6z2KXbBi>(me;;`_AcANn4|Dt~9p<^KWg0npEl zW);pDzNaOiF!TRL{)4g}XylS$sOF-#D2RrRt`A;u_7kwQy8vc^#(}M%HOoifWk8j0 zTl&i|ie?ljp$xrG)BiYrAOchyxCxa1wX?VHUjG2Rzs>eI9sk%=*W#2=m-IvqFpARiY2P1{>Hd@Wmo|9| za21iT;n2Xr@m&la#xOKuvA4IEae2Xe$qu-}kA$Qh)N+)?6k%d6p&|ceO}{G z(`M|MOkm|J90r>(wLf3~i2=c6)_K46Xv}(fjb(79JMXzmT?siCfUe*rOSd~xnV$wC zb)OL%rpX(_s?*KNW`4pID;Fo)Dpn_F7;8WqPI${77>>={gAvQNM$Q^0BGK!b#^|{4 zjB(^a6{pA zSp&4FL^b?Yv~Qd`^kdd!vei^Us_eg!)z;5qeT*26*h~o|>X*=!HWso(QZfPQZ9Hwx zi7xA!lpE?T^dVeJF22A7%sCjj@c354PLneYj?v`r{FZZ6Xp4jt2bS}a=r(ba%kD!5 z4DpGb9hW)I(`6oqn@q!*nmso+20OVLz({f081(_I6c!eCS)Q^7BLwp5VuIJ(5ix=1 zaP}zTVkV2a7Te;u}!`;bkAOPF;tnGS;dVD>)uSu#j;+9utB+ zWDQN5u$0iD1F%~L9I~q-IS~Af^a)Db2wssiVzkv10d`+R^nh8L28H0QpcsnO%z3-G z4V$7kik>MJoGDmtQOY;`2gz0?h*@om-V+o1PNjIY2u!s<_*1^+2dU^DVeDsm)bP3w zzo62*pTgOwJfpIx$U4C)f~Fu|fxK)kSl zD6)|GNHb*;B7atGPYj%I-(JNN=Y)BaDFM~a9IZNTb=fM=kAI)!KKlpB{apGcR3+ux z3%QYZf*Zvn`oxr7=&`>U?m}}R_7|7^NBg2{M{Lrs=-xg{-h(#N8Qgg<9(NJPs}YIB z`*`dk#NRF@aH7Zm0~j$8<{s>;UuJG;xz;{GN#K=!m9^!kx{lC|j}frW?K~&fwtv2W z%-K^De_za=oU-~bb+EgK8#Qi*Yp8KiAUE$u&5nFBT@q=vh11kvNcu9i?w(QUdZG@! zrxt&8G)~E-T1GWbWOSZ(*_B8d>5Lo&QZOt5O!)YxgiuHPe^;q6xV&02^Q5c5+D9M9 zc%i);N}L*pB(VJ;xDXdowwx?W6Vv&^%#1|PX%kd5wO+{APipCH_X_zkLeIVnSY9$g z{}eKzAW(Aj&IqmZgxrq=^+j*iGDABQQNLatZ^@cjY*RT=cci43?6ke~Z*>Y@eH*Ky zM#e1ZPMsp>Ho8U7!P!r+9}=9!ESm#+Sgfn=I_g^*w|JwhH}u=$n%#;}SKz)mB?|!5#n!a?FKQ5a!sE}b!M6$~dd!*p1Z}Vc~&5t`JLjS8RZF-~=Fn^SRRAC8RP^Gfl(s?cye>(0?)4F z{tXNbiie{aj1Sev@mv;%4OCU<|;Bn8cKGzq8%%V?Z~YM%5LfM2``rof)Ghfs-S+qh|8Kz82GSw z**?N~S^K!v@nae51;#L(FW1^|0qcsY*i^93(bccup=DERCxG7rXNh`I9WiLgo6TcB zi0&VOCh&%-PnK@hE(oU&H*WgJCUx_FiQ5F#%1`{~QeN6z2NDa~Vo-uu8Tb0I`AB4M zZ(V+NVzjdE%kwe`m-I7c!hNR&=3-0}FELMdf||u2ce`4JT|~?BO9vN=v-xXTf<2d5 z-v@e5rOq@?|`z+B`tlSq_$g_awDbbl2p zxZ1M*R}O-1Xu$WXedj?VD{jE%ff^9CNM-B1JLbLs!hP1O<=Mk5Ch20sf%-jQpG{M` z_ksZ_{m;-F5V(0ivl(>#G1G0Aw?6?@4lt;R@ezOm$HV|%9ssy20-Fj{AX>BcJ>oIKqCF&_OWxr{d)2 zA?3G!ONM^2(EoHWm^+){1h|dOEx3oTXmvF|y$~t6=N*7zz6OQ7G;aI9If_K=ZnY@DNgF$SYgmdH|q@K`MEDjz4-EYs(X?r?Y2bge!JyA%C7bd;8EuoY`(w70x%PJ~lCYGpp zTR0)pjNL~H5WwtvGUf%yQQ~;oere~SGmU_K1R7fFMm-24DpU&~Vuh-EqB`wi-c8J? zcNms^N(Nmci&Q2CdaOlfwVLf7ObLlc4@JSah0W3W-tX7Gset2Yg5gn|HF^9WKwV_k zS0PKUEng$#o8OxmXX-al-Qvg_1bnqRDm2Z7aly=y_L?j(J4~^H@pf|w`#uKCVARAw z{se);>dF!a^SW}f3nLQK2MIc{S6OaFmCS8dwD}UBMuf!Qdrl56m*QjfNz-gKP(()O zX2!1Sg@NR86-hG=)0$*}0Po-bZCzkf7co{||Kwzfy8SJFAfrMQyV^`>ta&A$;AV3v zDI&JMI<-;SfZXrY3EUS=b%ZD}0VW6Ojebuxjo|^u;#+nOzl0uw=TA9)={&w#PYmUD z{3|&Ap>0Rt%wl=}p_iT`T8@C#sbx@*Atp>tk*9YztTTPhX+1f+D$#waZL`Bj*|NpK z3q$vnun~r$_ak&$Qd3kYi5E~Qf8cQNwG&N6{V$+dsq4Q7&8*IdS*}`l#{NENW|6`g zm<9%X_Llca?n68g^3HVxlDtZ66+gc@ERw7*9wWPzwpr+CPN@z-9M@4SlwUL^Wdg_ z7=k>ShtT5px7_Uy@R6ubIjxK6Zdko9IVH#0tp2dPTQK2BanX}gEEK;88KK#X9F_f%OmL-Bm5B%!>(zhw9&TzriZ?*Cwr56*RLRdd-@lK~2#*kFV;QD~oM z`fP~*!eV2Y*uxWoN7?ZXwGpl;)f`tz5Ym_nLzOC2s58h!c-dTO`3X@ch&Kz98b(q0kS4&c!v8PAz zFQ9`;1h(HjloGKjSop@8NZ0c)dvdYQHI`_31eq9dnfnGb%O(Tp2OJk|J*}|%9B?N4 z@1FQB2ENeRM~3FA1-99*O+6jpc=j|>|NirpkagFOZ>WltKFjL!be~3O6&VSCj3FD0 z)owD??%-DRWY7jENk*2lJ$3X?P`DhPxRa}zM~!3j$rh}%M;lED#`gTnMu>4;_kn;) zJl9*0)r%|8^V;qX=#l3;Qfx0T{jmKUWpodPsN|BT7L_bk7tcOtypq1sO#UawJebBx z7gIinz@l*{k-dZ(G5*SBrPp%DrpG<`vM1>o#mJRwXtnhvZYOXw4cq(`hxiP0dnJ1E z$nE7St+ZKSVzRE&?J=qi(oOhgU-Ppta5mo)nklC2@43wAeA;+_-?34}U!}$rD9T2b z?T8x9Za|mBdPT)B`%3an*JMv$J0kH02h# zZ1vb5xS_G(Yo!WbkGm$4T%&-n{_033nW!8`k4D#oQuk$?)ka|vVtmm}{Exac zHk$?wF~;gmd&JEos&{JolV343ie|AU67W$x`N=RJI?bs>OMRA#FG#vNgw@Xgp?5@I zx#Fn=!c?r-i)B?`riKM<)oje{Yi(%*s&+9>Ii|M^fpmP&YhgX^YN|CuVAG(uU9wwXQ2DUpnzDBb!uo#uUy{`gy@2`qm{wKWs_w1 zvv#5GQ<3AHiex)2g=#53Hb2_jXfr$Bp4+uHOE)TFV7HW=DY-&3jqGu>a4gs999=g| zd#q#)QmQ+jlg!QtcQ<~BS#|l;xj;xQHjt=%%pl>lZrZA)BSxs~YcafA%0MFBzI8LK@c_xgqOEYZn4qy8{-Lb9k>SK47xRCB zx`WzcD%Ois<@cD4o~vxt4Z1bI4T3|o!4)&YR8PAv^(Wodb$+g)O^dvZyCfpjvwAbD zivE&=8vmv_ac%!**8X5n`?)GOU)mbHARai)6L6Z#Bg)L3Vt*4M#4#8}@)zrC#SZ5h zxB4Vvzw_P`F)4zMnV+doG-o@x6#pwwtc#M4WoM@9=Z5(iuyBu5bbskhv}DdB2+G$% z{rU%y+A*i{$2b)jKI1m@AomaFpPCRxqqbw9tp_Ei_!kmQVZ8eZTSOL)SkK8?Rt*1D z9u84s&Nbz+b;v&bo$QdH10 zfAG@z*#Os+!2Y7m(mEMx`;9)u8;Ha|>2x@PPMb9&Nxj@MX?Qzxb|#)3xMb~ldDrWh zQy)X9z6?SBX7k&=&mbGDXU}&l#px@7UA$LP%ZkHQh{(mS-i1FX{GRvQ`Au~wn1m|A z4yPwE$*uA;qVH7WbWbQ7%XUP)Okb!P6Iq>IiF;hgKAm6Cce_n_K8(FBi`?LdM0wpG zNrlA4(R4eDsm9sqX03K!z&Zw3cU4b?M<=JcY-KyBYV{L>1I7I}%IoE8(tV2h!ZXBi z6H1NOQwxIO0;QU70F3=pRdfu!~Sh4uzcrdF|JB`RuzNyEC_Q1(zEpOUpRs#0AYNPJ}XF;4*4ZQ_kjk{lB zmOg#0jU;YreUUXKP~eaOsQ<(6F%aJHIj-R+udW-sgIhBEC0UNy=XG?yfxD)e;~*LZ zf!M9HxzhUx`muJ2%j-OpXEySBj_Krw$@a~n>E=;m6-Gp}7=q%|U}PuZ)( zt6l+OQdc(`9;HhD=aL_8%)W*b(QOEVkn;t<$Km#t^OfgzXya?PtKM3jDW2=llV`|Z z!pwxY%?1%HK{(|Uq5Ed94&pc$@QnIEU;)U1EH>8zFC^fxB)I*Zsg@#a^445C3vr)C z4Jep*cbaop&pNYjkIj6k3r=BZV4v+Y?{ga1<`l~_fJ&xqxeH+GJwA09R5F0`C>- za<`(msn680E3TK=~wX= zf2D>e34nFsC)b9W{1@rY2Wy`SP<8bq7I(;BMGHC_%U%Lf!0XTPf+ALiM{6XAk~QxK z@JtOIrv&8(9M*{(f~yFfdQ(^xy0Y*(ER6?DJrS~!LJ>*0aT*a=k~^R^9IljRZ~pJa zHp#2wF&xYzVRx&QOEEU@x7K`Vow&3)B~OZ898Z74yUz2O&??v&^#xwn9%2* z`Xt_>7s@)dN4PvzYaE|tDUjqh8sbuxkPYCGksTRcdA&w`XiA4XK#bi|p{9HNI3+vk|H;um4jWAR&p7&92O{_f@k7DZ zbfG@OxjzYdq2FvmvO8-z{e3)6qrlmDlT`rGUpFV8I&uaC~Z)a0f^1`2>Y z6!`6-|81I_jG8vA2NmsG2X>!MCw*8CNW^4R|NmQXGANL>hDx6LXUPdX5)bC*%bhWk zeZ=u#hRG@p-*pt4bpIU*;2!T6Cc$!c^`B3jMZM7VBZPKX#jkBeO`D2Psg9sM_t#0b zT+=hVm1Rx`7_pJ4isPhU&uh_+8`jb+b8kOwfYr6N^(ANa~|KO*z=>9 ze%SkS*@2{$bH_Q0O@!`iJzPx3Zo2%>XS6u-uFQbe$pzyOu50*fafihB;HY;IMNv2@ z{}6M-VMTDrZy)Z@DsFCv;M7s0XP5hY@`V$}k35kWz&+O4(AO9_d6HdwekwdBVn}_52&r%uzwXEm~ zgz6tbkm3lWLW1lw}&ND_F_w+8$zIS{zL@~Lq?oN#p z{HYjHR(~z(ZcRD$)ph20uT@@aq<&R4@souW={jL+Q ziS`DWab%}{y2r0i@)cP&G5uv<$+9|^D}ZQ*QK~SiJU3>=fb`7X|#Q0 zu1mO3?fZm*Uzhj6J2o+X?c|L#8r9od65vWxjqrh2V3VhJnS}qQr1VRRr3TtD;}6dJZf3SGvi+0vZqfkPa=nhmL~*ES9&h) zaTC{atJjVZjoJTbM3;d|?z;_Rq5i~Ah zlJoP^r|EAU<#1%%9|#3Is>X$4)%k~&leQn!EYA{eLB0)bLoB_#D7dv#yAzXJlzW9k zhlupr4a{R~EY^^DbRm`=FR4=tb#FlhX|++ExKPCz%ZDgYNCK#2uq790l|8EEn%urv zyvNGll^C;{;&K?5cEgf(SH*3r@W{dE z!~zbhrxkBDv>ZxOrqU+b2ilM?{4xf*1^nj66wLe{y>nj{&97}sw6TPo(E;;#7w<^` zbw)-joAJOuIo5|Rivw%rL-c3k#hT?msgerZi z@yQ*qa^fDbUp;R{U)JzN; zdJUeoXcjAsZbO;Uu)S#R3;%Pv=3g3dK5T(pI|NyJ9?WXDR^ zO{rG98$=v3Hg~z#c(-`RTnsn0g}n4rT`DpgDTj~uPVKt$w&Mu5uvKT^orP|wk1|fr zn&wk_HD;Z1L*(H8j<*Qf#Rbu;lyjjSP`XRhQ*@``+{bzP2H6QT0m5SN_?>3lhn6Sk zOBef{MF>7TU>HP&twKIN_F2ki=X?V?5R-{Xqt@Y$wiEJr9x18IY&eYlfofBIrhief zEs_+>4$-q3CH@xnl4fX^o=I1@ZZell=@CIZBi#50a@8X7MX2#gUo{sh z+LwgY&2=m8L*yO3HpyV#H|OA;$11e`2A%B+vA-kX48PtA-)CuCuv(8O^G8YA-ba>ft@#3Bkq+6A$H+DlO5R37iC^>8)v6YWjq4~q!^TVQ7_obfoI-BOEa7&K7UVWWE8eIUc8IEXCO#6Yv?w&G