From 45ccb9909ebb13c146bb12f2171065e586105450 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 27 Aug 2023 18:46:20 -0400 Subject: [PATCH 001/143] duh --- worlds/ahit/Items.py | 190 ++++++++++ worlds/ahit/Locations.py | 682 +++++++++++++++++++++++++++++++++++ worlds/ahit/Options.py | 517 ++++++++++++++++++++++++++ worlds/ahit/Regions.py | 760 +++++++++++++++++++++++++++++++++++++++ worlds/ahit/Rules.py | 682 +++++++++++++++++++++++++++++++++++ worlds/ahit/Types.py | 28 ++ worlds/ahit/__init__.py | 276 ++++++++++++++ 7 files changed, 3135 insertions(+) create mode 100644 worlds/ahit/Items.py create mode 100644 worlds/ahit/Locations.py create mode 100644 worlds/ahit/Options.py create mode 100644 worlds/ahit/Regions.py create mode 100644 worlds/ahit/Rules.py create mode 100644 worlds/ahit/Types.py create mode 100644 worlds/ahit/__init__.py diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py new file mode 100644 index 0000000000..7c9b4297d3 --- /dev/null +++ b/worlds/ahit/Items.py @@ -0,0 +1,190 @@ +from BaseClasses import Item, ItemClassification +from worlds.AutoWorld import World +from .Types import HatDLC +import typing + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + dlc_flags: typing.Optional[HatDLC] = HatDLC.none + + +class HatInTimeItem(Item): + game: str = "A Hat in Time" + + +def item_dlc_enabled(world: World, name: str) -> bool: + data = item_table[name] + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.multiworld.EnableDLC1[world.player].value > 0: + return True + elif data.dlc_flags == HatDLC.dlc2 and world.multiworld.EnableDLC2[world.player].value > 0: + return True + elif data.dlc_flags == HatDLC.death_wish and world.multiworld.EnableDeathWish[world.player].value > 0: + return True + + return False + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.multiworld.EnableDLC1[world.player].value > 0: + count += 6 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + + +def create_item(world: World, name: str) -> Item: + data = item_table[name] + return HatInTimeItem(name, data.classification, data.code, world.player) + + +def create_multiple_items(world: World, name: str, count: int = 1) -> typing.List[Item]: + data = item_table[name] + itemlist: typing.List[Item] = [] + + for i in range(count): + itemlist += [HatInTimeItem(name, data.classification, data.code, world.player)] + + return itemlist + + +def create_junk_items(world: World, count: int) -> typing.List[Item]: + trap_chance = world.multiworld.TrapChance[world.player].value + junk_pool: typing.List[Item] = [] + junk_list: typing.Dict[str, int] = {} + trap_list: typing.Dict[str, int] = {} + ic: ItemClassification + + for name in item_table.keys(): + ic = item_table[name].classification + if ic == ItemClassification.filler: + junk_list[name] = junk_weights.get(name) + elif trap_chance > 0 and ic == ItemClassification.trap: + if name == "Baby Trap": + trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value + elif name == "Laser Trap": + trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value + elif name == "Parade Trap": + trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value + + for i in range(count): + if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance: + junk_pool += [world.create_item( + world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + else: + junk_pool += [world.create_item( + world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + + return junk_pool + + +ahit_items = { + "Yarn": ItemData(300001, ItemClassification.progression_skip_balancing), + "Time Piece": ItemData(300002, ItemClassification.progression_skip_balancing), + "Progressive Painting Unlock": ItemData(300003, ItemClassification.progression), + + # Relics + "Relic (Burger Patty)": ItemData(300006, ItemClassification.progression), + "Relic (Burger Cushion)": ItemData(300007, ItemClassification.progression), + "Relic (Mountain Set)": ItemData(300008, ItemClassification.progression), + "Relic (Train)": ItemData(300009, ItemClassification.progression), + "Relic (UFO)": ItemData(300010, ItemClassification.progression), + "Relic (Cow)": ItemData(300011, ItemClassification.progression), + "Relic (Cool Cow)": ItemData(300012, ItemClassification.progression), + "Relic (Tin-foil Hat Cow)": ItemData(300013, ItemClassification.progression), + "Relic (Crayon Box)": ItemData(300014, ItemClassification.progression), + "Relic (Red Crayon)": ItemData(300015, ItemClassification.progression), + "Relic (Blue Crayon)": ItemData(300016, ItemClassification.progression), + "Relic (Green Crayon)": ItemData(300017, ItemClassification.progression), + + # Badges + "Projectile Badge": ItemData(300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(300025, ItemClassification.useful), + "Hover Badge": ItemData(300026, ItemClassification.useful), + "Hookshot Badge": ItemData(300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(300028, ItemClassification.useful), + "No Bonk Badge": ItemData(300029, ItemClassification.useful), + "Compass Badge": ItemData(300030, ItemClassification.useful), + "Scooter Badge": ItemData(300031, ItemClassification.progression), + "Badge Pin": ItemData(300043, ItemClassification.useful), + + # Other + # "Rift Token": ItemData(300032, ItemClassification.filler), + "Random Cosmetic": ItemData(300044, ItemClassification.filler), + "Umbrella": ItemData(300033, ItemClassification.progression), + + # Garbage items + "25 Pons": ItemData(300034, ItemClassification.filler), + "50 Pons": ItemData(300035, ItemClassification.filler), + "100 Pons": ItemData(300036, ItemClassification.filler), + "Health Pon": ItemData(300037, ItemClassification.filler), + + # Traps + "Baby Trap": ItemData(300039, ItemClassification.trap), + "Laser Trap": ItemData(300040, ItemClassification.trap), + "Parade Trap": ItemData(300041, ItemClassification.trap), + + # DLC1 items + "Relic (Cake Stand)": ItemData(300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Cake)": ItemData(300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Cake Slice)": ItemData(300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(300021, ItemClassification.progression, HatDLC.dlc1), + + # DLC2 items + "Relic (Necklace Bust)": ItemData(300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(300023, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Yellow": ItemData(300045, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Green": ItemData(300046, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Blue": ItemData(300047, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Pink": ItemData(300048, ItemClassification.progression, HatDLC.dlc2), + + # Death Wish items + "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), +} + +act_contracts = { + "Snatcher's Contract - The Subcon Well": ItemData(300200, ItemClassification.progression), + "Snatcher's Contract - Toilet of Doom": ItemData(300201, ItemClassification.progression), + "Snatcher's Contract - Queen Vanessa's Manor": ItemData(300202, ItemClassification.progression), + "Snatcher's Contract - Mail Delivery Service": ItemData(300203, ItemClassification.progression), +} + +alps_hooks = { + "Zipline Unlock - The Birdhouse Path": ItemData(300204, ItemClassification.progression), + "Zipline Unlock - The Lava Cake Path": ItemData(300205, ItemClassification.progression), + "Zipline Unlock - The Windmill Path": ItemData(300206, ItemClassification.progression), + "Zipline Unlock - The Twilight Bell Path": ItemData(300207, ItemClassification.progression), +} + +relic_groups = { + "Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"}, + "Train": {"Relic (Mountain Set)", "Relic (Train)"}, + "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"}, + "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"}, + "Cake": {"Relic (Cake Stand)", "Relic (Cake)", "Relic (Cake Slice)", "Relic (Shortcake)"}, + "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"}, +} + +item_frequencies = { + "Badge Pin": 2, + "Progressive Painting Unlock": 3, +} + +junk_weights = { + "25 Pons": 50, + "50 Pons": 10, + "Health Pon": 35, + "100 Pons": 5, + "Random Cosmetic": 25, +} + +item_table = { + **ahit_items, + **act_contracts, + **alps_hooks, +} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py new file mode 100644 index 0000000000..f48c3d0b4a --- /dev/null +++ b/worlds/ahit/Locations.py @@ -0,0 +1,682 @@ +from BaseClasses import Location +from worlds.AutoWorld import World +from .Types import HatDLC, HatType +from typing import Optional, NamedTuple, List, Dict +from .Options import TasksanityCheckCount + + +class LocData(NamedTuple): + id: int + region: str + required_hats: Optional[List[HatType]] = [HatType.NONE] + required_tps: Optional[int] = 0 + hookshot: Optional[bool] = False + dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Progressive paintings required for Subcon painting shuffle + + # For UmbrellaLogic setting + umbrella: Optional[bool] = False # Umbrella required for this check + dweller_bell: Optional[int] = 0 # Dweller bell hit required, 1 means must hit bell, 2 means can bypass w/mask + + # Other + act_complete_event: Optional[bool] = True # Only used for event locations. Copy access rule from act completion + nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + + +class HatInTimeLocation(Location): + game: str = "A Hat in Time" + + +def get_total_locations(world: World) -> int: + total: int = 0 + + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 + + if world.multiworld.EnableDLC1[world.player].value > 0 and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + return total + + +def location_dlc_enabled(world: World, location: str) -> bool: + data = location_table.get(location) or event_locs.get(location) + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.multiworld.EnableDLC1[world.player].value > 0: + return True + elif data.dlc_flags == HatDLC.dlc2 and world.multiworld.EnableDLC2[world.player].value > 0: + return True + elif data.dlc_flags == HatDLC.death_wish and world.multiworld.EnableDeathWish[world.player].value > 0: + return True + + return False + + +def is_location_valid(world: World, location: str) -> bool: + if not location_dlc_enabled(world, location): + return False + + if location in storybook_pages.keys() \ + and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + return False + + if location in shop_locations and location not in world.shop_locs: + return False + + return True + + +def get_location_names() -> Dict[str, int]: + names = {name: data.id for name, data in location_table.items()} + id_start: int = 300204 + for i in range(TasksanityCheckCount.range_end): + names.setdefault(format("Tasksanity Check %i") % (i+1), id_start+i) + + return names + + +ahit_locations = { + "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", required_tps=4, dweller_bell=1), + # "Spaceship - Cooking Cat": LocData(301001, "Spaceship", required_tps=5), + + # 300000 range - Mafia Town/Batle of the Birds + "Welcome to Mafia Town - Umbrella": LocData(301002, "Welcome to Mafia Town"), + "Mafia Town - Old Man (Seaside Spaghetti)": LocData(303833, "Mafia Town Area"), + "Mafia Town - Old Man (Steel Beams)": LocData(303832, "Mafia Town Area"), + "Mafia Town - Blue Vault": LocData(302850, "Mafia Town Area"), + "Mafia Town - Green Vault": LocData(302851, "Mafia Town Area"), + "Mafia Town - Red Vault": LocData(302848, "Mafia Town Area"), + "Mafia Town - Blue Vault Brewing Crate": LocData(305572, "Mafia Town Area", required_hats=[HatType.BREWING]), + "Mafia Town - Plaza Under Boxes": LocData(304458, "Mafia Town Area"), + "Mafia Town - Small Boat": LocData(304460, "Mafia Town Area"), + "Mafia Town - Staircase Pon Cluster": LocData(304611, "Mafia Town Area"), + "Mafia Town - Palm Tree": LocData(304609, "Mafia Town Area"), + "Mafia Town - Port": LocData(305219, "Mafia Town Area"), + "Mafia Town - Docks Chest": LocData(303534, "Mafia Town Area"), + "Mafia Town - Ice Hat Cage": LocData(304831, "Mafia Town Area", required_hats=[HatType.ICE]), + "Mafia Town - Hidden Buttons Chest": LocData(303483, "Mafia Town Area"), + + # These can be accessed from HUMT, the above locations can't be + "Mafia Town - Dweller Boxes": LocData(304462, "Mafia Town Area (HUMT)"), + "Mafia Town - Ledge Chest": LocData(303530, "Mafia Town Area (HUMT)"), + "Mafia Town - Yellow Sphere Building Chest": LocData(303535, "Mafia Town Area (HUMT)"), + "Mafia Town - Beneath Scaffolding": LocData(304456, "Mafia Town Area (HUMT)"), + "Mafia Town - On Scaffolding": LocData(304457, "Mafia Town Area (HUMT)"), + "Mafia Town - Cargo Ship": LocData(304459, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Alcove": LocData(304463, "Mafia Town Area (HUMT)"), + "Mafia Town - Wood Cage": LocData(304606, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Patio": LocData(304610, "Mafia Town Area (HUMT)"), + "Mafia Town - Steel Beam Nest": LocData(304608, "Mafia Town Area (HUMT)"), + "Mafia Town - Top of Ruined Tower": LocData(304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Hot Air Balloon": LocData(304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Camera Badge 1": LocData(302003, "Mafia Town Area (HUMT)"), + "Mafia Town - Camera Badge 2": LocData(302004, "Mafia Town Area (HUMT)"), + "Mafia Town - Chest Beneath Aqueduct": LocData(303489, "Mafia Town Area (HUMT)"), + "Mafia Town - Secret Cave": LocData(305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), + "Mafia Town - Crow Chest": LocData(303532, "Mafia Town Area (HUMT)"), + "Mafia Town - Above Boats": LocData(305218, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Slip Slide Chest": LocData(303529, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind Faucet": LocData(304214, "Mafia Town Area (HUMT)"), + "Mafia Town - Clock Tower Chest": LocData(303481, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Top of Lighthouse": LocData(304213, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Mafia Geek Platform": LocData(304212, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind HQ Chest": LocData(303486, "Mafia Town Area (HUMT)"), + + "Mafia HQ - Hallway Brewing Crate": LocData(305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), + "Mafia HQ - Freezer Chest": LocData(303241, "Down with the Mafia!"), + "Mafia HQ - Secret Room": LocData(304979, "Down with the Mafia!", required_hats=[HatType.ICE]), + "Mafia HQ - Bathroom Stall Chest": LocData(303243, "Down with the Mafia!"), + + "Dead Bird Studio - Up the Ladder": LocData(304874, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Red Building Top": LocData(305024, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Behind Water Tower": LocData(305248, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Side of House": LocData(305247, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio", umbrella=True), + "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio", umbrella=True), + "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio", umbrella=True), + "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio", umbrella=True), + + "Murder on the Owl Express - Cafeteria": LocData(305313, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Top": LocData(305090, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Bottom": LocData(305091, "Murder on the Owl Express"), + + "Murder on the Owl Express - Raven Suite Room": LocData(305701, "Murder on the Owl Express", + required_hats=[HatType.BREWING]), + + "Murder on the Owl Express - Raven Suite Top": LocData(305312, "Murder on the Owl Express"), + "Murder on the Owl Express - Lounge Chest": LocData(303963, "Murder on the Owl Express"), + + "Picture Perfect - Behind Badge Seller": LocData(304307, "Picture Perfect"), + "Picture Perfect - Hats Buy Building": LocData(304530, "Picture Perfect"), + + "Dead Bird Studio Basement - Window Platform": LocData(305432, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cardboard Conductor": LocData(305059, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Above Conductor Sign": LocData(305057, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Logo Wall": LocData(305207, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Disco Room": LocData(305061, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(304813, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Vent Pipe": LocData(305430, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Tightrope": LocData(305058, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Locked Room": LocData(305819, "Dead Bird Studio Basement", hookshot=True), + + # 320000 range - Subcon Forest + "Contractual Obligations - Cherry Bomb Bone Cage": LocData(324761, "Contractual Obligations"), + "Subcon Village - Tree Top Ice Cube": LocData(325078, "Subcon Forest Area"), + "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), + "Subcon Village - House Top": LocData(325471, "Subcon Forest Area"), + "Subcon Village - Ice Cube House": LocData(325469, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), + "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), + + "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", + required_hats=[HatType.BREWING], + paintings=1), + + "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=1), + "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=3), + + "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=1), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", + paintings=3, + required_hats=[HatType.DWELLER], + hookshot=True), + + "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), + "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + paintings=2), + + "Subcon Forest - Boss Arena Chest": LocData(323735, "Subcon Forest Area"), + "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2, paintings=1), + + "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", + required_hats=[HatType.BREWING], + paintings=2), + + "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", + required_hats=[HatType.BREWING], + paintings=3), + + "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1, paintings=1), + "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1, paintings=1), + + "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + + # 330000 range - Alpine Skyline + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Goat Village"), + "Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"), + "Alpine Skyline - Bird Pass Fork": LocData(335911, "Alpine Skyline Area"), + "Alpine Skyline - Yellow Band Hills": LocData(335756, "Alpine Skyline Area", required_hats=[HatType.BREWING]), + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(335561, "Alpine Skyline Area"), + "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(334831, "Alpine Skyline Area"), + "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(334758, "The Birdhouse"), + + "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(336497, "The Birdhouse", + required_hats=[HatType.DWELLER]), + + "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(336496, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Hay Bale": LocData(335885, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(335886, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Outer Edge": LocData(335492, "The Birdhouse"), + + "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(337058, "Alpine Skyline Area"), + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(336052, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(336311, "Alpine Skyline Area"), + "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(335448, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(334291, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(335417, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Top Cake": LocData(335418, "The Lava Cake"), + "Alpine Skyline - The Twilight Path": LocData(334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), + "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(336478, "The Twilight Bell"), + "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(335826, "The Twilight Bell"), + "Alpine Skyline - Goat Outpost Horn": LocData(334760, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(334776, "Alpine Skyline Area"), + "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(336395, "The Windmill"), + "Alpine Skyline - The Windmill: Entrance": LocData(335783, "The Windmill"), + "Alpine Skyline - The Windmill: Dropdown": LocData(335815, "The Windmill"), + "Alpine Skyline - The Windmill: House Window": LocData(335389, "The Windmill"), + + "The Finale - Frozen Item": LocData(304108, "The Finale"), + + "Bon Voyage! - Lamp Post Top": LocData(305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "Bon Voyage! - Mafia Cargo Ship": LocData(304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Toilet": LocData(305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Bar": LocData(304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Dive Board Ledge": LocData(304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Top Balcony": LocData(304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room": LocData(305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room Top": LocData(304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Laundry Room": LocData(304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Ship Side": LocData(304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Silver Ring": LocData(305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Suitcase": LocData(304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Under Desk": LocData(304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Lamp Post": LocData(304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Iceberg Top": LocData(304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(304049, "Rock the Boat", dlc_flags=HatDLC.dlc1), + + "Nyakuza Metro - Main Station Dining Area": LocData(304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Top of Ramen Shop": LocData(304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Nyakuza Metro - Yellow Overpass Station Crate": LocData(305413, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.BREWING]), + + "Nyakuza Metro - Bluefin Tunnel Cat Vacuum": LocData(305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Nyakuza Metro - Pink Paw Station Cat Vacuum": LocData(305110, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Nyakuza Metro - Pink Paw Station Behind Fan": LocData(304106, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.TIME_STOP, HatType.DWELLER]), +} + +act_completions = { + # 310000 range - Act Completions + "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), + + "Act Completion (Welcome to Mafia Town)": LocData(311771, "Welcome to Mafia Town"), + "Act Completion (Barrel Battle)": LocData(311958, "Barrel Battle"), + "Act Completion (She Came from Outer Space)": LocData(312262, "She Came from Outer Space"), + "Act Completion (Down with the Mafia!)": LocData(311326, "Down with the Mafia!"), + "Act Completion (Cheating the Race)": LocData(312318, "Cheating the Race"), + "Act Completion (Heating Up Mafia Town)": LocData(311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (The Golden Vault)": LocData(312250, "The Golden Vault"), + "Act Completion (Time Rift - Bazaar)": LocData(312465, "Time Rift - Bazaar"), + "Act Completion (Time Rift - Sewers)": LocData(312484, "Time Rift - Sewers"), + "Act Completion (Time Rift - Mafia of Cooks)": LocData(311855, "Time Rift - Mafia of Cooks"), + + "Act Completion (Dead Bird Studio)": LocData(311383, "Dead Bird Studio", umbrella=True), + "Act Completion (Murder on the Owl Express)": LocData(311544, "Murder on the Owl Express"), + "Act Completion (Picture Perfect)": LocData(311587, "Picture Perfect"), + "Act Completion (Train Rush)": LocData(312481, "Train Rush", hookshot=True), + "Act Completion (The Big Parade)": LocData(311157, "The Big Parade", umbrella=True), + "Act Completion (Award Ceremony)": LocData(311488, "Award Ceremony"), + "Act Completion (Dead Bird Studio Basement)": LocData(312253, "Dead Bird Studio Basement", hookshot=True), + "Act Completion (Time Rift - The Owl Express)": LocData(312807, "Time Rift - The Owl Express"), + "Act Completion (Time Rift - The Moon)": LocData(312785, "Time Rift - The Moon"), + "Act Completion (Time Rift - Dead Bird Studio)": LocData(312577, "Time Rift - Dead Bird Studio"), + + "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations", paintings=1), + "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), + "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True, paintings=1), + "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + "Act Completion (Mail Delivery Service)": LocData(312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), + "Act Completion (Your Contract has Expired)": LocData(311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Time Rift - Pipe)": LocData(313069, "Time Rift - Pipe", hookshot=True), + "Act Completion (Time Rift - Village)": LocData(313056, "Time Rift - Village"), + "Act Completion (Time Rift - Sleepy Subcon)": LocData(312086, "Time Rift - Sleepy Subcon"), + + "Act Completion (The Birdhouse)": LocData(311428, "The Birdhouse"), + "Act Completion (The Lava Cake)": LocData(312509, "The Lava Cake"), + "Act Completion (The Twilight Bell)": LocData(311540, "The Twilight Bell"), + "Act Completion (The Windmill)": LocData(312263, "The Windmill"), + "Act Completion (The Illness has Spread)": LocData(312022, "The Illness has Spread", hookshot=True), + + "Act Completion (Time Rift - The Twilight Bell)": LocData(312399, "Time Rift - The Twilight Bell", + required_hats=[HatType.DWELLER]), + + "Act Completion (Time Rift - Curly Tail Trail)": LocData(313335, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Alpine Skyline)": LocData(311777, "Time Rift - Alpine Skyline"), + + "Act Completion (The Finale)": LocData(311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), + "Act Completion (Time Rift - Tour)": LocData(311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Act Completion (Bon Voyage!)": LocData(311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Ship Shape)": LocData(311451, "Ship Shape", dlc_flags=HatDLC.dlc1), + "Act Completion (Rock the Boat)": LocData(311437, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Act Completion (Time Rift - Balcony)": LocData(312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Time Rift - Deep Sea)": LocData(312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + + "Act Completion (Nyakuza Metro Intro)": LocData(311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Act Completion (Yellow Overpass Station)": LocData(311206, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + hookshot=True), + + "Act Completion (Yellow Overpass Manhole)": LocData(311387, "Yellow Overpass Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Green Clean Station)": LocData(311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), + + "Act Completion (Green Clean Manhole)": LocData(311388, "Green Clean Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE, HatType.DWELLER]), + + "Act Completion (Bluefin Tunnel)": LocData(311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Act Completion (Pink Paw Station)": LocData(311209, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Act Completion (Pink Paw Manhole)": LocData(311389, "Pink Paw Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Rush Hour)": LocData(311210, "Rush Hour", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.ICE, HatType.BREWING]), + + "Act Completion (Rumbi Factory)": LocData(312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), +} + +storybook_pages = { + "Mafia of Cooks - Page: Fish Pile": LocData(345091, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Trash Mound": LocData(345090, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Beside Red Building": LocData(345092, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Behind Shipping Containers": LocData(345095, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Top of Boat": LocData(345093, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Below Dock": LocData(345094, "Time Rift - Mafia of Cooks"), + + "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(345449, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(345447, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(345448, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(345450, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(345451, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Green Screen": LocData(345452, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: In The Corner": LocData(345453, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(345445, "Time Rift - Dead Bird Studio"), + + "Sleepy Subcon - Page: Behind Entrance Area": LocData(345373, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Near Wrecking Ball": LocData(345327, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Crane": LocData(345371, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Wrecked Treehouse": LocData(345326, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(345372, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Rotating Platform": LocData(345328, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(345329, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Frozen Tree": LocData(345330, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Secret Library": LocData(345370, "Time Rift - Sleepy Subcon"), + + "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(345016, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(345012, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(345015, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(345014, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(345013, "Time Rift - Alpine Skyline"), + + "Deep Sea - Page: Starfish": LocData(346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Mini Castle": LocData(346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Urchins": LocData(346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Big Castle": LocData(346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Castle Top Chest": LocData(304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Urchin Ledge": LocData(346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Hidden Castle Chest": LocData(304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Falling Platform": LocData(346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Lava Starfish": LocData(346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + + "Tour - Page: Mafia Town - Ledge": LocData(345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Beach": LocData(345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - Fragile Box": LocData(345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Top of Pillar": LocData(345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Birdhouse": LocData(345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: The Finale - Near Entrance": LocData(345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Rumbi Factory - Page: Manhole": LocData(345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Shutter Doors": LocData(345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(345892, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: 3rd Area Ledge": LocData(345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Green Box Assembly Line": LocData(345884, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Broken Window": LocData(345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Money Vault": LocData(345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Warehouse Boxes": LocData(345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Glass Shelf": LocData(345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Last Area": LocData(345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), +} + +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), +} + +shop_locations = { + "Badge Seller - Item 1": LocData(301003, "Badge Seller"), + "Badge Seller - Item 2": LocData(301004, "Badge Seller"), + "Badge Seller - Item 3": LocData(301005, "Badge Seller"), + "Badge Seller - Item 4": LocData(301006, "Badge Seller"), + "Badge Seller - Item 5": LocData(301007, "Badge Seller"), + "Badge Seller - Item 6": LocData(301008, "Badge Seller"), + "Badge Seller - Item 7": LocData(301009, "Badge Seller"), + "Badge Seller - Item 8": LocData(301010, "Badge Seller"), + "Badge Seller - Item 9": LocData(301011, "Badge Seller"), + "Badge Seller - Item 10": LocData(301012, "Badge Seller"), + "Mafia Boss Shop Item": LocData(301013, "Spaceship", required_tps=12), + + "Yellow Overpass Station - Yellow Ticket Booth": LocData(301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), + "Green Clean Station - Green Ticket Booth": LocData(301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Blue Ticket Booth": LocData(301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Pink Paw Station - Pink Ticket Booth": LocData(301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2), + + "Main Station Thug A - Item 1": LocData(301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 2": LocData(301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 3": LocData(301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 4": LocData(301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 5": LocData(301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + + "Main Station Thug B - Item 1": LocData(301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 2": LocData(301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 3": LocData(301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 4": LocData(301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 5": LocData(301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + + "Main Station Thug C - Item 1": LocData(301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 2": LocData(301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 3": LocData(301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 4": LocData(301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 5": LocData(301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + + "Yellow Overpass Thug A - Item 1": LocData(301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 2": LocData(301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 3": LocData(301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 4": LocData(301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 5": LocData(301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + + "Yellow Overpass Thug B - Item 1": LocData(301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 2": LocData(301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 3": LocData(301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 4": LocData(301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 5": LocData(301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + + "Yellow Overpass Thug C - Item 1": LocData(301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 2": LocData(301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 3": LocData(301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 4": LocData(301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 5": LocData(301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + + "Green Clean Station Thug A - Item 1": LocData(301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 2": LocData(301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 3": LocData(301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 4": LocData(301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 5": LocData(301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + + # This guy requires either the yellow ticket or the Ice Hat + "Green Clean Station Thug B - Item 1": LocData(301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 2": LocData(301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 3": LocData(301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 4": LocData(301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 5": LocData(301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + + "Bluefin Tunnel Thug - Item 1": LocData(301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 2": LocData(301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 3": LocData(301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 4": LocData(301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 5": LocData(301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + + "Pink Paw Station Thug - Item 1": LocData(301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 2": LocData(301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 3": LocData(301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 4": LocData(301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 5": LocData(301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + +} + +# Don't put any of the locations from peaks here, the rules for their entrances are set already +zipline_unlocks = { + "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path", + + "Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path", + + "Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path", + "Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path", + + "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", +} + +# Locations in Alpine that are available in The Illness has Spread +# Goat Village locations don't need to be put here +tihs_locations = [ + "Alpine Skyline - Bird Pass Fork", + "Alpine Skyline - Yellow Band Hills", + "Alpine Skyline - Ember Summit", + "Alpine Skyline - Goat Outpost Horn", + "Alpine Skyline - Windy Passage", +] + +event_locs = { + "Birdhouse Cleared": LocData(0, "The Birdhouse"), + "Lava Cake Cleared": LocData(0, "The Lava Cake"), + "Windmill Cleared": LocData(0, "The Windmill"), + "Twilight Bell Cleared": LocData(0, "The Twilight Bell"), + "Time Piece Cluster": LocData(0, "The Finale"), + + # not really an act + "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, act_complete_event=False), + + "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), + "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2), + "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2), + "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2), + "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2), + "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2), +} + +location_table = { + **ahit_locations, + **act_completions, + **storybook_pages, + **contract_locations, + **shop_locations, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py new file mode 100644 index 0000000000..772fa08aef --- /dev/null +++ b/worlds/ahit/Options.py @@ -0,0 +1,517 @@ +import typing +from worlds.AutoWorld import World +from Options import Option, Range, Toggle, DeathLink, Choice +from .Items import get_total_time_pieces + + +def adjust_options(world: World): + world.multiworld.HighestChapterCost[world.player].value = max( + world.multiworld.HighestChapterCost[world.player].value, + world.multiworld.LowestChapterCost[world.player].value) + + world.multiworld.LowestChapterCost[world.player].value = min( + world.multiworld.LowestChapterCost[world.player].value, + world.multiworld.HighestChapterCost[world.player].value) + + world.multiworld.FinalChapterMinCost[world.player].value = min( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value) + + world.multiworld.FinalChapterMaxCost[world.player].value = max( + world.multiworld.FinalChapterMaxCost[world.player].value, + world.multiworld.FinalChapterMinCost[world.player].value) + + world.multiworld.BadgeSellerMinItems[world.player].value = min( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.BadgeSellerMaxItems[world.player].value = max( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + total_tps: int = get_total_time_pieces(world) + if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: + world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: + world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + + # Don't allow Rush Hour goal if DLC2 content is disabled + if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: + world.multiworld.EndGoal[world.player].value = 1 + + +# General +class EndGoal(Choice): + """The end goal required to beat the game. + Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. + + Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 + will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. + Requires DLC2 content to be enabled.""" + display_name = "End Goal" + option_finale = 1 + option_rush_hour = 2 + default = 1 + + +class ActRandomizer(Choice): + """If enabled, shuffle the game's Acts between each other. + Light will cause Time Rifts to only be shuffled amongst each other, + and Blue Time Rifts and Purple Time Rifts are shuffled separately.""" + display_name = "Shuffle Acts" + option_false = 0 + option_light = 1 + option_insanity = 2 + default = 1 + + +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + +class VanillaAlpine(Choice): + """If enabled, force Alpine (and optionally its finale) onto their vanilla locations in act shuffle.""" + display_name = "Vanilla Alpine Skyline" + option_false = 0 + option_true = 1 + option_finale = 2 + default = 0 + + +class LogicDifficulty(Choice): + """Choose the difficulty setting for logic. Note that Hard or above will force SDJ logic on.""" + display_name = "Logic Difficulty" + option_normal = 0 + option_hard = 1 + option_expert = 2 + default = 0 + + +class RandomizeHatOrder(Toggle): + """Randomize the order that hats are stitched in.""" + display_name = "Randomize Hat Order" + default = 1 + + +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + +class StartWithCompassBadge(Toggle): + """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world + (instead of just Relics). Recommended if you're not familiar with where item locations are.""" + display_name = "Start with Compass Badge" + default = 1 + + +class CompassBadgeMode(Choice): + """closest - Compass Badge points to the closest item regardless of classification + important_only - Compass Badge points to progression/useful items only + important_first - Compass Badge points to progression/useful items first, then it will point to junk items""" + display_name = "Compass Badge Mode" + option_closest = 1 + option_important_only = 2 + option_important_first = 3 + default = 1 + + +class ShuffleStorybookPages(Toggle): + """If enabled, each storybook page in the purple Time Rifts is an item check. + The Compass Badge can track these down for you.""" + display_name = "Shuffle Storybook Pages" + default = 1 + + +class ShuffleActContracts(Toggle): + """If enabled, shuffle Snatcher's act contracts into the pool as items""" + display_name = "Shuffle Contracts" + default = 1 + + +class ShuffleSubconPaintings(Toggle): + """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. + These items are progressive, with the order of Village-Swamp-Courtyard.""" + display_name = "Shuffle Subcon Paintings" + default = 0 + + +class StartingChapter(Choice): + """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game.""" + display_name = "Starting Chapter" + option_1 = 1 + option_2 = 2 + option_3 = 3 + option_4 = 4 + default = 1 + + +class SDJLogic(Toggle): + """Allow the SDJ (Sprint Double Jump) technique to be considered in logic.""" + display_name = "SDJ Logic" + default = 0 + + +class CTRWithSprint(Toggle): + """If enabled, clearing Cheating the Race with just Sprint Hat can be in logic.""" + display_name = "Cheating the Race with Sprint Hat" + default = 0 + + +# DLC +class EnableDLC1(Toggle): + """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 6" + default = 0 + + +class Tasksanity(Toggle): + """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" + display_name = "Tasksanity" + default = 0 + + +class TasksanityTaskStep(Range): + """How many tasks the player must complete in Tasksanity to send a check.""" + display_name = "Tasksanity Task Step" + range_start = 1 + range_end = 3 + default = 1 + + +class TasksanityCheckCount(Range): + """How many Tasksanity checks there will be in total.""" + display_name = "Tasksanity Check Count" + range_start = 5 + range_end = 30 + default = 18 + + +class EnableDLC2(Toggle): + """Shuffle content from Nyakuza Metro (Chapter 7) into the game. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 7" + default = 0 + + +class MetroMinPonCost(Range): + """The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Minimum Pon Cost" + range_start = 10 + range_end = 800 + default = 50 + + +class MetroMaxPonCost(Range): + """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Minimum Pon Cost" + range_start = 10 + range_end = 800 + default = 200 + + +class NyakuzaThugMinShopItems(Range): + """The smallest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Minimum Shop Items" + range_start = 0 + range_end = 5 + default = 2 + + +class NyakuzaThugMaxShopItems(Range): + """The largest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Maximum Shop Items" + range_start = 0 + range_end = 5 + default = 4 + + +class BaseballBat(Toggle): + """Replace the Umbrella with the baseball bat from Nyakuza Metro. + DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" + display_name = "Baseball Bat" + default = 0 + + +class VanillaMetro(Choice): + """Force Nyakuza Metro (and optionally its finale) onto their vanilla locations in act shuffle.""" + display_name = "Vanilla Metro" + option_false = 0 + option_true = 1 + option_finale = 2 + + +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 5 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for Chapter 5).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for Chapter 5).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +# Death Wish +class EnableDeathWish(Toggle): + """NOT IMPLEMENTED Shuffle Death Wish contracts into the game. + Each contract by default will have a single check granted upon completion. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Enable Death Wish" + default = 0 + + +class DWEnableBonus(Toggle): + """NOT IMPLEMENTED In Death Wish, allow the full completion of contracts to reward items.""" + display_name = "Shuffle Death Wish Full Completions" + default = 0 + + +class DWExcludeAnnoyingContracts(Toggle): + """NOT IMPLEMENTED Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.""" + display_name = "Exclude Annoying Death Wish Contracts" + default = 1 + + +class DWExcludeAnnoyingBonuses(Toggle): + """NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions + from the pool""" + display_name = "Exclude Annoying Death Wish Full Completions" + default = 1 + + +# Yarn +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch each hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch each hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 75 + default = 45 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 400 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + +# Traps +class TrapChance(Range): + """The chance for any junk item in the pool to be replaced by a trap.""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 0 + + +class BabyTrapWeight(Range): + """The weight of Baby Traps in the trap pool. + Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance.""" + display_name = "Baby Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class LaserTrapWeight(Range): + """The weight of Laser Traps in the trap pool. + Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location.""" + display_name = "Laser Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class ParadeTrapWeight(Range): + """The weight of Parade Traps in the trap pool. + Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement.""" + display_name = "Parade Trap Weight" + range_start = 0 + range_end = 100 + default = 20 + + +ahit_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "VanillaAlpine": VanillaAlpine, + "LogicDifficulty": LogicDifficulty, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "StartingChapter": StartingChapter, + "SDJLogic": SDJLogic, + "CTRWithSprint": CTRWithSprint, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + + "EnableDeathWish": EnableDeathWish, + "EnableDLC2": EnableDLC2, + "BaseballBat": BaseballBat, + "VanillaMetro": VanillaMetro, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, + "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + + "LowestChapterCost": LowestChapterCost, + "HighestChapterCost": HighestChapterCost, + "ChapterCostIncrement": ChapterCostIncrement, + "ChapterCostMinDifference": ChapterCostMinDifference, + "MaxExtraTimePieces": MaxExtraTimePieces, + + "FinalChapterMinCost": FinalChapterMinCost, + "FinalChapterMaxCost": FinalChapterMaxCost, + + "YarnCostMin": YarnCostMin, + "YarnCostMax": YarnCostMax, + "YarnAvailable": YarnAvailable, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + "BadgeSellerMinItems": BadgeSellerMinItems, + "BadgeSellerMaxItems": BadgeSellerMaxItems, + + "TrapChance": TrapChance, + "BabyTrapWeight": BabyTrapWeight, + "LaserTrapWeight": LaserTrapWeight, + "ParadeTrapWeight": ParadeTrapWeight, + + "death_link": DeathLink, +} + +slot_data_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "SDJLogic": SDJLogic, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + + "EnableDeathWish": EnableDeathWish, + + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + + "death_link": DeathLink, +} diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py new file mode 100644 index 0000000000..f7c74e4f94 --- /dev/null +++ b/worlds/ahit/Regions.py @@ -0,0 +1,760 @@ +from worlds.AutoWorld import World +from BaseClasses import Region, Entrance, ItemClassification, Location +from .Locations import HatInTimeLocation, location_table, storybook_pages, event_locs, is_location_valid, shop_locations +from .Items import HatInTimeItem +from .Types import ChapterIndex +import typing +from .Rules import set_rift_rules + + +# ChapterIndex: region +chapter_regions = { + ChapterIndex.SPACESHIP: "Spaceship", + ChapterIndex.MAFIA: "Mafia Town", + ChapterIndex.BIRDS: "Battle of the Birds", + ChapterIndex.SUBCON: "Subcon Forest", + ChapterIndex.ALPINE: "Alpine Skyline", + ChapterIndex.FINALE: "Time's End", + ChapterIndex.CRUISE: "The Arctic Cruise", + ChapterIndex.METRO: "Nyakuza Metro", +} + +# entrance: region +act_entrances = { + "Welcome to Mafia Town": "Mafia Town - Act 1", + "Barrel Battle": "Mafia Town - Act 2", + "She Came from Outer Space": "Mafia Town - Act 3", + "Down with the Mafia!": "Mafia Town - Act 4", + "Cheating the Race": "Mafia Town - Act 5", + "Heating Up Mafia Town": "Mafia Town - Act 6", + "The Golden Vault": "Mafia Town - Act 7", + + "Dead Bird Studio": "Battle of the Birds - Act 1", + "Murder on the Owl Express": "Battle of the Birds - Act 2", + "Picture Perfect": "Battle of the Birds - Act 3", + "Train Rush": "Battle of the Birds - Act 4", + "The Big Parade": "Battle of the Birds - Act 5", + "Award Ceremony": "Battle of the Birds - Finale A", + "Dead Bird Studio Basement": "Battle of the Birds - Finale B", + + "Contractual Obligations": "Subcon Forest - Act 1", + "The Subcon Well": "Subcon Forest - Act 2", + "Toilet of Doom": "Subcon Forest - Act 3", + "Queen Vanessa's Manor": "Subcon Forest - Act 4", + "Mail Delivery Service": "Subcon Forest - Act 5", + "Your Contract has Expired": "Subcon Forest - Finale", + + "Alpine Free Roam": "Alpine Skyline - Free Roam", + "The Illness has Spread": "Alpine Skyline - Finale", + + "The Finale": "Time's End - Act 1", + + "Bon Voyage!": "The Arctic Cruise - Act 1", + "Ship Shape": "The Arctic Cruise - Act 2", + "Rock the Boat": "The Arctic Cruise - Finale", + + "Nyakuza Free Roam": "Nyakuza Metro - Free Roam", + "Rush Hour": "Nyakuza Metro - Finale", +} + +act_chapters = { + "Time Rift - Gallery": "Spaceship", + "Time Rift - The Lab": "Spaceship", + + "Welcome to Mafia Town": "Mafia Town", + "Barrel Battle": "Mafia Town", + "She Came from Outer Space": "Mafia Town", + "Down with the Mafia!": "Mafia Town", + "Cheating the Race": "Mafia Town", + "Heating Up Mafia Town": "Mafia Town", + "The Golden Vault": "Mafia Town", + "Time Rift - Mafia of Cooks": "Mafia Town", + "Time Rift - Sewers": "Mafia Town", + "Time Rift - Bazaar": "Mafia Town", + + "Dead Bird Studio": "Battle of the Birds", + "Murder on the Owl Express": "Battle of the Birds", + "Picture Perfect": "Battle of the Birds", + "Train Rush": "Battle of the Birds", + "The Big Parade": "Battle of the Birds", + "Award Ceremony": "Battle of the Birds", + "Dead Bird Studio Basement": "Battle of the Birds", + "Time Rift - Dead Bird Studio": "Battle of the Birds", + "Time Rift - The Owl Express": "Battle of the Birds", + "Time Rift - The Moon": "Battle of the Birds", + + "Contractual Obligations": "Subcon Forest", + "The Subcon Well": "Subcon Forest", + "Toilet of Doom": "Subcon Forest", + "Queen Vanessa's Manor": "Subcon Forest", + "Mail Delivery Service": "Subcon Forest", + "Your Contract has Expired": "Subcon Forest", + "Time Rift - Sleepy Subcon": "Subcon Forest", + "Time Rift - Pipe": "Subcon Forest", + "Time Rift - Village": "Subcon Forest", + + "Alpine Free Roam": "Alpine Skyline", + "The Illness has Spread": "Alpine Skyline", + "Time Rift - Alpine Skyline": "Alpine Skyline", + "Time Rift - The Twilight Bell": "Alpine Skyline", + "Time Rift - Curly Tail Trail": "Alpine Skyline", + + "The Finale": "Time's End", + "Time Rift - Tour": "Time's End", + + "Bon Voyage!": "The Arctic Cruise", + "Ship Shape": "The Arctic Cruise", + "Rock the Boat": "The Arctic Cruise", + "Time Rift - Balcony": "The Arctic Cruise", + "Time Rift - Deep Sea": "The Arctic Cruise", + + "Nyakuza Free Roam": "Nyakuza Metro", + "Rush Hour": "Nyakuza Metro", + "Time Rift - Rumbi Factory": "Nyakuza Metro", +} + +# region: list[Region] +rift_access_regions = { + "Time Rift - Gallery": ["Spaceship"], + "Time Rift - The Lab": ["Spaceship"], + + "Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "The Golden Vault"], + + "Time Rift - The Owl Express": ["Murder on the Owl Express"], + "Time Rift - The Moon": ["Picture Perfect", "The Big Parade"], + "Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"], + + "Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Village": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - The Twilight Bell": ["Alpine Free Roam"], + "Time Rift - Curly Tail Trail": ["Alpine Free Roam"], + "Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"], + + "Time Rift - Tour": ["Time's End"], + + "Time Rift - Balcony": ["Cruise Ship"], + "Time Rift - Deep Sea": ["Cruise Ship"], + + "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], +} + +# Hat_ChapterActInfo, from the game files to be used in act shuffle +chapter_act_info = { + "Time Rift - Gallery": "hatintime_chapterinfo.spaceship.Spaceship_WaterRift_Gallery", + "Time Rift - The Lab": "hatintime_chapterinfo.spaceship.Spaceship_WaterRift_MailRoom", + + "Welcome to Mafia Town": "hatintime_chapterinfo.MafiaTown.MafiaTown_Welcome", + "Barrel Battle": "hatintime_chapterinfo.MafiaTown.MafiaTown_BarrelBattle", + "She Came from Outer Space": "hatintime_chapterinfo.MafiaTown.MafiaTown_AlienChase", + "Down with the Mafia!": "hatintime_chapterinfo.MafiaTown.MafiaTown_MafiaBoss", + "Cheating the Race": "hatintime_chapterinfo.MafiaTown.MafiaTown_Race", + "Heating Up Mafia Town": "hatintime_chapterinfo.MafiaTown.MafiaTown_Lava", + "The Golden Vault": "hatintime_chapterinfo.MafiaTown.MafiaTown_GoldenVault", + "Time Rift - Mafia of Cooks": "hatintime_chapterinfo.MafiaTown.MafiaTown_CaveRift_Mafia", + "Time Rift - Sewers": "hatintime_chapterinfo.MafiaTown.MafiaTown_WaterRift_Easy", + "Time Rift - Bazaar": "hatintime_chapterinfo.MafiaTown.MafiaTown_WaterRift_Hard", + + "Dead Bird Studio": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_DeadBirdStudio", + "Murder on the Owl Express": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_Murder", + "Picture Perfect": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_PicturePerfect", + "Train Rush": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_TrainRush", + "The Big Parade": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_Parade", + "Award Ceremony": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_AwardCeremony", + "Dead Bird Studio Basement": "DeadBirdBasement", # Dead Bird Studio Basement has no ChapterActInfo + "Time Rift - Dead Bird Studio": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_CaveRift_Basement", + "Time Rift - The Owl Express": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_WaterRift_Panels", + "Time Rift - The Moon": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_WaterRift_Parade", + + "Contractual Obligations": "hatintime_chapterinfo.subconforest.SubconForest_IceWall", + "The Subcon Well": "hatintime_chapterinfo.subconforest.SubconForest_Cave", + "Toilet of Doom": "hatintime_chapterinfo.subconforest.SubconForest_Toilet", + "Queen Vanessa's Manor": "hatintime_chapterinfo.subconforest.SubconForest_Manor", + "Mail Delivery Service": "hatintime_chapterinfo.subconforest.SubconForest_MailDelivery", + "Your Contract has Expired": "hatintime_chapterinfo.subconforest.SubconForest_SnatcherBoss", + "Time Rift - Sleepy Subcon": "hatintime_chapterinfo.subconforest.SubconForest_CaveRift_Raccoon", + "Time Rift - Pipe": "hatintime_chapterinfo.subconforest.SubconForest_WaterRift_Hookshot", + "Time Rift - Village": "hatintime_chapterinfo.subconforest.SubconForest_WaterRift_Dwellers", + + "Alpine Free Roam": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_IntroMountain", + "The Illness has Spread": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_Finale", + "Time Rift - Alpine Skyline": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_CaveRift_Alpine", + "Time Rift - The Twilight Bell": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_WaterRift_Goats", + "Time Rift - Curly Tail Trail": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_WaterRift_Cats", + + "The Finale": "hatintime_chapterinfo.TheFinale.TheFinale_FinalBoss", + "Time Rift - Tour": "hatintime_chapterinfo_dlc1.spaceship.CaveRift_Tour", + + "Bon Voyage!": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Boarding", + "Ship Shape": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Working", + "Rock the Boat": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Sinking", + "Time Rift - Balcony": "hatintime_chapterinfo_dlc1.Cruise.Cruise_WaterRift_Slide", + "Time Rift - Deep Sea": "hatintime_chapterinfo_dlc1.Cruise.Cruise_CaveRift", + + "Nyakuza Free Roam": "hatintime_chapterinfo_dlc2.metro.Metro_FreeRoam", + "Rush Hour": "hatintime_chapterinfo_dlc2.metro.Metro_Escape", + "Time Rift - Rumbi Factory": "hatintime_chapterinfo_dlc2.metro.Metro_RumbiFactory" +} + +# Guarantee that the first level a player can access is a location dense area beatable with no items +guaranteed_first_acts = [ + "Welcome to Mafia Town", + "Barrel Battle", + "She Came from Outer Space", + "Down with the Mafia!", + "Heating Up Mafia Town", # Removed in umbrella logic + "The Golden Vault", + + "Contractual Obligations", # Removed in painting logic + "Queen Vanessa's Manor", # Removed in umbrella/painting logic +] + +purple_time_rifts = [ + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline", + "Time Rift - Deep Sea", + "Time Rift - Tour", + "Time Rift - Rumbi Factory", +] + +# Acts blacklisted in act shuffle +# entrance: region +blacklisted_acts = { + "Battle of the Birds - Finale A": "Award Ceremony", + "Time's End - Act 1": "The Finale", +} + + +def create_regions(world: World): + w = world + mw = world.multiworld + p = world.player + + # ------------------------------------------- HUB -------------------------------------------------- # + menu = create_region(w, "Menu") + spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) + create_rift_connections(w, create_region(w, "Time Rift - The Lab")) + + # ------------------------------------------- MAFIA TOWN ------------------------------------------- # + mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship) + mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) + mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town) + mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) + mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) + mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) + mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town) + mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town) + + # ------------------------------------------- BOTB ------------------------------------------------- # + botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) + dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) + create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) + create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) + create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) + create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) + create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) + create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) + create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) + create_rift_connections(w, create_region(w, "Time Rift - The Moon")) + + # Items near the Dead Bird Studio elevator can be reached from the basement act + ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) + connect_regions(mw.get_region("Dead Bird Studio Basement", p), ev_area, "DBS Basement -> Elevator Area", p) + + # ------------------------------------------- SUBCON FOREST --------------------------------------- # + subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) + sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) + sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) + sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) + sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) + sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) + create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + + # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # + alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) + alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) + alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) + goat_village = create_region_and_connect(w, "Goat Village", "ASA -> Goat Village", alpine_area) + + create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) + create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) + create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area) + create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) + + illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + connect_regions(illness, alpine_area, "TIHS -> Alpine Skyline Area", p) + connect_regions(illness, goat_village, "TIHS -> Goat Village", p) + create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) + create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) + create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) + + # ------------------------------------------- OTHER -------------------------------------------------- # + mt_area: Region = create_region(w, "Mafia Town Area") + mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") + connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p) + connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p) + connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p) + connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p) + connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p) + connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p) + connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p) + connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p) + + create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) + create_rift_connections(w, create_region(w, "Time Rift - Sewers")) + create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) + + sf_area: Region = create_region(w, "Subcon Forest Area") + connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p) + connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p) + connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p) + connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p) + connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p) + + create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) + create_rift_connections(w, create_region(w, "Time Rift - Pipe")) + create_rift_connections(w, create_region(w, "Time Rift - Village")) + + badge_seller = create_badge_seller(w) + connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) + connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) + connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) + connect_regions(mw.get_region("Dead Bird Studio", p), badge_seller, "DBS -> Badge Seller", p) + connect_regions(mw.get_region("Picture Perfect", p), badge_seller, "PP -> Badge Seller", p) + connect_regions(mw.get_region("Train Rush", p), badge_seller, "TR -> Badge Seller", p) + connect_regions(mw.get_region("Goat Village", p), badge_seller, "GV -> Badge Seller", p) + + times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) + create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) + + # ------------------------------------------- DLC1 ------------------------------------------------- # + if mw.EnableDLC1[p].value > 0: + arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) + cruise_ship = create_region(w, "Cruise Ship") + + ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) + ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) + ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) + + connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p) + connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p) + connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) + create_rift_connections(w, create_region(w, "Time Rift - Balcony")) + create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) + create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.Tasksanity[p].value > 0: + create_tasksanity_locations(w) + + # force recache + mw.get_region("Time Rift - Deep Sea", p) + + connect_regions(mw.get_region("Cruise Ship", p), badge_seller, "CS -> Badge Seller", p) + + if mw.EnableDLC2[p].value > 0: + nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) + metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) + create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) + + yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) + green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam) + pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) + create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole + + create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) + create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green) + create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) + + create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) + create_thug_shops(w) + + # force recache + mw.get_region("Time Rift - Sewers", p) + + +def create_rift_connections(world: World, region: Region): + i = 1 + for name in rift_access_regions[region.name]: + act_region = world.multiworld.get_region(name, world.player) + entrance_name = "{name} Portal - Entrance {num}" + connect_regions(act_region, region, entrance_name.format(name=region.name, num=i), world.player) + i += 1 + + +def create_tasksanity_locations(world: World): + ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) + id_start: int = 300204 + for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + location = HatInTimeLocation(world.player, format("Tasksanity Check %i" % (i+1)), id_start+i, ship_shape) + ship_shape.locations.append(location) + # world.location_name_to_id.setdefault(location.name, location.address) + + +def randomize_act_entrances(world: World): + region_list: typing.List[Region] = get_act_regions(world) + world.multiworld.random.shuffle(region_list) + + separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) + + for region in region_list.copy(): + if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ + and "Time Rift" not in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if "Time Rift" in region.name: + region_list.remove(region) + region_list.append(region) + + # Reverse the list, so we can do what we want to do first + region_list.reverse() + + shuffled_list: typing.List[Region] = [] + mapped_list: typing.List[Region] = [] + rift_dict: typing.Dict[str, Region] = {} + first_chapter: Region = get_first_chapter_region(world) + has_guaranteed: bool = False + + i: int = 0 + while i < len(region_list): + region = region_list[i] + i += 1 + + # Get the first accessible act, so we can map that to something first + if not has_guaranteed: + if act_chapters[region.name] != first_chapter.name: + continue + + if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ + and "Free Roam" not in act_entrances[region.name]: + continue + + i = 0 + + # Already mapped to something else + if region in mapped_list: + continue + + mapped_list.append(region) + + # Look for candidates to map this act to + candidate_list: typing.List[Region] = [] + for candidate in region_list: + + if world.multiworld.VanillaAlpine[world.player].value > 0 and region.name == "Alpine Free Roam" \ + or world.multiworld.VanillaAlpine[world.player].value == 2 and region.name == "The Illness has Spread": + candidate_list.append(region) + break + + if world.multiworld.VanillaMetro[world.player].value > 0 and region.name == "Nyakuza Free Roam": + candidate_list.append(region) + break + + if region.name == "Rush Hour": + if world.multiworld.EndGoal[world.player].value == 2 or \ + world.multiworld.VanillaMetro[world.player].value == 2: + candidate_list.append(region) + break + + # We're mapping something to the first act, make sure it is valid + if not has_guaranteed: + if candidate.name not in guaranteed_first_acts: + continue + + # Not completable without Umbrella + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): + continue + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \ + and "Subcon Forest" in act_entrances[candidate.name]: + continue + + candidate_list.append(candidate) + has_guaranteed = True + break + + # Already mapped onto something else + if candidate in shuffled_list: + continue + + if separate_rifts: + # Don't map Time Rifts to normal acts + if "Time Rift" in region.name and "Time Rift" not in candidate.name: + continue + + # Don't map normal acts to Time Rifts + if "Time Rift" not in region.name and "Time Rift" in candidate.name: + continue + + # Separate purple rifts + if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ + or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: + continue + + # Don't map Alpine to its own finale + if region.name == "The Illness has Spread" and candidate.name == "Alpine Free Roam": + continue + + # Ditto for Metro + if region.name == "Rush Hour" and candidate.name == "Nyakuza Free Roam": + continue + + if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: + continue + + candidate_list.append(candidate) + + candidate: Region = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + shuffled_list.append(candidate) + + # Vanilla + if candidate.name == region.name: + if region.name in rift_access_regions.keys(): + rift_dict.setdefault(region.name, candidate) + + world.update_chapter_act_info(region, candidate) + continue + + if region.name in rift_access_regions.keys(): + connect_time_rift(world, region, candidate) + rift_dict.setdefault(region.name, candidate) + else: + if candidate.name in rift_access_regions.keys(): + for e in candidate.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + del e.parent_region + del e.connected_region + + entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) + reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) + + world.update_chapter_act_info(region, candidate) + + for name in blacklisted_acts.values(): + if not is_act_blacklisted(world, name): + continue + + region: Region = world.multiworld.get_region(name, world.player) + world.update_chapter_act_info(region, region) + + set_rift_rules(world, rift_dict) + + +def connect_time_rift(world: World, time_rift: Region, exit_region: Region): + count: int = len(rift_access_regions[time_rift.name]) + i: int = 1 + while i <= count: + name = format("%s Portal - Entrance %i" % (time_rift.name, i)) + entrance: Entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) + i += 1 + + +def get_act_regions(world: World) -> typing.List[Region]: + act_list: typing.List[Region] = [] + for region in world.multiworld.get_regions(world.player): + if region.name in chapter_act_info.keys(): + if not is_act_blacklisted(world, region.name): + act_list.append(region) + + return act_list + + +def is_act_blacklisted(world: World, name: str) -> bool: + if name == "The Finale": + return world.multiworld.EndGoal[world.player].value == 1 + + return name in blacklisted_acts.values() + + +def create_region(world: World, name: str) -> Region: + reg = Region(name, world.player, world.multiworld) + + for (key, data) in location_table.items(): + if data.nyakuza_thug != "": + continue + + if data.region == name: + if key in storybook_pages.keys() \ + and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + continue + + location = HatInTimeLocation(world.player, key, data.id, reg) + reg.locations.append(location) + if location.name in shop_locations: + world.shop_locs.append(location.name) + + world.multiworld.regions.append(reg) + return reg + + +def create_badge_seller(world: World) -> Region: + badge_seller = Region("Badge Seller", world.player, world.multiworld) + world.multiworld.regions.append(badge_seller) + count: int = 0 + max_items: int = 0 + + if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: + max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + if max_items <= 0: + world.badge_seller_count = 0 + return badge_seller + + for (key, data) in shop_locations.items(): + if "Badge Seller" not in key: + continue + + location = HatInTimeLocation(world.player, key, data.id, badge_seller) + badge_seller.locations.append(location) + world.shop_locs.append(location.name) + + count += 1 + if count >= max_items: + break + + world.badge_seller_count = max_items + return badge_seller + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int): + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + + +# Takes an entrance, removes its old connections, and reconnects it between the two regions specified. +def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region): + if entrance in entrance.connected_region.entrances: + entrance.connected_region.entrances.remove(entrance) + + if entrance in entrance.parent_region.exits: + entrance.parent_region.exits.remove(entrance) + + if entrance in start_region.exits: + start_region.exits.remove(entrance) + + if entrance in exit_region.entrances: + exit_region.entrances.remove(entrance) + + entrance.parent_region = start_region + start_region.exits.append(entrance) + entrance.connect(exit_region) + + +def create_region_and_connect(world: World, + name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: + + reg: Region = create_region(world, name) + entrance_region: Region + exit_region: Region + + if is_exit: + entrance_region = connected_region + exit_region = reg + else: + entrance_region = reg + exit_region = connected_region + + connect_regions(entrance_region, exit_region, entrancename, world.player) + return reg + + +def get_first_chapter_region(world: World) -> Region: + start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player] + return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) + + +def get_act_original_chapter(world: World, act_name: str) -> Region: + return world.multiworld.get_region(act_chapters[act_name], world.player) + + +def create_thug_shops(world: World): + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value + count: int = -1 + step: int = 0 + old_name: str = "" + + for key, data in shop_locations.items(): + if data.nyakuza_thug == "": + continue + + if old_name != "" and old_name == data.nyakuza_thug: + continue + + try: + if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: + continue + except KeyError: + pass + + if count == -1: + count = world.multiworld.random.randint(min_items, max_items) + world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) + if count <= 0: + continue + + if count >= 1: + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + + step += 1 + if step >= count: + old_name = data.nyakuza_thug + step = 0 + count = -1 + + +def create_events(world: World) -> int: + count: int = 0 + + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + + if data.act_complete_event: + act_completion: str = format("Act Completion (%s)" % data.region) + event.access_rule = world.multiworld.get_location(act_completion, world.player).access_rule + + count += 1 + + return count + + +def create_event(name: str, region: Region, world: World) -> Location: + event = HatInTimeLocation(world.player, name, None, region) + region.locations.append(event) + event.place_locked_item(HatInTimeItem(name, ItemClassification.progression, None, world.player)) + return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py new file mode 100644 index 0000000000..4ba0b45f1a --- /dev/null +++ b/worlds/ahit/Rules.py @@ -0,0 +1,682 @@ +from worlds.AutoWorld import World, CollectionState +from worlds.generic.Rules import add_rule, set_rule +from .Locations import location_table, tihs_locations, zipline_unlocks, is_location_valid, contract_locations, \ + shop_locations +from .Types import HatType, ChapterIndex +from BaseClasses import Location, Entrance, Region +import typing + + +act_connections = { + "Mafia Town - Act 2": ["Mafia Town - Act 1"], + "Mafia Town - Act 3": ["Mafia Town - Act 1"], + "Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"], + "Mafia Town - Act 6": ["Mafia Town - Act 4"], + "Mafia Town - Act 7": ["Mafia Town - Act 4"], + "Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"], + + "Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"], + "Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"], + + "Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2", + "Subcon Forest - Act 3", "Subcon Forest - Act 4", + "Subcon Forest - Act 5"], + + "The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"], + "The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"], +} + + +def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: + return get_remaining_hat_cost(state, world, hat) <= 0 + + +def get_remaining_hat_cost(state: CollectionState, world: World, hat: HatType) -> int: + cost: int = 0 + for h in world.hat_craft_order: + cost += world.hat_yarn_costs.get(h) + if h == hat: + break + + return max(cost - state.count("Yarn", world.player), 0) + + +def can_sdj(state: CollectionState, world: World): + return can_use_hat(state, world, HatType.SPRINT) + + +def can_use_hookshot(state: CollectionState, world: World): + return state.has("Hookshot Badge", world.player) + + +def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: + return state.has_group(relic, world.player, len(world.item_name_groups[relic])) + + +def get_relic_count(state: CollectionState, world: World, relic: str) -> int: + return state.count_group(relic, world.player) + + +def can_hit_bells(state: CollectionState, world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0: + return True + + return state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING) + + +# Only use for rifts +def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: + entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) + if not state.can_reach(entrance.connected_region, player=world.player): + return False + + if "Free Roam" in entrance.connected_region.name: + return True + + name: str = format("Act Completion (%s)" % entrance.connected_region.name) + return world.multiworld.get_location(name, world.player).access_rule(state) + + +def can_clear_alpine(state: CollectionState, world: World) -> bool: + return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ + and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) + + +def can_clear_metro(state: CollectionState, world: World) -> bool: + return state.has("Nyakuza Intro Cleared", world.player) \ + and state.has("Yellow Overpass Station Cleared", world.player) \ + and state.has("Yellow Overpass Manhole Cleared", world.player) \ + and state.has("Green Clean Station Cleared", world.player) \ + and state.has("Green Clean Manhole Cleared", world.player) \ + and state.has("Bluefin Tunnel Cleared", world.player) \ + and state.has("Pink Paw Station Cleared", world.player) \ + and state.has("Pink Paw Manhole Cleared", world.player) + + +def set_rules(world: World): + w = world + mw = world.multiworld + p = world.player + + dlc1: bool = bool(mw.EnableDLC1[p].value > 0) + dlc2: bool = bool(mw.EnableDLC2[p].value > 0) + + # First, chapter access + starting_chapter = ChapterIndex(mw.StartingChapter[p].value) + w.set_chapter_cost(starting_chapter, 0) + + # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale + chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] + + final_chapter = ChapterIndex.FINALE + if mw.EndGoal[p].value == 2: + final_chapter = ChapterIndex.METRO + chapter_list.append(ChapterIndex.FINALE) + + if dlc1: + chapter_list.append(ChapterIndex.CRUISE) + + if dlc2 and final_chapter is not ChapterIndex.METRO: + chapter_list.append(ChapterIndex.METRO) + + chapter_list.remove(starting_chapter) + mw.random.shuffle(chapter_list) + + if starting_chapter is not ChapterIndex.ALPINE and dlc1 or dlc2: + index1: int = 69 + index2: int = 69 + lowest_index: int + chapter_list.remove(ChapterIndex.ALPINE) + + if dlc1: + index1 = chapter_list.index(ChapterIndex.CRUISE) + + if dlc2 and final_chapter is not ChapterIndex.METRO: + index2 = chapter_list.index(ChapterIndex.METRO) + + lowest_index = min(index1, index2) + if lowest_index == 0: + pos = 0 + else: + pos = mw.random.randint(0, lowest_index) + + chapter_list.insert(pos, ChapterIndex.ALPINE) + + lowest_cost: int = mw.LowestChapterCost[p].value + highest_cost: int = mw.HighestChapterCost[p].value + + cost_increment: int = mw.ChapterCostIncrement[p].value + min_difference: int = mw.ChapterCostMinDifference[p].value + last_cost: int = 0 + cost: int + loop_count: int = 0 + + for chapter in chapter_list: + min_range: int = lowest_cost + (cost_increment * loop_count) + if min_range >= highest_cost: + min_range = highest_cost-1 + + value: int = mw.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) + + cost = mw.random.randint(value, min(value + cost_increment, highest_cost)) + if loop_count >= 1: + if last_cost + min_difference > cost: + cost = last_cost + min_difference + + cost = min(cost, highest_cost) + w.set_chapter_cost(chapter, cost) + last_cost = cost + loop_count += 1 + + w.set_chapter_cost(final_chapter, mw.random.randint(mw.FinalChapterMinCost[p].value, + mw.FinalChapterMaxCost[p].value)) + + add_rule(mw.get_entrance("Telescope -> Mafia Town", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.MAFIA))) + + add_rule(mw.get_entrance("Telescope -> Battle of the Birds", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(mw.get_entrance("Telescope -> Subcon Forest", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.SUBCON))) + + add_rule(mw.get_entrance("Telescope -> Alpine Skyline", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + + add_rule(mw.get_entrance("Telescope -> Time's End", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.FINALE)) + and can_use_hat(state, w, HatType.BREWING) and can_use_hat(state, w, HatType.DWELLER)) + + if dlc1: + add_rule(mw.get_entrance("Telescope -> The Arctic Cruise", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.CRUISE))) + + if dlc2: + add_rule(mw.get_entrance("Telescope -> Nyakuza Metro", p), + lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.METRO)) + and can_use_hat(state, w, HatType.DWELLER) and can_use_hat(state, w, HatType.ICE)) + + if mw.ActRandomizer[p].value == 0: + set_default_rift_rules(w) + + location: Location + for (key, data) in location_table.items(): + if not is_location_valid(w, key): + continue + + if key in contract_locations.keys(): + continue + + location = mw.get_location(key, p) + + # Not all locations in Alpine can be reached from The Illness has Spread + # as many of the ziplines are blocked off + if data.region == "Alpine Skyline Area" and key not in tihs_locations: + add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + + if data.region == "The Birdhouse" or data.region == "The Lava Cake" \ + or data.region == "The Windmill" or data.region == "The Twilight Bell": + add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(location, lambda state: can_use_hat(state, w, hat)) + + if data.required_tps > 0: + add_rule(location, lambda state: state.has("Time Piece", p, data.required_tps)) + + if data.hookshot: + add_rule(location, lambda state: can_use_hookshot(state, w)) + + if data.umbrella and mw.UmbrellaLogic[p].value > 0: + add_rule(location, lambda state: state.has("Umbrella", p)) + + if data.dweller_bell > 0: + if data.dweller_bell == 1: # Required to be hit regardless of Dweller Mask + add_rule(location, lambda state: can_hit_bells(state, w)) + else: # Can bypass with Dweller Mask + add_rule(location, lambda state: can_hit_bells(state, w) or can_use_hat(state, w, HatType.DWELLER)) + + if data.paintings > 0 and mw.ShuffleSubconPaintings[p].value > 0: + value: int = data.paintings + add_rule(location, lambda state: state.count("Progressive Painting Unlock", p) >= value) + + set_specific_rules(w) + + if mw.LogicDifficulty[p].value >= 1: + mw.SDJLogic[p].value = 1 + + if mw.SDJLogic[p].value > 0: + set_sdj_rules(world) + + if mw.ShuffleAlpineZiplines[p].value > 0: + set_alps_zipline_rules(w) + + for (key, acts) in act_connections.items(): + if "Arctic Cruise" in key and not dlc1: + continue + + i: int = 1 + entrance: Entrance = mw.get_entrance(key, p) + region: Region = entrance.connected_region + access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + entrance.parent_region.exits.remove(entrance) + del entrance.parent_region + + # Entrances to this act that we have to set access_rules on + entrances: typing.List[Entrance] = [] + + for act in acts: + act_entrance: Entrance = mw.get_entrance(act, p) + + required_region = act_entrance.connected_region + name: str = format("%s: Connection %i" % (key, i)) + new_entrance: Entrance = connect_regions(required_region, region, name, p) + entrances.append(new_entrance) + + # Copy access rules from act completions + if "Free Roam" not in required_region.name: + rule: typing.Callable[[CollectionState], bool] + name = format("Act Completion (%s)" % required_region.name) + rule = mw.get_location(name, p).access_rule + access_rules.append(rule) + + i += 1 + + for e in entrances: + for rules in access_rules: + add_rule(e, rules) + + for entrance in mw.get_region("Alpine Free Roam", p).entrances: + add_rule(entrance, lambda state: can_use_hookshot(state, w)) + if mw.UmbrellaLogic[p].value > 0: + add_rule(entrance, lambda state: state.has("Umbrella", p)) + + if mw.EndGoal[p].value == 1: + mw.completion_condition[p] = lambda state: state.has("Time Piece Cluster", p) + elif mw.EndGoal[p].value == 2: + mw.completion_condition[p] = lambda state: state.has("Rush Hour Cleared", p) + + +def set_specific_rules(world: World): + mw = world.multiworld + w = world + p = world.player + dlc1: bool = bool(mw.EnableDLC1[p].value > 0) + + add_rule(mw.get_entrance("Alpine Skyline - Finale", p), + lambda state: can_clear_alpine(state, w)) + + # Normal logic + if mw.LogicDifficulty[p].value == 0: + add_rule(mw.get_entrance("-> The Birdhouse", p), + lambda state: can_use_hat(state, w, HatType.BREWING)) + + add_rule(mw.get_location("Alpine Skyline - Yellow Band Hills", p), + lambda state: can_use_hat(state, w, HatType.BREWING)) + + if dlc1: + add_rule(mw.get_location("Act Completion (Time Rift - Deep Sea)", p), + lambda state: can_use_hat(state, w, HatType.DWELLER)) + + add_rule(mw.get_location("Rock the Boat - Post Captain Rescue", p), + lambda state: can_use_hat(state, w, HatType.ICE)) + + add_rule(mw.get_location("Act Completion (Rock the Boat)", p), + lambda state: can_use_hat(state, w, HatType.ICE)) + + # Hard logic, includes SDJ stuff + if mw.LogicDifficulty[p].value >= 1: + add_rule(mw.get_location("Act Completion (Time Rift - The Twilight Bell)", p), + lambda state: can_use_hat(state, w, HatType.SPRINT) and state.has("Scooter Badge", p), "or") + + # Expert logic + if mw.LogicDifficulty[p].value >= 2: + set_rule(mw.get_location("Alpine Skyline - The Twilight Path", p), lambda state: True) + else: + add_rule(mw.get_entrance("-> The Twilight Bell", p), + lambda state: can_use_hat(state, w, HatType.DWELLER)) + + add_rule(mw.get_location("Mafia Town - Behind HQ Chest", p), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", p) + or state.can_reach("Down with the Mafia!", "Region", p) + or state.can_reach("Cheating the Race", "Region", p) + or state.can_reach("The Golden Vault", "Region", p)) + + # Old guys don't appear in SCFOS + add_rule(mw.get_location("Mafia Town - Old Man (Steel Beams)", p), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) + or state.can_reach("Barrel Battle", "Region", p) + or state.can_reach("Cheating the Race", "Region", p) + or state.can_reach("The Golden Vault", "Region", p) + or state.can_reach("Down with the Mafia!", "Region", p)) + + add_rule(mw.get_location("Mafia Town - Old Man (Seaside Spaghetti)", p), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) + or state.can_reach("Barrel Battle", "Region", p) + or state.can_reach("Cheating the Race", "Region", p) + or state.can_reach("The Golden Vault", "Region", p) + or state.can_reach("Down with the Mafia!", "Region", p)) + + # Only available outside She Came from Outer Space + add_rule(mw.get_location("Mafia Town - Mafia Geek Platform", p), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) + or state.can_reach("Barrel Battle", "Region", p) + or state.can_reach("Down with the Mafia!", "Region", p) + or state.can_reach("Cheating the Race", "Region", p) + or state.can_reach("Heating Up Mafia Town", "Region", p) + or state.can_reach("The Golden Vault", "Region", p)) + + # Only available outside Down with the Mafia! (for some reason) + add_rule(mw.get_location("Mafia Town - On Scaffolding", p), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) + or state.can_reach("Barrel Battle", "Region", p) + or state.can_reach("She Came from Outer Space", "Region", p) + or state.can_reach("Cheating the Race", "Region", p) + or state.can_reach("Heating Up Mafia Town", "Region", p) + or state.can_reach("The Golden Vault", "Region", p)) + + # For some reason, the brewing crate is removed in HUMT + set_rule(mw.get_location("Mafia Town - Secret Cave", p), + lambda state: state.can_reach("Heating Up Mafia Town", "Region", p) + or can_use_hat(state, w, HatType.BREWING)) + + # Can bounce across the lava to get this without Hookshot (need to die though :P) + set_rule(mw.get_location("Mafia Town - Above Boats", p), + lambda state: state.can_reach("Heating Up Mafia Town", "Region", p) + or can_use_hookshot(state, w)) + + set_rule(mw.get_location("Act Completion (Cheating the Race)", p), + lambda state: can_use_hat(state, w, HatType.TIME_STOP) + or mw.CTRWithSprint[p].value > 0 and can_use_hat(state, w, HatType.SPRINT)) + + set_rule(mw.get_location("Subcon Forest - Boss Arena Chest", p), + lambda state: state.can_reach("Toilet of Doom", "Region", p) + and (mw.ShuffleSubconPaintings[p].value == 0 or state.has("Progressive Painting Unlock", p, 1)) + or state.can_reach("Your Contract has Expired", "Region", p)) + + if mw.UmbrellaLogic[p].value > 0: + add_rule(mw.get_location("Act Completion (Toilet of Doom)", p), + lambda state: state.has("Umbrella", p) or can_use_hat(state, w, HatType.BREWING)) + + set_rule(mw.get_location("Act Completion (Time Rift - Village)", p), + lambda state: can_use_hat(state, w, HatType.BREWING) or state.has("Umbrella", p) + or can_use_hat(state, w, HatType.DWELLER)) + + add_rule(mw.get_entrance("Subcon Forest - Act 2", p), + lambda state: state.has("Snatcher's Contract - The Subcon Well", p)) + + add_rule(mw.get_entrance("Subcon Forest - Act 3", p), + lambda state: state.has("Snatcher's Contract - Toilet of Doom", p)) + + add_rule(mw.get_entrance("Subcon Forest - Act 4", p), + lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", p)) + + add_rule(mw.get_entrance("Subcon Forest - Act 5", p), + lambda state: state.has("Snatcher's Contract - Mail Delivery Service", p)) + + if mw.ShuffleSubconPaintings[p].value > 0: + for key in contract_locations: + if key == "Snatcher's Contract - The Subcon Well": + continue + + add_rule(mw.get_location(key, p), + lambda state: state.has("Progressive Painting Unlock", p, 1)) + + add_rule(mw.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", p), + lambda state: can_use_hat(state, w, HatType.SPRINT) or can_use_hat(state, w, HatType.TIME_STOP)) + + if mw.EnableDLC1[p].value > 0: + add_rule(mw.get_entrance("Cruise Ship Entrance BV", p), lambda state: can_use_hookshot(state, w)) + + # This particular item isn't present in Act 3 for some reason, yes in vanilla too + add_rule(mw.get_location("The Arctic Cruise - Toilet", p), + lambda state: state.can_reach("Bon Voyage!", "Region", p) + or state.can_reach("Ship Shape", "Region", p)) + + if mw.EnableDLC2[p].value > 0: + add_rule(mw.get_entrance("-> Bluefin Tunnel", p), + lambda state: state.has("Metro Ticket - Green", p) or state.has("Metro Ticket - Blue", p)) + + add_rule(mw.get_entrance("-> Pink Paw Station", p), + lambda state: state.has("Metro Ticket - Pink", p) + or state.has("Metro Ticket - Yellow", p) and state.has("Metro Ticket - Blue", p)) + + add_rule(mw.get_entrance("Nyakuza Metro - Finale", p), + lambda state: can_clear_metro(state, w)) + + add_rule(mw.get_location("Act Completion (Rush Hour)", p), + lambda state: state.has("Metro Ticket - Yellow", p) and state.has("Metro Ticket - Blue", p) + and state.has("Metro Ticket - Pink", p)) + + for key in shop_locations.keys(): + if "Green Clean Station Thug B" in key and is_location_valid(w, key): + add_rule(mw.get_location(key, p), lambda state: state.has("Metro Ticket - Yellow", p), "or") + + +def set_sdj_rules(world: World): + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), + lambda state: can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), + lambda state: can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: can_sdj(state, world), "or") + + +def set_alps_zipline_rules(world: World): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) + + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), lambda state: state.has(zipline, world.player)) + + +def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): + reg: Region + entrance: Entrance + if isinstance(region, str): + reg = world.multiworld.get_region(region, world.player) + else: + reg = region + + if isinstance(unlocked_entrance, str): + entrance = world.multiworld.get_entrance(unlocked_entrance, world.player) + else: + entrance = unlocked_entrance + + world.multiworld.register_indirect_condition(reg, entrance) + + +# See randomize_act_entrances in Regions.py +# Called BEFORE set_rules! +def set_rift_rules(world: World, regions: typing.Dict[str, Region]): + w = world + mw = world.multiworld + p = world.player + + # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. + for entrance in regions["Time Rift - Gallery"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, w, HatType.BREWING) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in regions["Time Rift - The Lab"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, w, HatType.DWELLER) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in regions["Time Rift - Sewers"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Mafia Town - Act 4")) + reg_act_connection(w, mw.get_entrance("Mafia Town - Act 4", p).connected_region, entrance) + + for entrance in regions["Time Rift - Bazaar"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Mafia Town - Act 6")) + reg_act_connection(w, mw.get_entrance("Mafia Town - Act 6", p).connected_region, entrance) + + for entrance in regions["Time Rift - Mafia of Cooks"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Burger")) + + for entrance in regions["Time Rift - The Owl Express"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 3")) + reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 2", p).connected_region, entrance) + reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 3", p).connected_region, entrance) + + for entrance in regions["Time Rift - The Moon"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 5")) + reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 4", p).connected_region, entrance) + reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 5", p).connected_region, entrance) + + for entrance in regions["Time Rift - Dead Bird Studio"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Train")) + + for entrance in regions["Time Rift - Pipe"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Subcon Forest - Act 2")) + reg_act_connection(w, mw.get_entrance("Subcon Forest - Act 2", p).connected_region, entrance) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + + for entrance in regions["Time Rift - Village"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "Subcon Forest - Act 4")) + reg_act_connection(w, mw.get_entrance("Subcon Forest - Act 4", p).connected_region, entrance) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + + for entrance in regions["Time Rift - Sleepy Subcon"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "UFO")) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 3)) + + for entrance in regions["Time Rift - Curly Tail Trail"].entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", p)) + + for entrance in regions["Time Rift - The Twilight Bell"].entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", p)) + + for entrance in regions["Time Rift - Alpine Skyline"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Crayon")) + + if mw.EnableDLC1[p].value > 0: + for entrance in regions["Time Rift - Balcony"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "The Arctic Cruise - Finale")) + + for entrance in regions["Time Rift - Deep Sea"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Cake")) + + if mw.EnableDLC2[p].value > 0: + for entrance in regions["Time Rift - Rumbi Factory"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Necklace")) + + +# Basically the same as above, but without the need of the dict since we are just setting defaults +# Called if Act Rando is disabled +def set_default_rift_rules(world: World): + w = world + mw = world.multiworld + p = world.player + + for entrance in mw.get_region("Time Rift - Gallery", p).entrances: + add_rule(entrance, lambda state: can_use_hat(state, w, HatType.BREWING) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in mw.get_region("Time Rift - The Lab", p).entrances: + add_rule(entrance, lambda state: can_use_hat(state, w, HatType.DWELLER) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in mw.get_region("Time Rift - Sewers", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(w, "Down with the Mafia!", entrance.name) + + for entrance in mw.get_region("Time Rift - Bazaar", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(w, "Heating Up Mafia Town", entrance.name) + + for entrance in mw.get_region("Time Rift - Mafia of Cooks", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Burger")) + + for entrance in mw.get_region("Time Rift - The Owl Express", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(w, "Murder on the Owl Express", entrance.name) + reg_act_connection(w, "Picture Perfect", entrance.name) + + for entrance in mw.get_region("Time Rift - The Moon", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(w, "Train Rush", entrance.name) + reg_act_connection(w, "The Big Parade", entrance.name) + + for entrance in mw.get_region("Time Rift - Dead Bird Studio", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Train")) + + for entrance in mw.get_region("Time Rift - Pipe", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(w, "The Subcon Well", entrance.name) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + + for entrance in mw.get_region("Time Rift - Village", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(w, "Queen Vanessa's Manor", entrance.name) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + + for entrance in mw.get_region("Time Rift - Sleepy Subcon", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "UFO")) + if mw.ShuffleSubconPaintings[p].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 3)) + + for entrance in mw.get_region("Time Rift - Curly Tail Trail", p).entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", p)) + + for entrance in mw.get_region("Time Rift - The Twilight Bell", p).entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", p)) + + for entrance in mw.get_region("Time Rift - Alpine Skyline", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Crayon")) + + if mw.EnableDLC1[p].value > 0: + for entrance in mw.get_region("Time Rift - Balcony", p).entrances: + add_rule(entrance, lambda state: can_clear_act(state, w, "The Arctic Cruise - Finale")) + + for entrance in mw.get_region("Time Rift - Deep Sea", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Cake")) + + if mw.EnableDLC2[p].value > 0: + for entrance in mw.get_region("Time Rift - Rumbi Factory", p).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, w, "Necklace")) + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py new file mode 100644 index 0000000000..45f5535b58 --- /dev/null +++ b/worlds/ahit/Types.py @@ -0,0 +1,28 @@ +from enum import IntEnum, IntFlag + + +class HatType(IntEnum): + NONE = -1 + SPRINT = 0 + BREWING = 1 + ICE = 2 + DWELLER = 3 + TIME_STOP = 4 + + +class HatDLC(IntFlag): + none = 0b000 + dlc1 = 0b001 + dlc2 = 0b010 + death_wish = 0b100 + + +class ChapterIndex(IntEnum): + SPACESHIP = 0 + MAFIA = 1 + BIRDS = 2 + SUBCON = 3 + ALPINE = 4 + FINALE = 5 + CRUISE = 6 + METRO = 7 diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py new file mode 100644 index 0000000000..b3d78ad4b6 --- /dev/null +++ b/worlds/ahit/__init__.py @@ -0,0 +1,276 @@ +from BaseClasses import Item, ItemClassification, Region, LocationProgressType + +from .Items import HatInTimeItem, item_table, item_frequencies, item_dlc_enabled, junk_weights,\ + create_item, create_multiple_items, create_junk_items, relic_groups, act_contracts, alps_hooks, \ + get_total_time_pieces + +from .Regions import create_region, create_regions, connect_regions, randomize_act_entrances, chapter_act_info, \ + create_events, chapter_regions, act_chapters + +from .Locations import HatInTimeLocation, location_table, get_total_locations, contract_locations, is_location_valid, \ + get_location_names + +from .Types import HatDLC, HatType, ChapterIndex +from .Options import ahit_options, slot_data_options, adjust_options +from worlds.AutoWorld import World +from .Rules import set_rules +import typing + + +class HatInTimeWorld(World): + """ + A Hat in Time is a cute-as-heck 3D platformer featuring a little girl who stitches hats for wicked powers! + Freely explore giant worlds and recover Time Pieces to travel to new heights! + """ + + game = "A Hat in Time" + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = get_location_names() + + option_definitions = ahit_options + + hat_craft_order: typing.List[HatType] + hat_yarn_costs: typing.Dict[HatType, int] + chapter_timepiece_costs: typing.Dict[ChapterIndex, int] + act_connections: typing.Dict[str, str] = {} + nyakuza_thug_items: typing.Dict[str, int] = {} + shop_locs: typing.List[str] = [] + item_name_groups = relic_groups + badge_seller_count: int = 0 + + def generate_early(self): + adjust_options(self) + + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory + # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock + start_chapter: int = self.multiworld.StartingChapter[self.player].value + + if start_chapter == 4 or start_chapter == 3: + if self.multiworld.ActRandomizer[self.player].value == 0: + if start_chapter == 4: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + + if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + def create_regions(self): + self.nyakuza_thug_items = {} + self.shop_locs = [] + create_regions(self) + + # place default contract locations if contract shuffle is off so logic can still utilize them + if self.multiworld.ShuffleActContracts[self.player].value == 0: + for name in contract_locations.keys(): + self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + else: + # The bag trap contract check needs to be excluded, because if the player has the Subcon Well contract, + # the trap will not activate, locking the player out of the check permanently + self.multiworld.get_location("Snatcher's Contract - The Subcon Well", + self.player).progress_type = LocationProgressType.EXCLUDED + + def create_items(self): + self.hat_yarn_costs = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + + self.hat_craft_order = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] + + self.topology_present = self.multiworld.ActRandomizer[self.player].value + + # Item Pool + itempool: typing.List[Item] = [] + self.calculate_yarn_costs() + yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value) + + # 1/5 is progression balanced + for i in range(len(yarn_pool) // 5): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + if self.multiworld.RandomizeHatOrder[self.player].value > 0: + self.multiworld.random.shuffle(self.hat_craft_order) + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(self, name): + continue + + item_type: ItemClassification = item_table.get(name).classification + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and self.multiworld.ShuffleActContracts[self.player].value == 0: + continue + + if name in alps_hooks.keys() and self.multiworld.ShuffleAlpineZiplines[self.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and self.multiworld.ShuffleSubconPaintings[self.player].value == 0: + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if self.multiworld.EnableDLC1[self.player].value > 0: + max_extra += 6 + + if self.multiworld.EnableDLC2[self.player].value > 0: + max_extra += 10 + + tp_count += min(max_extra, self.multiworld.MaxExtraTimePieces[self.player].value) + tp_list: typing.List[Item] = create_multiple_items(self, name, tp_count) + + # 1/5 is progression balanced + for i in range(len(tp_list) // 5): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(self, name, item_frequencies.get(name, 1)) + + create_events(self) + total_locations: int = get_total_locations(self) + itempool += create_junk_items(self, total_locations-len(itempool)) + self.multiworld.itempool += itempool + + def set_rules(self): + self.act_connections = {} + self.chapter_timepiece_costs = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} + + if self.multiworld.ActRandomizer[self.player].value > 0: + randomize_act_entrances(self) + + set_rules(self) + + def create_item(self, name: str) -> Item: + return create_item(self, name) + + def fill_slot_data(self) -> dict: + slot_data: dict = {"SprintYarnCost": self.hat_yarn_costs[HatType.SPRINT], + "BrewingYarnCost": self.hat_yarn_costs[HatType.BREWING], + "IceYarnCost": self.hat_yarn_costs[HatType.ICE], + "DwellerYarnCost": self.hat_yarn_costs[HatType.DWELLER], + "TimeStopYarnCost": self.hat_yarn_costs[HatType.TIME_STOP], + "Chapter1Cost": self.chapter_timepiece_costs[ChapterIndex.MAFIA], + "Chapter2Cost": self.chapter_timepiece_costs[ChapterIndex.BIRDS], + "Chapter3Cost": self.chapter_timepiece_costs[ChapterIndex.SUBCON], + "Chapter4Cost": self.chapter_timepiece_costs[ChapterIndex.ALPINE], + "Chapter5Cost": self.chapter_timepiece_costs[ChapterIndex.FINALE], + "Chapter6Cost": self.chapter_timepiece_costs[ChapterIndex.CRUISE], + "Chapter7Cost": self.chapter_timepiece_costs[ChapterIndex.METRO], + "Hat1": int(self.hat_craft_order[0]), + "Hat2": int(self.hat_craft_order[1]), + "Hat3": int(self.hat_craft_order[2]), + "Hat4": int(self.hat_craft_order[3]), + "Hat5": int(self.hat_craft_order[4]), + "BadgeSellerItemCount": self.badge_seller_count, + "SeedNumber": self.multiworld.seed} # For shop prices + + if self.multiworld.ActRandomizer[self.player].value > 0: + for name in self.act_connections.keys(): + slot_data[name] = self.act_connections[name] + + if self.multiworld.EnableDLC2[self.player].value > 0: + for name in self.nyakuza_thug_items.keys(): + slot_data[name] = self.nyakuza_thug_items[name] + + for option_name in slot_data_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + new_hint_data = {} + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell"] + metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] + + for key, data in location_table.items(): + if not is_location_valid(self, key): + continue + + location = self.multiworld.get_location(key, self.player) + region_name: str + + if data.region in alpine_regions: + region_name = "Alpine Free Roam" + elif data.region in metro_regions: + region_name = "Nyakuza Free Roam" + elif data.region in chapter_act_info.keys(): + region_name = location.parent_region.name + else: + continue + + new_hint_data[location.address] = self.get_shuffled_region(region_name) + + if self.multiworld.EnableDLC1[self.player].value > 0 and self.multiworld.Tasksanity[self.player].value > 0: + ship_shape_region = self.get_shuffled_region("Ship Shape") + id_start: int = 300204 + for i in range(self.multiworld.TasksanityCheckCount[self.player].value): + new_hint_data[id_start+i] = ship_shape_region + + hint_data[self.player] = new_hint_data + + def write_spoiler_header(self, spoiler_handle: typing.TextIO): + for i in self.chapter_timepiece_costs.keys(): + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.chapter_timepiece_costs[ChapterIndex(i)])) + + for hat in self.hat_craft_order: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) + + def calculate_yarn_costs(self): + mw = self.multiworld + p = self.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + self.hat_yarn_costs[HatType(i)] = cost + max_cost += cost + + available_yarn = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + # make sure we always have at least 8 extra + if max_cost + 8 > available_yarn: + mw.YarnAvailable[p].value += (max_cost + 8) - available_yarn + + def set_chapter_cost(self, chapter: ChapterIndex, cost: int): + self.chapter_timepiece_costs[chapter] = cost + + def get_chapter_cost(self, chapter: ChapterIndex) -> int: + return self.chapter_timepiece_costs.get(chapter) + + # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game + def update_chapter_act_info(self, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + self.act_connections[original_act_info] = new_act_info + + def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name From 173896bd749c77d7e93e45233a45388d3c65465c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 14:25:01 -0400 Subject: [PATCH 002/143] Fuck it --- worlds/ahit/Locations.py | 108 ++++++++++++----------------- worlds/ahit/Rules.py | 145 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 181 insertions(+), 72 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index f48c3d0b4a..eb242c96b2 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -9,10 +9,8 @@ class LocData(NamedTuple): id: int region: str required_hats: Optional[List[HatType]] = [HatType.NONE] - required_tps: Optional[int] = 0 hookshot: Optional[bool] = False dlc_flags: Optional[HatDLC] = HatDLC.none - paintings: Optional[int] = 0 # Progressive paintings required for Subcon painting shuffle # For UmbrellaLogic setting umbrella: Optional[bool] = False # Umbrella required for this check @@ -79,8 +77,7 @@ def get_location_names() -> Dict[str, int]: ahit_locations = { - "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", required_tps=4, dweller_bell=1), - # "Spaceship - Cooking Cat": LocData(301001, "Spaceship", required_tps=5), + "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", dweller_bell=1), # 300000 range - Mafia Town/Batle of the Birds "Welcome to Mafia Town - Umbrella": LocData(301002, "Welcome to Mafia Town"), @@ -169,85 +166,66 @@ ahit_locations = { "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), "Subcon Village - House Top": LocData(325471, "Subcon Forest Area"), "Subcon Village - Ice Cube House": LocData(325469, "Subcon Forest Area"), - "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area"), "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), - "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", - required_hats=[HatType.BREWING], - paintings=1), + "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", required_hats=[HatType.BREWING],), - "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area"), + "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area"), + "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area"), + "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area"), + "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area"), + "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area"), - "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=3), + "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", required_hats=[HatType.DWELLER]), - "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=3), + "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", required_hats=[HatType.DWELLER]), - "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), + "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area"), - "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=3), + "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", required_hats=[HatType.DWELLER]), - "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=1), - "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=1), + "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area"), + "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area"), - "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=3), + "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area"), - "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=1), + "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", required_hats=[HatType.DWELLER]), "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", - paintings=3, required_hats=[HatType.DWELLER], hookshot=True), - "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area", paintings=2), - "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area", paintings=2), - "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), - "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), - "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), - "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), + "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area"), + "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area"), + "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area"), + "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area"), + "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area"), + "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area"), + "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area"), + "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True), + "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area"), - "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - paintings=2), + "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", required_hats=[HatType.DWELLER]), "Subcon Forest - Boss Arena Chest": LocData(323735, "Subcon Forest Area"), - "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2, paintings=1), + "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2), - "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", - required_hats=[HatType.BREWING], - paintings=2), + "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", required_hats=[HatType.BREWING]), - "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", - required_hats=[HatType.BREWING], - paintings=3), + "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", required_hats=[HatType.BREWING]), - "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1, paintings=1), - "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1, paintings=1), - "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1, paintings=1), - "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1), + "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1), + "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1), + "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1), - "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2), + "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2), + "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2), + "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2), # 330000 range - Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), @@ -350,10 +328,10 @@ act_completions = { "Act Completion (Time Rift - The Moon)": LocData(312785, "Time Rift - The Moon"), "Act Completion (Time Rift - Dead Bird Studio)": LocData(312577, "Time Rift - Dead Bird Studio"), - "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations", paintings=1), - "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), - "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True, paintings=1), - "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations"), + "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True), + "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True), + "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True), "Act Completion (Mail Delivery Service)": LocData(312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), "Act Completion (Your Contract has Expired)": LocData(311390, "Your Contract has Expired", umbrella=True), "Act Completion (Time Rift - Pipe)": LocData(313069, "Time Rift - Pipe", hookshot=True), @@ -501,7 +479,7 @@ shop_locations = { "Badge Seller - Item 8": LocData(301010, "Badge Seller"), "Badge Seller - Item 9": LocData(301011, "Badge Seller"), "Badge Seller - Item 10": LocData(301012, "Badge Seller"), - "Mafia Boss Shop Item": LocData(301013, "Spaceship", required_tps=12), + "Mafia Boss Shop Item": LocData(301013, "Spaceship"), "Yellow Overpass Station - Yellow Ticket Booth": LocData(301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), "Green Clean Station - Green Ticket Booth": LocData(301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 4ba0b45f1a..d9b39e0d11 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -229,9 +229,6 @@ def set_rules(world: World): if hat is not HatType.NONE: add_rule(location, lambda state: can_use_hat(state, w, hat)) - if data.required_tps > 0: - add_rule(location, lambda state: state.has("Time Piece", p, data.required_tps)) - if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, w)) @@ -244,10 +241,6 @@ def set_rules(world: World): else: # Can bypass with Dweller Mask add_rule(location, lambda state: can_hit_bells(state, w) or can_use_hat(state, w, HatType.DWELLER)) - if data.paintings > 0 and mw.ShuffleSubconPaintings[p].value > 0: - value: int = data.paintings - add_rule(location, lambda state: state.count("Progressive Painting Unlock", p) >= value) - set_specific_rules(w) if mw.LogicDifficulty[p].value >= 1: @@ -259,6 +252,9 @@ def set_rules(world: World): if mw.ShuffleAlpineZiplines[p].value > 0: set_alps_zipline_rules(w) + if mw.ShuffleSubconPaintings[p].value > 0: + set_painting_rules(w) + for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not dlc1: continue @@ -314,6 +310,13 @@ def set_specific_rules(world: World): add_rule(mw.get_entrance("Alpine Skyline - Finale", p), lambda state: can_clear_alpine(state, w)) + add_rule(mw.get_location("Mafia Boss Shop Item", p), + lambda state: state.has("Time Piece", p, 12) + and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(mw.get_location("Spaceship - Rumbi Abuse", p), + lambda state: state.has("Time Piece", p, 4)) + # Normal logic if mw.LogicDifficulty[p].value == 0: add_rule(mw.get_entrance("-> The Birdhouse", p), @@ -680,3 +683,131 @@ def connect_regions(start_region: Region, exit_region: Region, entrancename: str start_region.exits.append(entrance) entrance.connect(exit_region) return entrance + + +def set_painting_rules(world: World): + add_rule(world.multiworld.get_location("Subcon Village - Snatcher Statue Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Gravestone", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Near Well", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree A", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree B", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Ice Wall", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Treehouse", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Ice Cube Shack", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Well - On Pipe", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Cellar", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Bedroom Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Hall Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Chandelier", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Subcon Forest - Burning House", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Burning Tree Climb", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Burning Stump Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Burning Forest Treehouse", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage A", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage B", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Triple Spider Bounce", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Infinite Yarn Bush", world.player), + lambda state: state.has("Progressive Painting Unlock", 2)) + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Stump", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree A", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Giant Time Piece", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Gallows", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Shack", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), + lambda state: state.has("Progressive Painting Unlock", 3)) + + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Act Completion (The Subcon Well)", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) + + add_rule(world.multiworld.get_location("Act Completion (Queen Vanessa's Manor)", world.player), + lambda state: state.has("Progressive Painting Unlock", 1)) From cbdf4d903b1e04f32cfea0e60ce38b60f70140d5 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 19:01:32 -0400 Subject: [PATCH 003/143] Major fixes --- .gitignore | 1 + worlds/ahit/Rules.py | 11 +++-- worlds/ahit/__init__.py | 89 +++++++++++++++++++++++------------------ 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index 8e4cc86657..02d441fee1 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage +/oot/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index d9b39e0d11..04470690fe 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -37,8 +37,8 @@ def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: def get_remaining_hat_cost(state: CollectionState, world: World, hat: HatType) -> int: cost: int = 0 - for h in world.hat_craft_order: - cost += world.hat_yarn_costs.get(h) + for h in world.get_hat_craft_order(): + cost += world.get_hat_yarn_costs().get(h) if h == hat: break @@ -218,8 +218,11 @@ def set_rules(world: World): # Not all locations in Alpine can be reached from The Illness has Spread # as many of the ziplines are blocked off - if data.region == "Alpine Skyline Area" and key not in tihs_locations: - add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + if data.region == "Alpine Skyline Area": + if key not in tihs_locations: + add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + else: + add_rule(location, lambda state: can_use_hookshot(state, w)) if data.region == "The Birdhouse" or data.region == "The Lava Cake" \ or data.region == "The Windmill" or data.region == "The Twilight Bell": diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index b3d78ad4b6..caa30a8775 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -16,6 +16,10 @@ from worlds.AutoWorld import World from .Rules import set_rules import typing +hat_craft_order: typing.Dict[int, typing.List[HatType]] = {} +hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} +chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {} + class HatInTimeWorld(World): """ @@ -30,10 +34,6 @@ class HatInTimeWorld(World): location_name_to_id = get_location_names() option_definitions = ahit_options - - hat_craft_order: typing.List[HatType] - hat_yarn_costs: typing.Dict[HatType, int] - chapter_timepiece_costs: typing.Dict[ChapterIndex, int] act_connections: typing.Dict[str, str] = {} nyakuza_thug_items: typing.Dict[str, int] = {} shop_locs: typing.List[str] = [] @@ -74,11 +74,11 @@ class HatInTimeWorld(World): self.player).progress_type = LocationProgressType.EXCLUDED def create_items(self): - self.hat_yarn_costs = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, - HatType.DWELLER: -1, HatType.TIME_STOP: -1} + hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} - self.hat_craft_order = [HatType.SPRINT, HatType.BREWING, HatType.ICE, - HatType.DWELLER, HatType.TIME_STOP] + hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] self.topology_present = self.multiworld.ActRandomizer[self.player].value @@ -94,7 +94,7 @@ class HatInTimeWorld(World): itempool += yarn_pool if self.multiworld.RandomizeHatOrder[self.player].value > 0: - self.multiworld.random.shuffle(self.hat_craft_order) + self.multiworld.random.shuffle(hat_craft_order[self.player]) for name in item_table.keys(): if name == "Yarn": @@ -145,13 +145,13 @@ class HatInTimeWorld(World): def set_rules(self): self.act_connections = {} - self.chapter_timepiece_costs = {ChapterIndex.MAFIA: -1, - ChapterIndex.BIRDS: -1, - ChapterIndex.SUBCON: -1, - ChapterIndex.ALPINE: -1, - ChapterIndex.FINALE: -1, - ChapterIndex.CRUISE: -1, - ChapterIndex.METRO: -1} + chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} if self.multiworld.ActRandomizer[self.player].value > 0: randomize_act_entrances(self) @@ -162,23 +162,23 @@ class HatInTimeWorld(World): return create_item(self, name) def fill_slot_data(self) -> dict: - slot_data: dict = {"SprintYarnCost": self.hat_yarn_costs[HatType.SPRINT], - "BrewingYarnCost": self.hat_yarn_costs[HatType.BREWING], - "IceYarnCost": self.hat_yarn_costs[HatType.ICE], - "DwellerYarnCost": self.hat_yarn_costs[HatType.DWELLER], - "TimeStopYarnCost": self.hat_yarn_costs[HatType.TIME_STOP], - "Chapter1Cost": self.chapter_timepiece_costs[ChapterIndex.MAFIA], - "Chapter2Cost": self.chapter_timepiece_costs[ChapterIndex.BIRDS], - "Chapter3Cost": self.chapter_timepiece_costs[ChapterIndex.SUBCON], - "Chapter4Cost": self.chapter_timepiece_costs[ChapterIndex.ALPINE], - "Chapter5Cost": self.chapter_timepiece_costs[ChapterIndex.FINALE], - "Chapter6Cost": self.chapter_timepiece_costs[ChapterIndex.CRUISE], - "Chapter7Cost": self.chapter_timepiece_costs[ChapterIndex.METRO], - "Hat1": int(self.hat_craft_order[0]), - "Hat2": int(self.hat_craft_order[1]), - "Hat3": int(self.hat_craft_order[2]), - "Hat4": int(self.hat_craft_order[3]), - "Hat5": int(self.hat_craft_order[4]), + slot_data: dict = {"SprintYarnCost": hat_yarn_costs[self.player][HatType.SPRINT], + "BrewingYarnCost": hat_yarn_costs[self.player][HatType.BREWING], + "IceYarnCost": hat_yarn_costs[self.player][HatType.ICE], + "DwellerYarnCost": hat_yarn_costs[self.player][HatType.DWELLER], + "TimeStopYarnCost": hat_yarn_costs[self.player][HatType.TIME_STOP], + "Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], + "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], + "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], + "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], + "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], + "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], + "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], + "Hat1": int(hat_craft_order[self.player][0]), + "Hat2": int(hat_craft_order[self.player][1]), + "Hat3": int(hat_craft_order[self.player][2]), + "Hat4": int(hat_craft_order[self.player][3]), + "Hat5": int(hat_craft_order[self.player][4]), "BadgeSellerItemCount": self.badge_seller_count, "SeedNumber": self.multiworld.seed} # For shop prices @@ -228,11 +228,11 @@ class HatInTimeWorld(World): hint_data[self.player] = new_hint_data def write_spoiler_header(self, spoiler_handle: typing.TextIO): - for i in self.chapter_timepiece_costs.keys(): - spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.chapter_timepiece_costs[ChapterIndex(i)])) + for i in self.get_chapter_costs(): + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) - for hat in self.hat_craft_order: - spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) + for hat in hat_craft_order[self.player]: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) def calculate_yarn_costs(self): mw = self.multiworld @@ -243,7 +243,7 @@ class HatInTimeWorld(World): max_cost: int = 0 for i in range(5): cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - self.hat_yarn_costs[HatType(i)] = cost + hat_yarn_costs[self.player][HatType(i)] = cost max_cost += cost available_yarn = mw.YarnAvailable[p].value @@ -256,10 +256,10 @@ class HatInTimeWorld(World): mw.YarnAvailable[p].value += (max_cost + 8) - available_yarn def set_chapter_cost(self, chapter: ChapterIndex, cost: int): - self.chapter_timepiece_costs[chapter] = cost + chapter_timepiece_costs[self.player][chapter] = cost def get_chapter_cost(self, chapter: ChapterIndex) -> int: - return self.chapter_timepiece_costs.get(chapter) + return chapter_timepiece_costs[self.player].get(chapter) # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game def update_chapter_act_info(self, original_region: Region, new_region: Region): @@ -274,3 +274,12 @@ class HatInTimeWorld(World): for name in chapter_act_info.keys(): if chapter_act_info[name] == key: return name + + def get_hat_craft_order(self): + return hat_craft_order[self.player] + + def get_hat_yarn_costs(self): + return hat_yarn_costs[self.player] + + def get_chapter_costs(self): + return chapter_timepiece_costs[self.player] From 5c49ccabbe9b9d053102f2bb0b9f6ed36e028e95 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 19:05:41 -0400 Subject: [PATCH 004/143] a --- worlds/ahit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index caa30a8775..ff2fe53284 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -61,6 +61,7 @@ class HatInTimeWorld(World): def create_regions(self): self.nyakuza_thug_items = {} self.shop_locs = [] + self.badge_seller_count = 0 create_regions(self) # place default contract locations if contract shuffle is off so logic can still utilize them From 6442c40900c3b1478fa03c1745bf8b81e818a672 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 19:08:04 -0400 Subject: [PATCH 005/143] b --- worlds/ahit/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index ff2fe53284..ef4b9fc3fe 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -118,6 +118,9 @@ class HatInTimeWorld(World): and self.multiworld.ShuffleSubconPaintings[self.player].value == 0: continue + if self.multiworld.StartWithCompassBadge[self.player].value > 0 and name == "Compass Badge": + continue + if name == "Time Piece": tp_count: int = 40 max_extra: int = 0 From 906c41275efca1b680881e99b150a77268ea8d18 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 20:40:33 -0400 Subject: [PATCH 006/143] Even more fixes --- worlds/ahit/Rules.py | 86 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 04470690fe..3a420c943e 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -274,7 +274,7 @@ def set_rules(world: World): for act in acts: act_entrance: Entrance = mw.get_entrance(act, p) - + access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region name: str = format("%s: Connection %i" % (key, i)) new_entrance: Entrance = connect_regions(required_region, region, name, p) @@ -690,127 +690,127 @@ def connect_regions(start_region: Region, exit_region: Region, entrancename: str def set_painting_rules(world: World): add_rule(world.multiworld.get_location("Subcon Village - Snatcher Statue Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Gravestone", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Near Well", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree A", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree B", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Ice Wall", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Ice Cube Shack", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Well - On Pipe", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Cellar", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Bedroom Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Hall Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Chandelier", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Subcon Forest - Burning House", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Burning Tree Climb", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Burning Stump Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Burning Forest Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage A", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage B", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Triple Spider Bounce", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Infinite Yarn Bush", world.player), - lambda state: state.has("Progressive Painting Unlock", 2)) + lambda state: state.has("Progressive Painting Unlock", world.player, 2)) add_rule(world.multiworld.get_location("Subcon Forest - Dweller Stump", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree A", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Giant Time Piece", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Gallows", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Dweller Shack", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), - lambda state: state.has("Progressive Painting Unlock", 3)) + lambda state: state.has("Progressive Painting Unlock", world.player, 3)) add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Act Completion (The Subcon Well)", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) add_rule(world.multiworld.get_location("Act Completion (Queen Vanessa's Manor)", world.player), - lambda state: state.has("Progressive Painting Unlock", 1)) + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) From 1fd2a21e5ba1676cba0741604fb20ca3e4e878f3 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 28 Aug 2023 22:19:57 -0400 Subject: [PATCH 007/143] New option - NoFreeRoamFinale --- worlds/ahit/Options.py | 10 ++++++++-- worlds/ahit/Regions.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 772fa08aef..8044013b22 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -81,6 +81,11 @@ class VanillaAlpine(Choice): default = 0 +class NoFreeRoamFinale(Toggle): + """If enabled, prevent Free Roam acts from being shuffled onto chapter finales.""" + default = 1 + + class LogicDifficulty(Choice): """Choose the difficulty setting for logic. Note that Hard or above will force SDJ logic on.""" display_name = "Logic Difficulty" @@ -264,7 +269,7 @@ class ChapterCostMinDifference(Range): class LowestChapterCost(Range): """Value determining the lowest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for Chapter 5).""" + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" display_name = "Lowest Possible Chapter Cost" range_start = 0 range_end = 10 @@ -273,7 +278,7 @@ class LowestChapterCost(Range): class HighestChapterCost(Range): """Value determining the highest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for Chapter 5).""" + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" display_name = "Highest Possible Chapter Cost" range_start = 15 range_end = 45 @@ -432,6 +437,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "ActRandomizer": ActRandomizer, "ShuffleAlpineZiplines": ShuffleAlpineZiplines, "VanillaAlpine": VanillaAlpine, + "NoFreeRoamFinale": NoFreeRoamFinale, "LogicDifficulty": LogicDifficulty, "RandomizeHatOrder": RandomizeHatOrder, "UmbrellaLogic": UmbrellaLogic, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index f7c74e4f94..a6dda2b368 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -237,6 +237,14 @@ purple_time_rifts = [ "Time Rift - Rumbi Factory", ] +chapter_finales = [ + "Dead Bird Studio Basement", + "Your Contract has Expired", + "The Illness has Spread", + "Rock the Boat", + "Rush Hour", +] + # Acts blacklisted in act shuffle # entrance: region blacklisted_acts = { @@ -473,6 +481,11 @@ def randomize_act_entrances(world: World): candidate_list.append(region) break + if world.multiworld.NoFreeRoamFinale[world.player].value > 0 and "Free Roam" in candidate.name: + # CTR entrance isn't a finale, but has a fuck ton of unlock requirements + if region.name in chapter_finales or region.name == "Cheating the Race": + continue + if region.name == "Rush Hour": if world.multiworld.EndGoal[world.player].value == 2 or \ world.multiworld.VanillaMetro[world.player].value == 2: From b5475ac24290cc397e2e8ab2c7abb9fa4a853a1b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 12:38:29 -0400 Subject: [PATCH 008/143] a --- worlds/ahit/Items.py | 3 +++ worlds/ahit/Options.py | 6 ++++++ worlds/ahit/Regions.py | 13 +++++++------ worlds/ahit/Rules.py | 6 +++--- worlds/ahit/__init__.py | 3 ++- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 7c9b4297d3..fc6d82b1af 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -34,6 +34,9 @@ def get_total_time_pieces(world: World) -> int: if world.multiworld.EnableDLC1[world.player].value > 0: count += 6 + if world.multiworld.EnableDLC2[world.player].value > 0: + count += 10 + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 8044013b22..77daad0e2d 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -33,9 +33,15 @@ def adjust_options(world: World): if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + if world.multiworld.LowestChapterCost[world.player].value > total_tps-5: + world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5) + if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + if world.multiworld.FinalChapterMinCost[world.player].value > total_tps: + world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5) + # Don't allow Rush Hour goal if DLC2 content is disabled if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: world.multiworld.EndGoal[world.player].value = 1 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index a6dda2b368..260c31a596 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -152,7 +152,7 @@ rift_access_regions = { "Time Rift - Tour": ["Time's End"], "Time Rift - Balcony": ["Cruise Ship"], - "Time Rift - Deep Sea": ["Cruise Ship"], + "Time Rift - Deep Sea": ["Bon Voyage!"], "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], } @@ -481,11 +481,6 @@ def randomize_act_entrances(world: World): candidate_list.append(region) break - if world.multiworld.NoFreeRoamFinale[world.player].value > 0 and "Free Roam" in candidate.name: - # CTR entrance isn't a finale, but has a fuck ton of unlock requirements - if region.name in chapter_finales or region.name == "Cheating the Race": - continue - if region.name == "Rush Hour": if world.multiworld.EndGoal[world.player].value == 2 or \ world.multiworld.VanillaMetro[world.player].value == 2: @@ -537,6 +532,12 @@ def randomize_act_entrances(world: World): if region.name == "Rush Hour" and candidate.name == "Nyakuza Free Roam": continue + # CTR entrance and Tour aren't a finale, but have a fuck ton of unlock requirements + if world.multiworld.NoFreeRoamFinale[world.player].value > 0 and "Free Roam" in candidate.name: + if region.name in chapter_finales or region.name == "Cheating the Race" \ + or world.multiworld.EndGoal[world.player].value == 1 and region.name == "Time Rift - Tour": + continue + if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: continue diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 3a420c943e..83d0f19882 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -32,17 +32,17 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: - return get_remaining_hat_cost(state, world, hat) <= 0 + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) -def get_remaining_hat_cost(state: CollectionState, world: World, hat: HatType) -> int: +def get_hat_cost(world: World, hat: HatType) -> int: cost: int = 0 for h in world.get_hat_craft_order(): cost += world.get_hat_yarn_costs().get(h) if h == hat: break - return max(cost - state.count("Yarn", world.player), 0) + return cost def can_sdj(state: CollectionState, world: World): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index ef4b9fc3fe..5dc5e48356 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -48,7 +48,8 @@ class HatInTimeWorld(World): start_chapter: int = self.multiworld.StartingChapter[self.player].value if start_chapter == 4 or start_chapter == 3: - if self.multiworld.ActRandomizer[self.player].value == 0: + if self.multiworld.ActRandomizer[self.player].value == 0 \ + or self.multiworld.VanillaAlpine[self.player].value > 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) From 6d9bfbcf367f0cd6d52f43538abd67ffdee59052 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 13:12:47 -0400 Subject: [PATCH 009/143] Hat Logic Fix --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 83d0f19882..8ff4002a8d 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -230,7 +230,7 @@ def set_rules(world: World): for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state: can_use_hat(state, w, hat)) + add_rule(location, lambda state, hat=hat: can_use_hat(state, w, hat)) if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, w)) From 149a42b175b583daa296b08a16a002bf7b9fd8b7 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 14:30:59 -0400 Subject: [PATCH 010/143] Just to be safe --- worlds/ahit/Locations.py | 100 +++--- worlds/ahit/Regions.py | 6 +- worlds/ahit/Rules.py | 698 ++++++++++++++++----------------------- worlds/ahit/__init__.py | 23 +- 4 files changed, 359 insertions(+), 468 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index eb242c96b2..c2bfe7a13b 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -11,6 +11,7 @@ class LocData(NamedTuple): required_hats: Optional[List[HatType]] = [HatType.NONE] hookshot: Optional[bool] = False dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle # For UmbrellaLogic setting umbrella: Optional[bool] = False # Umbrella required for this check @@ -69,13 +70,17 @@ def is_location_valid(world: World, location: str) -> bool: def get_location_names() -> Dict[str, int]: names = {name: data.id for name, data in location_table.items()} - id_start: int = 300204 + id_start: int = get_tasksanity_start_id() for i in range(TasksanityCheckCount.range_end): names.setdefault(format("Tasksanity Check %i") % (i+1), id_start+i) return names +def get_tasksanity_start_id() -> int: + return 300204 + + ahit_locations = { "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", dweller_bell=1), @@ -166,66 +171,75 @@ ahit_locations = { "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), "Subcon Village - House Top": LocData(325471, "Subcon Forest Area"), "Subcon Village - Ice Cube House": LocData(325469, "Subcon Forest Area"), - "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), - "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", required_hats=[HatType.BREWING],), + "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=1), - "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area"), - "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area"), - "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area"), - "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area"), - "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area"), - "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area"), + "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), - "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", required_hats=[HatType.DWELLER]), + "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", required_hats=[HatType.DWELLER]), + "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area"), + "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), - "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", required_hats=[HatType.DWELLER]), + "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area"), - "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area"), + "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), - "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area"), + "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), - "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", required_hats=[HatType.DWELLER]), + "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", required_hats=[HatType.DWELLER], - hookshot=True), + hookshot=True, + paintings=3), - "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area"), - "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area"), - "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area"), - "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area"), - "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area"), - "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area"), - "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area"), - "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True), - "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area"), + "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), + "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), - "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", required_hats=[HatType.DWELLER]), + "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=2), "Subcon Forest - Boss Arena Chest": LocData(323735, "Subcon Forest Area"), - "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2), + "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2, paintings=1), - "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", required_hats=[HatType.BREWING]), + "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=2), - "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", required_hats=[HatType.BREWING]), + "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=3), - "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1), - "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1), - "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1), - "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1), + "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1, paintings=1), + "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1, paintings=1), - "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2), - "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2), - "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2), - "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2), + "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), # 330000 range - Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), @@ -328,10 +342,10 @@ act_completions = { "Act Completion (Time Rift - The Moon)": LocData(312785, "Time Rift - The Moon"), "Act Completion (Time Rift - Dead Bird Studio)": LocData(312577, "Time Rift - Dead Bird Studio"), - "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations"), - "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True), - "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True), - "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True), + "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations", paintings=1), + "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), + "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True, paintings=1), + "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), "Act Completion (Mail Delivery Service)": LocData(312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), "Act Completion (Your Contract has Expired)": LocData(311390, "Your Contract has Expired", umbrella=True), "Act Completion (Time Rift - Pipe)": LocData(313069, "Time Rift - Pipe", hookshot=True), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 260c31a596..05bb706dff 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,6 +1,7 @@ from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location -from .Locations import HatInTimeLocation, location_table, storybook_pages, event_locs, is_location_valid, shop_locations +from .Locations import HatInTimeLocation, location_table, storybook_pages, event_locs, is_location_valid, \ + shop_locations, get_tasksanity_start_id from .Items import HatInTimeItem from .Types import ChapterIndex import typing @@ -413,11 +414,10 @@ def create_rift_connections(world: World, region: Region): def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) - id_start: int = 300204 + id_start: int = get_tasksanity_start_id() for i in range(world.multiworld.TasksanityCheckCount[world.player].value): location = HatInTimeLocation(world.player, format("Tasksanity Check %i" % (i+1)), id_start+i, ship_shape) ship_shape.locations.append(location) - # world.location_name_to_id.setdefault(location.name, location.address) def randomize_act_entrances(world: World): diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 8ff4002a8d..91568e5af5 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -32,17 +32,7 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: - return state.count("Yarn", world.player) >= get_hat_cost(world, hat) - - -def get_hat_cost(world: World, hat: HatType) -> int: - cost: int = 0 - for h in world.get_hat_craft_order(): - cost += world.get_hat_yarn_costs().get(h) - if h == hat: - break - - return cost + return state.has("Yarn", world.player, world.get_hat_yarn_costs().get(hat)) def can_sdj(state: CollectionState, world: World): @@ -98,23 +88,19 @@ def can_clear_metro(state: CollectionState, world: World) -> bool: def set_rules(world: World): - w = world - mw = world.multiworld - p = world.player - - dlc1: bool = bool(mw.EnableDLC1[p].value > 0) - dlc2: bool = bool(mw.EnableDLC2[p].value > 0) + dlc1: bool = bool(world.multiworld.EnableDLC1[world.player].value > 0) + dlc2: bool = bool(world.multiworld.EnableDLC2[world.player].value > 0) # First, chapter access - starting_chapter = ChapterIndex(mw.StartingChapter[p].value) - w.set_chapter_cost(starting_chapter, 0) + starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) + world.set_chapter_cost(starting_chapter, 0) # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, ChapterIndex.SUBCON, ChapterIndex.ALPINE] final_chapter = ChapterIndex.FINALE - if mw.EndGoal[p].value == 2: + if world.multiworld.EndGoal[world.player].value == 2: final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) @@ -125,7 +111,7 @@ def set_rules(world: World): chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) - mw.random.shuffle(chapter_list) + world.multiworld.random.shuffle(chapter_list) if starting_chapter is not ChapterIndex.ALPINE and dlc1 or dlc2: index1: int = 69 @@ -143,15 +129,15 @@ def set_rules(world: World): if lowest_index == 0: pos = 0 else: - pos = mw.random.randint(0, lowest_index) + pos = world.multiworld.random.randint(0, lowest_index) chapter_list.insert(pos, ChapterIndex.ALPINE) - lowest_cost: int = mw.LowestChapterCost[p].value - highest_cost: int = mw.HighestChapterCost[p].value + lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value + highest_cost: int = world.multiworld.HighestChapterCost[world.player].value - cost_increment: int = mw.ChapterCostIncrement[p].value - min_difference: int = mw.ChapterCostMinDifference[p].value + cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value + min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value last_cost: int = 0 cost: int loop_count: int = 0 @@ -161,109 +147,112 @@ def set_rules(world: World): if min_range >= highest_cost: min_range = highest_cost-1 - value: int = mw.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) + value: int = world.multiworld.random.randint(min_range, min(highest_cost, + max(lowest_cost, last_cost + cost_increment))) - cost = mw.random.randint(value, min(value + cost_increment, highest_cost)) + cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost)) if loop_count >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference cost = min(cost, highest_cost) - w.set_chapter_cost(chapter, cost) + world.set_chapter_cost(chapter, cost) last_cost = cost loop_count += 1 - w.set_chapter_cost(final_chapter, mw.random.randint(mw.FinalChapterMinCost[p].value, - mw.FinalChapterMaxCost[p].value)) + world.set_chapter_cost(final_chapter, world.multiworld.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) - add_rule(mw.get_entrance("Telescope -> Mafia Town", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.MAFIA))) + add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) - add_rule(mw.get_entrance("Telescope -> Battle of the Birds", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) - add_rule(mw.get_entrance("Telescope -> Subcon Forest", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.SUBCON))) + add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON))) - add_rule(mw.get_entrance("Telescope -> Alpine Skyline", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) - add_rule(mw.get_entrance("Telescope -> Time's End", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.FINALE)) - and can_use_hat(state, w, HatType.BREWING) and can_use_hat(state, w, HatType.DWELLER)) + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) + and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) if dlc1: - add_rule(mw.get_entrance("Telescope -> The Arctic Cruise", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE)) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.CRUISE))) + add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) if dlc2: - add_rule(mw.get_entrance("Telescope -> Nyakuza Metro", p), - lambda state: state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE)) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.METRO)) - and can_use_hat(state, w, HatType.DWELLER) and can_use_hat(state, w, HatType.ICE)) + add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) + and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) - if mw.ActRandomizer[p].value == 0: - set_default_rift_rules(w) + if world.multiworld.ActRandomizer[world.player].value == 0: + set_default_rift_rules(world) location: Location for (key, data) in location_table.items(): - if not is_location_valid(w, key): + if not is_location_valid(world, key): continue if key in contract_locations.keys(): continue - location = mw.get_location(key, p) + location = world.multiworld.get_location(key, world.player) # Not all locations in Alpine can be reached from The Illness has Spread # as many of the ziplines are blocked off if data.region == "Alpine Skyline Area": if key not in tihs_locations: - add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", world.player), "and") else: - add_rule(location, lambda state: can_use_hookshot(state, w)) + add_rule(location, lambda state: can_use_hookshot(state, world)) if data.region == "The Birdhouse" or data.region == "The Lava Cake" \ or data.region == "The Windmill" or data.region == "The Twilight Bell": - add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", p), "and") + add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", world.player), "and") for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state, hat=hat: can_use_hat(state, w, hat)) + add_rule(location, lambda state, required_hat=hat: can_use_hat(state, world, required_hat)) if data.hookshot: - add_rule(location, lambda state: can_use_hookshot(state, w)) + add_rule(location, lambda state: can_use_hookshot(state, world)) - if data.umbrella and mw.UmbrellaLogic[p].value > 0: - add_rule(location, lambda state: state.has("Umbrella", p)) + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(location, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(location, lambda state, paintings=data.paintings: + state.count("Progressive Painting Unlock", world.player) >= paintings) if data.dweller_bell > 0: if data.dweller_bell == 1: # Required to be hit regardless of Dweller Mask - add_rule(location, lambda state: can_hit_bells(state, w)) + add_rule(location, lambda state: can_hit_bells(state, world)) else: # Can bypass with Dweller Mask - add_rule(location, lambda state: can_hit_bells(state, w) or can_use_hat(state, w, HatType.DWELLER)) + add_rule(location, lambda state: can_hit_bells(state, world) or can_use_hat(state, world, HatType.DWELLER)) - set_specific_rules(w) + set_specific_rules(world) - if mw.LogicDifficulty[p].value >= 1: - mw.SDJLogic[p].value = 1 + if world.multiworld.LogicDifficulty[world.player].value >= 1: + world.multiworld.SDJLogic[world.player].value = 1 - if mw.SDJLogic[p].value > 0: + if world.multiworld.SDJLogic[world.player].value > 0: set_sdj_rules(world) - if mw.ShuffleAlpineZiplines[p].value > 0: - set_alps_zipline_rules(w) - - if mw.ShuffleSubconPaintings[p].value > 0: - set_painting_rules(w) + if world.multiworld.ShuffleAlpineZiplines[world.player].value > 0: + set_alps_zipline_rules(world) for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not dlc1: continue i: int = 1 - entrance: Entrance = mw.get_entrance(key, p) + entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] entrance.parent_region.exits.remove(entrance) @@ -273,18 +262,18 @@ def set_rules(world: World): entrances: typing.List[Entrance] = [] for act in acts: - act_entrance: Entrance = mw.get_entrance(act, p) + act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region name: str = format("%s: Connection %i" % (key, i)) - new_entrance: Entrance = connect_regions(required_region, region, name, p) + new_entrance: Entrance = connect_regions(required_region, region, name, world.player) entrances.append(new_entrance) # Copy access rules from act completions if "Free Roam" not in required_region.name: rule: typing.Callable[[CollectionState], bool] name = format("Act Completion (%s)" % required_region.name) - rule = mw.get_location(name, p).access_rule + rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) i += 1 @@ -293,186 +282,193 @@ def set_rules(world: World): for rules in access_rules: add_rule(e, rules) - for entrance in mw.get_region("Alpine Free Roam", p).entrances: - add_rule(entrance, lambda state: can_use_hookshot(state, w)) - if mw.UmbrellaLogic[p].value > 0: - add_rule(entrance, lambda state: state.has("Umbrella", p)) + for entrance in world.multiworld.get_region("Alpine Free Roam", world.player).entrances: + add_rule(entrance, lambda state: can_use_hookshot(state, world)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Umbrella", world.player)) - if mw.EndGoal[p].value == 1: - mw.completion_condition[p] = lambda state: state.has("Time Piece Cluster", p) - elif mw.EndGoal[p].value == 2: - mw.completion_condition[p] = lambda state: state.has("Rush Hour Cleared", p) + if world.multiworld.EndGoal[world.player].value == 1: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) + elif world.multiworld.EndGoal[world.player].value == 2: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) def set_specific_rules(world: World): - mw = world.multiworld - w = world - p = world.player - dlc1: bool = bool(mw.EnableDLC1[p].value > 0) + dlc1: bool = bool(world.multiworld.EnableDLC1[world.player].value > 0) - add_rule(mw.get_entrance("Alpine Skyline - Finale", p), - lambda state: can_clear_alpine(state, w)) + add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), + lambda state: can_clear_alpine(state, world)) - add_rule(mw.get_location("Mafia Boss Shop Item", p), - lambda state: state.has("Time Piece", p, 12) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), + lambda state: state.has("Time Piece", world.player, 12) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) - add_rule(mw.get_location("Spaceship - Rumbi Abuse", p), - lambda state: state.has("Time Piece", p, 4)) + add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), + lambda state: state.has("Time Piece", world.player, 4)) # Normal logic - if mw.LogicDifficulty[p].value == 0: - add_rule(mw.get_entrance("-> The Birdhouse", p), - lambda state: can_use_hat(state, w, HatType.BREWING)) + if world.multiworld.LogicDifficulty[world.player].value == 0: + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) - add_rule(mw.get_location("Alpine Skyline - Yellow Band Hills", p), - lambda state: can_use_hat(state, w, HatType.BREWING)) + add_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) if dlc1: - add_rule(mw.get_location("Act Completion (Time Rift - Deep Sea)", p), - lambda state: can_use_hat(state, w, HatType.DWELLER)) + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) - add_rule(mw.get_location("Rock the Boat - Post Captain Rescue", p), - lambda state: can_use_hat(state, w, HatType.ICE)) + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) - add_rule(mw.get_location("Act Completion (Rock the Boat)", p), - lambda state: can_use_hat(state, w, HatType.ICE)) + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) # Hard logic, includes SDJ stuff - if mw.LogicDifficulty[p].value >= 1: - add_rule(mw.get_location("Act Completion (Time Rift - The Twilight Bell)", p), - lambda state: can_use_hat(state, w, HatType.SPRINT) and state.has("Scooter Badge", p), "or") + if world.multiworld.LogicDifficulty[world.player].value >= 1: + add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") # Expert logic - if mw.LogicDifficulty[p].value >= 2: - set_rule(mw.get_location("Alpine Skyline - The Twilight Path", p), lambda state: True) + if world.multiworld.LogicDifficulty[world.player].value >= 2: + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) else: - add_rule(mw.get_entrance("-> The Twilight Bell", p), - lambda state: can_use_hat(state, w, HatType.DWELLER)) + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) - add_rule(mw.get_location("Mafia Town - Behind HQ Chest", p), - lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", p) - or state.can_reach("Down with the Mafia!", "Region", p) - or state.can_reach("Cheating the Race", "Region", p) - or state.can_reach("The Golden Vault", "Region", p)) + add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) # Old guys don't appear in SCFOS - add_rule(mw.get_location("Mafia Town - Old Man (Steel Beams)", p), - lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) - or state.can_reach("Barrel Battle", "Region", p) - or state.can_reach("Cheating the Race", "Region", p) - or state.can_reach("The Golden Vault", "Region", p) - or state.can_reach("Down with the Mafia!", "Region", p)) + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) - add_rule(mw.get_location("Mafia Town - Old Man (Seaside Spaghetti)", p), - lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) - or state.can_reach("Barrel Battle", "Region", p) - or state.can_reach("Cheating the Race", "Region", p) - or state.can_reach("The Golden Vault", "Region", p) - or state.can_reach("Down with the Mafia!", "Region", p)) + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) # Only available outside She Came from Outer Space - add_rule(mw.get_location("Mafia Town - Mafia Geek Platform", p), - lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) - or state.can_reach("Barrel Battle", "Region", p) - or state.can_reach("Down with the Mafia!", "Region", p) - or state.can_reach("Cheating the Race", "Region", p) - or state.can_reach("Heating Up Mafia Town", "Region", p) - or state.can_reach("The Golden Vault", "Region", p)) + add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) # Only available outside Down with the Mafia! (for some reason) - add_rule(mw.get_location("Mafia Town - On Scaffolding", p), - lambda state: state.can_reach("Welcome to Mafia Town", "Region", p) - or state.can_reach("Barrel Battle", "Region", p) - or state.can_reach("She Came from Outer Space", "Region", p) - or state.can_reach("Cheating the Race", "Region", p) - or state.can_reach("Heating Up Mafia Town", "Region", p) - or state.can_reach("The Golden Vault", "Region", p)) + add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("She Came from Outer Space", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) # For some reason, the brewing crate is removed in HUMT - set_rule(mw.get_location("Mafia Town - Secret Cave", p), - lambda state: state.can_reach("Heating Up Mafia Town", "Region", p) - or can_use_hat(state, w, HatType.BREWING)) + set_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), + lambda state: state.can_reach("Heating Up Mafia Town", "Region", world.player) + or can_use_hat(state, world, HatType.BREWING)) - # Can bounce across the lava to get this without Hookshot (need to die though :P) - set_rule(mw.get_location("Mafia Town - Above Boats", p), - lambda state: state.can_reach("Heating Up Mafia Town", "Region", p) - or can_use_hookshot(state, w)) + # Can bounce across the lava to get this without Hookshot (need to die though :world.player) + set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: state.can_reach("Heating Up Mafia Town", "Region", world.player) + or can_use_hookshot(state, world)) - set_rule(mw.get_location("Act Completion (Cheating the Race)", p), - lambda state: can_use_hat(state, w, HatType.TIME_STOP) - or mw.CTRWithSprint[p].value > 0 and can_use_hat(state, w, HatType.SPRINT)) + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.TIME_STOP) + or world.multiworld.CTRWithSprint[world.player].value > 0 and can_use_hat(state, world, HatType.SPRINT)) - set_rule(mw.get_location("Subcon Forest - Boss Arena Chest", p), - lambda state: state.can_reach("Toilet of Doom", "Region", p) - and (mw.ShuffleSubconPaintings[p].value == 0 or state.has("Progressive Painting Unlock", p, 1)) - or state.can_reach("Your Contract has Expired", "Region", p)) + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: state.can_reach("Toilet of Doom", "Region", world.player) + and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 + or state.has("Progressive Painting Unlock", world.player, 1)) + or state.can_reach("Your Contract has Expired", "Region", world.player)) - if mw.UmbrellaLogic[p].value > 0: - add_rule(mw.get_location("Act Completion (Toilet of Doom)", p), - lambda state: state.has("Umbrella", p) or can_use_hat(state, w, HatType.BREWING)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) - set_rule(mw.get_location("Act Completion (Time Rift - Village)", p), - lambda state: can_use_hat(state, w, HatType.BREWING) or state.has("Umbrella", p) - or can_use_hat(state, w, HatType.DWELLER)) + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.DWELLER)) - add_rule(mw.get_entrance("Subcon Forest - Act 2", p), - lambda state: state.has("Snatcher's Contract - The Subcon Well", p)) + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player), + lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player)) - add_rule(mw.get_entrance("Subcon Forest - Act 3", p), - lambda state: state.has("Snatcher's Contract - Toilet of Doom", p)) + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player), + lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player)) - add_rule(mw.get_entrance("Subcon Forest - Act 4", p), - lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", p)) + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player), + lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player)) - add_rule(mw.get_entrance("Subcon Forest - Act 5", p), - lambda state: state.has("Snatcher's Contract - Mail Delivery Service", p)) + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player), + lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) - if mw.ShuffleSubconPaintings[p].value > 0: + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: for key in contract_locations: if key == "Snatcher's Contract - The Subcon Well": continue - add_rule(mw.get_location(key, p), - lambda state: state.has("Progressive Painting Unlock", p, 1)) + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - add_rule(mw.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", p), - lambda state: can_use_hat(state, w, HatType.SPRINT) or can_use_hat(state, w, HatType.TIME_STOP)) + add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) - if mw.EnableDLC1[p].value > 0: - add_rule(mw.get_entrance("Cruise Ship Entrance BV", p), lambda state: can_use_hookshot(state, w)) + if world.multiworld.EnableDLC1[world.player].value > 0: + add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), + lambda state: can_use_hookshot(state, world)) # This particular item isn't present in Act 3 for some reason, yes in vanilla too - add_rule(mw.get_location("The Arctic Cruise - Toilet", p), - lambda state: state.can_reach("Bon Voyage!", "Region", p) - or state.can_reach("Ship Shape", "Region", p)) + add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), + lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + or state.can_reach("Ship Shape", "Region", world.player)) - if mw.EnableDLC2[p].value > 0: - add_rule(mw.get_entrance("-> Bluefin Tunnel", p), - lambda state: state.has("Metro Ticket - Green", p) or state.has("Metro Ticket - Blue", p)) + if world.multiworld.EnableDLC2[world.player].value > 0: + add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), + lambda state: state.has("Metro Ticket - Green", world.player) + or state.has("Metro Ticket - Blue", world.player)) - add_rule(mw.get_entrance("-> Pink Paw Station", p), - lambda state: state.has("Metro Ticket - Pink", p) - or state.has("Metro Ticket - Yellow", p) and state.has("Metro Ticket - Blue", p)) + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) - add_rule(mw.get_entrance("Nyakuza Metro - Finale", p), - lambda state: can_clear_metro(state, w)) + add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), + lambda state: can_clear_metro(state, world)) - add_rule(mw.get_location("Act Completion (Rush Hour)", p), - lambda state: state.has("Metro Ticket - Yellow", p) and state.has("Metro Ticket - Blue", p) - and state.has("Metro Ticket - Pink", p)) + add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) for key in shop_locations.keys(): - if "Green Clean Station Thug B" in key and is_location_valid(w, key): - add_rule(mw.get_location(key, p), lambda state: state.has("Metro Ticket - Yellow", p), "or") + if "Green Clean Station Thug B" in key and is_location_valid(world, key): + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player), "or") def set_sdj_rules(world: World): add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: can_sdj(state, world), "or") + lambda state: can_sdj(state, world) + and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 + or state.count("Progressive Painting Unlock", world.player) >= 2), "or") add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), - lambda state: can_sdj(state, world), "or") + lambda state: can_sdj(state, world) + and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 + or state.count("Progressive Painting Unlock", world.player) >= 3), "or") add_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), lambda state: can_sdj(state, world), "or") @@ -525,160 +521,164 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked # See randomize_act_entrances in Regions.py # Called BEFORE set_rules! def set_rift_rules(world: World, regions: typing.Dict[str, Region]): - w = world - mw = world.multiworld - p = world.player # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. for entrance in regions["Time Rift - Gallery"].entrances: - add_rule(entrance, lambda state: can_use_hat(state, w, HatType.BREWING) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) for entrance in regions["Time Rift - The Lab"].entrances: - add_rule(entrance, lambda state: can_use_hat(state, w, HatType.DWELLER) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) for entrance in regions["Time Rift - Sewers"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Mafia Town - Act 4")) - reg_act_connection(w, mw.get_entrance("Mafia Town - Act 4", p).connected_region, entrance) + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Bazaar"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Mafia Town - Act 6")) - reg_act_connection(w, mw.get_entrance("Mafia Town - Act 6", p).connected_region, entrance) + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Mafia of Cooks"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Burger")) + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) for entrance in regions["Time Rift - The Owl Express"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 2")) - add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 3")) - reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 2", p).connected_region, entrance) - reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 3", p).connected_region, entrance) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3", + world.player).connected_region, entrance) for entrance in regions["Time Rift - The Moon"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 4")) - add_rule(entrance, lambda state: can_clear_act(state, w, "Battle of the Birds - Act 5")) - reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 4", p).connected_region, entrance) - reg_act_connection(w, mw.get_entrance("Battle of the Birds - Act 5", p).connected_region, entrance) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Dead Bird Studio"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Train")) + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) for entrance in regions["Time Rift - Pipe"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Subcon Forest - Act 2")) - reg_act_connection(w, mw.get_entrance("Subcon Forest - Act 2", p).connected_region, entrance) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", + world.player).connected_region, entrance) + + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) for entrance in regions["Time Rift - Village"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "Subcon Forest - Act 4")) - reg_act_connection(w, mw.get_entrance("Subcon Forest - Act 4", p).connected_region, entrance) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", + world.player).connected_region, entrance) + + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) for entrance in regions["Time Rift - Sleepy Subcon"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "UFO")) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 3)) + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 3)) for entrance in regions["Time Rift - Curly Tail Trail"].entrances: - add_rule(entrance, lambda state: state.has("Windmill Cleared", p)) + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) for entrance in regions["Time Rift - The Twilight Bell"].entrances: - add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", p)) + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) for entrance in regions["Time Rift - Alpine Skyline"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Crayon")) + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) - if mw.EnableDLC1[p].value > 0: + if world.multiworld.EnableDLC1[world.player].value > 0: for entrance in regions["Time Rift - Balcony"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "The Arctic Cruise - Finale")) + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) for entrance in regions["Time Rift - Deep Sea"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Cake")) + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) - if mw.EnableDLC2[p].value > 0: + if world.multiworld.EnableDLC2[world.player].value > 0: for entrance in regions["Time Rift - Rumbi Factory"].entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Necklace")) + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) # Basically the same as above, but without the need of the dict since we are just setting defaults # Called if Act Rando is disabled def set_default_rift_rules(world: World): - w = world - mw = world.multiworld - p = world.player - for entrance in mw.get_region("Time Rift - Gallery", p).entrances: - add_rule(entrance, lambda state: can_use_hat(state, w, HatType.BREWING) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.BIRDS))) + for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) - for entrance in mw.get_region("Time Rift - The Lab", p).entrances: - add_rule(entrance, lambda state: can_use_hat(state, w, HatType.DWELLER) - and state.has("Time Piece", p, w.get_chapter_cost(ChapterIndex.ALPINE))) + for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) - for entrance in mw.get_region("Time Rift - Sewers", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) - reg_act_connection(w, "Down with the Mafia!", entrance.name) + reg_act_connection(world, "Down with the Mafia!", entrance.name) - for entrance in mw.get_region("Time Rift - Bazaar", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) - reg_act_connection(w, "Heating Up Mafia Town", entrance.name) + reg_act_connection(world, "Heating Up Mafia Town", entrance.name) - for entrance in mw.get_region("Time Rift - Mafia of Cooks", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Burger")) + for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) - for entrance in mw.get_region("Time Rift - The Owl Express", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) - reg_act_connection(w, "Murder on the Owl Express", entrance.name) - reg_act_connection(w, "Picture Perfect", entrance.name) + reg_act_connection(world, "Murder on the Owl Express", entrance.name) + reg_act_connection(world, "Picture Perfect", entrance.name) - for entrance in mw.get_region("Time Rift - The Moon", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) - reg_act_connection(w, "Train Rush", entrance.name) - reg_act_connection(w, "The Big Parade", entrance.name) + reg_act_connection(world, "Train Rush", entrance.name) + reg_act_connection(world, "The Big Parade", entrance.name) - for entrance in mw.get_region("Time Rift - Dead Bird Studio", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Train")) + for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) - for entrance in mw.get_region("Time Rift - Pipe", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) - reg_act_connection(w, "The Subcon Well", entrance.name) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + reg_act_connection(world, "The Subcon Well", entrance.name) + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - for entrance in mw.get_region("Time Rift - Village", p).entrances: + for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) - reg_act_connection(w, "Queen Vanessa's Manor", entrance.name) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 2)) + reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - for entrance in mw.get_region("Time Rift - Sleepy Subcon", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "UFO")) - if mw.ShuffleSubconPaintings[p].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", p, 3)) + for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - for entrance in mw.get_region("Time Rift - Curly Tail Trail", p).entrances: - add_rule(entrance, lambda state: state.has("Windmill Cleared", p)) + for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) - for entrance in mw.get_region("Time Rift - The Twilight Bell", p).entrances: - add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", p)) + for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) - for entrance in mw.get_region("Time Rift - Alpine Skyline", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Crayon")) + for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) - if mw.EnableDLC1[p].value > 0: - for entrance in mw.get_region("Time Rift - Balcony", p).entrances: - add_rule(entrance, lambda state: can_clear_act(state, w, "The Arctic Cruise - Finale")) + if world.multiworld.EnableDLC1[world.player].value > 0: + for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) - for entrance in mw.get_region("Time Rift - Deep Sea", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Cake")) + for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) - if mw.EnableDLC2[p].value > 0: - for entrance in mw.get_region("Time Rift - Rumbi Factory", p).entrances: - add_rule(entrance, lambda state: has_relic_combo(state, w, "Necklace")) + if world.multiworld.EnableDLC2[world.player].value > 0: + for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: @@ -686,131 +686,3 @@ def connect_regions(start_region: Region, exit_region: Region, entrancename: str start_region.exits.append(entrance) entrance.connect(exit_region) return entrance - - -def set_painting_rules(world: World): - add_rule(world.multiworld.get_location("Subcon Village - Snatcher Statue Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Gravestone", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Near Well", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree A", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree B", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Ice Wall", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Swamp Tree Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Ice Cube Shack", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Well - On Pipe", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Cellar", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Bedroom Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Hall Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Queen Vanessa's Manor - Chandelier", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Subcon Forest - Burning House", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Burning Tree Climb", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Burning Stump Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Burning Forest Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage A", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Spider Bone Cage B", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Triple Spider Bounce", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Infinite Yarn Bush", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 2)) - - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Stump", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree A", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Giant Time Piece", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Gallows", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Shack", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 3)) - - add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Act Completion (The Subcon Well)", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) - - add_rule(world.multiworld.get_location("Act Completion (Queen Vanessa's Manor)", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 5dc5e48356..cd6da357eb 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -8,7 +8,7 @@ from .Regions import create_region, create_regions, connect_regions, randomize_a create_events, chapter_regions, act_chapters from .Locations import HatInTimeLocation, location_table, get_total_locations, contract_locations, is_location_valid, \ - get_location_names + get_location_names, get_tasksanity_start_id from .Types import HatDLC, HatType, ChapterIndex from .Options import ahit_options, slot_data_options, adjust_options @@ -18,6 +18,7 @@ import typing hat_craft_order: typing.Dict[int, typing.List[HatType]] = {} hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} +slot_data_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {} @@ -79,6 +80,9 @@ class HatInTimeWorld(World): hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, HatType.DWELLER: -1, HatType.TIME_STOP: -1} + slot_data_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] @@ -167,11 +171,11 @@ class HatInTimeWorld(World): return create_item(self, name) def fill_slot_data(self) -> dict: - slot_data: dict = {"SprintYarnCost": hat_yarn_costs[self.player][HatType.SPRINT], - "BrewingYarnCost": hat_yarn_costs[self.player][HatType.BREWING], - "IceYarnCost": hat_yarn_costs[self.player][HatType.ICE], - "DwellerYarnCost": hat_yarn_costs[self.player][HatType.DWELLER], - "TimeStopYarnCost": hat_yarn_costs[self.player][HatType.TIME_STOP], + slot_data: dict = {"SprintYarnCost": slot_data_yarn_costs[self.player][HatType.SPRINT], + "BrewingYarnCost": slot_data_yarn_costs[self.player][HatType.BREWING], + "IceYarnCost": slot_data_yarn_costs[self.player][HatType.ICE], + "DwellerYarnCost": slot_data_yarn_costs[self.player][HatType.DWELLER], + "TimeStopYarnCost": slot_data_yarn_costs[self.player][HatType.TIME_STOP], "Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], @@ -203,7 +207,7 @@ class HatInTimeWorld(World): def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): new_hint_data = {} - alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell"] + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] for key, data in location_table.items(): @@ -226,7 +230,7 @@ class HatInTimeWorld(World): if self.multiworld.EnableDLC1[self.player].value > 0 and self.multiworld.Tasksanity[self.player].value > 0: ship_shape_region = self.get_shuffled_region("Ship Shape") - id_start: int = 300204 + id_start: int = get_tasksanity_start_id() for i in range(self.multiworld.TasksanityCheckCount[self.player].value): new_hint_data[id_start+i] = ship_shape_region @@ -248,7 +252,8 @@ class HatInTimeWorld(World): max_cost: int = 0 for i in range(5): cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - hat_yarn_costs[self.player][HatType(i)] = cost + hat_yarn_costs[self.player][HatType(i)] = cost + max_cost + slot_data_yarn_costs[self.player][HatType(i)] = cost max_cost += cost available_yarn = mw.YarnAvailable[p].value From 2b44619a8cae5439f94bc381fad36ff7fc1df094 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 18:00:11 -0400 Subject: [PATCH 011/143] multiworld.random to world.random --- worlds/ahit/Items.py | 6 +++--- worlds/ahit/Regions.py | 10 +++++----- worlds/ahit/Rules.py | 10 +++++----- worlds/ahit/__init__.py | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index fc6d82b1af..699568678c 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -75,12 +75,12 @@ def create_junk_items(world: World, count: int) -> typing.List[Item]: trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value for i in range(count): - if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance: + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: junk_pool += [world.create_item( - world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] else: junk_pool += [world.create_item( - world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] return junk_pool diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 05bb706dff..7102c5c434 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -422,7 +422,7 @@ def create_tasksanity_locations(world: World): def randomize_act_entrances(world: World): region_list: typing.List[Region] = get_act_regions(world) - world.multiworld.random.shuffle(region_list) + world.random.shuffle(region_list) separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) @@ -543,7 +543,7 @@ def randomize_act_entrances(world: World): candidate_list.append(candidate) - candidate: Region = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + candidate: Region = candidate_list[world.random.randint(0, len(candidate_list)-1)] shuffled_list.append(candidate) # Vanilla @@ -635,8 +635,8 @@ def create_badge_seller(world: World) -> Region: max_items: int = 0 if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: - max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, - world.multiworld.BadgeSellerMaxItems[world.player].value) + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) if max_items <= 0: world.badge_seller_count = 0 @@ -731,7 +731,7 @@ def create_thug_shops(world: World): pass if count == -1: - count = world.multiworld.random.randint(min_items, max_items) + count = world.random.randint(min_items, max_items) world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) if count <= 0: continue diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 91568e5af5..73da7b02c8 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -111,7 +111,7 @@ def set_rules(world: World): chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) - world.multiworld.random.shuffle(chapter_list) + world.random.shuffle(chapter_list) if starting_chapter is not ChapterIndex.ALPINE and dlc1 or dlc2: index1: int = 69 @@ -129,7 +129,7 @@ def set_rules(world: World): if lowest_index == 0: pos = 0 else: - pos = world.multiworld.random.randint(0, lowest_index) + pos = world.random.randint(0, lowest_index) chapter_list.insert(pos, ChapterIndex.ALPINE) @@ -147,10 +147,10 @@ def set_rules(world: World): if min_range >= highest_cost: min_range = highest_cost-1 - value: int = world.multiworld.random.randint(min_range, min(highest_cost, + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) - cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost)) + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) if loop_count >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference @@ -160,7 +160,7 @@ def set_rules(world: World): last_cost = cost loop_count += 1 - world.set_chapter_cost(final_chapter, world.multiworld.random.randint( + world.set_chapter_cost(final_chapter, world.random.randint( world.multiworld.FinalChapterMinCost[world.player].value, world.multiworld.FinalChapterMaxCost[world.player].value)) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index cd6da357eb..02cff1950d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -100,7 +100,7 @@ class HatInTimeWorld(World): itempool += yarn_pool if self.multiworld.RandomizeHatOrder[self.player].value > 0: - self.multiworld.random.shuffle(hat_craft_order[self.player]) + self.random.shuffle(hat_craft_order[self.player]) for name in item_table.keys(): if name == "Yarn": From 0dc211f0e6486e8bbabb3df03fe28f8f44a7e071 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 20:59:44 -0400 Subject: [PATCH 012/143] KeyError fix --- worlds/ahit/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index c2bfe7a13b..633acac040 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -407,7 +407,7 @@ act_completions = { hookshot=True, required_hats=[HatType.ICE, HatType.BREWING]), - "Act Completion (Rumbi Factory)": LocData(312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Act Completion (Time Rift - Rumbi Factory)": LocData(312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), } storybook_pages = { From c0adc9085674df10f36be1d9e7b68536f34710b6 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 21:16:49 -0400 Subject: [PATCH 013/143] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 02d441fee1..27b2230e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -192,3 +192,5 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini +A Hat in Time.yaml +ahit.apworld From e3388261dfb634fdb1a11667b5ddc496576856f9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 21:35:42 -0400 Subject: [PATCH 014/143] Update __init__.py --- worlds/ahit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 02cff1950d..71de2ceb94 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -241,7 +241,7 @@ class HatInTimeWorld(World): spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) for hat in hat_craft_order[self.player]: - spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, slot_data_yarn_costs[self.player][hat])) def calculate_yarn_costs(self): mw = self.multiworld From a7190b6e3c5405b4e8381489be1b53747074ba2b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 22:48:34 -0400 Subject: [PATCH 015/143] Zoinks Scoob --- worlds/ahit/Items.py | 6 +++--- worlds/ahit/Locations.py | 2 +- worlds/ahit/Regions.py | 8 ++++---- worlds/ahit/Rules.py | 12 ++++++------ worlds/ahit/__init__.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 699568678c..fc6d82b1af 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -75,12 +75,12 @@ def create_junk_items(world: World, count: int) -> typing.List[Item]: trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value for i in range(count): - if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: + if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance: junk_pool += [world.create_item( - world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] else: junk_pool += [world.create_item( - world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] return junk_pool diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 633acac040..ad107f9f73 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -159,7 +159,7 @@ ahit_locations = { "Dead Bird Studio Basement - Above Conductor Sign": LocData(305057, "Dead Bird Studio Basement", hookshot=True), "Dead Bird Studio Basement - Logo Wall": LocData(305207, "Dead Bird Studio Basement"), "Dead Bird Studio Basement - Disco Room": LocData(305061, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Small Room": LocData(304813, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(304813, "Dead Bird Studio Basement"), "Dead Bird Studio Basement - Vent Pipe": LocData(305430, "Dead Bird Studio Basement"), "Dead Bird Studio Basement - Tightrope": LocData(305058, "Dead Bird Studio Basement", hookshot=True), "Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 7102c5c434..f4a019ad6d 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -422,7 +422,7 @@ def create_tasksanity_locations(world: World): def randomize_act_entrances(world: World): region_list: typing.List[Region] = get_act_regions(world) - world.random.shuffle(region_list) + world.multiworld.random.shuffle(region_list) separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) @@ -543,7 +543,7 @@ def randomize_act_entrances(world: World): candidate_list.append(candidate) - candidate: Region = candidate_list[world.random.randint(0, len(candidate_list)-1)] + candidate: Region = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] shuffled_list.append(candidate) # Vanilla @@ -635,7 +635,7 @@ def create_badge_seller(world: World) -> Region: max_items: int = 0 if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: - max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, world.multiworld.BadgeSellerMaxItems[world.player].value) if max_items <= 0: @@ -731,7 +731,7 @@ def create_thug_shops(world: World): pass if count == -1: - count = world.random.randint(min_items, max_items) + count = world.multiworld.random.randint(min_items, max_items) world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) if count <= 0: continue diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 73da7b02c8..8934393ab5 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -111,9 +111,9 @@ def set_rules(world: World): chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) - world.random.shuffle(chapter_list) + world.multiworld.random.shuffle(chapter_list) - if starting_chapter is not ChapterIndex.ALPINE and dlc1 or dlc2: + if starting_chapter is not ChapterIndex.ALPINE and (dlc1 or dlc2): index1: int = 69 index2: int = 69 lowest_index: int @@ -129,7 +129,7 @@ def set_rules(world: World): if lowest_index == 0: pos = 0 else: - pos = world.random.randint(0, lowest_index) + pos = world.multiworld.random.randint(0, lowest_index) chapter_list.insert(pos, ChapterIndex.ALPINE) @@ -147,10 +147,10 @@ def set_rules(world: World): if min_range >= highest_cost: min_range = highest_cost-1 - value: int = world.random.randint(min_range, min(highest_cost, + value: int = world.multiworld.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) - cost = world.random.randint(value, min(value + cost_increment, highest_cost)) + cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost)) if loop_count >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference @@ -160,7 +160,7 @@ def set_rules(world: World): last_cost = cost loop_count += 1 - world.set_chapter_cost(final_chapter, world.random.randint( + world.set_chapter_cost(final_chapter, world.multiworld.random.randint( world.multiworld.FinalChapterMinCost[world.player].value, world.multiworld.FinalChapterMaxCost[world.player].value)) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 71de2ceb94..302df057df 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -100,7 +100,7 @@ class HatInTimeWorld(World): itempool += yarn_pool if self.multiworld.RandomizeHatOrder[self.player].value > 0: - self.random.shuffle(hat_craft_order[self.player]) + self.multiworld.random.shuffle(hat_craft_order[self.player]) for name in item_table.keys(): if name == "Yarn": From a3fd86539d7531fee5153fe89ea9f43b1a8dee71 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 29 Aug 2023 23:46:59 -0400 Subject: [PATCH 016/143] ffs --- worlds/ahit/Rules.py | 14 ++++++++++++-- worlds/ahit/__init__.py | 19 +++++++------------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 8934393ab5..c776d24a14 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -32,7 +32,17 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: - return state.has("Yarn", world.player, world.get_hat_yarn_costs().get(hat)) + return state.has("Yarn", world.player, get_hat_cost(world, hat)) + + +def get_hat_cost(world: World, hat: HatType) -> int: + cost: int = 0 + for h in world.get_hat_craft_order(): + cost += world.get_hat_yarn_costs().get(h) + if h == hat: + break + + return cost def can_sdj(state: CollectionState, world: World): @@ -218,7 +228,7 @@ def set_rules(world: World): for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state, required_hat=hat: can_use_hat(state, world, required_hat)) + add_rule(location, lambda state, hat=hat: can_use_hat(state, world, hat)) if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, world)) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 302df057df..858e3ac2d5 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -18,7 +18,6 @@ import typing hat_craft_order: typing.Dict[int, typing.List[HatType]] = {} hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} -slot_data_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {} @@ -80,9 +79,6 @@ class HatInTimeWorld(World): hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, HatType.DWELLER: -1, HatType.TIME_STOP: -1} - slot_data_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, - HatType.DWELLER: -1, HatType.TIME_STOP: -1} - hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] @@ -171,11 +167,11 @@ class HatInTimeWorld(World): return create_item(self, name) def fill_slot_data(self) -> dict: - slot_data: dict = {"SprintYarnCost": slot_data_yarn_costs[self.player][HatType.SPRINT], - "BrewingYarnCost": slot_data_yarn_costs[self.player][HatType.BREWING], - "IceYarnCost": slot_data_yarn_costs[self.player][HatType.ICE], - "DwellerYarnCost": slot_data_yarn_costs[self.player][HatType.DWELLER], - "TimeStopYarnCost": slot_data_yarn_costs[self.player][HatType.TIME_STOP], + slot_data: dict = {"SprintYarnCost": hat_yarn_costs[self.player][HatType.SPRINT], + "BrewingYarnCost": hat_yarn_costs[self.player][HatType.BREWING], + "IceYarnCost": hat_yarn_costs[self.player][HatType.ICE], + "DwellerYarnCost": hat_yarn_costs[self.player][HatType.DWELLER], + "TimeStopYarnCost": hat_yarn_costs[self.player][HatType.TIME_STOP], "Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], @@ -241,7 +237,7 @@ class HatInTimeWorld(World): spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) for hat in hat_craft_order[self.player]: - spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, slot_data_yarn_costs[self.player][hat])) + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) def calculate_yarn_costs(self): mw = self.multiworld @@ -252,8 +248,7 @@ class HatInTimeWorld(World): max_cost: int = 0 for i in range(5): cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - hat_yarn_costs[self.player][HatType(i)] = cost + max_cost - slot_data_yarn_costs[self.player][HatType(i)] = cost + hat_yarn_costs[self.player][HatType(i)] = cost max_cost += cost available_yarn = mw.YarnAvailable[p].value From 949f152ec20f8a1104d3e4cc2473451aa564492d Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 30 Aug 2023 00:20:37 -0400 Subject: [PATCH 017/143] Ruh Roh Raggy, more r-r-r-random bugs! --- worlds/ahit/Locations.py | 1 + worlds/ahit/Regions.py | 14 ++++++++++++++ worlds/ahit/Rules.py | 14 ++++++-------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index ad107f9f73..874cb0edf3 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -646,6 +646,7 @@ tihs_locations = [ ] event_locs = { + "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), "Birdhouse Cleared": LocData(0, "The Birdhouse"), "Lava Cake Cleared": LocData(0, "The Lava Cake"), "Windmill Cleared": LocData(0, "The Windmill"), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index f4a019ad6d..b2e91103d6 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -474,16 +474,30 @@ def randomize_act_entrances(world: World): if world.multiworld.VanillaAlpine[world.player].value > 0 and region.name == "Alpine Free Roam" \ or world.multiworld.VanillaAlpine[world.player].value == 2 and region.name == "The Illness has Spread": + candidate_list.clear() candidate_list.append(region) break + if world.multiworld.VanillaAlpine[world.player].value > 0 and candidate.name == "Alpine Free Roam" \ + or world.multiworld.VanillaAlpine[world.player].value == 2 and candidate.name == "The Illness has Spread": + continue + if world.multiworld.VanillaMetro[world.player].value > 0 and region.name == "Nyakuza Free Roam": + candidate_list.clear() candidate_list.append(region) break + if world.multiworld.VanillaMetro[world.player].value > 0 and candidate.name == "Nyakuza Free Roam": + continue + + if candidate.name == "Rush Hour" and world.multiworld.EndGoal[world.player].value == 2 or \ + world.multiworld.VanillaMetro[world.player].value == 2: + continue + if region.name == "Rush Hour": if world.multiworld.EndGoal[world.player].value == 2 or \ world.multiworld.VanillaMetro[world.player].value == 2: + candidate_list.clear() candidate_list.append(region) break diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index c776d24a14..b4629a0d69 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -32,7 +32,7 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: - return state.has("Yarn", world.player, get_hat_cost(world, hat)) + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) def get_hat_cost(world: World, hat: HatType) -> int: @@ -387,14 +387,12 @@ def set_specific_rules(world: World): or state.can_reach("The Golden Vault", "Region", world.player)) # For some reason, the brewing crate is removed in HUMT - set_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), - lambda state: state.can_reach("Heating Up Mafia Town", "Region", world.player) - or can_use_hat(state, world, HatType.BREWING)) + add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), + lambda state: state.has("HUMT Access", world.player), "or") - # Can bounce across the lava to get this without Hookshot (need to die though :world.player) - set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), - lambda state: state.can_reach("Heating Up Mafia Town", "Region", world.player) - or can_use_hookshot(state, world)) + # Can bounce across the lava to get this without Hookshot (need to die though) + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: state.has("HUMT Access", world.player), "or") set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: can_use_hat(state, world, HatType.TIME_STOP) From d673b87b7eafac43fe8df60a222105d54524f1e6 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 31 Aug 2023 21:43:40 -0400 Subject: [PATCH 018/143] 0.9b - cleanup + expanded logic difficulty --- worlds/ahit/Items.py | 12 +- worlds/ahit/Locations.py | 72 +++---- worlds/ahit/Options.py | 38 +++- worlds/ahit/Regions.py | 48 ++--- worlds/ahit/Rules.py | 440 ++++++++++++++++++++++++++------------- worlds/ahit/__init__.py | 22 +- 6 files changed, 398 insertions(+), 234 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index fc6d82b1af..deac2587b6 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -19,11 +19,11 @@ def item_dlc_enabled(world: World, name: str) -> bool: if data.dlc_flags == HatDLC.none: return True - elif data.dlc_flags == HatDLC.dlc1 and world.multiworld.EnableDLC1[world.player].value > 0: + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): return True - elif data.dlc_flags == HatDLC.dlc2 and world.multiworld.EnableDLC2[world.player].value > 0: + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): return True - elif data.dlc_flags == HatDLC.death_wish and world.multiworld.EnableDeathWish[world.player].value > 0: + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): return True return False @@ -31,10 +31,10 @@ def item_dlc_enabled(world: World, name: str) -> bool: def get_total_time_pieces(world: World) -> int: count: int = 40 - if world.multiworld.EnableDLC1[world.player].value > 0: + if world.is_dlc1(): count += 6 - if world.multiworld.EnableDLC2[world.player].value > 0: + if world.is_dlc2(): count += 10 return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) @@ -110,7 +110,7 @@ ahit_items = { "Hover Badge": ItemData(300026, ItemClassification.useful), "Hookshot Badge": ItemData(300027, ItemClassification.progression), "Item Magnet Badge": ItemData(300028, ItemClassification.useful), - "No Bonk Badge": ItemData(300029, ItemClassification.useful), + "No Bonk Badge": ItemData(300029, ItemClassification.progression), "Compass Badge": ItemData(300030, ItemClassification.useful), "Scooter Badge": ItemData(300031, ItemClassification.progression), "Badge Pin": ItemData(300043, ItemClassification.useful), diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 874cb0edf3..4de7c24e99 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -15,7 +15,7 @@ class LocData(NamedTuple): # For UmbrellaLogic setting umbrella: Optional[bool] = False # Umbrella required for this check - dweller_bell: Optional[int] = 0 # Dweller bell hit required, 1 means must hit bell, 2 means can bypass w/mask + hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) # Other act_complete_event: Optional[bool] = True # Only used for event locations. Copy access rule from act completion @@ -33,7 +33,7 @@ def get_total_locations(world: World) -> int: if is_location_valid(world, name): total += 1 - if world.multiworld.EnableDLC1[world.player].value > 0 and world.multiworld.Tasksanity[world.player].value > 0: + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: total += world.multiworld.TasksanityCheckCount[world.player].value return total @@ -44,11 +44,11 @@ def location_dlc_enabled(world: World, location: str) -> bool: if data.dlc_flags == HatDLC.none: return True - elif data.dlc_flags == HatDLC.dlc1 and world.multiworld.EnableDLC1[world.player].value > 0: + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): return True - elif data.dlc_flags == HatDLC.dlc2 and world.multiworld.EnableDLC2[world.player].value > 0: + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): return True - elif data.dlc_flags == HatDLC.death_wish and world.multiworld.EnableDeathWish[world.player].value > 0: + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): return True return False @@ -82,7 +82,7 @@ def get_tasksanity_start_id() -> int: ahit_locations = { - "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", dweller_bell=1), + "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", hit_requirement=1), # 300000 range - Mafia Town/Batle of the Birds "Welcome to Mafia Town - Umbrella": LocData(301002, "Welcome to Mafia Town"), @@ -136,10 +136,10 @@ ahit_locations = { "Dead Bird Studio - Red Building Top": LocData(305024, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Behind Water Tower": LocData(305248, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Side of House": LocData(305247, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio", umbrella=True), - "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio", umbrella=True), - "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio", umbrella=True), - "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio", umbrella=True), + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio", hit_requirement=1), + "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio", hit_requirement=1), + "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio", hit_requirement=1), + "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio", hit_requirement=1), "Murder on the Owl Express - Cafeteria": LocData(305313, "Murder on the Owl Express"), "Murder on the Owl Express - Luggage Room Top": LocData(305090, "Murder on the Owl Express"), @@ -223,7 +223,7 @@ ahit_locations = { required_hats=[HatType.DWELLER], paintings=2), "Subcon Forest - Boss Arena Chest": LocData(323735, "Subcon Forest Area"), - "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", dweller_bell=2, paintings=1), + "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", hit_requirement=2, paintings=1), "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=2), @@ -231,15 +231,15 @@ ahit_locations = { "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=3), - "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", dweller_bell=1, paintings=1), - "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", dweller_bell=1, paintings=1), - "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, dweller_bell=1, paintings=1), - "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", dweller_bell=1, paintings=1), + "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), + "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", hit_requirement=1, paintings=1), - "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), - "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", dweller_bell=2, paintings=1), + "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), # 330000 range - Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), @@ -298,26 +298,26 @@ ahit_locations = { "Nyakuza Metro - Main Station Dining Area": LocData(304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), "Nyakuza Metro - Top of Ramen Shop": LocData(304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), - "Nyakuza Metro - Yellow Overpass Station Crate": LocData(305413, "Yellow Overpass Station", - dlc_flags=HatDLC.dlc2, - required_hats=[HatType.BREWING]), + "Yellow Overpass Station - Brewing Crate": LocData(305413, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.BREWING]), - "Nyakuza Metro - Bluefin Tunnel Cat Vacuum": LocData(305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Cat Vacuum": LocData(305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Nyakuza Metro - Pink Paw Station Cat Vacuum": LocData(305110, "Pink Paw Station", - dlc_flags=HatDLC.dlc2, - hookshot=True, - required_hats=[HatType.DWELLER]), + "Pink Paw Station - Cat Vacuum": LocData(305110, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), - "Nyakuza Metro - Pink Paw Station Behind Fan": LocData(304106, "Pink Paw Station", - dlc_flags=HatDLC.dlc2, - hookshot=True, - required_hats=[HatType.TIME_STOP, HatType.DWELLER]), + "Pink Paw Station - Behind Fan": LocData(304106, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.TIME_STOP, HatType.DWELLER]), } act_completions = { # 310000 range - Act Completions - "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery"), "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), "Act Completion (Welcome to Mafia Town)": LocData(311771, "Welcome to Mafia Town"), @@ -331,7 +331,7 @@ act_completions = { "Act Completion (Time Rift - Sewers)": LocData(312484, "Time Rift - Sewers"), "Act Completion (Time Rift - Mafia of Cooks)": LocData(311855, "Time Rift - Mafia of Cooks"), - "Act Completion (Dead Bird Studio)": LocData(311383, "Dead Bird Studio", umbrella=True), + "Act Completion (Dead Bird Studio)": LocData(311383, "Dead Bird Studio", hit_requirement=1), "Act Completion (Murder on the Owl Express)": LocData(311544, "Murder on the Owl Express"), "Act Completion (Picture Perfect)": LocData(311587, "Picture Perfect"), "Act Completion (Train Rush)": LocData(312481, "Train Rush", hookshot=True), @@ -389,7 +389,7 @@ act_completions = { "Act Completion (Green Clean Manhole)": LocData(311388, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, - required_hats=[HatType.ICE, HatType.DWELLER]), + required_hats=[HatType.ICE]), "Act Completion (Bluefin Tunnel)": LocData(311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), @@ -498,7 +498,8 @@ shop_locations = { "Yellow Overpass Station - Yellow Ticket Booth": LocData(301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), "Green Clean Station - Green Ticket Booth": LocData(301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), "Bluefin Tunnel - Blue Ticket Booth": LocData(301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Pink Paw Station - Pink Ticket Booth": LocData(301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2), + "Pink Paw Station - Pink Ticket Booth": LocData(301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + hookshot=True, required_hats=[HatType.DWELLER]), "Main Station Thug A - Item 1": LocData(301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), @@ -647,6 +648,7 @@ tihs_locations = [ event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), + "Subcon Forest Access": LocData(0, "Subcon Forest Area", act_complete_event=False), "Birdhouse Cleared": LocData(0, "The Birdhouse"), "Lava Cake Cleared": LocData(0, "The Lava Cake"), "Windmill Cleared": LocData(0, "The Windmill"), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 77daad0e2d..4b9d290fa7 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -93,7 +93,7 @@ class NoFreeRoamFinale(Toggle): class LogicDifficulty(Choice): - """Choose the difficulty setting for logic. Note that Hard or above will force SDJ logic on.""" + """Choose the difficulty setting for logic.""" display_name = "Logic Difficulty" option_normal = 0 option_hard = 1 @@ -101,12 +101,36 @@ class LogicDifficulty(Choice): default = 0 +class KnowledgeChecks(Toggle): + """Put tricks into logic that are not necessarily difficult, + but require knowledge that is not obvious or commonly known. Can include glitches such as No Bonk Surfing. + This option will be forced on if logic difficulty is at least hard.""" + display_name = "Knowledge Checks" + default = 0 + + class RandomizeHatOrder(Toggle): """Randomize the order that hats are stitched in.""" display_name = "Randomize Hat Order" default = 1 +class YarnBalancePercent(Range): + """How much (in percentage) of the yarn in the pool that will be progression balanced.""" + display_name = "Yarn Balance Percentage" + default = 20 + range_start = 0 + range_end = 100 + + +class TimePieceBalancePercent(Range): + """How much (in percentage) of time pieces in the pool that will be progression balanced.""" + display_name = "Time Piece Balance Percentage" + default = 35 + range_start = 0 + range_end = 100 + + class UmbrellaLogic(Toggle): """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" display_name = "Umbrella Logic" @@ -161,12 +185,6 @@ class StartingChapter(Choice): default = 1 -class SDJLogic(Toggle): - """Allow the SDJ (Sprint Double Jump) technique to be considered in logic.""" - display_name = "SDJ Logic" - default = 0 - - class CTRWithSprint(Toggle): """If enabled, clearing Cheating the Race with just Sprint Hat can be in logic.""" display_name = "Cheating the Race with Sprint Hat" @@ -445,6 +463,9 @@ ahit_options: typing.Dict[str, type(Option)] = { "VanillaAlpine": VanillaAlpine, "NoFreeRoamFinale": NoFreeRoamFinale, "LogicDifficulty": LogicDifficulty, + "KnowledgeChecks": KnowledgeChecks, + "YarnBalancePercent": YarnBalancePercent, + "TimePieceBalancePercent": TimePieceBalancePercent, "RandomizeHatOrder": RandomizeHatOrder, "UmbrellaLogic": UmbrellaLogic, "StartWithCompassBadge": StartWithCompassBadge, @@ -453,7 +474,6 @@ ahit_options: typing.Dict[str, type(Option)] = { "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, "StartingChapter": StartingChapter, - "SDJLogic": SDJLogic, "CTRWithSprint": CTRWithSprint, "EnableDLC1": EnableDLC1, @@ -502,13 +522,13 @@ slot_data_options: typing.Dict[str, type(Option)] = { "ActRandomizer": ActRandomizer, "ShuffleAlpineZiplines": ShuffleAlpineZiplines, "LogicDifficulty": LogicDifficulty, + "KnowledgeChecks": KnowledgeChecks, "RandomizeHatOrder": RandomizeHatOrder, "UmbrellaLogic": UmbrellaLogic, "CompassBadgeMode": CompassBadgeMode, "ShuffleStorybookPages": ShuffleStorybookPages, "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, - "SDJLogic": SDJLogic, "EnableDLC1": EnableDLC1, "Tasksanity": Tasksanity, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index b2e91103d6..61be4813a9 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -250,7 +250,6 @@ chapter_finales = [ # entrance: region blacklisted_acts = { "Battle of the Birds - Finale A": "Award Ceremony", - "Time's End - Act 1": "The Finale", } @@ -359,7 +358,7 @@ def create_regions(world: World): create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) # ------------------------------------------- DLC1 ------------------------------------------------- # - if mw.EnableDLC1[p].value > 0: + if w.is_dlc1(): arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) cruise_ship = create_region(w, "Cruise Ship") @@ -382,7 +381,7 @@ def create_regions(world: World): connect_regions(mw.get_region("Cruise Ship", p), badge_seller, "CS -> Badge Seller", p) - if mw.EnableDLC2[p].value > 0: + if w.is_dlc2(): nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) @@ -471,36 +470,6 @@ def randomize_act_entrances(world: World): # Look for candidates to map this act to candidate_list: typing.List[Region] = [] for candidate in region_list: - - if world.multiworld.VanillaAlpine[world.player].value > 0 and region.name == "Alpine Free Roam" \ - or world.multiworld.VanillaAlpine[world.player].value == 2 and region.name == "The Illness has Spread": - candidate_list.clear() - candidate_list.append(region) - break - - if world.multiworld.VanillaAlpine[world.player].value > 0 and candidate.name == "Alpine Free Roam" \ - or world.multiworld.VanillaAlpine[world.player].value == 2 and candidate.name == "The Illness has Spread": - continue - - if world.multiworld.VanillaMetro[world.player].value > 0 and region.name == "Nyakuza Free Roam": - candidate_list.clear() - candidate_list.append(region) - break - - if world.multiworld.VanillaMetro[world.player].value > 0 and candidate.name == "Nyakuza Free Roam": - continue - - if candidate.name == "Rush Hour" and world.multiworld.EndGoal[world.player].value == 2 or \ - world.multiworld.VanillaMetro[world.player].value == 2: - continue - - if region.name == "Rush Hour": - if world.multiworld.EndGoal[world.player].value == 2 or \ - world.multiworld.VanillaMetro[world.player].value == 2: - candidate_list.clear() - candidate_list.append(region) - break - # We're mapping something to the first act, make sure it is valid if not has_guaranteed: if candidate.name not in guaranteed_first_acts: @@ -618,6 +587,19 @@ def is_act_blacklisted(world: World, name: str) -> bool: if name == "The Finale": return world.multiworld.EndGoal[world.player].value == 1 + if name == "Alpine Free Roam": + return world.multiworld.VanillaAlpine[world.player].value > 0 + + if name == "The Illness has Spread": + return world.multiworld.VanillaAlpine[world.player].value == 2 + + if name == "Nyakuza Free Roam": + return world.multiworld.VanillaMetro[world.player].value > 0 + + if name == "Rush Hour": + return world.multiworld.EndGoal[world.player].value == 2 \ + or world.multiworld.VanillaMetro[world.player].value == 2 + return name in blacklisted_acts.values() diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b4629a0d69..b1b629b112 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -49,10 +49,60 @@ def can_sdj(state: CollectionState, world: World): return can_use_hat(state, world, HatType.SPRINT) +def painting_logic(world: World) -> bool: + return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 + + +def is_player_knowledgeable(world: World) -> bool: + return world.multiworld.KnowledgeChecks[world.player].value > 0 + + +# 0 = Normal, 1 = Hard, 2 = Expert +def get_difficulty(world: World) -> int: + return world.multiworld.LogicDifficulty[world.player].value + + +def has_paintings(state: CollectionState, world: World, count: int) -> bool: + if not painting_logic(world): + return True + + # Cherry Hover + if get_difficulty(world) == 2: + return True + + # All paintings can be skipped with No Bonk, very easily, if the player knows + if is_player_knowledgeable(world) and can_surf(state, world): + return True + + paintings: int = state.count("Progressive Painting Unlock", world.player) + + if is_player_knowledgeable(world): + # Green paintings can also be skipped very easily without No Bonk + if paintings >= 1 and count == 3: + return True + + return paintings >= count + + +def zipline_logic(world: World) -> bool: + return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0 + + def can_use_hookshot(state: CollectionState, world: World): return state.has("Hookshot Badge", world.player) +def can_hit(state: CollectionState, world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0: + return True + + return state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING) + + +def can_surf(state: CollectionState, world: World): + return state.has("No Bonk Badge", world.player) + + def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: return state.has_group(relic, world.player, len(world.item_name_groups[relic])) @@ -61,13 +111,6 @@ def get_relic_count(state: CollectionState, world: World, relic: str) -> int: return state.count_group(relic, world.player) -def can_hit_bells(state: CollectionState, world: World): - if world.multiworld.UmbrellaLogic[world.player].value == 0: - return True - - return state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING) - - # Only use for rifts def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) @@ -98,9 +141,6 @@ def can_clear_metro(state: CollectionState, world: World) -> bool: def set_rules(world: World): - dlc1: bool = bool(world.multiworld.EnableDLC1[world.player].value > 0) - dlc2: bool = bool(world.multiworld.EnableDLC2[world.player].value > 0) - # First, chapter access starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) world.set_chapter_cost(starting_chapter, 0) @@ -114,25 +154,26 @@ def set_rules(world: World): final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) - if dlc1: + if world.is_dlc1(): chapter_list.append(ChapterIndex.CRUISE) - if dlc2 and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) world.multiworld.random.shuffle(chapter_list) - if starting_chapter is not ChapterIndex.ALPINE and (dlc1 or dlc2): + if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1: int = 69 index2: int = 69 + pos: int lowest_index: int chapter_list.remove(ChapterIndex.ALPINE) - if dlc1: + if world.is_dlc1(): index1 = chapter_list.index(ChapterIndex.CRUISE) - if dlc2 and final_chapter is not ChapterIndex.METRO: + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: index2 = chapter_list.index(ChapterIndex.METRO) lowest_index = min(index1, index2) @@ -143,6 +184,14 @@ def set_rules(world: World): chapter_list.insert(pos, ChapterIndex.ALPINE) + if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.remove(ChapterIndex.METRO) + index = chapter_list.index(ChapterIndex.CRUISE) + if index >= len(chapter_list): + chapter_list.append(ChapterIndex.METRO) + else: + chapter_list.insert(world.multiworld.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value highest_cost: int = world.multiworld.HighestChapterCost[world.player].value @@ -190,12 +239,12 @@ def set_rules(world: World): lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) - if dlc1: + if world.is_dlc1(): add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) - if dlc2: + if world.is_dlc2(): add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) @@ -237,28 +286,21 @@ def set_rules(world: World): add_rule(location, lambda state: state.has("Umbrella", world.player)) if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(location, lambda state, paintings=data.paintings: - state.count("Progressive Painting Unlock", world.player) >= paintings) + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.dweller_bell > 0: - if data.dweller_bell == 1: # Required to be hit regardless of Dweller Mask - add_rule(location, lambda state: can_hit_bells(state, world)) - else: # Can bypass with Dweller Mask - add_rule(location, lambda state: can_hit_bells(state, world) or can_use_hat(state, world, HatType.DWELLER)) + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(location, lambda state: can_hit(state, world)) + else: # Can bypass with Dweller Mask (dweller bells) + add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + if get_difficulty(world) >= 1: + world.multiworld.KnowledgeChecks[world.player].value = 1 set_specific_rules(world) - if world.multiworld.LogicDifficulty[world.player].value >= 1: - world.multiworld.SDJLogic[world.player].value = 1 - - if world.multiworld.SDJLogic[world.player].value > 0: - set_sdj_rules(world) - - if world.multiworld.ShuffleAlpineZiplines[world.player].value > 0: - set_alps_zipline_rules(world) - for (key, acts) in act_connections.items(): - if "Arctic Cruise" in key and not dlc1: + if "Arctic Cruise" in key and not world.is_dlc1(): continue i: int = 1 @@ -304,11 +346,6 @@ def set_rules(world: World): def set_specific_rules(world: World): - dlc1: bool = bool(world.multiworld.EnableDLC1[world.player].value > 0) - - add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), - lambda state: can_clear_alpine(state, world)) - add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), lambda state: state.has("Time Piece", world.player, 12) and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) @@ -316,37 +353,158 @@ def set_specific_rules(world: World): add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), lambda state: state.has("Time Piece", world.player, 4)) - # Normal logic - if world.multiworld.LogicDifficulty[world.player].value == 0: - add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + set_mafia_town_rules(world) + set_subcon_rules(world) + set_alps_rules(world) - add_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + if world.is_dlc1(): + set_dlc1_rules(world) - if dlc1: - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), - lambda state: can_use_hat(state, world, HatType.DWELLER)) + if world.is_dlc2(): + set_dlc2_rules(world) - add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), - lambda state: can_use_hat(state, world, HatType.ICE)) + difficulty: int = get_difficulty(world) + if is_player_knowledgeable(world) or difficulty >= 1: + set_knowledge_rules(world) - add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), - lambda state: can_use_hat(state, world, HatType.ICE)) + if difficulty == 0: + set_normal_rules(world) - # Hard logic, includes SDJ stuff - if world.multiworld.LogicDifficulty[world.player].value >= 1: - add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), - lambda state: can_use_hat(state, world, HatType.SPRINT) - and state.has("Scooter Badge", world.player), "or") + if difficulty >= 1: + set_hard_rules(world) - # Expert logic - if world.multiworld.LogicDifficulty[world.player].value >= 2: - set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) - else: - add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + if difficulty >= 2: + set_expert_rules(world) + + +def set_normal_rules(world: World): + # Hard: get to Birdhouse without Brewing Hat + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + add_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + # Hard: gallery without Brewing Hat + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + if world.is_dlc1(): + # Hard: clear Deep Sea without Dweller Mask + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), lambda state: can_use_hat(state, world, HatType.DWELLER)) + # Hard: clear Rock the Boat without Ice Hat + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + if world.is_dlc2(): + # Hard: clear Green Clean Manhole without Dweller Mask + add_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) + + +def set_hard_rules(world: World): + # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only + add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + # Hard: Cross Subcon boss arena gap with No Bonk + SDJ, + # allowing access to the boss arena chest, and Toilet of Doom without Hookshot + # Doing this in reverse from YCHE is expert logic, which expects you to cherry hover + add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_surf(state, world) and can_sdj(state, world) and can_hit(state, world), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: can_surf(state, world) and can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: can_sdj(state, world) + and has_paintings(state, world, 2), "or") + + add_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: can_sdj(state, world), "or") + + +def set_expert_rules(world: World): + # Expert: get to and clear Twilight Bell without Dweller Mask using SDJ. Brewing Hat required to complete act. + add_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), + lambda state: can_sdj(state, world) + and (not zipline_logic(world) or state.has("Zipline Unlock - The Twilight Bell Path", world.player)), "or") + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_sdj(state, world) and can_use_hookshot(state, world) + and (not zipline_logic(world) or state.has("Zipline Unlock - The Twilight Bell Path", world.player)), "or") + + add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player), + lambda state: can_sdj(state, world) and can_use_hat(state, world, HatType.BREWING), "or") + + # Expert: enter and clear The Subcon Well with No Bonk Badge only + for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: + add_rule(loc, lambda state: can_surf(state, world) and has_paintings(state, world, 1), "or") + + # Expert: Cherry Hovering + connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), + world.multiworld.get_region("Subcon Forest Area", world.player), + "Subcon Forest Entrance YCHE", world.player) + + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_hit(state, world)) + + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: True) + + # Manor hover with 1 painting unlock + for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: + set_rule(loc, lambda state: not painting_logic(world) + or state.count("Progressive Painting Unlock", world.player) >= 1) + + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), + lambda state: not painting_logic(world) + or state.count("Progressive Painting Unlock", world.player) >= 1) + + +def set_knowledge_rules(world: World): + # Can jump down from HQ to get these + add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player), "or") + + add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player), "or") + + # Dweller Mask requirement in Pink Paw can also be skipped by jumping on lamp post. + # The item behind the Time Stop fan can be walked past without Time Stop hat as well. + # (just set hookshot rule, because dweller requirement is skipped, but hookshot is still necessary) + if world.is_dlc2(): + # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: can_use_hookshot(state, world), "or") + + for loc in world.multiworld.get_region("Pink Paw Station", world.player).locations: + + # Can't jump back down to the manhole due to a fall damage trigger. + if loc.name == "Act Completion (Pink Paw Manhole)": + set_rule(loc, lambda state: (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)) + and can_use_hat(state, world, HatType.ICE)) + + continue + + set_rule(loc, lambda state: can_use_hookshot(state, world)) + + +def set_mafia_town_rules(world: World): add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) or state.can_reach("Down with the Mafia!", "Region", world.player) @@ -398,15 +556,16 @@ def set_specific_rules(world: World): lambda state: can_use_hat(state, world, HatType.TIME_STOP) or world.multiworld.CTRWithSprint[world.player].value > 0 and can_use_hat(state, world, HatType.SPRINT)) + +def set_subcon_rules(world: World): set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: state.can_reach("Toilet of Doom", "Region", world.player) - and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 - or state.has("Progressive Painting Unlock", world.player, 1)) + and (not painting_logic(world) or has_paintings(state, world, 1)) or state.can_reach("Your Contract has Expired", "Region", world.player)) if world.multiworld.UmbrellaLogic[world.player].value > 0: add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + lambda state: can_hit(state, world)) set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) @@ -424,90 +583,84 @@ def set_specific_rules(world: World): add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player), lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + if painting_logic(world): for key in contract_locations: if key == "Snatcher's Contract - The Subcon Well": continue - - add_rule(world.multiworld.get_location(key, world.player), - lambda state: state.has("Progressive Painting Unlock", world.player, 1)) + + add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) + + +def set_alps_rules(world: World): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world)) + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: can_use_hookshot(state, world)) + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER) and can_use_hookshot(state, world)) add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) - if world.multiworld.EnableDLC1[world.player].value > 0: - add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), - lambda state: can_use_hookshot(state, world)) + add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), + lambda state: can_clear_alpine(state, world)) - # This particular item isn't present in Act 3 for some reason, yes in vanilla too - add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), - lambda state: state.can_reach("Bon Voyage!", "Region", world.player) - or state.can_reach("Ship Shape", "Region", world.player)) + if zipline_logic(world): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) - if world.multiworld.EnableDLC2[world.player].value > 0: - add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), - lambda state: state.has("Metro Ticket - Green", world.player) - or state.has("Metro Ticket - Blue", world.player)) + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) - add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), - lambda state: state.has("Metro Ticket - Pink", world.player) - or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) - add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), - lambda state: can_clear_metro(state, world)) + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) - add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), - lambda state: state.has("Metro Ticket - Yellow", world.player) - and state.has("Metro Ticket - Blue", world.player) - and state.has("Metro Ticket - Pink", world.player)) + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) - for key in shop_locations.keys(): - if "Green Clean Station Thug B" in key and is_location_valid(world, key): - add_rule(world.multiworld.get_location(key, world.player), - lambda state: state.has("Metro Ticket - Yellow", world.player), "or") + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), lambda state: state.has(zipline, world.player)) -def set_sdj_rules(world: World): - add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: can_sdj(state, world) - and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 - or state.count("Progressive Painting Unlock", world.player) >= 2), "or") +def set_dlc1_rules(world: World): + add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), + lambda state: can_use_hookshot(state, world)) - add_rule(world.multiworld.get_location("Subcon Forest - Green and Purple Dweller Rocks", world.player), - lambda state: can_sdj(state, world) - and (world.multiworld.ShuffleSubconPaintings[world.player].value == 0 - or state.count("Progressive Painting Unlock", world.player) >= 3), "or") - - add_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), - lambda state: can_sdj(state, world), "or") - - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), - lambda state: can_sdj(state, world), "or") - - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), - lambda state: can_sdj(state, world), "or") + # This particular item isn't present in Act 3 for some reason, yes in vanilla too + add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), + lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + or state.can_reach("Ship Shape", "Region", world.player)) -def set_alps_zipline_rules(world: World): - add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), - lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) +def set_dlc2_rules(world: World): + add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), + lambda state: state.has("Metro Ticket - Green", world.player) + or state.has("Metro Ticket - Blue", world.player)) - add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), - lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) - add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), - lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), + lambda state: can_clear_metro(state, world)) - add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), - lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) - add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), - lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) - and state.has("Zipline Unlock - The Lava Cake Path", world.player) - and state.has("Zipline Unlock - The Windmill Path", world.player)) - - for (loc, zipline) in zipline_unlocks.items(): - add_rule(world.multiworld.get_location(loc, world.player), lambda state: state.has(zipline, world.player)) + for key in shop_locations.keys(): + if "Green Clean Station Thug B" in key and is_location_valid(world, key): + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player), "or") def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): @@ -575,22 +728,21 @@ def set_rift_rules(world: World, regions: typing.Dict[str, Region]): add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", world.player).connected_region, entrance) - - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in regions["Time Rift - Village"].entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", world.player).connected_region, entrance) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in regions["Time Rift - Sleepy Subcon"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 3)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) for entrance in regions["Time Rift - Curly Tail Trail"].entrances: add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) @@ -601,14 +753,14 @@ def set_rift_rules(world: World, regions: typing.Dict[str, Region]): for entrance in regions["Time Rift - Alpine Skyline"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) - if world.multiworld.EnableDLC1[world.player].value > 0: + if world.is_dlc1() > 0: for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) - if world.multiworld.EnableDLC2[world.player].value > 0: + if world.is_dlc2() > 0: for entrance in regions["Time Rift - Rumbi Factory"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) @@ -654,19 +806,19 @@ def set_default_rift_rules(world: World): for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) reg_act_connection(world, "The Subcon Well", entrance.name) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 2)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Progressive Painting Unlock", world.player, 3)) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances: add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) @@ -677,14 +829,14 @@ def set_default_rift_rules(world: World): for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) - if world.multiworld.EnableDLC1[world.player].value > 0: + if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) - if world.multiworld.EnableDLC2[world.player].value > 0: + if world.is_dlc2(): for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 858e3ac2d5..e809484569 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -90,7 +90,7 @@ class HatInTimeWorld(World): yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value) # 1/5 is progression balanced - for i in range(len(yarn_pool) // 5): + for i in range(int(len(yarn_pool) * (0.01 * self.multiworld.YarnBalancePercent[self.player].value))): yarn_pool[i].classification = ItemClassification.progression itempool += yarn_pool @@ -125,17 +125,16 @@ class HatInTimeWorld(World): if name == "Time Piece": tp_count: int = 40 max_extra: int = 0 - if self.multiworld.EnableDLC1[self.player].value > 0: + if self.is_dlc1(): max_extra += 6 - if self.multiworld.EnableDLC2[self.player].value > 0: + if self.is_dlc2(): max_extra += 10 tp_count += min(max_extra, self.multiworld.MaxExtraTimePieces[self.player].value) tp_list: typing.List[Item] = create_multiple_items(self, name, tp_count) - # 1/5 is progression balanced - for i in range(len(tp_list) // 5): + for i in range(int(len(tp_list) * (0.01 * self.multiworld.TimePieceBalancePercent[self.player].value))): tp_list[i].classification = ItemClassification.progression itempool += tp_list @@ -191,7 +190,7 @@ class HatInTimeWorld(World): for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] - if self.multiworld.EnableDLC2[self.player].value > 0: + if self.is_dlc2(): for name in self.nyakuza_thug_items.keys(): slot_data[name] = self.nyakuza_thug_items[name] @@ -224,7 +223,7 @@ class HatInTimeWorld(World): new_hint_data[location.address] = self.get_shuffled_region(region_name) - if self.multiworld.EnableDLC1[self.player].value > 0 and self.multiworld.Tasksanity[self.player].value > 0: + if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: ship_shape_region = self.get_shuffled_region("Ship Shape") id_start: int = get_tasksanity_start_id() for i in range(self.multiworld.TasksanityCheckCount[self.player].value): @@ -288,3 +287,12 @@ class HatInTimeWorld(World): def get_chapter_costs(self): return chapter_timepiece_costs[self.player] + + def is_dlc1(self) -> bool: + return self.multiworld.EnableDLC1[self.player].value > 0 + + def is_dlc2(self) -> bool: + return self.multiworld.EnableDLC2[self.player].value > 0 + + def is_dw(self) -> bool: + return self.multiworld.EnableDeathWish[self.player].value > 0 From 77ac8a7afbe04677364c0c17c3cb4bb1694ed9c3 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 31 Aug 2023 21:59:51 -0400 Subject: [PATCH 019/143] Update Rules.py --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b1b629b112..b6628e16e6 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -448,7 +448,7 @@ def set_expert_rules(world: World): # Expert: enter and clear The Subcon Well with No Bonk Badge only for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: - add_rule(loc, lambda state: can_surf(state, world) and has_paintings(state, world, 1), "or") + add_rule(loc, lambda state: can_surf(state, world), "or") # Expert: Cherry Hovering connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), From 2ef442290bb4a64db2387e41065dad33926be50f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 31 Aug 2023 22:11:09 -0400 Subject: [PATCH 020/143] Update Regions.py --- worlds/ahit/Regions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 61be4813a9..ba839a1810 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -436,6 +436,12 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) + # We want to do these first as well, since they can be blocked from being shuffled onto freeroam + for region in region_list.copy(): + if region.name in chapter_finales or region.name == "Cheating the Race": + region_list.remove(region) + region_list.append(region) + # Reverse the list, so we can do what we want to do first region_list.reverse() From 1f916e85bc1a94215d7bb7bae515e91609936d38 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 2 Sep 2023 11:56:57 -0400 Subject: [PATCH 021/143] AttributeError fix --- worlds/ahit/DeathWishLocations.py | 52 +++++++++++++++++++++++++++++++ worlds/ahit/DeathWishRules.py | 0 worlds/ahit/Regions.py | 2 -- worlds/ahit/Rules.py | 1 - worlds/ahit/__init__.py | 1 - 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 worlds/ahit/DeathWishLocations.py create mode 100644 worlds/ahit/DeathWishRules.py diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py new file mode 100644 index 0000000000..244c790ffb --- /dev/null +++ b/worlds/ahit/DeathWishLocations.py @@ -0,0 +1,52 @@ + +death_wishes = [ + "Beat the Heat", + "So You're Back From Outer Space", + "Mafia's Jumps", + "Collect-a-thon", + "She Speedran from Outerspace", + "Vault Codes in the Wind", + "Encore! Encore!", + "Rift Collapse: Mafia of Cooks", + "Snatcher's Hit List", + "Snatcher Coins in Mafia Town" + + "Security Breach", + "10 Seconds until Self-Destruct", + "The Great Big Hootenanny", + "Killing Two Birds", + "Rift Collapse: Dead Bird Studio", + "Snatcher Coins in Battle of the Birds", + "Zero Jumps", + + "Speedrun Well", + "Boss Rush", + "Quality Time with Snatcher", + "Breaching the Contract", + "Rift Collapse: Sleepy Subcon", + "Snatcher Coins in Subcon Forest", + + "Bird Sanctuary", + "Wound-Up Windmill", + "The Illness has Speedrun", + "Rift Collapse: Alpine Skyline", + "Snatcher Coins in Alpine Skyline", + "Camera Tourist", + + "The Mustache Gauntlet", + "No More Bad Guys", + + "Seal the Deal", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + + "Community Map: Rhythm Jump Studio", + "Community Map: Twilight Travels", + "Community Map: The Mountain Rift", + "Snatcher Coins in Nyakuza Metro", +] + +dw_prereqs = { + "Snatcher's Hit List": ["Beat the Heat"], + +} \ No newline at end of file diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index ba839a1810..1473d2f605 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -551,8 +551,6 @@ def randomize_act_entrances(world: World): for e in candidate.entrances.copy(): e.parent_region.exits.remove(e) e.connected_region.entrances.remove(e) - del e.parent_region - del e.connected_region entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b6628e16e6..eb08c2a0c3 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -308,7 +308,6 @@ def set_rules(world: World): region: Region = entrance.connected_region access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] entrance.parent_region.exits.remove(entrance) - del entrance.parent_region # Entrances to this act that we have to set access_rules on entrances: typing.List[Entrance] = [] diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index e809484569..0f6b4aff23 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -89,7 +89,6 @@ class HatInTimeWorld(World): self.calculate_yarn_costs() yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value) - # 1/5 is progression balanced for i in range(int(len(yarn_pool) * (0.01 * self.multiworld.YarnBalancePercent[self.player].value))): yarn_pool[i].classification = ItemClassification.progression From 215594d0945e9feca22c666db0d8639aabe4b7fb Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 2 Sep 2023 19:53:39 -0400 Subject: [PATCH 022/143] 0.10b - New Options --- worlds/ahit/Locations.py | 4 ++ worlds/ahit/Options.py | 57 +++++++++-------- worlds/ahit/Regions.py | 129 +++++++++++++++++++++++++++++++-------- worlds/ahit/Rules.py | 2 +- 4 files changed, 139 insertions(+), 53 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 4de7c24e99..5e32240b30 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -65,6 +65,10 @@ def is_location_valid(world: World, location: str) -> bool: if location in shop_locations and location not in world.shop_locs: return False + data = location_table.get(location) or event_locs.get(location) + if data.region == "Time Rift - Tour" and world.multiworld.ExcludeTour[world.player].value > 0: + return False + return True diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 4b9d290fa7..97cd838762 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,6 +1,6 @@ import typing from worlds.AutoWorld import World -from Options import Option, Range, Toggle, DeathLink, Choice +from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict from .Items import get_total_time_pieces @@ -72,26 +72,22 @@ class ActRandomizer(Choice): default = 1 +class ActPlando(OptionDict): + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + display_name = "Act Plando" + + class ShuffleAlpineZiplines(Toggle): """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" display_name = "Shuffle Alpine Ziplines" default = 0 -class VanillaAlpine(Choice): - """If enabled, force Alpine (and optionally its finale) onto their vanilla locations in act shuffle.""" - display_name = "Vanilla Alpine Skyline" - option_false = 0 - option_true = 1 - option_finale = 2 +class FinaleShuffle(Toggle): + """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" default = 0 -class NoFreeRoamFinale(Toggle): - """If enabled, prevent Free Roam acts from being shuffled onto chapter finales.""" - default = 1 - - class LogicDifficulty(Choice): """Choose the difficulty setting for logic.""" display_name = "Logic Difficulty" @@ -221,6 +217,22 @@ class TasksanityCheckCount(Range): default = 18 +class ExcludeTour(Toggle): + """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with + important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages + when your goal is Time's End.""" + display_name = "Exclude Tour Time Rift" + default = 0 + + +class ShipShapeCustomTaskGoal(Range): + """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + display_name = "Ship Shape Custom Task Goal" + range_start = 5 + range_end = 30 + default = 18 + + class EnableDLC2(Toggle): """Shuffle content from Nyakuza Metro (Chapter 7) into the game. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" @@ -238,7 +250,7 @@ class MetroMinPonCost(Range): class MetroMaxPonCost(Range): """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" - display_name = "Metro Shops Minimum Pon Cost" + display_name = "Metro Shops Maximum Pon Cost" range_start = 10 range_end = 800 default = 200 @@ -267,14 +279,6 @@ class BaseballBat(Toggle): default = 0 -class VanillaMetro(Choice): - """Force Nyakuza Metro (and optionally its finale) onto their vanilla locations in act shuffle.""" - display_name = "Vanilla Metro" - option_false = 0 - option_true = 1 - option_finale = 2 - - class ChapterCostIncrement(Range): """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" display_name = "Chapter Cost Increment" @@ -357,7 +361,7 @@ class DWExcludeAnnoyingContracts(Toggle): class DWExcludeAnnoyingBonuses(Toggle): """NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions - from the pool""" + from the pool. DANGER! DISABLE AT YOUR OWN RISK! THIS OPTION WHEN DISABLED CAN CREATE VERY DIFFICULT SEEDS!!!""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 @@ -459,9 +463,9 @@ ahit_options: typing.Dict[str, type(Option)] = { "EndGoal": EndGoal, "ActRandomizer": ActRandomizer, + "ActPlando": ActPlando, "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "VanillaAlpine": VanillaAlpine, - "NoFreeRoamFinale": NoFreeRoamFinale, + "FinaleShuffle": FinaleShuffle, "LogicDifficulty": LogicDifficulty, "KnowledgeChecks": KnowledgeChecks, "YarnBalancePercent": YarnBalancePercent, @@ -480,11 +484,12 @@ ahit_options: typing.Dict[str, type(Option)] = { "Tasksanity": Tasksanity, "TasksanityTaskStep": TasksanityTaskStep, "TasksanityCheckCount": TasksanityCheckCount, + "ExcludeTour": ExcludeTour, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, "EnableDeathWish": EnableDeathWish, "EnableDLC2": EnableDLC2, "BaseballBat": BaseballBat, - "VanillaMetro": VanillaMetro, "MetroMinPonCost": MetroMinPonCost, "MetroMaxPonCost": MetroMaxPonCost, "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, @@ -534,6 +539,8 @@ slot_data_options: typing.Dict[str, type(Option)] = { "Tasksanity": Tasksanity, "TasksanityTaskStep": TasksanityTaskStep, "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, "EnableDeathWish": EnableDeathWish, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 1473d2f605..be13fa1f22 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -252,6 +252,16 @@ blacklisted_acts = { "Battle of the Birds - Finale A": "Award Ceremony", } +# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. +blacklisted_combos = { + "The Illness has Spread": ["Alpine Free Roam"], + "Rush Hour": ["Nyakuza Free Roam"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam"], +} + def create_regions(world: World): w = world @@ -371,7 +381,9 @@ def create_regions(world: World): connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) create_rift_connections(w, create_region(w, "Time Rift - Balcony")) create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) - create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.ExcludeTour[world.player].value == 0: + create_rift_connections(w, create_region(w, "Time Rift - Tour")) if mw.Tasksanity[p].value > 0: create_tasksanity_locations(w) @@ -419,9 +431,52 @@ def create_tasksanity_locations(world: World): ship_shape.locations.append(location) +def is_valid_plando(world: World, region: str) -> bool: + if region in blacklisted_acts.values(): + return False + + if region not in world.multiworld.ActPlando[world.player].keys(): + return False + + act = world.multiworld.ActPlando[world.player].get(region) + if act in blacklisted_acts.values(): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ + and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) + + if is_first_act: + if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + return False + + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): + return False + + if act not in guaranteed_first_acts: + return False + + # Don't allow straight up impossible mappings + if region == "The Illness has Spread" and act == "Alpine Free Roam": + return False + + if region == "Rush Hour" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + world.multiworld.get_regions(world.player)) + + def randomize_act_entrances(world: World): region_list: typing.List[Region] = get_act_regions(world) - world.multiworld.random.shuffle(region_list) + world.random.shuffle(region_list) separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) @@ -436,12 +491,21 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) - # We want to do these first as well, since they can be blocked from being shuffled onto freeroam for region in region_list.copy(): - if region.name in chapter_finales or region.name == "Cheating the Race": + if region.name in chapter_finales: region_list.remove(region) region_list.append(region) + for region in region_list.copy(): + if region.name in world.multiworld.ActPlando[world.player].keys(): + if is_valid_plando(world, region.name): + region_list.remove(region) + region_list.append(region) + else: + print("Disallowing act plando for", + world.multiworld.player_name[world.player], + "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + # Reverse the list, so we can do what we want to do first region_list.reverse() @@ -465,6 +529,9 @@ def randomize_act_entrances(world: World): and "Free Roam" not in act_entrances[region.name]: continue + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + has_guaranteed = True + i = 0 # Already mapped to something else @@ -481,6 +548,9 @@ def randomize_act_entrances(world: World): if candidate.name not in guaranteed_first_acts: continue + if candidate.name in world.multiworld.ActPlando[world.player].values(): + continue + # Not completable without Umbrella if world.multiworld.UmbrellaLogic[world.player].value > 0 \ and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): @@ -495,6 +565,12 @@ def randomize_act_entrances(world: World): has_guaranteed = True break + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + candidate_list.clear() + candidate_list.append( + world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + break + # Already mapped onto something else if candidate in shuffled_list: continue @@ -513,18 +589,11 @@ def randomize_act_entrances(world: World): or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: continue - # Don't map Alpine to its own finale - if region.name == "The Illness has Spread" and candidate.name == "Alpine Free Roam": + if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: continue - # Ditto for Metro - if region.name == "Rush Hour" and candidate.name == "Nyakuza Free Roam": - continue - - # CTR entrance and Tour aren't a finale, but have a fuck ton of unlock requirements - if world.multiworld.NoFreeRoamFinale[world.player].value > 0 and "Free Roam" in candidate.name: - if region.name in chapter_finales or region.name == "Cheating the Race" \ - or world.multiworld.EndGoal[world.player].value == 1 and region.name == "Time Rift - Tour": + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if candidate.name not in chapter_finales: continue if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: @@ -532,8 +601,18 @@ def randomize_act_entrances(world: World): candidate_list.append(candidate) - candidate: Region = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + candidate: Region + if len(candidate_list) > 0: + candidate = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + else: + # plando can still break certain rules, so acts may not always end up shuffled. + for c in region_list: + if c not in shuffled_list: + candidate = c + break + shuffled_list.append(candidate) + # print(region, candidate) # Vanilla if candidate.name == region.name: @@ -588,21 +667,17 @@ def get_act_regions(world: World) -> typing.List[Region]: def is_act_blacklisted(world: World, name: str) -> bool: + plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ + or name in world.multiworld.ActPlando[world.player].values() + if name == "The Finale": - return world.multiworld.EndGoal[world.player].value == 1 - - if name == "Alpine Free Roam": - return world.multiworld.VanillaAlpine[world.player].value > 0 - - if name == "The Illness has Spread": - return world.multiworld.VanillaAlpine[world.player].value == 2 - - if name == "Nyakuza Free Roam": - return world.multiworld.VanillaMetro[world.player].value > 0 + return not plando and world.multiworld.EndGoal[world.player].value == 1 if name == "Rush Hour": - return world.multiworld.EndGoal[world.player].value == 2 \ - or world.multiworld.VanillaMetro[world.player].value == 2 + return not plando and world.multiworld.EndGoal[world.player].value == 2 + + if name == "Time Rift - Tour": + return world.multiworld.ExcludeTour[world.player].value > 0 return name in blacklisted_acts.values() diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index eb08c2a0c3..7fc5bf93e7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -291,7 +291,7 @@ def set_rules(world: World): if data.hit_requirement > 0: if data.hit_requirement == 1: add_rule(location, lambda state: can_hit(state, world)) - else: # Can bypass with Dweller Mask (dweller bells) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) if get_difficulty(world) >= 1: From 1daafff04b10195837666f6dedb23f262f328c0e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Sep 2023 14:27:28 -0400 Subject: [PATCH 023/143] 1.0 Preparations --- worlds/ahit/DeathWishLocations.py | 276 ++++++++++++++--- worlds/ahit/DeathWishRules.py | 491 ++++++++++++++++++++++++++++++ worlds/ahit/Items.py | 170 ++++++++--- worlds/ahit/Locations.py | 207 ++++++++++--- worlds/ahit/Options.py | 475 +++++++++++++++++++---------- worlds/ahit/Regions.py | 71 +++-- worlds/ahit/Rules.py | 44 +-- worlds/ahit/__init__.py | 301 +++++++++--------- 8 files changed, 1568 insertions(+), 467 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 244c790ffb..e8e2e7941a 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -1,52 +1,240 @@ +from .Locations import HatInTimeLocation, death_wishes +from .Regions import connect_regions, create_region +from BaseClasses import Region, LocationProgressType +from worlds.generic.Rules import add_rule +from worlds.AutoWorld import World +from typing import List -death_wishes = [ - "Beat the Heat", - "So You're Back From Outer Space", - "Mafia's Jumps", - "Collect-a-thon", - "She Speedran from Outerspace", - "Vault Codes in the Wind", - "Encore! Encore!", - "Rift Collapse: Mafia of Cooks", +dw_prereqs = { + "So You're Back From Outer Space": ["Beat the Heat"], + "Snatcher's Hit List": ["Beat the Heat"], + "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"], + "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"], + "Collect-a-thon": ["So You're Back From Outer Space"], + "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"], + "Mafia's Jumps": ["She Speedran from Outer Space"], + "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"], + "Encore! Encore!": ["Collect-a-thon"], + + "Security Breach": ["Beat the Heat"], + "Rift Collapse: Dead Bird Studio": ["Security Breach"], + "The Great Big Hootenanny": ["Security Breach"], + "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"], + "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"], + "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"], + "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"], + "Zero Jumps": ["Rift Collapse: Dead Bird Studio"], + "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"], + + "Speedrun Well": ["Beat the Heat"], + "Rift Collapse: Sleepy Subcon": ["Speedrun Well"], + "Boss Rush": ["Speedrun Well"], + "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"], + "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"], + "Community Rift: Twilight Travels": ["Quality Time with Snatcher"], + "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"], + + "Bird Sanctuary": ["Beat the Heat"], + "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"], + "Wound-Up Windmill": ["Bird Sanctuary"], + "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"], + "Camera Tourist": ["Rift Collapse: Alpine Skyline"], + "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"], + "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"], + + "The Mustache Gauntlet": ["Wound-Up Windmill"], + "No More Bad Guys": ["The Mustache Gauntlet"], + "Seal the Deal": ["Encore! Encore!", "Killing Two Birds", + "Breaching the Contract", "No More Bad Guys"], + + "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio", + "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"], + + "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"], +} + +dw_candles = [ "Snatcher's Hit List", - "Snatcher Coins in Mafia Town" - - "Security Breach", - "10 Seconds until Self-Destruct", - "The Great Big Hootenanny", - "Killing Two Birds", - "Rift Collapse: Dead Bird Studio", - "Snatcher Coins in Battle of the Birds", "Zero Jumps", - - "Speedrun Well", - "Boss Rush", - "Quality Time with Snatcher", - "Breaching the Contract", - "Rift Collapse: Sleepy Subcon", - "Snatcher Coins in Subcon Forest", - - "Bird Sanctuary", - "Wound-Up Windmill", - "The Illness has Speedrun", - "Rift Collapse: Alpine Skyline", - "Snatcher Coins in Alpine Skyline", "Camera Tourist", - - "The Mustache Gauntlet", - "No More Bad Guys", - - "Seal the Deal", - "Rift Collapse: Deep Sea", - "Cruisin' for a Bruisin'", - - "Community Map: Rhythm Jump Studio", - "Community Map: Twilight Travels", - "Community Map: The Mountain Rift", + "Snatcher Coins in Mafia Town", + "Snatcher Coins in Battle of the Birds", + "Snatcher Coins in Subcon Forest", + "Snatcher Coins in Alpine Skyline", "Snatcher Coins in Nyakuza Metro", ] -dw_prereqs = { - "Snatcher's Hit List": ["Beat the Heat"], +annoying_dws = [ + "Vault Codes in the Wind", + "Boss Rush", + "Camera Tourist", + "The Mustache Gauntlet", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + "Seal the Deal", # Non-excluded if goal +] -} \ No newline at end of file +# includes the above as well +annoying_bonuses = [ + "So You're Back From Outer Space", + "Encore! Encore!", + "Snatcher's Hit List", + "10 Seconds until Self-Destruct", + "Killing Two Birds", + "Snatcher Coins in Battle of the Birds", + "Zero Jumps", + "Bird Sanctuary", + "Wound-Up Windmill", + "Snatcher Coins in Alpine Skyline", + "Seal the Deal", +] + +dw_classes = { + "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder", + "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace", + "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody", + "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy", + "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown", + "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX", + "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien", + "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien", + "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault", + "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown", + + "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards", + "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade", + "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds", + "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime", + "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX", + "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds", + "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses", + + "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell", + "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon", + "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush", + "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest", + "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX", + "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon", + + "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse", + "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps", + "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill", + "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness", + "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps", + "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1", + + "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle", + "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX", + + "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX", + "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise", + "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks", + + "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump", + "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels", + "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift", + + "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro", +} + + +def create_dw_regions(world: World): + if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + for name in annoying_dws: + world.get_excluded_dws().append(name) + + if world.multiworld.DWEnableBonus[world.player].value == 0 \ + or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + for name in death_wishes: + world.get_excluded_bonuses().append(name) + elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + for name in annoying_bonuses: + world.get_excluded_bonuses().append(name) + + if world.multiworld.DWExcludeCandles[world.player].value > 0: + for name in dw_candles: + if name in world.get_excluded_dws(): + continue + world.get_excluded_dws().append(name) + + spaceship = world.multiworld.get_region("Spaceship", world.player) + dw_map: Region = create_region(world, "Death Wish Map") + entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + + add_rule(entrance, lambda state: state.has("Time Piece", world.player, + world.multiworld.DWTimePieceRequirement[world.player].value)) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list: List[str] = [] + for name in death_wishes.keys(): + if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): + continue + + dw_list.append(name) + + world.random.shuffle(dw_list) + count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + dw_shuffle: List[str] = [] + total = min(len(dw_list), count) + for i in range(total): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.multiworld.EndGoal[world.player].value == 3: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.set_dw_shuffle(dw_shuffle) + prev_dw: Region + for i in range(len(dw_shuffle)): + name = dw_shuffle[i] + dw = create_region(world, name) + + if i == 0: + connect_regions(dw_map, dw, f"-> {name}", world.player) + else: + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + + loc_id = death_wishes[name] + main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + + if name in world.get_excluded_bonuses(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) + prev_dw = dw + else: + for key, loc_id in death_wishes.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + world.get_excluded_dws().append(key) + continue + + dw = create_region(world, key) + + if key == "Beat the Heat": + connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + elif key in dw_prereqs.keys(): + for name in dw_prereqs[key]: + parent = world.multiworld.get_region(name, world.player) + connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + + main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + + if key in world.get_excluded_bonuses(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(key): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) \ No newline at end of file diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index e69de29bb2..d40da5d90f 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,491 @@ +from worlds.AutoWorld import World, CollectionState +from .Locations import LocData, death_wishes, HatInTimeLocation +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings +from .Types import HatType +from .DeathWishLocations import dw_prereqs, dw_candles +from .Items import HatInTimeItem +from BaseClasses import Entrance, Location, ItemClassification +from worlds.generic.Rules import add_rule +from typing import List, Callable + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "Beat the Heat": LocData(umbrella=True), + "So You're Back From Outer Space": LocData(hookshot=True), + "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), + "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), + + "Security Breach": LocData(hit_requirement=1), + "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), + + "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Boss Rush": LocData(hit_requirement=1, umbrella=True), + "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Bird Sanctuary": LocData(hookshot=True), + "Wound-Up Windmill": LocData(hookshot=True), + "The Illness has Speedrun": LocData(hookshot=True, required_hats=[HatType.SPRINT]), + "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Camera Tourist": LocData(misc_required=["Camera Badge"]), + + "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Rift Collapse - Deep Sea": LocData(hookshot=True, required_hats=[HatType.DWELLER]), +} + +# Includes main objective requirements +dw_bonus_requirements = { + # Some One-Hit Hero requirements need badge pins as well because of Hookshot + "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]), + + "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge"]), + "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), + + "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), + "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), + + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.SPRINT]), +} + +dw_stamp_costs = { + "So You're Back From Outer Space": 2, + "Collect-a-thon": 5, + "She Speedran from Outer Space": 8, + "Encore! Encore!": 10, + + "Security Breach": 4, + "The Great Big Hootenanny": 7, + "10 Seconds until Self-Destruct": 15, + "Killing Two Birds": 25, + "Snatcher Coins in Nyakuza Metro": 30, + + "Speedrun Well": 10, + "Boss Rush": 15, + "Quality Time with Snatcher": 20, + "Breaching the Contract": 40, + + "Bird Sanctuary": 15, + "Wound-Up Windmill": 30, + "The Illness has Speedrun": 35, + + "The Mustache Gauntlet": 35, + "No More Bad Guys": 50, + "Seal the Deal": 70, +} + + +def set_dw_rules(world: World): + if "Snatcher's Hit List" not in world.get_excluded_dws() \ + or "Camera Tourist" not in world.get_excluded_dws(): + create_enemy_events(world) + + dw_list: List[str] = [] + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list = world.get_dw_shuffle() + else: + for name in death_wishes.keys(): + dw_list.append(name) + + for name in dw_list: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + dw = world.multiworld.get_region(name, world.player) + temp_list: List[Location] = [] + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + temp_list.append(main_objective) + temp_list.append(full_clear) + + if world.multiworld.DWShuffle[world.player].value == 0: + if name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + + if world.multiworld.DWEnableBonus[world.player].value == 0: + # place nothing, but let the locations exist still, so we can use them for bonus stamp rules + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + full_clear.show_in_spoiler = False + + # Stamps are event locations + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + # Specific Rules + if name == "The Illness has Speedrun": + # killing the flowers without the umbrella is way too slow + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + elif name == "The Mustache Gauntlet": + # don't get burned bonus requires a way to kill fire crows without being burned + add_rule(full_clear, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE)) + elif name == "Vault Codes in the Wind": + add_rule(main_objective, lambda state: can_use_hat(state, world, HatType.TIME_STOP), "or") + + if name in dw_candles: + set_candle_dw_rules(name, world) + + main_rule: Callable[[CollectionState], bool] + + for i in range(len(temp_list)): + loc = temp_list[i] + data: LocData + + if loc.name == main_objective.name: + data = dw_requirements.get(name) + else: + data = dw_bonus_requirements.get(name) + + if data is None: + continue + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(loc, lambda state, paintings=data.paintings: state.has("Progressive Painting Unlock", + world.player, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(loc, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + main_rule = main_objective.access_rule + + if loc.name == main_objective.name: + add_rule(main_stamp, loc.access_rule) + elif loc.name == full_clear.name: + add_rule(loc, main_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + and not world.is_bonus_excluded(loc.name): + add_rule(bonus_stamps, loc.access_rule) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_shuffle = world.get_dw_shuffle() + for i in range(len(dw_shuffle)): + if i == 0: + continue + + name = dw_shuffle[i] + prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) + add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) + else: + for key, reqs in dw_prereqs.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + access_rules: List[Callable[[CollectionState], bool]] = [] + entrances: List[Entrance] = [] + + for parent in reqs: + entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player) + entrances.append(entrance) + + if not world.is_dw_excluded(parent): + access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player)) + + for entrance in entrances: + for rule in access_rules: + add_rule(entrance, rule) + + if world.multiworld.EndGoal[world.player].value == 3: + world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", + world.player) + + +def get_total_dw_stamps(state: CollectionState, world: World) -> int: + if world.multiworld.DWShuffle[world.player].value > 0: + return 999 # no stamp costs in death wish shuffle + + count: int = 0 + peace_and_tranquility: bool = world.multiworld.DWEnableBonus[world.player].value == 0 \ + and world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 + + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + if state.has(f"1 Stamp - {name}", world.player): + count += 1 + else: + continue + + # If bonus rewards and auto bonus completion is off, obtaining stamps via P&T is in logic + # Candles don't have P&T + if peace_and_tranquility and name not in dw_candles: + count += 2 + continue + + if state.has(f"2 Stamps - {name}", world.player): + count += 2 + elif name not in dw_candles: + # all non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + count += 1 + + return count + + +def set_candle_dw_rules(name: str, world: World): + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "Zero Jumps": + add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) + add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + and state.has("Train Rush Cleared", world.player) and can_use_hat(state, world, HatType.ICE)) + + elif name == "Snatcher's Hit List": + add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) + add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + + elif name == "Camera Tourist": + add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world)) + + elif name == "Snatcher Coins in Mafia Town": + add_rule(main_objective, lambda state: state.has("MT Access", world.player) + or state.has("HUMT Access", world.player)) + + add_rule(full_clear, lambda state: state.has("CTR Access", world.player) + or state.has("HUMT Access", world.player) + and (world.multiworld.UmbrellaLogic[world.player].value == 0 or state.has("Umbrella", world.player)) + or state.has("DWTM Access", world.player) + or state.has("TGV Access", world.player)) + + elif name == "Snatcher Coins in Battle of the Birds": + add_rule(main_objective, lambda state: state.has("PP Access", world.player) + or state.has("DBS Access", world.player) + or state.has("Train Rush Cleared", world.player)) + + add_rule(full_clear, lambda state: state.has("PP Access", world.player) + and state.has("DBS Access", world.player) + and state.has("Train Rush Cleared", world.player)) + + elif name == "Snatcher Coins in Subcon Forest": + add_rule(main_objective, lambda state: state.has("SF Access", world.player)) + + add_rule(main_objective, lambda state: has_paintings(state, world, 1) and (can_use_hookshot(state, world) + or can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + or has_paintings(state, world, 3)) + + add_rule(full_clear, lambda state: has_paintings(state, world, 3) and can_use_hookshot(state, world) + and (can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))) + + elif name == "Snatcher Coins in Alpine Skyline": + add_rule(main_objective, lambda state: state.has("LC Access", world.player) + or state.has("WM Access", world.player)) + + add_rule(full_clear, lambda state: state.has("LC Access", world.player) + and state.has("WM Access", world.player)) + + elif name == "Snatcher Coins in Nyakuza Metro": + add_rule(main_objective, lambda state: state.has("Bluefin Tunnel Cleared", world.player) + or (state.has("Nyakuza Intro Cleared", world.player) + and (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)))) + + add_rule(full_clear, lambda state: state.has("Bluefin Tunnel Cleared", world.player) + and (state.has("Nyakuza Intro Cleared", world.player) + and (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)))) + + +def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: + total: int = 0 + + for name, hats in zero_jumps.items(): + if not state.has(f"{name} Cleared", world.player): + continue + + valid: bool = True + + for hat in hats: + if not can_use_hat(state, world, hat): + valid = False + break + + if valid: + total += 1 + + return total + + +def get_reachable_enemy_count(state: CollectionState, world: World) -> int: + count: int = 0 + for enemy in hit_list.keys(): + if enemy in bosses: + continue + + if state.has(enemy, world.player): + count += 1 + + return count + + +def can_reach_all_bosses(state: CollectionState, world: World) -> bool: + for boss in bosses: + if not state.has(boss, world.player): + return False + + return True + + +def create_enemy_events(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and not world.is_dlc1() \ + or world.multiworld.ExcludeTour[world.player].value > 0: + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(area, world.player) + event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region) + event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + + if enemy == "Toxic Flower": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + if area == "The Illness has Spread": + add_rule(event, lambda state: not zipline_logic(world) or + state.has("Zipline Unlock - The Birdhouse Path", world.player) + or state.has("Zipline Unlock - The Lava Cake Path", world.player) + or state.has("Zipline Unlock - The Windmill Path", world.player)) + + elif enemy == "Director": + if area == "Dead Bird Studio Basement": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + elif enemy == "Snatcher" or enemy == "Mustache Girl": + if area == "Boss Rush": + # need to be able to kill toilet + add_rule(event, lambda state: can_hit(state, world)) + + elif area == "The Finale" and enemy == "Mustache Girl": + add_rule(event, lambda state: can_use_hookshot(state, world) + and can_use_hat(state, world, HatType.DWELLER)) + + elif enemy == "Shock Squid" or enemy == "Ninja Cat": + if area == "Time Rift - Deep Sea": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + +# Zero Jumps completable levels, with required hats if any +zero_jumps = { + "Welcome to Mafia Town": [], + "Cheating the Race": [HatType.TIME_STOP], + "Picture Perfect": [], + "Train Rush": [HatType.ICE], + "Contractual Obligations": [], + "Your Contract has Expired": [], + "Mail Delivery Service": [], # rule for needing sprint is already on act completion +} + +# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them +hit_list = { + "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", + "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks", + "So You're Back From Outer Space"], + + "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell", + "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet", + "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"], + + "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"], + + "Rat": ["Down with the Mafia!", "Bluefin Tunnel"], + + "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea", + "Rift Collapse: Sleepy Subcon"], + + "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"], + + "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well", + "The Lava Cake", "The Windmill"], + + "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary", + "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"], + + "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"], + + "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"], + + "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"], + + "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet", + "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea", + "Rift Collapse: Alpine Skyline"], + + # Bosses + "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], + + "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Toilet": ["Toilet of Doom", "Boss Rush"], + + "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", + "Quality Time with Snatcher"], + + "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"], + + "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], +} + +bosses = [ + "Mafia Boss", + "Conductor", + "Toilet", + "Snatcher", + "Toxic Flower", + "Mustache Girl", +] diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index deac2587b6..729350da1d 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,19 +1,121 @@ from BaseClasses import Item, ItemClassification from worlds.AutoWorld import World -from .Types import HatDLC -import typing +from .Types import HatDLC, HatType +from .Locations import get_total_locations +from .Rules import get_difficulty, is_player_knowledgeable +from typing import Optional, NamedTuple, List, Dict -class ItemData(typing.NamedTuple): - code: typing.Optional[int] +class ItemData(NamedTuple): + code: Optional[int] classification: ItemClassification - dlc_flags: typing.Optional[HatDLC] = HatDLC.none + dlc_flags: Optional[HatDLC] = HatDLC.none class HatInTimeItem(Item): game: str = "A Hat in Time" +def create_itempool(world: World) -> List[Item]: + itempool: List[Item] = [] + if not world.is_dw_only(): + calculate_yarn_costs(world) + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.multiworld.YarnAvailable[world.player].value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(world, name): + continue + + item_type: ItemClassification = item_table.get(name).classification + if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \ + and (name == "Scooter Badge" or name == "No Bonk Badge"): + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin": + item_type = ItemClassification.progression + + if world.is_dw_only(): + if item_type is ItemClassification.progression \ + or item_type is ItemClassification.progression_skip_balancing: + continue + + # progression balance anything useful, since we have basically no progression in this mode + if item_type is ItemClassification.useful: + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + continue + + if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + continue + + if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if world.is_dlc1(): + max_extra += 6 + + if world.is_dlc2(): + max_extra += 10 + + tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) + + for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) + + total_locations: int = get_total_locations(world) + itempool += create_junk_items(world, total_locations - len(itempool)) + return itempool + + +def calculate_yarn_costs(world: World): + mw = world.multiworld + p = world.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + world.get_hat_yarn_costs()[HatType(i)] = cost + max_cost += cost + + available_yarn: int = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + if max_cost + mw.MinExtraYarn[p].value > available_yarn: + mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + + def item_dlc_enabled(world: World, name: str) -> bool: data = item_table[name] @@ -29,43 +131,38 @@ def item_dlc_enabled(world: World, name: str) -> bool: return False -def get_total_time_pieces(world: World) -> int: - count: int = 40 - if world.is_dlc1(): - count += 6 - - if world.is_dlc2(): - count += 10 - - return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) - - def create_item(world: World, name: str) -> Item: data = item_table[name] return HatInTimeItem(name, data.classification, data.code, world.player) -def create_multiple_items(world: World, name: str, count: int = 1) -> typing.List[Item]: +def create_multiple_items(world: World, name: str, count: int = 1, + item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + data = item_table[name] - itemlist: typing.List[Item] = [] + itemlist: List[Item] = [] for i in range(count): - itemlist += [HatInTimeItem(name, data.classification, data.code, world.player)] + itemlist += [HatInTimeItem(name, item_type, data.code, world.player)] return itemlist -def create_junk_items(world: World, count: int) -> typing.List[Item]: +def create_junk_items(world: World, count: int) -> List[Item]: trap_chance = world.multiworld.TrapChance[world.player].value - junk_pool: typing.List[Item] = [] - junk_list: typing.Dict[str, int] = {} - trap_list: typing.Dict[str, int] = {} + junk_pool: List[Item] = [] + junk_list: Dict[str, int] = {} + trap_list: Dict[str, int] = {} ic: ItemClassification for name in item_table.keys(): ic = item_table[name].classification if ic == ItemClassification.filler: + if world.is_dw_only() and "Pons" in name: + continue + junk_list[name] = junk_weights.get(name) + elif trap_chance > 0 and ic == ItemClassification.trap: if name == "Baby Trap": trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value @@ -75,12 +172,12 @@ def create_junk_items(world: World, count: int) -> typing.List[Item]: trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value for i in range(count): - if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance: + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: junk_pool += [world.create_item( - world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] else: junk_pool += [world.create_item( - world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] return junk_pool @@ -110,21 +207,22 @@ ahit_items = { "Hover Badge": ItemData(300026, ItemClassification.useful), "Hookshot Badge": ItemData(300027, ItemClassification.progression), "Item Magnet Badge": ItemData(300028, ItemClassification.useful), - "No Bonk Badge": ItemData(300029, ItemClassification.progression), + "No Bonk Badge": ItemData(300029, ItemClassification.useful), "Compass Badge": ItemData(300030, ItemClassification.useful), - "Scooter Badge": ItemData(300031, ItemClassification.progression), - "Badge Pin": ItemData(300043, ItemClassification.useful), + "Scooter Badge": ItemData(300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), # Other - # "Rift Token": ItemData(300032, ItemClassification.filler), - "Random Cosmetic": ItemData(300044, ItemClassification.filler), "Umbrella": ItemData(300033, ItemClassification.progression), + "Badge Pin": ItemData(300043, ItemClassification.useful), # Garbage items "25 Pons": ItemData(300034, ItemClassification.filler), "50 Pons": ItemData(300035, ItemClassification.filler), "100 Pons": ItemData(300036, ItemClassification.filler), "Health Pon": ItemData(300037, ItemClassification.filler), + "Random Cosmetic": ItemData(300044, ItemClassification.filler), # Traps "Baby Trap": ItemData(300039, ItemClassification.trap), @@ -144,10 +242,6 @@ ahit_items = { "Metro Ticket - Green": ItemData(300046, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Blue": ItemData(300047, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Pink": ItemData(300048, ItemClassification.progression, HatDLC.dlc2), - - # Death Wish items - "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), - "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), } act_contracts = { @@ -180,10 +274,10 @@ item_frequencies = { junk_weights = { "25 Pons": 50, - "50 Pons": 10, + "50 Pons": 25, + "100 Pons": 10, "Health Pon": 35, - "100 Pons": 5, - "Random Cosmetic": 25, + "Random Cosmetic": 35, } item_table = { diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 5e32240b30..8e63dad539 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -6,12 +6,13 @@ from .Options import TasksanityCheckCount class LocData(NamedTuple): - id: int - region: str + id: Optional[int] = 0 + region: Optional[str] = "" required_hats: Optional[List[HatType]] = [HatType.NONE] hookshot: Optional[bool] = False dlc_flags: Optional[HatDLC] = HatDLC.none paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] # For UmbrellaLogic setting umbrella: Optional[bool] = False # Umbrella required for this check @@ -29,12 +30,28 @@ class HatInTimeLocation(Location): def get_total_locations(world: World) -> int: total: int = 0 - for (name) in location_table.keys(): - if is_location_valid(world, name): - total += 1 + if not world.is_dw_only(): + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 - if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: - total += world.multiworld.TasksanityCheckCount[world.player].value + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + if world.is_dw(): + if world.multiworld.DWShuffle[world.player].value > 0: + total += len(world.get_dw_shuffle()) + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += len(world.get_dw_shuffle()) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += 37 + if world.is_dlc2(): + total += 1 return total @@ -58,15 +75,20 @@ def is_location_valid(world: World, location: str) -> bool: if not location_dlc_enabled(world, location): return False - if location in storybook_pages.keys() \ - and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + and location in storybook_pages.keys(): return False - if location in shop_locations and location not in world.shop_locs: + if location not in world.shop_locs and location in shop_locations: return False data = location_table.get(location) or event_locs.get(location) - if data.region == "Time Rift - Tour" and world.multiworld.ExcludeTour[world.player].value > 0: + if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + return False + + # No need for all those event items if we're not doing candles + if data.dlc_flags is HatDLC.death_wish and world.multiworld.DWExcludeCandles[world.player].value > 0 \ + and location in event_locs.keys(): return False return True @@ -76,7 +98,11 @@ def get_location_names() -> Dict[str, int]: names = {name: data.id for name, data in location_table.items()} id_start: int = get_tasksanity_start_id() for i in range(TasksanityCheckCount.range_end): - names.setdefault(format("Tasksanity Check %i") % (i+1), id_start+i) + names.setdefault(f"Tasksanity Check {i+1}", id_start+i) + + for (key, loc_id) in death_wishes.items(): + names.setdefault(f"{key} - Main Objective", loc_id) + names.setdefault(f"{key} - All Clear", loc_id+1) return names @@ -169,7 +195,7 @@ ahit_locations = { "Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True), "Dead Bird Studio Basement - Locked Room": LocData(305819, "Dead Bird Studio Basement", hookshot=True), - # 320000 range - Subcon Forest + # Subcon Forest "Contractual Obligations - Cherry Bomb Bone Cage": LocData(324761, "Contractual Obligations"), "Subcon Village - Tree Top Ice Cube": LocData(325078, "Subcon Forest Area"), "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), @@ -178,7 +204,8 @@ ahit_locations = { "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), - + + "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=1), @@ -189,30 +216,6 @@ ahit_locations = { "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), - "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), - "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - hookshot=True, - paintings=3), - "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), @@ -221,7 +224,6 @@ ahit_locations = { "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), - "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=2), @@ -234,6 +236,30 @@ ahit_locations = { "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=3), + + "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + hookshot=True, + paintings=3), "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", hit_requirement=1, paintings=1), "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", hit_requirement=1, paintings=1), @@ -245,7 +271,7 @@ ahit_locations = { "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - # 330000 range - Alpine Skyline + # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Goat Village"), "Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"), @@ -320,7 +346,6 @@ ahit_locations = { } act_completions = { - # 310000 range - Act Completions "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery"), "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), @@ -479,13 +504,6 @@ storybook_pages = { "Rumbi Factory - Page: Last Area": LocData(345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), } -contract_locations = { - "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), - "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), - "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), - "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), -} - shop_locations = { "Badge Seller - Item 1": LocData(301003, "Badge Seller"), "Badge Seller - Item 2": LocData(301004, "Badge Seller"), @@ -623,6 +641,13 @@ shop_locations = { } +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), +} + # Don't put any of the locations from peaks here, the rules for their entrances are set already zipline_unlocks = { "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", @@ -652,7 +677,9 @@ tihs_locations = [ event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), - "Subcon Forest Access": LocData(0, "Subcon Forest Area", act_complete_event=False), + "TOD Access": LocData(0, "Toilet of Doom", act_complete_event=False), + "YCHE Access": LocData(0, "Your Contract has Expired", act_complete_event=False), + "Birdhouse Cleared": LocData(0, "The Birdhouse"), "Lava Cake Cleared": LocData(0, "The Lava Cake"), "Windmill Cleared": LocData(0, "The Windmill"), @@ -670,6 +697,39 @@ event_locs = { "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2), "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2), "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2), + + + # -------------- Death Wish Candle Related --------------- # + + + # Snatcher Coins + "MT Access": LocData(0, "Mafia Town Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "DWTM Access": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "CTR Access": LocData(0, "Cheating the Race", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "TGV Access": LocData(0, "The Golden Vault", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "DBS Access": LocData(0, "Dead Bird Studio - Elevator Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "PP Access": LocData(0, "Picture Perfect", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "SF Access": LocData(0, "Subcon Forest Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "LC Access": LocData(0, "The Lava Cake", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "WM Access": LocData(0, "The Windmill", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + # Camera Tourist + "Mafia Boss": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Conductor": LocData(0, "Dead Bird Studio Basement", dlc_flags=HatDLC.death_wish), + "Snatcher": LocData(0, "Your Contract has Expired", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Evil Flower": LocData(0, "The Illness has Spread", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + # Zero Jumps + "Welcome to Mafia Town Cleared": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + "Picture Perfect Cleared": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + "Contractual Obligations Cleared": LocData(0, "Contractual Obligations", dlc_flags=HatDLC.death_wish), + "Your Contract has Expired Cleared": LocData(0, "Your Contract has Expired", dlc_flags=HatDLC.death_wish), + "Mail Delivery Service Cleared": LocData(0, "Mail Delivery Service", dlc_flags=HatDLC.death_wish), + "Cheating the Race Cleared": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Train Rush Cleared": LocData(0, "Train Rush", dlc_flags=HatDLC.death_wish), } location_table = { @@ -679,3 +739,52 @@ location_table = { **contract_locations, **shop_locations, } + +# DO NOT ALTER THE ORDER OF THIS LIST +# This file is in here instead of DeathWishLocations.py to prevent circular import problems +death_wishes = { + "Beat the Heat": 350000, + "Snatcher's Hit List": 350002, + "So You're Back From Outer Space": 350004, + "Collect-a-thon": 350006, + "Rift Collapse: Mafia of Cooks": 350008, + "She Speedran from Outer Space": 350010, + "Mafia's Jumps": 350012, + "Vault Codes in the Wind": 350014, + "Encore! Encore!": 350016, + "Snatcher Coins in Mafia Town": 350018, + + "Security Breach": 350020, + "The Great Big Hootenanny": 350022, + "Rift Collapse: Dead Bird Studio": 350024, + "10 Seconds until Self-Destruct": 350026, + "Killing Two Birds": 350028, + "Snatcher Coins in Battle of the Birds": 350030, + "Zero Jumps": 350032, + + "Speedrun Well": 350034, + "Rift Collapse: Sleepy Subcon": 350036, + "Boss Rush": 350038, + "Quality Time with Snatcher": 350040, + "Breaching the Contract": 350042, + "Snatcher Coins in Subcon Forest": 350044, + + "Bird Sanctuary": 350046, + "Rift Collapse: Alpine Skyline": 350048, + "Wound-Up Windmill": 350050, + "The Illness has Speedrun": 350052, + "Snatcher Coins in Alpine Skyline": 350054, + "Camera Tourist": 350056, + + "The Mustache Gauntlet": 350058, + "No More Bad Guys": 350060, + + "Seal the Deal": 350062, + "Rift Collapse: Deep Sea": 350064, + "Cruisin' for a Bruisin'": 350066, + + "Community Rift: Rhythm Jump Studio": 350068, + "Community Rift: Twilight Travels": 350070, + "Community Rift: The Mountain Rift": 350072, + "Snatcher Coins in Nyakuza Metro": 350074, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 97cd838762..2356302913 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,7 +1,6 @@ import typing from worlds.AutoWorld import World from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict -from .Items import get_total_time_pieces def adjust_options(world: World): @@ -46,25 +45,59 @@ def adjust_options(world: World): if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: world.multiworld.EndGoal[world.player].value = 1 + # Don't allow Seal the Deal goal if Death Wish content is disabled + if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): + world.multiworld.EndGoal[world.player].value = 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + + if world.is_dw_only(): + world.multiworld.EndGoal[world.player].value = 3 + world.multiworld.ActRandomizer[world.player].value = 0 + world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 + world.multiworld.ShuffleSubconPaintings[world.player].value = 0 + world.multiworld.ShuffleStorybookPages[world.player].value = 0 + world.multiworld.ShuffleActContracts[world.player].value = 0 + world.multiworld.EnableDLC1[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = 0 + world.multiworld.KnowledgeChecks[world.player].value = 0 + world.multiworld.DWTimePieceRequirement[world.player].value = 0 + world.multiworld.progression_balancing[world.player].value = 0 + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + -# General class EndGoal(Choice): """The end goal required to beat the game. Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. - Requires DLC2 content to be enabled.""" + Requires DLC2 content to be enabled. + + Seal the Deal: Reach and complete the Seal the Deal death wish main objective. + Requires Death Wish content to be enabled.""" display_name = "End Goal" option_finale = 1 option_rush_hour = 2 + option_seal_the_deal = 3 default = 1 class ActRandomizer(Choice): """If enabled, shuffle the game's Acts between each other. Light will cause Time Rifts to only be shuffled amongst each other, - and Blue Time Rifts and Purple Time Rifts are shuffled separately.""" + and Blue Time Rifts and Purple Time Rifts to be shuffled separately.""" display_name = "Shuffle Acts" option_false = 0 option_light = 1 @@ -77,14 +110,9 @@ class ActPlando(OptionDict): display_name = "Act Plando" -class ShuffleAlpineZiplines(Toggle): - """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" - display_name = "Shuffle Alpine Ziplines" - default = 0 - - class FinaleShuffle(Toggle): """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" + display_name = "Finale Shuffle" default = 0 @@ -105,9 +133,13 @@ class KnowledgeChecks(Toggle): default = 0 -class RandomizeHatOrder(Toggle): - """Randomize the order that hats are stitched in.""" +class RandomizeHatOrder(Choice): + """Randomize the order that hats are stitched in. + Time Stop Last will force Time Stop to be the last hat in the sequence.""" display_name = "Randomize Hat Order" + option_false = 0 + option_true = 1 + option_time_stop_last = 2 default = 1 @@ -127,12 +159,6 @@ class TimePieceBalancePercent(Range): range_end = 100 -class UmbrellaLogic(Toggle): - """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" - display_name = "Umbrella Logic" - default = 0 - - class StartWithCompassBadge(Toggle): """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world (instead of just Relics). Recommended if you're not familiar with where item locations are.""" @@ -151,6 +177,12 @@ class CompassBadgeMode(Choice): default = 1 +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + class ShuffleStorybookPages(Toggle): """If enabled, each storybook page in the purple Time Rifts is an item check. The Compass Badge can track these down for you.""" @@ -164,6 +196,12 @@ class ShuffleActContracts(Toggle): default = 1 +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + class ShuffleSubconPaintings(Toggle): """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. These items are progressive, with the order of Village-Swamp-Courtyard.""" @@ -181,13 +219,138 @@ class StartingChapter(Choice): default = 1 +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 4 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch a hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch a hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 75 + default = 45 + + +class MinExtraYarn(Range): + """The minimum amount of extra yarn in the item pool. + There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, + there must be at least 50 yarn in the pool.""" + display_name = "Max Extra Yarn" + range_start = 0 + range_end = 15 + default = 10 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 300 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + class CTRWithSprint(Toggle): """If enabled, clearing Cheating the Race with just Sprint Hat can be in logic.""" display_name = "Cheating the Race with Sprint Hat" default = 0 -# DLC class EnableDLC1(Toggle): """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" @@ -279,151 +442,118 @@ class BaseballBat(Toggle): default = 0 -class ChapterCostIncrement(Range): - """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" - display_name = "Chapter Cost Increment" - range_start = 1 - range_end = 8 - default = 4 - - -class ChapterCostMinDifference(Range): - """The minimum difference between chapter costs.""" - display_name = "Minimum Chapter Cost Difference" - range_start = 1 - range_end = 8 - default = 5 - - -class LowestChapterCost(Range): - """Value determining the lowest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" - display_name = "Lowest Possible Chapter Cost" - range_start = 0 - range_end = 10 - default = 5 - - -class HighestChapterCost(Range): - """Value determining the highest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" - display_name = "Highest Possible Chapter Cost" - range_start = 15 - range_end = 45 - default = 25 - - -class FinalChapterMinCost(Range): - """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" - display_name = "Final Chapter Minimum Time Piece Cost" - range_start = 0 - range_end = 50 - default = 30 - - -class FinalChapterMaxCost(Range): - """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" - display_name = "Final Chapter Maximum Time Piece Cost" - range_start = 0 - range_end = 50 - default = 35 - - -class MaxExtraTimePieces(Range): - """Maximum amount of extra Time Pieces from the DLCs. - Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" - display_name = "Max Extra Time Pieces" - range_start = 0 - range_end = 16 - default = 16 - - -# Death Wish class EnableDeathWish(Toggle): - """NOT IMPLEMENTED Shuffle Death Wish contracts into the game. - Each contract by default will have a single check granted upon completion. + """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" display_name = "Enable Death Wish" default = 0 +class DeathWishOnly(Toggle): + """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed. + This has the following effects: + - Death Wish is instantly unlocked from the start + - All hats and other progression items are instantly given to you + - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start + - All chapters and their levels are unlocked, act shuffle is forced off + - Any checks other than Death Wish contracts are completely removed + - All Pons in the item pool are replaced with Health Pons or random cosmetics + - The EndGoal option is forced to complete Seal the Deal""" + display_name = "Death Wish Only" + default = 0 + + +class DWShuffle(Toggle): + """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order. + Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence. + If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. + Disabling candles is highly recommended.""" + display_name = "Death Wish Shuffle" + default = 0 + + +class DWShuffleCountMin(Range): + """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Minimum Count" + range_start = 5 + range_end = 38 + default = 18 + + +class DWShuffleCountMax(Range): + """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Maximum Count" + range_start = 5 + range_end = 38 + default = 25 + + class DWEnableBonus(Toggle): - """NOT IMPLEMENTED In Death Wish, allow the full completion of contracts to reward items.""" + """In Death Wish, allow the full completion of contracts to reward items. + WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! + ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! + Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" display_name = "Shuffle Death Wish Full Completions" default = 0 +class DWAutoCompleteBonuses(Toggle): + """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. + This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" + display_name = "Auto Complete Bonus Stamps" + default = 1 + + class DWExcludeAnnoyingContracts(Toggle): - """NOT IMPLEMENTED Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.""" + """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. + Excluded Death Wishes are automatically completed as soon as they are unlocked. + This option currently excludes the following contracts: + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" display_name = "Exclude Annoying Death Wish Contracts" default = 1 class DWExcludeAnnoyingBonuses(Toggle): - """NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions - from the pool. DANGER! DISABLE AT YOUR OWN RISK! THIS OPTION WHEN DISABLED CAN CREATE VERY DIFFICULT SEEDS!!!""" + """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. + Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. + This option currently excludes the following bonuses: + - So You're Back From Outer Space + - Encore! Encore! + - Snatcher's Hit List + - 10 Seconds until Self-Destruct + - Killing Two Birds + - Snatcher Coins in Battle of the Birds + - Zero Jumps + - Bird Sanctuary + - Wound-Up Windmill + - Snatcher Coins in Alpine Skyline + - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 -# Yarn -class YarnCostMin(Range): - """The minimum possible yarn needed to stitch each hat.""" - display_name = "Minimum Yarn Cost" - range_start = 1 - range_end = 12 - default = 4 +class DWExcludeCandles(Toggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + default = 1 -class YarnCostMax(Range): - """The maximum possible yarn needed to stitch each hat.""" - display_name = "Maximum Yarn Cost" - range_start = 1 - range_end = 12 - default = 8 - - -class YarnAvailable(Range): - """How much yarn is available to collect in the item pool.""" - display_name = "Yarn Available" - range_start = 30 - range_end = 75 - default = 45 - - -class MinPonCost(Range): - """The minimum amount of Pons that any shop item can cost.""" - display_name = "Minimum Shop Pon Cost" - range_start = 10 - range_end = 800 - default = 75 - - -class MaxPonCost(Range): - """The maximum amount of Pons that any shop item can cost.""" - display_name = "Maximum Shop Pon Cost" - range_start = 10 - range_end = 800 - default = 400 - - -class BadgeSellerMinItems(Range): - """The smallest amount of items that the Badge Seller can have for sale.""" - display_name = "Badge Seller Minimum Items" +class DWTimePieceRequirement(Range): + """How many Time Pieces that will be required to unlock Death Wish.""" + display_name = "Death Wish Time Piece Requirement" range_start = 0 - range_end = 10 - default = 4 + range_end = 35 + default = 15 -class BadgeSellerMaxItems(Range): - """The largest amount of items that the Badge Seller can have for sale.""" - display_name = "Badge Seller Maximum Items" - range_start = 0 - range_end = 10 - default = 8 - - -# Traps class TrapChance(Range): """The chance for any junk item in the pool to be replaced by a trap.""" display_name = "Trap Chance" @@ -487,7 +617,18 @@ ahit_options: typing.Dict[str, type(Option)] = { "ExcludeTour": ExcludeTour, "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, - "EnableDeathWish": EnableDeathWish, + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DWShuffleCountMin": DWShuffleCountMin, + "DWShuffleCountMax": DWShuffleCountMax, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, + "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, + "DWExcludeCandles": DWExcludeCandles, + "DWTimePieceRequirement": DWTimePieceRequirement, + "EnableDLC2": EnableDLC2, "BaseballBat": BaseballBat, "MetroMinPonCost": MetroMinPonCost, @@ -507,6 +648,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "YarnCostMin": YarnCostMin, "YarnCostMax": YarnCostMax, "YarnAvailable": YarnAvailable, + "MinExtraYarn": MinExtraYarn, "MinPonCost": MinPonCost, "MaxPonCost": MaxPonCost, @@ -523,34 +665,39 @@ ahit_options: typing.Dict[str, type(Option)] = { slot_data_options: typing.Dict[str, type(Option)] = { - "EndGoal": EndGoal, - "ActRandomizer": ActRandomizer, - "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "LogicDifficulty": LogicDifficulty, - "KnowledgeChecks": KnowledgeChecks, - "RandomizeHatOrder": RandomizeHatOrder, - "UmbrellaLogic": UmbrellaLogic, - "CompassBadgeMode": CompassBadgeMode, - "ShuffleStorybookPages": ShuffleStorybookPages, - "ShuffleActContracts": ShuffleActContracts, - "ShuffleSubconPaintings": ShuffleSubconPaintings, + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "KnowledgeChecks": KnowledgeChecks, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, - "EnableDLC1": EnableDLC1, - "Tasksanity": Tasksanity, - "TasksanityTaskStep": TasksanityTaskStep, - "TasksanityCheckCount": TasksanityCheckCount, - "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, - "ExcludeTour": ExcludeTour, + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, - "EnableDeathWish": EnableDeathWish, + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWTimePieceRequirement": DWTimePieceRequirement, - "EnableDLC2": EnableDLC2, - "MetroMinPonCost": MetroMinPonCost, - "MetroMaxPonCost": MetroMaxPonCost, - "BaseballBat": BaseballBat, + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, - "MinPonCost": MinPonCost, - "MaxPonCost": MaxPonCost, + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, - "death_link": DeathLink, + "death_link": DeathLink, } diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index be13fa1f22..d7711cbdd6 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -254,12 +254,16 @@ blacklisted_acts = { # Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. blacklisted_combos = { - "The Illness has Spread": ["Alpine Free Roam"], - "Rush Hour": ["Nyakuza Free Roam"], - "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam"], + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!"], "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"], "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam"], "Time Rift - Rumbi Factory": ["Alpine Free Roam"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam"], } @@ -271,6 +275,11 @@ def create_regions(world: World): # ------------------------------------------- HUB -------------------------------------------------- # menu = create_region(w, "Menu") spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + + # we only need the menu and the spaceship regions + if world.is_dw_only(): + return + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) create_rift_connections(w, create_region(w, "Time Rift - The Lab")) @@ -418,8 +427,8 @@ def create_rift_connections(world: World, region: Region): i = 1 for name in rift_access_regions[region.name]: act_region = world.multiworld.get_region(name, world.player) - entrance_name = "{name} Portal - Entrance {num}" - connect_regions(act_region, region, entrance_name.format(name=region.name, num=i), world.player) + entrance_name = f"{region.name} Portal - Entrance {i}" + connect_regions(act_region, region, entrance_name, world.player) i += 1 @@ -427,7 +436,7 @@ def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = get_tasksanity_start_id() for i in range(world.multiworld.TasksanityCheckCount[world.player].value): - location = HatInTimeLocation(world.player, format("Tasksanity Check %i" % (i+1)), id_start+i, ship_shape) + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) @@ -603,7 +612,7 @@ def randomize_act_entrances(world: World): candidate: Region if len(candidate_list) > 0: - candidate = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] else: # plando can still break certain rules, so acts may not always end up shuffled. for c in region_list: @@ -619,7 +628,7 @@ def randomize_act_entrances(world: World): if region.name in rift_access_regions.keys(): rift_dict.setdefault(region.name, candidate) - world.update_chapter_act_info(region, candidate) + update_chapter_act_info(world, region, candidate) continue if region.name in rift_access_regions.keys(): @@ -634,14 +643,14 @@ def randomize_act_entrances(world: World): entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) - world.update_chapter_act_info(region, candidate) + update_chapter_act_info(world, region, candidate) for name in blacklisted_acts.values(): if not is_act_blacklisted(world, name): continue region: Region = world.multiworld.get_region(name, world.player) - world.update_chapter_act_info(region, region) + update_chapter_act_info(world, region, region) set_rift_rules(world, rift_dict) @@ -650,7 +659,7 @@ def connect_time_rift(world: World, time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 while i <= count: - name = format("%s Portal - Entrance %i" % (time_rift.name, i)) + name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance = world.multiworld.get_entrance(name, world.player) reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 @@ -686,6 +695,9 @@ def create_region(world: World, name: str) -> Region: reg = Region(name, world.player, world.multiworld) for (key, data) in location_table.items(): + if world.is_dw_only(): + break + if data.nyakuza_thug != "": continue @@ -710,11 +722,11 @@ def create_badge_seller(world: World) -> Region: max_items: int = 0 if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: - max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, world.multiworld.BadgeSellerMaxItems[world.player].value) if max_items <= 0: - world.badge_seller_count = 0 + world.set_badge_seller_count(0) return badge_seller for (key, data) in shop_locations.items(): @@ -729,14 +741,15 @@ def create_badge_seller(world: World) -> Region: if count >= max_items: break - world.badge_seller_count = max_items + world.set_badge_seller_count(max_items) return badge_seller -def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int): +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: entrance = Entrance(player, entrancename, start_region) start_region.exits.append(entrance) entrance.connect(exit_region) + return entrance # Takes an entrance, removes its old connections, and reconnects it between the two regions specified. @@ -785,12 +798,29 @@ def get_act_original_chapter(world: World, act_name: str) -> Region: return world.multiworld.get_region(act_chapters[act_name], world.player) +# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game +def update_chapter_act_info(world: World, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + world.act_connections[original_act_info] = new_act_info + + +def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name + + def create_thug_shops(world: World): min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value count: int = -1 step: int = 0 old_name: str = "" + thug_items = world.get_nyakuza_thug_items() for key, data in shop_locations.items(): if data.nyakuza_thug == "": @@ -800,14 +830,14 @@ def create_thug_shops(world: World): continue try: - if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: + if thug_items[data.nyakuza_thug] <= 0: continue except KeyError: pass if count == -1: - count = world.multiworld.random.randint(min_items, max_items) - world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) + count = world.random.randint(min_items, max_items) + thug_items.setdefault(data.nyakuza_thug, count) if count <= 0: continue @@ -823,6 +853,8 @@ def create_thug_shops(world: World): step = 0 count = -1 + world.set_nyakuza_thug_items(thug_items) + def create_events(world: World) -> int: count: int = 0 @@ -832,9 +864,10 @@ def create_events(world: World) -> int: continue event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + event.show_in_spoiler = False if data.act_complete_event: - act_completion: str = format("Act Completion (%s)" % data.region) + act_completion: str = f"Act Completion ({data.region})" event.access_rule = world.multiworld.get_location(act_completion, world.player).access_rule count += 1 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7fc5bf93e7..4030a27e3c 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -37,8 +37,9 @@ def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: def get_hat_cost(world: World, hat: HatType) -> int: cost: int = 0 + costs = world.get_hat_yarn_costs() for h in world.get_hat_craft_order(): - cost += world.get_hat_yarn_costs().get(h) + cost += costs[h] if h == hat: break @@ -120,7 +121,7 @@ def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bo if "Free Roam" in entrance.connected_region.name: return True - name: str = format("Act Completion (%s)" % entrance.connected_region.name) + name: str = f"Act Completion ({entrance.connected_region.name})" return world.multiworld.get_location(name, world.player).access_rule(state) @@ -153,6 +154,9 @@ def set_rules(world: World): if world.multiworld.EndGoal[world.player].value == 2: final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) + elif world.multiworld.EndGoal[world.player].value == 3: + final_chapter = None + chapter_list.append(ChapterIndex.FINALE) if world.is_dlc1(): chapter_list.append(ChapterIndex.CRUISE) @@ -161,7 +165,7 @@ def set_rules(world: World): chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) - world.multiworld.random.shuffle(chapter_list) + world.random.shuffle(chapter_list) if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1: int = 69 @@ -180,7 +184,7 @@ def set_rules(world: World): if lowest_index == 0: pos = 0 else: - pos = world.multiworld.random.randint(0, lowest_index) + pos = world.random.randint(0, lowest_index) chapter_list.insert(pos, ChapterIndex.ALPINE) @@ -190,7 +194,7 @@ def set_rules(world: World): if index >= len(chapter_list): chapter_list.append(ChapterIndex.METRO) else: - chapter_list.insert(world.multiworld.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value highest_cost: int = world.multiworld.HighestChapterCost[world.player].value @@ -206,10 +210,9 @@ def set_rules(world: World): if min_range >= highest_cost: min_range = highest_cost-1 - value: int = world.multiworld.random.randint(min_range, min(highest_cost, - max(lowest_cost, last_cost + cost_increment))) + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) - cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost)) + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) if loop_count >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference @@ -219,9 +222,10 @@ def set_rules(world: World): last_cost = cost loop_count += 1 - world.set_chapter_cost(final_chapter, world.multiworld.random.randint( - world.multiworld.FinalChapterMinCost[world.player].value, - world.multiworld.FinalChapterMaxCost[world.player].value)) + if final_chapter is not None: + world.set_chapter_cost(final_chapter, world.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) @@ -277,7 +281,7 @@ def set_rules(world: World): for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state, hat=hat: can_use_hat(state, world, hat)) + add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, world)) @@ -294,6 +298,9 @@ def set_rules(world: World): elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + for misc in data.misc_required: + add_rule(location, lambda state, item=misc: state.has(item, world.player)) + if get_difficulty(world) >= 1: world.multiworld.KnowledgeChecks[world.player].value = 1 @@ -316,14 +323,14 @@ def set_rules(world: World): act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region - name: str = format("%s: Connection %i" % (key, i)) + name: str = f"{key}: Connection {i}" new_entrance: Entrance = connect_regions(required_region, region, name, world.player) entrances.append(new_entrance) # Copy access rules from act completions if "Free Roam" not in required_region.name: rule: typing.Callable[[CollectionState], bool] - name = format("Act Completion (%s)" % required_region.name) + name = f"Act Completion ({required_region.name})" rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) @@ -558,9 +565,9 @@ def set_mafia_town_rules(world: World): def set_subcon_rules(world: World): set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: state.can_reach("Toilet of Doom", "Region", world.player) + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) and (not painting_logic(world) or has_paintings(state, world, 1)) - or state.can_reach("Your Contract has Expired", "Region", world.player)) + or state.has("YCHE Access", world.player)) if world.multiworld.UmbrellaLogic[world.player].value > 0: add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), @@ -583,6 +590,9 @@ def set_subcon_rules(world: World): lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) if painting_logic(world): + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: state.has("Progressive Painting Unlock", world.player)) + for key in contract_locations: if key == "Snatcher's Contract - The Subcon Well": continue @@ -679,7 +689,7 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked # See randomize_act_entrances in Regions.py -# Called BEFORE set_rules! +# Called before set_rules def set_rift_rules(world: World, regions: typing.Dict[str, Region]): # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 0f6b4aff23..61870c4e65 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,29 +1,40 @@ -from BaseClasses import Item, ItemClassification, Region, LocationProgressType - -from .Items import HatInTimeItem, item_table, item_frequencies, item_dlc_enabled, junk_weights,\ - create_item, create_multiple_items, create_junk_items, relic_groups, act_contracts, alps_hooks, \ - get_total_time_pieces - -from .Regions import create_region, create_regions, connect_regions, randomize_act_entrances, chapter_act_info, \ - create_events, chapter_regions, act_chapters - -from .Locations import HatInTimeLocation, location_table, get_total_locations, contract_locations, is_location_valid, \ - get_location_names, get_tasksanity_start_id - -from .Types import HatDLC, HatType, ChapterIndex -from .Options import ahit_options, slot_data_options, adjust_options -from worlds.AutoWorld import World +from BaseClasses import Item, ItemClassification, LocationProgressType, Tutorial +from .Items import HatInTimeItem, item_table, create_item, relic_groups, act_contracts, create_itempool +from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id from .Rules import set_rules -import typing +from .Options import ahit_options, slot_data_options, adjust_options +from .Types import HatType, ChapterIndex +from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes +from .DeathWishRules import set_dw_rules +from worlds.AutoWorld import World, WebWorld +from typing import List, Dict, TextIO -hat_craft_order: typing.Dict[int, typing.List[HatType]] = {} -hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} -chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {} +hat_craft_order: Dict[int, List[HatType]] = {} +hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} +chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} +excluded_dws: Dict[int, List[str]] = {} +excluded_bonuses: Dict[int, List[str]] = {} +dw_shuffle: Dict[int, List[str]] = {} +nyakuza_thug_items: Dict[int, Dict[str, int]] = {} +badge_seller_count: Dict[int, int] = {} + + +class AWebInTime(WebWorld): + theme = "partyTime" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide for setting up A Hat in Time to be played in Archipelago.", + "English", + "ahit_en.md", + "setup/en", + ["CookieCat"] + )] class HatInTimeWorld(World): """ - A Hat in Time is a cute-as-heck 3D platformer featuring a little girl who stitches hats for wicked powers! + A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers! Freely explore giant worlds and recover Time Pieces to travel to new heights! """ @@ -34,37 +45,50 @@ class HatInTimeWorld(World): location_name_to_id = get_location_names() option_definitions = ahit_options - act_connections: typing.Dict[str, str] = {} - nyakuza_thug_items: typing.Dict[str, int] = {} - shop_locs: typing.List[str] = [] + act_connections: Dict[str, str] = {} + shop_locs: List[str] = [] item_name_groups = relic_groups - badge_seller_count: int = 0 def generate_early(self): adjust_options(self) + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + if self.is_dw_only(): + return + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock start_chapter: int = self.multiworld.StartingChapter[self.player].value if start_chapter == 4 or start_chapter == 3: - if self.multiworld.ActRandomizer[self.player].value == 0 \ - or self.multiworld.VanillaAlpine[self.player].value > 0: + if self.multiworld.ActRandomizer[self.player].value == 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) - if self.multiworld.StartWithCompassBadge[self.player].value > 0: - self.multiworld.push_precollected(self.create_item("Compass Badge")) - def create_regions(self): - self.nyakuza_thug_items = {} + excluded_dws[self.player] = [] + excluded_bonuses[self.player] = [] + dw_shuffle[self.player] = [] + nyakuza_thug_items[self.player] = {} + badge_seller_count[self.player] = 0 self.shop_locs = [] - self.badge_seller_count = 0 + self.topology_present = self.multiworld.ActRandomizer[self.player].value + create_regions(self) + if self.multiworld.EnableDeathWish[self.player].value > 0: + create_dw_regions(self) + + if self.is_dw_only(): + return + + create_events(self) + # place default contract locations if contract shuffle is off so logic can still utilize them if self.multiworld.ShuffleActContracts[self.player].value == 0: for name in contract_locations.keys(): @@ -82,69 +106,13 @@ class HatInTimeWorld(World): hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] - self.topology_present = self.multiworld.ActRandomizer[self.player].value - - # Item Pool - itempool: typing.List[Item] = [] - self.calculate_yarn_costs() - yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value) - - for i in range(int(len(yarn_pool) * (0.01 * self.multiworld.YarnBalancePercent[self.player].value))): - yarn_pool[i].classification = ItemClassification.progression - - itempool += yarn_pool - if self.multiworld.RandomizeHatOrder[self.player].value > 0: - self.multiworld.random.shuffle(hat_craft_order[self.player]) + self.random.shuffle(hat_craft_order[self.player]) + if self.multiworld.RandomizeHatOrder[self.player].value == 2: + hat_craft_order[self.player].remove(HatType.TIME_STOP) + hat_craft_order[self.player].append(HatType.TIME_STOP) - for name in item_table.keys(): - if name == "Yarn": - continue - - if not item_dlc_enabled(self, name): - continue - - item_type: ItemClassification = item_table.get(name).classification - if item_type is ItemClassification.filler or item_type is ItemClassification.trap: - continue - - if name in act_contracts.keys() and self.multiworld.ShuffleActContracts[self.player].value == 0: - continue - - if name in alps_hooks.keys() and self.multiworld.ShuffleAlpineZiplines[self.player].value == 0: - continue - - if name == "Progressive Painting Unlock" \ - and self.multiworld.ShuffleSubconPaintings[self.player].value == 0: - continue - - if self.multiworld.StartWithCompassBadge[self.player].value > 0 and name == "Compass Badge": - continue - - if name == "Time Piece": - tp_count: int = 40 - max_extra: int = 0 - if self.is_dlc1(): - max_extra += 6 - - if self.is_dlc2(): - max_extra += 10 - - tp_count += min(max_extra, self.multiworld.MaxExtraTimePieces[self.player].value) - tp_list: typing.List[Item] = create_multiple_items(self, name, tp_count) - - for i in range(int(len(tp_list) * (0.01 * self.multiworld.TimePieceBalancePercent[self.player].value))): - tp_list[i].classification = ItemClassification.progression - - itempool += tp_list - continue - - itempool += create_multiple_items(self, name, item_frequencies.get(name, 1)) - - create_events(self) - total_locations: int = get_total_locations(self) - itempool += create_junk_items(self, total_locations-len(itempool)) - self.multiworld.itempool += itempool + self.multiworld.itempool += create_itempool(self) def set_rules(self): self.act_connections = {} @@ -156,11 +124,37 @@ class HatInTimeWorld(World): ChapterIndex.CRUISE: -1, ChapterIndex.METRO: -1} + if self.is_dw_only(): + # we already have all items if this is the case, no need for rules + self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, + None, self.player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", + self.player) + + if self.multiworld.DWEnableBonus[self.player].value == 0: + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): + continue + + if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + continue + + full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player)) + full_clear.show_in_spoiler = False + + return + if self.multiworld.ActRandomizer[self.player].value > 0: randomize_act_entrances(self) set_rules(self) + if self.multiworld.EnableDeathWish[self.player].value > 0: + set_dw_rules(self) + def create_item(self, name: str) -> Item: return create_item(self, name) @@ -182,16 +176,39 @@ class HatInTimeWorld(World): "Hat3": int(hat_craft_order[self.player][2]), "Hat4": int(hat_craft_order[self.player][3]), "Hat5": int(hat_craft_order[self.player][4]), - "BadgeSellerItemCount": self.badge_seller_count, + "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": self.multiworld.seed} # For shop prices if self.multiworld.ActRandomizer[self.player].value > 0: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] - if self.is_dlc2(): - for name in self.nyakuza_thug_items.keys(): - slot_data[name] = self.nyakuza_thug_items[name] + if self.is_dlc2() and not self.is_dw_only(): + for name in nyakuza_thug_items[self.player].keys(): + slot_data[name] = nyakuza_thug_items[self.player][name] + + if self.is_dw(): + i: int = 0 + for name in excluded_dws[self.player]: + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + continue + + slot_data[f"excluded_dw{i}"] = dw_classes[name] + i += 1 + + i = 0 + if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + for name in excluded_bonuses[self.player]: + if name in excluded_dws[self.player]: + continue + + slot_data[f"excluded_bonus{i}"] = dw_classes[name] + i += 1 + + if self.multiworld.DWShuffle[self.player].value > 0: + shuffled_dws = self.get_dw_shuffle() + for i in range(len(shuffled_dws)): + slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] for option_name in slot_data_options: option = getattr(self.multiworld, option_name)[self.player] @@ -199,7 +216,10 @@ class HatInTimeWorld(World): return slot_data - def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only(): + return + new_hint_data = {} alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] @@ -220,63 +240,28 @@ class HatInTimeWorld(World): else: continue - new_hint_data[location.address] = self.get_shuffled_region(region_name) + new_hint_data[location.address] = get_shuffled_region(self, region_name) if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: - ship_shape_region = self.get_shuffled_region("Ship Shape") + ship_shape_region = get_shuffled_region(self, "Ship Shape") id_start: int = get_tasksanity_start_id() for i in range(self.multiworld.TasksanityCheckCount[self.player].value): new_hint_data[id_start+i] = ship_shape_region hint_data[self.player] = new_hint_data - def write_spoiler_header(self, spoiler_handle: typing.TextIO): + def write_spoiler_header(self, spoiler_handle: TextIO): for i in self.get_chapter_costs(): spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) for hat in hat_craft_order[self.player]: spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) - def calculate_yarn_costs(self): - mw = self.multiworld - p = self.player - min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) - max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) - - max_cost: int = 0 - for i in range(5): - cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - hat_yarn_costs[self.player][HatType(i)] = cost - max_cost += cost - - available_yarn = mw.YarnAvailable[p].value - if max_cost > available_yarn: - mw.YarnAvailable[p].value = max_cost - available_yarn = max_cost - - # make sure we always have at least 8 extra - if max_cost + 8 > available_yarn: - mw.YarnAvailable[p].value += (max_cost + 8) - available_yarn - def set_chapter_cost(self, chapter: ChapterIndex, cost: int): chapter_timepiece_costs[self.player][chapter] = cost def get_chapter_cost(self, chapter: ChapterIndex) -> int: - return chapter_timepiece_costs[self.player].get(chapter) - - # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game - def update_chapter_act_info(self, original_region: Region, new_region: Region): - original_act_info = chapter_act_info[original_region.name] - new_act_info = chapter_act_info[new_region.name] - self.act_connections[original_act_info] = new_act_info - - def get_shuffled_region(self, region: str) -> str: - ci: str = chapter_act_info[region] - for key, val in self.act_connections.items(): - if val == ci: - for name in chapter_act_info.keys(): - if chapter_act_info[name] == key: - return name + return chapter_timepiece_costs[self.player][chapter] def get_hat_craft_order(self): return hat_craft_order[self.player] @@ -295,3 +280,47 @@ class HatInTimeWorld(World): def is_dw(self) -> bool: return self.multiworld.EnableDeathWish[self.player].value > 0 + + def is_dw_only(self) -> bool: + return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + + def get_excluded_dws(self): + return excluded_dws[self.player] + + def get_excluded_bonuses(self): + return excluded_bonuses[self.player] + + def is_dw_excluded(self, name: str) -> bool: + # don't exclude Seal the Deal if it's our goal + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + return False + + if name in excluded_dws[self.player]: + return True + + return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + + def is_bonus_excluded(self, name: str) -> bool: + if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + return True + + return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + + def get_dw_shuffle(self): + return dw_shuffle[self.player] + + def set_dw_shuffle(self, shuffle: List[str]): + dw_shuffle[self.player] = shuffle + + def get_badge_seller_count(self) -> int: + return badge_seller_count[self.player] + + def set_badge_seller_count(self, value: int): + badge_seller_count[self.player] = value + + def get_nyakuza_thug_items(self): + return nyakuza_thug_items[self.player] + + def set_nyakuza_thug_items(self, items: Dict[str, int]): + nyakuza_thug_items[self.player] = items From 02087edf597d90cf44e0d0b8563a304f12fb5d06 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Sep 2023 15:38:10 -0400 Subject: [PATCH 024/143] Docs --- worlds/ahit/docs/en_A Hat in Time.md | 31 ++++++++++++++++++++ worlds/ahit/docs/setup_en.md | 42 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 worlds/ahit/docs/en_A Hat in Time.md create mode 100644 worlds/ahit/docs/setup_en.md diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md new file mode 100644 index 0000000000..c4a4341763 --- /dev/null +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -0,0 +1,31 @@ +# A Hat in Time + +## 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? + +Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. + +To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. + +## What items and locations get shuffled? + +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations. + +Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. + +## 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. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in A Hat in Time? + +Items belonging to other worlds are represented by a badge with the Archipelago logo on it. + +## When the player receives an item, what happens? + +When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md new file mode 100644 index 0000000000..8bdd7c9945 --- /dev/null +++ b/worlds/ahit/docs/setup_en.md @@ -0,0 +1,42 @@ +# Setup Guide for A Hat in Time in Archipelago + +## Required Software +- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/) + +- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601) + + +## Instructions + +1. Have Steam running. Open the Steam console with [this link](steam://open/console) + +2. In the Steam console, enter the following command: `download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. + +3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + +4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. + +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number `253230` on the first line. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled `Live Game Events` you should be running the correct version of the game. In Game Settings, make sure `Enable Developer Console` is checked. + + + +## Connecting to the Archipelago server + +When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command. + + +## Console Commands + +Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure `Enable Developer Console` is checked in Game Settings and press the tilde key or TAB while in-game. + +`ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. + +`ap_deathlink` - Toggle Death Link. + +`ap_set_connection_info ` - Set the connection info for the save file. The IP address MUST BE IN QUOTES! + +`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file From 32561dde3c523adfc0f0bae77617c4ea54e8a00e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Sep 2023 16:03:15 -0400 Subject: [PATCH 025/143] Docs 2 --- worlds/ahit/__init__.py | 1 + worlds/ahit/docs/setup_en.md | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 61870c4e65..e42da92961 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -48,6 +48,7 @@ class HatInTimeWorld(World): act_connections: Dict[str, str] = {} shop_locs: List[str] = [] item_name_groups = relic_groups + web = AWebInTime() def generate_early(self): adjust_options(self) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 8bdd7c9945..d2db2fe47f 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -8,19 +8,20 @@ ## Instructions -1. Have Steam running. Open the Steam console with [this link](steam://open/console) +1. Have Steam running. Open the Steam console with [this link.](steam://open/console) -2. In the Steam console, enter the following command: `download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. +2. In the Steam console, enter the following command: +`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. 4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number `253230` on the first line. +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. 6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. -7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled `Live Game Events` you should be running the correct version of the game. In Game Settings, make sure `Enable Developer Console` is checked. +7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. @@ -31,7 +32,7 @@ When you create a new save file, you should be prompted to enter your slot name, ## Console Commands -Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure `Enable Developer Console` is checked in Game Settings and press the tilde key or TAB while in-game. +Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. `ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. From 6c56f79abf840ef4f74aba488c4f6b75d5f748a2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Sep 2023 21:13:16 -0400 Subject: [PATCH 026/143] Fixes --- worlds/ahit/DeathWishLocations.py | 2 +- worlds/ahit/DeathWishRules.py | 4 ++-- worlds/ahit/Items.py | 3 +-- worlds/ahit/Locations.py | 4 ++++ worlds/ahit/Regions.py | 6 ++++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index e8e2e7941a..b764690dc5 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -237,4 +237,4 @@ def create_dw_regions(world: World): full_clear.progress_type = LocationProgressType.EXCLUDED dw.locations.append(main_objective) - dw.locations.append(full_clear) \ No newline at end of file + dw.locations.append(full_clear) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index d40da5d90f..8b51e9be03 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -377,8 +377,8 @@ def create_enemy_events(world: World): if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): continue - if area == "Time Rift - Tour" and not world.is_dlc1() \ - or world.multiworld.ExcludeTour[world.player].value > 0: + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 729350da1d..f88702db7d 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -90,8 +90,7 @@ def create_itempool(world: World) -> List[Item]: itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) - total_locations: int = get_total_locations(world) - itempool += create_junk_items(world, total_locations - len(itempool)) + itempool += create_junk_items(world, get_total_locations(world) - len(itempool)) return itempool diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 8e63dad539..a817047423 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -79,6 +79,10 @@ def is_location_valid(world: World, location: str) -> bool: and location in storybook_pages.keys(): return False + if world.multiworld.ShuffleActContracts[world.player].value == 0 \ + and location in contract_locations.keys(): + return False + if location not in world.shop_locs and location in shop_locations: return False diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index d7711cbdd6..4dc0e3acec 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -601,6 +601,12 @@ def randomize_act_entrances(world: World): if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: continue + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.multiworld.ShuffleActContracts[world.player].value == 0: + if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ + and candidate.name == "Contractual Obligations": + continue + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: if candidate.name not in chapter_finales: continue From da20a77125b2a75bf118cb549f537fbce968e068 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 9 Sep 2023 12:20:13 -0400 Subject: [PATCH 027/143] Update __init__.py --- worlds/ahit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index e42da92961..20a63d455a 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -218,7 +218,7 @@ class HatInTimeWorld(World): return slot_data def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): - if self.is_dw_only(): + if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0: return new_hint_data = {} From c5b811336ffbead131197b57c6a48ba64b9d15c0 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 9 Sep 2023 18:23:33 -0400 Subject: [PATCH 028/143] Fixes --- worlds/ahit/DeathWishLocations.py | 25 +++++++++++++++++- worlds/ahit/DeathWishRules.py | 42 ++++++++++++++++++++----------- worlds/ahit/Options.py | 3 +-- worlds/ahit/Regions.py | 22 ++++++++-------- worlds/ahit/__init__.py | 7 +++++- 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index b764690dc5..0fa4884b13 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -1,6 +1,7 @@ from .Locations import HatInTimeLocation, death_wishes +from .Items import HatInTimeItem from .Regions import connect_regions, create_region -from BaseClasses import Region, LocationProgressType +from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule from worlds.AutoWorld import World from typing import List @@ -202,6 +203,17 @@ def create_dw_regions(world: World): loc_id = death_wishes[name] main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) if name in world.get_excluded_bonuses(): main_objective.progress_type = LocationProgressType.EXCLUDED @@ -229,6 +241,17 @@ def create_dw_regions(world: World): main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", + ItemClassification.progression, None, world.player)) if key in world.get_excluded_bonuses(): main_objective.progress_type = LocationProgressType.EXCLUDED diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 8b51e9be03..be27bc37cc 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -83,7 +83,7 @@ dw_stamp_costs = { def set_dw_rules(world: World): if "Snatcher's Hit List" not in world.get_excluded_dws() \ or "Camera Tourist" not in world.get_excluded_dws(): - create_enemy_events(world) + set_enemy_rules(world) dw_list: List[str] = [] if world.multiworld.DWShuffle[world.player].value > 0: @@ -100,6 +100,8 @@ def set_dw_rules(world: World): temp_list: List[Location] = [] main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) + bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) temp_list.append(main_objective) temp_list.append(full_clear) @@ -114,19 +116,6 @@ def set_dw_rules(world: World): full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) full_clear.show_in_spoiler = False - # Stamps are event locations - main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) - bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) - main_stamp.show_in_spoiler = False - bonus_stamps.show_in_spoiler = False - dw.locations.append(main_stamp) - dw.locations.append(bonus_stamps) - - main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", - ItemClassification.progression, None, world.player)) - bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", - ItemClassification.progression, None, world.player)) - # No need for rules if excluded - stamps will be auto-granted if world.is_dw_excluded(name): continue @@ -394,6 +383,31 @@ def create_enemy_events(world: World): region.locations.append(event) event.show_in_spoiler = False + +def set_enemy_rules(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + event = world.multiworld.get_location(f"{enemy} - {area}", world.player) + if enemy == "Toxic Flower": add_rule(event, lambda state: can_use_hookshot(state, world)) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 2356302913..9edb7e3ab5 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -63,7 +63,6 @@ def adjust_options(world: World): world.multiworld.LogicDifficulty[world.player].value = 0 world.multiworld.KnowledgeChecks[world.player].value = 0 world.multiworld.DWTimePieceRequirement[world.player].value = 0 - world.multiworld.progression_balancing[world.player].value = 0 def get_total_time_pieces(world: World) -> int: @@ -308,7 +307,7 @@ class MinExtraYarn(Range): For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, there must be at least 50 yarn in the pool.""" display_name = "Max Extra Yarn" - range_start = 0 + range_start = 5 range_end = 15 default = 10 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 4dc0e3acec..62e089e3cc 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -254,16 +254,18 @@ blacklisted_acts = { # Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. blacklisted_combos = { - "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam"], - "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam"], - "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!"], - "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"], - "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"], - "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam"], - "Time Rift - The Twilight Bell": ["Nyakuza Free Roam"], - "Time Rift - Alpine Skyline": ["Nyakuza Free Roam"], - "Time Rift - Rumbi Factory": ["Alpine Free Roam"], - "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam"], + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", + "Contractual Obligations"], + + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], } diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 20a63d455a..9c251e902c 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -6,7 +6,7 @@ from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options from .Types import HatType, ChapterIndex from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes -from .DeathWishRules import set_dw_rules +from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld from typing import List, Dict, TextIO @@ -18,6 +18,7 @@ excluded_bonuses: Dict[int, List[str]] = {} dw_shuffle: Dict[int, List[str]] = {} nyakuza_thug_items: Dict[int, Dict[str, int]] = {} badge_seller_count: Dict[int, int] = {} +badge_seller_count: Dict[int, int] = {} class AWebInTime(WebWorld): @@ -89,6 +90,10 @@ class HatInTimeWorld(World): return create_events(self) + if self.is_dw(): + if "Snatcher's Hit List" not in self.get_excluded_dws() \ + or "Camera Tourist" not in self.get_excluded_dws(): + create_enemy_events(self) # place default contract locations if contract shuffle is off so logic can still utilize them if self.multiworld.ShuffleActContracts[self.player].value == 0: From 6cedbe8b3d736563351963ce1c5af545eaa649d5 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 9 Sep 2023 19:47:18 -0400 Subject: [PATCH 029/143] variable capture my beloathed --- worlds/ahit/Rules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 4030a27e3c..6dea5555e7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -636,7 +636,8 @@ def set_alps_rules(world: World): and state.has("Zipline Unlock - The Windmill Path", world.player)) for (loc, zipline) in zipline_unlocks.items(): - add_rule(world.multiworld.get_location(loc, world.player), lambda state: state.has(zipline, world.player)) + add_rule(world.multiworld.get_location(loc, world.player), + lambda state, z=zipline: state.has(z, world.player)) def set_dlc1_rules(world: World): From 0d07ae0d726ba309a35441d1808c5933c487edf2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 11 Sep 2023 16:24:15 -0400 Subject: [PATCH 030/143] Fixes --- worlds/ahit/DeathWishLocations.py | 4 ++-- worlds/ahit/Items.py | 21 +++++++++------------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 0fa4884b13..951b85f49a 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -215,7 +215,7 @@ def create_dw_regions(world: World): bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", ItemClassification.progression, None, world.player)) - if name in world.get_excluded_bonuses(): + if name in world.get_excluded_dws(): main_objective.progress_type = LocationProgressType.EXCLUDED full_clear.progress_type = LocationProgressType.EXCLUDED elif world.is_bonus_excluded(name): @@ -253,7 +253,7 @@ def create_dw_regions(world: World): bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", ItemClassification.progression, None, world.player)) - if key in world.get_excluded_bonuses(): + if key in world.get_excluded_dws(): main_objective.progress_type = LocationProgressType.EXCLUDED full_clear.progress_type = LocationProgressType.EXCLUDED elif world.is_bonus_excluded(key): diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index f88702db7d..4bf7157166 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -37,22 +37,19 @@ def create_itempool(world: World) -> List[Item]: continue item_type: ItemClassification = item_table.get(name).classification - if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \ - and (name == "Scooter Badge" or name == "No Bonk Badge"): - item_type = ItemClassification.progression - - # some death wish bonuses require one hit hero + hookshot - if world.is_dw() and name == "Badge Pin": - item_type = ItemClassification.progression if world.is_dw_only(): if item_type is ItemClassification.progression \ - or item_type is ItemClassification.progression_skip_balancing: + or item_type is ItemClassification.progression_skip_balancing: continue - # progression balance anything useful, since we have basically no progression in this mode - if item_type is ItemClassification.useful: - item_type = ItemClassification.progression + if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \ + and (name == "Scooter Badge" or name == "No Bonk Badge") and not world.is_dw_only(): + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): + item_type = ItemClassification.progression if item_type is ItemClassification.filler or item_type is ItemClassification.trap: continue @@ -64,7 +61,7 @@ def create_itempool(world: World) -> List[Item]: continue if name == "Progressive Painting Unlock" \ - and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: continue if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": From e3e1c0d1f8db414762a85fc4004fac48bad2e2db Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 11 Sep 2023 17:28:48 -0400 Subject: [PATCH 031/143] a --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 6dea5555e7..c9bda9abb8 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -115,7 +115,7 @@ def get_relic_count(state: CollectionState, world: World, relic: str) -> int: # Only use for rifts def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) - if not state.can_reach(entrance.connected_region, player=world.player): + if not state.can_reach(entrance.connected_region, "Region", world.player): return False if "Free Roam" in entrance.connected_region.name: From a1395eeae01cfa4805c1f81d580e522971957159 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 12 Sep 2023 12:17:45 -0400 Subject: [PATCH 032/143] 10 Seconds logic fix --- worlds/ahit/DeathWishRules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index be27bc37cc..8a3f6e5915 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -17,6 +17,7 @@ dw_requirements = { "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), "Security Breach": LocData(hit_requirement=1), + "10 Seconds until Self-Destruct": LocData(hookshot=True), "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), From 714241b07af782586017249185a87b06da237de4 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 15 Sep 2023 18:38:43 -0400 Subject: [PATCH 033/143] 1.1 --- worlds/ahit/Items.py | 18 ++++++++++++++---- worlds/ahit/Options.py | 8 ++++++++ worlds/ahit/Regions.py | 10 +++++----- worlds/ahit/Rules.py | 23 ++++++++++++++++++++--- worlds/ahit/Types.py | 9 +++++++++ worlds/ahit/__init__.py | 27 ++++++++++++++------------- worlds/ahit/test/TestActs.py | 24 ++++++++++++++++++++++++ worlds/ahit/test/TestBase.py | 5 +++++ worlds/ahit/test/__init__.py | 0 9 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 worlds/ahit/test/TestActs.py create mode 100644 worlds/ahit/test/TestBase.py create mode 100644 worlds/ahit/test/__init__.py diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 4bf7157166..d987d4662b 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,6 +1,6 @@ from BaseClasses import Item, ItemClassification from worlds.AutoWorld import World -from .Types import HatDLC, HatType +from .Types import HatDLC, HatType, hat_type_to_item from .Locations import get_total_locations from .Rules import get_difficulty, is_player_knowledgeable from typing import Optional, NamedTuple, List, Dict @@ -18,7 +18,7 @@ class HatInTimeItem(Item): def create_itempool(world: World) -> List[Item]: itempool: List[Item] = [] - if not world.is_dw_only(): + if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0: calculate_yarn_costs(world) yarn_pool: List[Item] = create_multiple_items(world, "Yarn", world.multiworld.YarnAvailable[world.player].value, @@ -36,6 +36,9 @@ def create_itempool(world: World) -> List[Item]: if not item_dlc_enabled(world, name): continue + if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values(): + continue + item_type: ItemClassification = item_table.get(name).classification if world.is_dw_only(): @@ -181,7 +184,13 @@ def create_junk_items(world: World, count: int) -> List[Item]: ahit_items = { "Yarn": ItemData(300001, ItemClassification.progression_skip_balancing), "Time Piece": ItemData(300002, ItemClassification.progression_skip_balancing), - "Progressive Painting Unlock": ItemData(300003, ItemClassification.progression), + + # for HatItems option + "Sprint Hat": ItemData(300049, ItemClassification.progression), + "Brewing Hat": ItemData(300050, ItemClassification.progression), + "Ice Hat": ItemData(300051, ItemClassification.progression), + "Dweller Mask": ItemData(300052, ItemClassification.progression), + "Time Stop Hat": ItemData(300053, ItemClassification.progression), # Relics "Relic (Burger Patty)": ItemData(300006, ItemClassification.progression), @@ -210,8 +219,9 @@ ahit_items = { "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), # Other - "Umbrella": ItemData(300033, ItemClassification.progression), "Badge Pin": ItemData(300043, ItemClassification.useful), + "Umbrella": ItemData(300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(300003, ItemClassification.progression), # Garbage items "25 Pons": ItemData(300034, ItemClassification.filler), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 9edb7e3ab5..39501f17b9 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -312,6 +312,12 @@ class MinExtraYarn(Range): default = 10 +class HatItems(Toggle): + """Removes all yarn from the pool and turns the hats into individual items instead.""" + display_name = "Hat Items" + default = 0 + + class MinPonCost(Range): """The minimum amount of Pons that any shop item can cost.""" display_name = "Minimum Shop Pon Cost" @@ -648,6 +654,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "YarnCostMax": YarnCostMax, "YarnAvailable": YarnAvailable, "MinExtraYarn": MinExtraYarn, + "HatItems": HatItems, "MinPonCost": MinPonCost, "MaxPonCost": MaxPonCost, @@ -675,6 +682,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "ShuffleStorybookPages": ShuffleStorybookPages, "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, + "HatItems": HatItems, "EnableDLC1": EnableDLC1, "Tasksanity": Tasksanity, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 62e089e3cc..5c7beec2b8 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -433,6 +433,11 @@ def create_rift_connections(world: World, region: Region): connect_regions(act_region, region, entrance_name, world.player) i += 1 + # fix for some weird keyerror from tests + if region.name == "Time Rift - Rumbi Factory": + for entrance in region.entrances: + world.multiworld.get_entrance(entrance.name, world.player) + def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) @@ -873,11 +878,6 @@ def create_events(world: World) -> int: event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) event.show_in_spoiler = False - - if data.act_complete_event: - act_completion: str = f"Act Completion ({data.region})" - event.access_rule = world.multiworld.get_location(act_completion, world.player).access_rule - count += 1 return count diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index c9bda9abb8..b6e3ef50bf 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,8 +1,8 @@ from worlds.AutoWorld import World, CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, tihs_locations, zipline_unlocks, is_location_valid, contract_locations, \ - shop_locations -from .Types import HatType, ChapterIndex + shop_locations, event_locs +from .Types import HatType, ChapterIndex, hat_type_to_item from BaseClasses import Location, Entrance, Region import typing @@ -32,6 +32,9 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: + if world.multiworld.HatItems[world.player].value > 0: + return state.has(hat_type_to_item[hat], world.player) + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) @@ -257,8 +260,9 @@ def set_rules(world: World): if world.multiworld.ActRandomizer[world.player].value == 0: set_default_rift_rules(world) + table = location_table | event_locs location: Location - for (key, data) in location_table.items(): + for (key, data) in table.items(): if not is_location_valid(world, key): continue @@ -340,6 +344,8 @@ def set_rules(world: World): for rules in access_rules: add_rule(e, rules) + set_event_rules(world) + for entrance in world.multiworld.get_region("Alpine Free Roam", world.player).entrances: add_rule(entrance, lambda state: can_use_hookshot(state, world)) if world.multiworld.UmbrellaLogic[world.player].value > 0: @@ -851,6 +857,17 @@ def set_default_rift_rules(world: World): add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) +def set_event_rules(world: World): + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + event: Location = world.multiworld.get_location(name, world.player) + + if data.act_complete_event: + add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) + + def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: entrance = Entrance(player, entrancename, start_region) start_region.exits.append(entrance) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 45f5535b58..2915512f57 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -26,3 +26,12 @@ class ChapterIndex(IntEnum): FINALE = 5 CRUISE = 6 METRO = 7 + + +hat_type_to_item = { + HatType.SPRINT: "Sprint Hat", + HatType.BREWING: "Brewing Hat", + HatType.ICE: "Ice Hat", + HatType.DWELLER: "Dweller Mask", + HatType.TIME_STOP: "Time Stop Hat", +} diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 9c251e902c..5b2b902770 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -18,7 +18,6 @@ excluded_bonuses: Dict[int, List[str]] = {} dw_shuffle: Dict[int, List[str]] = {} nyakuza_thug_items: Dict[int, Dict[str, int]] = {} badge_seller_count: Dict[int, int] = {} -badge_seller_count: Dict[int, int] = {} class AWebInTime(WebWorld): @@ -112,7 +111,7 @@ class HatInTimeWorld(World): hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] - if self.multiworld.RandomizeHatOrder[self.player].value > 0: + if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0: self.random.shuffle(hat_craft_order[self.player]) if self.multiworld.RandomizeHatOrder[self.player].value == 2: hat_craft_order[self.player].remove(HatType.TIME_STOP) @@ -165,26 +164,28 @@ class HatInTimeWorld(World): return create_item(self, name) def fill_slot_data(self) -> dict: - slot_data: dict = {"SprintYarnCost": hat_yarn_costs[self.player][HatType.SPRINT], - "BrewingYarnCost": hat_yarn_costs[self.player][HatType.BREWING], - "IceYarnCost": hat_yarn_costs[self.player][HatType.ICE], - "DwellerYarnCost": hat_yarn_costs[self.player][HatType.DWELLER], - "TimeStopYarnCost": hat_yarn_costs[self.player][HatType.TIME_STOP], - "Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], + slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], - "Hat1": int(hat_craft_order[self.player][0]), - "Hat2": int(hat_craft_order[self.player][1]), - "Hat3": int(hat_craft_order[self.player][2]), - "Hat4": int(hat_craft_order[self.player][3]), - "Hat5": int(hat_craft_order[self.player][4]), "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": self.multiworld.seed} # For shop prices + if self.multiworld.HatItems[self.player].value == 0: + slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) + slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) + slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) + slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER]) + slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP]) + slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0])) + slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1])) + slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2])) + slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) + slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) + if self.multiworld.ActRandomizer[self.player].value > 0: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py new file mode 100644 index 0000000000..0b9983ca15 --- /dev/null +++ b/worlds/ahit/test/TestActs.py @@ -0,0 +1,24 @@ +from worlds.ahit.Regions import act_chapters +from worlds.ahit.test.TestBase import HatInTimeTestBase + + +class TestActs(HatInTimeTestBase): + options = { + "ActRandomizer": 2, + "EnableDLC1": 1, + "EnableDLC2": 1, + } + + def test_act_shuffle(self): + for i in range(1000): + self.world_setup() + self.collect_all_but([""]) + + for name in act_chapters.keys(): + region = self.multiworld.get_region(name, 1) + for entrance in region.entrances: + self.assertTrue(self.can_reach_entrance(entrance.name), + f"Can't reach {name} from {entrance}\n" + f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} " + f"-> {entrance} -> {name}" + f" (expected method of access)") diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py new file mode 100644 index 0000000000..1eb4dd6555 --- /dev/null +++ b/worlds/ahit/test/TestBase.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 1afb5694b770ffe789a212928b7545931314a5c2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 3 Oct 2023 21:19:24 -0400 Subject: [PATCH 034/143] 1.2 --- worlds/ahit/DeathWishRules.py | 91 +++++---- worlds/ahit/Items.py | 15 +- worlds/ahit/Locations.py | 44 ++--- worlds/ahit/Options.py | 32 ++-- worlds/ahit/Regions.py | 19 +- worlds/ahit/Rules.py | 345 +++++++++++++++++++--------------- worlds/ahit/Types.py | 7 + worlds/ahit/__init__.py | 5 - worlds/ahit/test/TestActs.py | 1 + 9 files changed, 315 insertions(+), 244 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 8a3f6e5915..d547d19ba2 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,11 +1,11 @@ from worlds.AutoWorld import World, CollectionState from .Locations import LocData, death_wishes, HatInTimeLocation -from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings -from .Types import HatType +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings, get_difficulty +from .Types import HatType, Difficulty from .DeathWishLocations import dw_prereqs, dw_candles from .Items import HatInTimeItem from BaseClasses import Entrance, Location, ItemClassification -from worlds.generic.Rules import add_rule +from worlds.generic.Rules import add_rule, set_rule from typing import List, Callable # Any speedruns expect the player to have Sprint Hat @@ -21,18 +21,18 @@ dw_requirements = { "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), - "Boss Rush": LocData(hit_requirement=1, umbrella=True), + "Boss Rush": LocData(umbrella=True, hookshot=True), "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), "Bird Sanctuary": LocData(hookshot=True), "Wound-Up Windmill": LocData(hookshot=True), - "The Illness has Speedrun": LocData(hookshot=True, required_hats=[HatType.SPRINT]), + "The Illness has Speedrun": LocData(hookshot=True), "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), "Camera Tourist": LocData(misc_required=["Camera Badge"]), "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), - "Rift Collapse - Deep Sea": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Rift Collapse - Deep Sea": LocData(hookshot=True), } # Includes main objective requirements @@ -43,15 +43,16 @@ dw_bonus_requirements = { "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), - "Boss Rush": LocData(misc_required=["One-Hit Hero Badge"]), + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]), "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), - "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.SPRINT]), + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]), } dw_stamp_costs = { @@ -122,21 +123,9 @@ def set_dw_rules(world: World): continue # Specific Rules - if name == "The Illness has Speedrun": - # killing the flowers without the umbrella is way too slow - add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) - elif name == "The Mustache Gauntlet": - # don't get burned bonus requires a way to kill fire crows without being burned - add_rule(full_clear, lambda state: state.has("Umbrella", world.player) - or can_use_hat(state, world, HatType.ICE)) - elif name == "Vault Codes in the Wind": - add_rule(main_objective, lambda state: can_use_hat(state, world, HatType.TIME_STOP), "or") - - if name in dw_candles: - set_candle_dw_rules(name, world) + modify_dw_rules(world, name) main_rule: Callable[[CollectionState], bool] - for i in range(len(temp_list)): loc = temp_list[i] data: LocData @@ -217,13 +206,55 @@ def set_dw_rules(world: World): world.player) +def modify_dw_rules(world: World, name: str): + difficulty: Difficulty = get_difficulty(world) + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "The Illness has Speedrun": + # All stamps with hookshot only in Expert + if difficulty >= Difficulty.EXPERT: + set_rule(full_clear, lambda state: True) + else: + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + + elif name == "The Mustache Gauntlet": + # Need a way to kill fire crows without being burned. + add_rule(main_objective, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING)) + add_rule(full_clear, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE)) + + elif name == "Vault Codes in the Wind": + # Sprint is normally expected here + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Speedrun Well": + # All stamps with nothing :) + if difficulty >= Difficulty.EXPERT: + set_rule(main_objective, lambda state: True) + + elif name == "Mafia's Jumps": + # Main objective without Ice, still expected for bonuses + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + set_rule(full_clear, lambda state: can_use_hat(state, world, HatType.ICE)) + + elif name == "So You're Back from Outer Space": + # Without Hookshot + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + if name in dw_candles: + set_candle_dw_rules(name, world) + + def get_total_dw_stamps(state: CollectionState, world: World) -> int: if world.multiworld.DWShuffle[world.player].value > 0: return 999 # no stamp costs in death wish shuffle count: int = 0 - peace_and_tranquility: bool = world.multiworld.DWEnableBonus[world.player].value == 0 \ - and world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 for name in death_wishes: if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): @@ -234,12 +265,6 @@ def get_total_dw_stamps(state: CollectionState, world: World) -> int: else: continue - # If bonus rewards and auto bonus completion is off, obtaining stamps via P&T is in logic - # Candles don't have P&T - if peace_and_tranquility and name not in dw_candles: - count += 2 - continue - if state.has(f"2 Stamps - {name}", world.player): count += 2 elif name not in dw_candles: @@ -272,7 +297,7 @@ def set_candle_dw_rules(name: str, world: World): add_rule(full_clear, lambda state: state.has("CTR Access", world.player) or state.has("HUMT Access", world.player) - and (world.multiworld.UmbrellaLogic[world.player].value == 0 or state.has("Umbrella", world.player)) + and can_hit(state, world, True) or state.has("DWTM Access", world.player) or state.has("TGV Access", world.player)) @@ -424,8 +449,10 @@ def set_enemy_rules(world: World): elif enemy == "Snatcher" or enemy == "Mustache Girl": if area == "Boss Rush": - # need to be able to kill toilet - add_rule(event, lambda state: can_hit(state, world)) + # need to be able to kill toilet and snatcher + add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world)) + if enemy == "Mustache Girl": + add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world)) elif area == "The Finale" and enemy == "Mustache Girl": add_rule(event, lambda state: can_use_hookshot(state, world) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index d987d4662b..bd9150d98c 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,8 +1,8 @@ from BaseClasses import Item, ItemClassification from worlds.AutoWorld import World -from .Types import HatDLC, HatType, hat_type_to_item +from .Types import HatDLC, HatType, hat_type_to_item, Difficulty from .Locations import get_total_locations -from .Rules import get_difficulty, is_player_knowledgeable +from .Rules import get_difficulty from typing import Optional, NamedTuple, List, Dict @@ -45,10 +45,13 @@ def create_itempool(world: World) -> List[Item]: if item_type is ItemClassification.progression \ or item_type is ItemClassification.progression_skip_balancing: continue - - if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \ - and (name == "Scooter Badge" or name == "No Bonk Badge") and not world.is_dw_only(): - item_type = ItemClassification.progression + else: + if name == "Scooter Badge": + if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + elif name == "No Bonk Badge": + if get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression # some death wish bonuses require one hit hero + hookshot if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index a817047423..954c54818f 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -170,10 +170,10 @@ ahit_locations = { "Dead Bird Studio - Red Building Top": LocData(305024, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Behind Water Tower": LocData(305248, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Side of House": LocData(305247, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio", hit_requirement=1), - "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio", hit_requirement=1), - "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio", hit_requirement=1), - "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio", hit_requirement=1), + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), "Murder on the Owl Express - Cafeteria": LocData(305313, "Murder on the Owl Express"), "Murder on the Owl Express - Luggage Room Top": LocData(305090, "Murder on the Owl Express"), @@ -276,11 +276,11 @@ ahit_locations = { "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), # Alpine Skyline - "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), - "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Goat Village"), + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"), - "Alpine Skyline - Bird Pass Fork": LocData(335911, "Alpine Skyline Area"), - "Alpine Skyline - Yellow Band Hills": LocData(335756, "Alpine Skyline Area", required_hats=[HatType.BREWING]), + "Alpine Skyline - Bird Pass Fork": LocData(335911, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Yellow Band Hills": LocData(335756, "Alpine Skyline Area (TIHS)", required_hats=[HatType.BREWING]), "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(335561, "Alpine Skyline Area"), "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(334831, "Alpine Skyline Area"), "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(334758, "The Birdhouse"), @@ -295,7 +295,7 @@ ahit_locations = { "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(337058, "Alpine Skyline Area"), "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(336052, "Alpine Skyline Area"), - "Alpine Skyline - Ember Summit": LocData(336311, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(336311, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(335448, "The Lava Cake"), "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(334291, "The Lava Cake"), "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(335417, "The Lava Cake"), @@ -304,7 +304,7 @@ ahit_locations = { "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(336478, "The Twilight Bell"), "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(335826, "The Twilight Bell"), "Alpine Skyline - Goat Outpost Horn": LocData(334760, "Alpine Skyline Area"), - "Alpine Skyline - Windy Passage": LocData(334776, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(334776, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(336395, "The Windmill"), "Alpine Skyline - The Windmill: Entrance": LocData(335783, "The Windmill"), "Alpine Skyline - The Windmill: Dropdown": LocData(335815, "The Windmill"), @@ -327,7 +327,8 @@ ahit_locations = { "Rock the Boat - Reception Room - Under Desk": LocData(304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), "Rock the Boat - Lamp Post": LocData(304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), "Rock the Boat - Iceberg Top": LocData(304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Post Captain Rescue": LocData(304049, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), "Nyakuza Metro - Main Station Dining Area": LocData(304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), "Nyakuza Metro - Top of Ramen Shop": LocData(304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), @@ -350,14 +351,14 @@ ahit_locations = { } act_completions = { - "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery"), + "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), "Act Completion (Welcome to Mafia Town)": LocData(311771, "Welcome to Mafia Town"), "Act Completion (Barrel Battle)": LocData(311958, "Barrel Battle"), "Act Completion (She Came from Outer Space)": LocData(312262, "She Came from Outer Space"), "Act Completion (Down with the Mafia!)": LocData(311326, "Down with the Mafia!"), - "Act Completion (Cheating the Race)": LocData(312318, "Cheating the Race"), + "Act Completion (Cheating the Race)": LocData(312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), "Act Completion (Heating Up Mafia Town)": LocData(311481, "Heating Up Mafia Town", umbrella=True), "Act Completion (The Golden Vault)": LocData(312250, "The Golden Vault"), "Act Completion (Time Rift - Bazaar)": LocData(312465, "Time Rift - Bazaar"), @@ -404,9 +405,10 @@ act_completions = { "Act Completion (Bon Voyage!)": LocData(311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), "Act Completion (Ship Shape)": LocData(311451, "Ship Shape", dlc_flags=HatDLC.dlc1), - "Act Completion (Rock the Boat)": LocData(311437, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Act Completion (Rock the Boat)": LocData(311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, required_hats=[HatType.ICE]), "Act Completion (Time Rift - Balcony)": LocData(312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, hookshot=True), - "Act Completion (Time Rift - Deep Sea)": LocData(312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Time Rift - Deep Sea)": LocData(312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), "Act Completion (Nyakuza Metro Intro)": LocData(311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), @@ -422,7 +424,7 @@ act_completions = { "Act Completion (Green Clean Manhole)": LocData(311388, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, - required_hats=[HatType.ICE]), + required_hats=[HatType.ICE, HatType.DWELLER]), "Act Completion (Bluefin Tunnel)": LocData(311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), @@ -669,16 +671,6 @@ zipline_unlocks = { "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", } -# Locations in Alpine that are available in The Illness has Spread -# Goat Village locations don't need to be put here -tihs_locations = [ - "Alpine Skyline - Bird Pass Fork", - "Alpine Skyline - Yellow Band Hills", - "Alpine Skyline - Ember Summit", - "Alpine Skyline - Goat Outpost Horn", - "Alpine Skyline - Windy Passage", -] - event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), "TOD Access": LocData(0, "Toilet of Doom", act_complete_event=False), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 39501f17b9..c8eced5836 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -60,7 +60,7 @@ def adjust_options(world: World): world.multiworld.ShuffleStorybookPages[world.player].value = 0 world.multiworld.ShuffleActContracts[world.player].value = 0 world.multiworld.EnableDLC1[world.player].value = 0 - world.multiworld.LogicDifficulty[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = -1 world.multiworld.KnowledgeChecks[world.player].value = 0 world.multiworld.DWTimePieceRequirement[world.player].value = 0 @@ -118,17 +118,20 @@ class FinaleShuffle(Toggle): class LogicDifficulty(Choice): """Choose the difficulty setting for logic.""" display_name = "Logic Difficulty" - option_normal = 0 + option_normal = -1 + option_moderate = 0 option_hard = 1 option_expert = 2 - default = 0 + default = -1 -class KnowledgeChecks(Toggle): - """Put tricks into logic that are not necessarily difficult, - but require knowledge that is not obvious or commonly known. Can include glitches such as No Bonk Surfing. - This option will be forced on if logic difficulty is at least hard.""" - display_name = "Knowledge Checks" +class CTRLogic(Choice): + """Choose how you want to logically clear Cheating the Race.""" + display_name = "Cheating the Race Logic" + option_time_stop_only = 0 + option_scooter = 1 + option_sprint = 2 + option_nothing = 3 default = 0 @@ -350,12 +353,6 @@ class BadgeSellerMaxItems(Range): default = 8 -class CTRWithSprint(Toggle): - """If enabled, clearing Cheating the Race with just Sprint Hat can be in logic.""" - display_name = "Cheating the Race with Sprint Hat" - default = 0 - - class EnableDLC1(Toggle): """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" @@ -396,7 +393,7 @@ class ExcludeTour(Toggle): class ShipShapeCustomTaskGoal(Range): """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" display_name = "Ship Shape Custom Task Goal" - range_start = 5 + range_start = 1 range_end = 30 default = 18 @@ -602,7 +599,6 @@ ahit_options: typing.Dict[str, type(Option)] = { "ShuffleAlpineZiplines": ShuffleAlpineZiplines, "FinaleShuffle": FinaleShuffle, "LogicDifficulty": LogicDifficulty, - "KnowledgeChecks": KnowledgeChecks, "YarnBalancePercent": YarnBalancePercent, "TimePieceBalancePercent": TimePieceBalancePercent, "RandomizeHatOrder": RandomizeHatOrder, @@ -613,7 +609,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, "StartingChapter": StartingChapter, - "CTRWithSprint": CTRWithSprint, + "CTRLogic": CTRLogic, "EnableDLC1": EnableDLC1, "Tasksanity": Tasksanity, @@ -675,7 +671,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "ActRandomizer": ActRandomizer, "ShuffleAlpineZiplines": ShuffleAlpineZiplines, "LogicDifficulty": LogicDifficulty, - "KnowledgeChecks": KnowledgeChecks, + "CTRLogic": CTRLogic, "RandomizeHatOrder": RandomizeHatOrder, "UmbrellaLogic": UmbrellaLogic, "CompassBadgeMode": CompassBadgeMode, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 5c7beec2b8..c3fbfe8359 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -3,7 +3,7 @@ from BaseClasses import Region, Entrance, ItemClassification, Location from .Locations import HatInTimeLocation, location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, get_tasksanity_start_id from .Items import HatInTimeItem -from .Types import ChapterIndex +from .Types import ChapterIndex, Difficulty import typing from .Rules import set_rift_rules @@ -308,9 +308,13 @@ def create_regions(world: World): create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) create_rift_connections(w, create_region(w, "Time Rift - The Moon")) - # Items near the Dead Bird Studio elevator can be reached from the basement act + # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - connect_regions(mw.get_region("Dead Bird Studio Basement", p), ev_area, "DBS Basement -> Elevator Area", p) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + basement = mw.get_region("Dead Bird Studio Basement", p) + connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) + if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -325,7 +329,9 @@ def create_regions(world: World): alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) - goat_village = create_region_and_connect(w, "Goat Village", "ASA -> Goat Village", alpine_area) + + # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness + alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", alpine_area) create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) @@ -333,8 +339,7 @@ def create_regions(world: World): create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) - connect_regions(illness, alpine_area, "TIHS -> Alpine Skyline Area", p) - connect_regions(illness, goat_village, "TIHS -> Goat Village", p) + connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p) create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) @@ -373,7 +378,7 @@ def create_regions(world: World): connect_regions(mw.get_region("Dead Bird Studio", p), badge_seller, "DBS -> Badge Seller", p) connect_regions(mw.get_region("Picture Perfect", p), badge_seller, "PP -> Badge Seller", p) connect_regions(mw.get_region("Train Rush", p), badge_seller, "TR -> Badge Seller", p) - connect_regions(mw.get_region("Goat Village", p), badge_seller, "GV -> Badge Seller", p) + connect_regions(mw.get_region("Alpine Skyline Area (TIHS)", p), badge_seller, "ASA -> Badge Seller", p) times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b6e3ef50bf..5123250d85 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,8 +1,8 @@ from worlds.AutoWorld import World, CollectionState from worlds.generic.Rules import add_rule, set_rule -from .Locations import location_table, tihs_locations, zipline_unlocks, is_location_valid, contract_locations, \ +from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ shop_locations, event_locs -from .Types import HatType, ChapterIndex, hat_type_to_item +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty from BaseClasses import Location, Entrance, Region import typing @@ -57,13 +57,9 @@ def painting_logic(world: World) -> bool: return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 -def is_player_knowledgeable(world: World) -> bool: - return world.multiworld.KnowledgeChecks[world.player].value > 0 - - -# 0 = Normal, 1 = Hard, 2 = Expert -def get_difficulty(world: World) -> int: - return world.multiworld.LogicDifficulty[world.player].value +# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert +def get_difficulty(world: World) -> Difficulty: + return Difficulty(world.multiworld.LogicDifficulty[world.player].value) def has_paintings(state: CollectionState, world: World, count: int) -> bool: @@ -71,18 +67,17 @@ def has_paintings(state: CollectionState, world: World, count: int) -> bool: return True # Cherry Hover - if get_difficulty(world) == 2: + if get_difficulty(world) >= Difficulty.EXPERT: return True # All paintings can be skipped with No Bonk, very easily, if the player knows - if is_player_knowledgeable(world) and can_surf(state, world): + if get_difficulty(world) >= Difficulty.MODERATE and can_surf(state, world): return True paintings: int = state.count("Progressive Painting Unlock", world.player) - - if is_player_knowledgeable(world): - # Green paintings can also be skipped very easily without No Bonk - if paintings >= 1 and count == 3: + if get_difficulty(world) >= Difficulty.MODERATE: + # Green+Yellow paintings can also be skipped easily + if count == 1 or paintings >= 1 and count == 3: return True return paintings >= count @@ -96,11 +91,11 @@ def can_use_hookshot(state: CollectionState, world: World): return state.has("Hookshot Badge", world.player) -def can_hit(state: CollectionState, world: World): +def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): if world.multiworld.UmbrellaLogic[world.player].value == 0: return True - return state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING) + return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) def can_surf(state: CollectionState, world: World): @@ -271,18 +266,6 @@ def set_rules(world: World): location = world.multiworld.get_location(key, world.player) - # Not all locations in Alpine can be reached from The Illness has Spread - # as many of the ziplines are blocked off - if data.region == "Alpine Skyline Area": - if key not in tihs_locations: - add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", world.player), "and") - else: - add_rule(location, lambda state: can_use_hookshot(state, world)) - - if data.region == "The Birdhouse" or data.region == "The Lava Cake" \ - or data.region == "The Windmill" or data.region == "The Twilight Bell": - add_rule(location, lambda state: state.can_reach("Alpine Free Roam", "Region", world.player), "and") - for hat in data.required_hats: if hat is not HatType.NONE: add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) @@ -305,11 +288,44 @@ def set_rules(world: World): for misc in data.misc_required: add_rule(location, lambda state, item=misc: state.has(item, world.player)) - if get_difficulty(world) >= 1: - world.multiworld.KnowledgeChecks[world.player].value = 1 - set_specific_rules(world) + # Putting all of this here, so it doesn't get overridden by anything + # Illness starts the player past the intro + alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) + add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) + + if zipline_logic(world): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) + + if zipline_logic(world): + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), + lambda state, z=zipline: state.has(z, world.player)) + + for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: + if "Goat Village" in loc.name: + continue + + add_rule(loc, lambda state: can_use_hookshot(state, world)) + for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): continue @@ -346,11 +362,6 @@ def set_rules(world: World): set_event_rules(world) - for entrance in world.multiworld.get_region("Alpine Free Roam", world.player).entrances: - add_rule(entrance, lambda state: can_use_hookshot(state, world)) - if world.multiworld.UmbrellaLogic[world.player].value > 0: - add_rule(entrance, lambda state: state.has("Umbrella", world.player)) - if world.multiworld.EndGoal[world.player].value == 1: world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) elif world.multiworld.EndGoal[world.player].value == 2: @@ -375,48 +386,85 @@ def set_specific_rules(world: World): if world.is_dlc2(): set_dlc2_rules(world) - difficulty: int = get_difficulty(world) - if is_player_knowledgeable(world) or difficulty >= 1: - set_knowledge_rules(world) + difficulty: Difficulty = get_difficulty(world) - if difficulty == 0: - set_normal_rules(world) + if difficulty >= Difficulty.MODERATE: + set_moderate_rules(world) - if difficulty >= 1: + if difficulty >= Difficulty.HARD: set_hard_rules(world) if difficulty >= 2: set_expert_rules(world) -def set_normal_rules(world: World): - # Hard: get to Birdhouse without Brewing Hat - add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) +def set_moderate_rules(world: World): + # Moderate: Gallery without Brewing Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) - add_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + # Moderate: Clock Tower Chest + Ruined Tower with nothing + add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) - # Hard: gallery without Brewing Hat - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + # Moderate: hitting the bell is not required to enter Subcon Well, however hookshot is still expected to clear + set_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player), + lambda state: has_paintings(state, world, 1)) + set_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player), + lambda state: has_paintings(state, world, 1)) + set_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player), + lambda state: has_paintings(state, world, 1)) + + # Moderate: Vanessa Manor with nothing + for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) + + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat + set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world)) + set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access + set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: True) if world.is_dlc1(): - # Hard: clear Deep Sea without Dweller Mask - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), - lambda state: can_use_hat(state, world, HatType.DWELLER)) + # Moderate: clear Rock the Boat without Ice Hat + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) - # Hard: clear Rock the Boat without Ice Hat - add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), - lambda state: can_use_hat(state, world, HatType.ICE)) - - add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), - lambda state: can_use_hat(state, world, HatType.ICE)) + # Moderate: clear Deep Sea without Ice Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. + # Yellow Overpass time piece can also be reached without Hookshot quite easily. if world.is_dlc2(): - # Hard: clear Green Clean Manhole without Dweller Mask - add_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), - lambda state: can_use_hat(state, world, HatType.DWELLER)) + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) + + # The player can quite literally walk past the fan from the side without Time Stop. + set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) + + # The player can't jump back down to the manhole due to a fall damage volume preventing them from doing so + set_rule(world.multiworld.get_location("Act Completion (Pink Paw Manhole)", world.player), + lambda state: (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)) + and can_use_hat(state, world, HatType.ICE)) + + # Moderate: clear Rush Hour without Hookshot + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and can_use_hat(state, world, HatType.ICE) + and can_use_hat(state, world, HatType.BREWING)) def set_hard_rules(world: World): @@ -425,95 +473,105 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.SPRINT) and state.has("Scooter Badge", world.player), "or") - # Hard: Cross Subcon boss arena gap with No Bonk + SDJ, - # allowing access to the boss arena chest, and Toilet of Doom without Hookshot + # Hard: Cross Subcon boss arena gap with No Bonk + SDJ, allowing access to the boss arena chest # Doing this in reverse from YCHE is expert logic, which expects you to cherry hover - add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: can_surf(state, world) and can_sdj(state, world) and can_hit(state, world), "or") - add_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: can_surf(state, world) and can_sdj(state, world), "or") + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), + lambda state: has_paintings(state, world, 3)) + + # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") - add_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), - lambda state: can_sdj(state, world), "or") + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_sdj(state, world), "or") + add_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER) and can_sdj(state, world), "or") + + # Hard: Mystifying Time Mesa time trial without hats + set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hookshot(state, world)) + + if world.is_dlc1(): + # Hard: clear Deep Sea without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world)) + + if world.is_dlc2(): + # Hard: clear Green Clean Manhole without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + # Hard: clear Rush Hour with Brewing Hat only + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + def set_expert_rules(world: World): - # Expert: get to and clear Twilight Bell without Dweller Mask using SDJ. Brewing Hat required to complete act. - add_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), - lambda state: can_sdj(state, world) - and (not zipline_logic(world) or state.has("Zipline Unlock - The Twilight Bell Path", world.player)), "or") + # Expert: Mafia Town - Above Boats with nothing + set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + + # Expert: Clear Dead Bird Studio with nothing + for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True) + + # Expert: get to and clear Twilight Bell without Dweller Mask. + # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), - lambda state: can_sdj(state, world) and can_use_hookshot(state, world) - and (not zipline_logic(world) or state.has("Zipline Unlock - The Twilight Bell Path", world.player)), "or") + lambda state: can_use_hookshot(state, world), "or") add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player), - lambda state: can_sdj(state, world) and can_use_hat(state, world, HatType.BREWING), "or") + lambda state: can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER) + or can_use_hat(state, world, HatType.SPRINT) + or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player))) - # Expert: enter and clear The Subcon Well with No Bonk Badge only + # Expert: Time Rift - Curly Tail Trail with nothing + # Time Rift - Twilight Bell and Time Rift - Village with nothing + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: True) + + # Expert: enter and clear The Subcon Well with nothing for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: - add_rule(loc, lambda state: can_surf(state, world), "or") + set_rule(loc, lambda state: True) # Expert: Cherry Hovering connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), world.multiworld.get_region("Subcon Forest Area", world.player), "Subcon Forest Entrance YCHE", world.player) - set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: can_hit(state, world)) - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), lambda state: True) - # Manor hover with 1 painting unlock - for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: - set_rule(loc, lambda state: not painting_logic(world) - or state.count("Progressive Painting Unlock", world.player) >= 1) + # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him + connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), + world.multiworld.get_region("Your Contract has Expired", world.player), + "Snatcher Hover", world.player) + set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), + lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), - lambda state: not painting_logic(world) - or state.count("Progressive Painting Unlock", world.player) >= 1) - - -def set_knowledge_rules(world: World): - # Can jump down from HQ to get these - add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), - lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) - or state.can_reach("Cheating the Race", "Region", world.player) - or state.can_reach("The Golden Vault", "Region", world.player), "or") - - add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), - lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) - or state.can_reach("Cheating the Race", "Region", world.player) - or state.can_reach("The Golden Vault", "Region", world.player), "or") - - # Dweller Mask requirement in Pink Paw can also be skipped by jumping on lamp post. - # The item behind the Time Stop fan can be walked past without Time Stop hat as well. - # (just set hookshot rule, because dweller requirement is skipped, but hookshot is still necessary) if world.is_dlc2(): - # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw - add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), - lambda state: can_use_hookshot(state, world), "or") - - for loc in world.multiworld.get_region("Pink Paw Station", world.player).locations: - - # Can't jump back down to the manhole due to a fall damage trigger. - if loc.name == "Act Completion (Pink Paw Manhole)": - set_rule(loc, lambda state: (state.has("Metro Ticket - Pink", world.player) - or state.has("Metro Ticket - Yellow", world.player) - and state.has("Metro Ticket - Blue", world.player)) - and can_use_hat(state, world, HatType.ICE)) - - continue - - set_rule(loc, lambda state: can_use_hookshot(state, world)) + # Expert: clear Rush Hour with nothing + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) def set_mafia_town_rules(world: World): @@ -564,9 +622,16 @@ def set_mafia_town_rules(world: World): add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: state.has("HUMT Access", world.player), "or") - set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), - lambda state: can_use_hat(state, world, HatType.TIME_STOP) - or world.multiworld.CTRWithSprint[world.player].value > 0 and can_use_hat(state, world, HatType.SPRINT)) + ctr_logic: int = world.multiworld.CTRLogic[world.player].value + if ctr_logic == 3: + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) + elif ctr_logic == 2: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT), "or") + elif ctr_logic == 1: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") def set_subcon_rules(world: World): @@ -608,14 +673,16 @@ def set_subcon_rules(world: World): def set_alps_rules(world: World): add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), - lambda state: can_use_hookshot(state, world)) + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), lambda state: can_use_hookshot(state, world)) + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), lambda state: can_use_hookshot(state, world)) add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), - lambda state: can_use_hat(state, world, HatType.DWELLER) and can_use_hookshot(state, world)) + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) @@ -623,28 +690,6 @@ def set_alps_rules(world: World): add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), lambda state: can_clear_alpine(state, world)) - if zipline_logic(world): - add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), - lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) - - add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), - lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) - - add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), - lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) - - add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), - lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) - - add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), - lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) - and state.has("Zipline Unlock - The Lava Cake Path", world.player) - and state.has("Zipline Unlock - The Windmill Path", world.player)) - - for (loc, zipline) in zipline_unlocks.items(): - add_rule(world.multiworld.get_location(loc, world.player), - lambda state, z=zipline: state.has(z, world.player)) - def set_dlc1_rules(world: World): add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 2915512f57..a62ad581cc 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -28,6 +28,13 @@ class ChapterIndex(IntEnum): METRO = 7 +class Difficulty(IntEnum): + NORMAL = -1 + MODERATE = 0 + HARD = 1 + EXPERT = 2 + + hat_type_to_item = { HatType.SPRINT: "Sprint Hat", HatType.BREWING: "Brewing Hat", diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 5b2b902770..67a9fb0816 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -98,11 +98,6 @@ class HatInTimeWorld(World): if self.multiworld.ShuffleActContracts[self.player].value == 0: for name in contract_locations.keys(): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) - else: - # The bag trap contract check needs to be excluded, because if the player has the Subcon Well contract, - # the trap will not activate, locking the player out of the check permanently - self.multiworld.get_location("Snatcher's Contract - The Subcon Well", - self.player).progress_type = LocationProgressType.EXCLUDED def create_items(self): hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 0b9983ca15..3c5918c0db 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -7,6 +7,7 @@ class TestActs(HatInTimeTestBase): "ActRandomizer": 2, "EnableDLC1": 1, "EnableDLC2": 1, + "ShuffleActContracts": 0, } def test_act_shuffle(self): From 13acb67bd02420b4734f3208824cbbbc0f7319f4 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 13 Oct 2023 16:49:09 -0400 Subject: [PATCH 035/143] a --- worlds/ahit/DeathWishLocations.py | 7 +- worlds/ahit/DeathWishRules.py | 164 ++--- worlds/ahit/Items.py | 130 ++-- worlds/ahit/Locations.py | 1073 ++++++++++++++++------------- worlds/ahit/Options.py | 3 +- worlds/ahit/Regions.py | 19 +- worlds/ahit/Rules.py | 68 +- worlds/ahit/Types.py | 35 + worlds/ahit/__init__.py | 6 +- 9 files changed, 840 insertions(+), 665 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 951b85f49a..f51d4948ee 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -1,10 +1,11 @@ -from .Locations import HatInTimeLocation, death_wishes -from .Items import HatInTimeItem +from .Types import HatInTimeLocation, HatInTimeItem from .Regions import connect_regions, create_region from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule from worlds.AutoWorld import World from typing import List +from .Locations import death_wishes + dw_prereqs = { "So You're Back From Outer Space": ["Beat the Heat"], @@ -82,11 +83,9 @@ annoying_bonuses = [ "Snatcher's Hit List", "10 Seconds until Self-Destruct", "Killing Two Birds", - "Snatcher Coins in Battle of the Birds", "Zero Jumps", "Bird Sanctuary", "Wound-Up Windmill", - "Snatcher Coins in Alpine Skyline", "Seal the Deal", ] diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index d547d19ba2..7f6211f417 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,12 +1,12 @@ from worlds.AutoWorld import World, CollectionState -from .Locations import LocData, death_wishes, HatInTimeLocation -from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings, get_difficulty -from .Types import HatType, Difficulty +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HatDLC from .DeathWishLocations import dw_prereqs, dw_candles -from .Items import HatInTimeItem from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule from typing import List, Callable +from .Regions import act_chapters +from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes # Any speedruns expect the player to have Sprint Hat dw_requirements = { @@ -81,6 +81,23 @@ dw_stamp_costs = { "Seal the Deal": 70, } +required_snatcher_coins = { + "Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower", + "Snatcher Coin - Under Ruined Tower"], + + "Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush", + "Snatcher Coin - Picture Perfect"], + + "Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof", + "Snatcher Coin - Giant Time Piece"], + + "Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake", + "Snatcher Coin - Windmill"], + + "Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train", + "Snatcher Coin - Pink Paw Fence"], +} + def set_dw_rules(world: World): if "Snatcher's Hit List" not in world.get_excluded_dws() \ @@ -219,11 +236,8 @@ def modify_dw_rules(world: World, name: str): add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) elif name == "The Mustache Gauntlet": - # Need a way to kill fire crows without being burned. add_rule(main_objective, lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING)) - add_rule(full_clear, lambda state: state.has("Umbrella", world.player) - or can_use_hat(state, world, HatType.ICE)) elif name == "Vault Codes in the Wind": # Sprint is normally expected here @@ -236,16 +250,21 @@ def modify_dw_rules(world: World, name: str): set_rule(main_objective, lambda state: True) elif name == "Mafia's Jumps": - # Main objective without Ice, still expected for bonuses if difficulty >= Difficulty.HARD: set_rule(main_objective, lambda state: True) - set_rule(full_clear, lambda state: can_use_hat(state, world, HatType.ICE)) + set_rule(full_clear, lambda state: True) elif name == "So You're Back from Outer Space": # Without Hookshot if difficulty >= Difficulty.HARD: set_rule(main_objective, lambda state: True) + elif name == "Wound-Up Windmill": + # No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it. + if difficulty >= Difficulty.MODERATE: + set_rule(full_clear, lambda state: can_use_hookshot(state, world) + and state.has("One-Hit Hero Badge", world.player)) + if name in dw_candles: set_candle_dw_rules(name, world) @@ -268,7 +287,7 @@ def get_total_dw_stamps(state: CollectionState, world: World) -> int: if state.has(f"2 Stamps - {name}", world.player): count += 2 elif name not in dw_candles: - # all non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + # most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) count += 1 return count @@ -281,7 +300,13 @@ def set_candle_dw_rules(name: str, world: World): if name == "Zero Jumps": add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 - and state.has("Train Rush Cleared", world.player) and can_use_hat(state, world, HatType.ICE)) + and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) + + # No Ice Hat/painting required in Expert for Toilet Zero Jumps + if get_difficulty(world) >= Difficulty.EXPERT: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) + and can_hit(state, world)) elif name == "Snatcher's Hit List": add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) @@ -289,74 +314,33 @@ def set_candle_dw_rules(name: str, world: World): elif name == "Camera Tourist": add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) - add_rule(full_clear, lambda state: can_reach_all_bosses(state, world)) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) + and state.has("Triple Enemy Picture", world.player)) - elif name == "Snatcher Coins in Mafia Town": - add_rule(main_objective, lambda state: state.has("MT Access", world.player) - or state.has("HUMT Access", world.player)) - - add_rule(full_clear, lambda state: state.has("CTR Access", world.player) - or state.has("HUMT Access", world.player) - and can_hit(state, world, True) - or state.has("DWTM Access", world.player) - or state.has("TGV Access", world.player)) - - elif name == "Snatcher Coins in Battle of the Birds": - add_rule(main_objective, lambda state: state.has("PP Access", world.player) - or state.has("DBS Access", world.player) - or state.has("Train Rush Cleared", world.player)) - - add_rule(full_clear, lambda state: state.has("PP Access", world.player) - and state.has("DBS Access", world.player) - and state.has("Train Rush Cleared", world.player)) - - elif name == "Snatcher Coins in Subcon Forest": - add_rule(main_objective, lambda state: state.has("SF Access", world.player)) - - add_rule(main_objective, lambda state: has_paintings(state, world, 1) and (can_use_hookshot(state, world) - or can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) - or has_paintings(state, world, 3)) - - add_rule(full_clear, lambda state: has_paintings(state, world, 3) and can_use_hookshot(state, world) - and (can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))) - - elif name == "Snatcher Coins in Alpine Skyline": - add_rule(main_objective, lambda state: state.has("LC Access", world.player) - or state.has("WM Access", world.player)) - - add_rule(full_clear, lambda state: state.has("LC Access", world.player) - and state.has("WM Access", world.player)) - - elif name == "Snatcher Coins in Nyakuza Metro": - add_rule(main_objective, lambda state: state.has("Bluefin Tunnel Cleared", world.player) - or (state.has("Nyakuza Intro Cleared", world.player) - and (state.has("Metro Ticket - Pink", world.player) - or state.has("Metro Ticket - Yellow", world.player) - and state.has("Metro Ticket - Blue", world.player)))) - - add_rule(full_clear, lambda state: state.has("Bluefin Tunnel Cleared", world.player) - and (state.has("Nyakuza Intro Cleared", world.player) - and (state.has("Metro Ticket - Pink", world.player) - or state.has("Metro Ticket - Yellow", world.player) - and state.has("Metro Ticket - Blue", world.player)))) + elif "Snatcher Coins" in name: + for coin in required_snatcher_coins[name]: + add_rule(main_objective, lambda state: state.has(coin, world.player), "or") + add_rule(full_clear, lambda state: state.has(coin, world.player)) def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: total: int = 0 - for name, hats in zero_jumps.items(): - if not state.has(f"{name} Cleared", world.player): + for name in act_chapters.keys(): + n = f"{name} (Zero Jumps)" + if n not in zero_jumps: continue - valid: bool = True + if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard: + continue - for hat in hats: - if not can_use_hat(state, world, hat): - valid = False - break + if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert: + continue - if valid: - total += 1 + if not state.has(n, world.player): + continue + + total += 1 return total @@ -399,7 +383,7 @@ def create_enemy_events(world: World): if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \ and area not in world.get_dw_shuffle(): continue @@ -409,6 +393,22 @@ def create_enemy_events(world: World): region.locations.append(event) event.show_in_spoiler = False + for name in triple_enemy_locations: + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \ + and name not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(name, world.player) + event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + if name == "The Mustache Gauntlet": + add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + def set_enemy_rules(world: World): no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() @@ -422,7 +422,7 @@ def set_enemy_rules(world: World): continue if area == "Time Rift - Tour" and (not world.is_dlc1() - or world.multiworld.ExcludeTour[world.player].value > 0): + or world.multiworld.ExcludeTour[world.player].value > 0): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): @@ -463,17 +463,6 @@ def set_enemy_rules(world: World): add_rule(event, lambda state: can_use_hookshot(state, world)) -# Zero Jumps completable levels, with required hats if any -zero_jumps = { - "Welcome to Mafia Town": [], - "Cheating the Race": [HatType.TIME_STOP], - "Picture Perfect": [], - "Train Rush": [HatType.ICE], - "Contractual Obligations": [], - "Your Contract has Expired": [], - "Mail Delivery Service": [], # rule for needing sprint is already on act completion -} - # Enemies for Snatcher's Hit List/Camera Tourist, and where to find them hit_list = { "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", @@ -523,6 +512,17 @@ hit_list = { "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], } +# Camera Tourist has a bonus that requires getting three different types of enemies in one picture. +triple_enemy_locations = [ + "She Came from Outer Space", + "She Speedran from Outer Space", + "Mafia's Jumps", + "The Mustache Gauntlet", + "The Birdhouse", + "Bird Sanctuary", + "Time Rift - Tour", +] + bosses = [ "Mafia Boss", "Conductor", diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index bd9150d98c..c9bb76739c 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,19 +1,9 @@ from BaseClasses import Item, ItemClassification from worlds.AutoWorld import World -from .Types import HatDLC, HatType, hat_type_to_item, Difficulty +from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem from .Locations import get_total_locations from .Rules import get_difficulty -from typing import Optional, NamedTuple, List, Dict - - -class ItemData(NamedTuple): - code: Optional[int] - classification: ItemClassification - dlc_flags: Optional[HatDLC] = HatDLC.none - - -class HatInTimeItem(Item): - game: str = "A Hat in Time" +from typing import Optional, List, Dict def create_itempool(world: World) -> List[Item]: @@ -185,86 +175,86 @@ def create_junk_items(world: World, count: int) -> List[Item]: ahit_items = { - "Yarn": ItemData(300001, ItemClassification.progression_skip_balancing), - "Time Piece": ItemData(300002, ItemClassification.progression_skip_balancing), + "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing), + "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing), # for HatItems option - "Sprint Hat": ItemData(300049, ItemClassification.progression), - "Brewing Hat": ItemData(300050, ItemClassification.progression), - "Ice Hat": ItemData(300051, ItemClassification.progression), - "Dweller Mask": ItemData(300052, ItemClassification.progression), - "Time Stop Hat": ItemData(300053, ItemClassification.progression), + "Sprint Hat": ItemData(2000300049, ItemClassification.progression), + "Brewing Hat": ItemData(2000300050, ItemClassification.progression), + "Ice Hat": ItemData(2000300051, ItemClassification.progression), + "Dweller Mask": ItemData(2000300052, ItemClassification.progression), + "Time Stop Hat": ItemData(2000300053, ItemClassification.progression), # Relics - "Relic (Burger Patty)": ItemData(300006, ItemClassification.progression), - "Relic (Burger Cushion)": ItemData(300007, ItemClassification.progression), - "Relic (Mountain Set)": ItemData(300008, ItemClassification.progression), - "Relic (Train)": ItemData(300009, ItemClassification.progression), - "Relic (UFO)": ItemData(300010, ItemClassification.progression), - "Relic (Cow)": ItemData(300011, ItemClassification.progression), - "Relic (Cool Cow)": ItemData(300012, ItemClassification.progression), - "Relic (Tin-foil Hat Cow)": ItemData(300013, ItemClassification.progression), - "Relic (Crayon Box)": ItemData(300014, ItemClassification.progression), - "Relic (Red Crayon)": ItemData(300015, ItemClassification.progression), - "Relic (Blue Crayon)": ItemData(300016, ItemClassification.progression), - "Relic (Green Crayon)": ItemData(300017, ItemClassification.progression), + "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression), + "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression), + "Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression), + "Relic (Train)": ItemData(2000300009, ItemClassification.progression), + "Relic (UFO)": ItemData(2000300010, ItemClassification.progression), + "Relic (Cow)": ItemData(2000300011, ItemClassification.progression), + "Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression), + "Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression), + "Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression), + "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression), + "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression), + "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression), # Badges - "Projectile Badge": ItemData(300024, ItemClassification.useful), - "Fast Hatter Badge": ItemData(300025, ItemClassification.useful), - "Hover Badge": ItemData(300026, ItemClassification.useful), - "Hookshot Badge": ItemData(300027, ItemClassification.progression), - "Item Magnet Badge": ItemData(300028, ItemClassification.useful), - "No Bonk Badge": ItemData(300029, ItemClassification.useful), - "Compass Badge": ItemData(300030, ItemClassification.useful), - "Scooter Badge": ItemData(300031, ItemClassification.useful), - "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), - "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), + "Projectile Badge": ItemData(2000300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), + "Hover Badge": ItemData(2000300026, ItemClassification.useful), + "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), + "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), + "Compass Badge": ItemData(2000300030, ItemClassification.useful), + "Scooter Badge": ItemData(2000300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), # Other - "Badge Pin": ItemData(300043, ItemClassification.useful), - "Umbrella": ItemData(300033, ItemClassification.progression), - "Progressive Painting Unlock": ItemData(300003, ItemClassification.progression), + "Badge Pin": ItemData(2000300043, ItemClassification.useful), + "Umbrella": ItemData(2000300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), # Garbage items - "25 Pons": ItemData(300034, ItemClassification.filler), - "50 Pons": ItemData(300035, ItemClassification.filler), - "100 Pons": ItemData(300036, ItemClassification.filler), - "Health Pon": ItemData(300037, ItemClassification.filler), - "Random Cosmetic": ItemData(300044, ItemClassification.filler), + "25 Pons": ItemData(2000300034, ItemClassification.filler), + "50 Pons": ItemData(2000300035, ItemClassification.filler), + "100 Pons": ItemData(2000300036, ItemClassification.filler), + "Health Pon": ItemData(2000300037, ItemClassification.filler), + "Random Cosmetic": ItemData(2000300044, ItemClassification.filler), # Traps - "Baby Trap": ItemData(300039, ItemClassification.trap), - "Laser Trap": ItemData(300040, ItemClassification.trap), - "Parade Trap": ItemData(300041, ItemClassification.trap), + "Baby Trap": ItemData(2000300039, ItemClassification.trap), + "Laser Trap": ItemData(2000300040, ItemClassification.trap), + "Parade Trap": ItemData(2000300041, ItemClassification.trap), # DLC1 items - "Relic (Cake Stand)": ItemData(300018, ItemClassification.progression, HatDLC.dlc1), - "Relic (Cake)": ItemData(300019, ItemClassification.progression, HatDLC.dlc1), - "Relic (Cake Slice)": ItemData(300020, ItemClassification.progression, HatDLC.dlc1), - "Relic (Shortcake)": ItemData(300021, ItemClassification.progression, HatDLC.dlc1), + "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), # DLC2 items - "Relic (Necklace Bust)": ItemData(300022, ItemClassification.progression, HatDLC.dlc2), - "Relic (Necklace)": ItemData(300023, ItemClassification.progression, HatDLC.dlc2), - "Metro Ticket - Yellow": ItemData(300045, ItemClassification.progression, HatDLC.dlc2), - "Metro Ticket - Green": ItemData(300046, ItemClassification.progression, HatDLC.dlc2), - "Metro Ticket - Blue": ItemData(300047, ItemClassification.progression, HatDLC.dlc2), - "Metro Ticket - Pink": ItemData(300048, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2), } act_contracts = { - "Snatcher's Contract - The Subcon Well": ItemData(300200, ItemClassification.progression), - "Snatcher's Contract - Toilet of Doom": ItemData(300201, ItemClassification.progression), - "Snatcher's Contract - Queen Vanessa's Manor": ItemData(300202, ItemClassification.progression), - "Snatcher's Contract - Mail Delivery Service": ItemData(300203, ItemClassification.progression), + "Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression), + "Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression), + "Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression), + "Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression), } alps_hooks = { - "Zipline Unlock - The Birdhouse Path": ItemData(300204, ItemClassification.progression), - "Zipline Unlock - The Lava Cake Path": ItemData(300205, ItemClassification.progression), - "Zipline Unlock - The Windmill Path": ItemData(300206, ItemClassification.progression), - "Zipline Unlock - The Twilight Bell Path": ItemData(300207, ItemClassification.progression), + "Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression), + "Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression), + "Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression), + "Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression), } relic_groups = { diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 954c54818f..64f1074d7f 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -1,32 +1,9 @@ -from BaseClasses import Location from worlds.AutoWorld import World -from .Types import HatDLC, HatType -from typing import Optional, NamedTuple, List, Dict +from .Types import HatDLC, HatType, LocData, Difficulty +from typing import Dict from .Options import TasksanityCheckCount -class LocData(NamedTuple): - id: Optional[int] = 0 - region: Optional[str] = "" - required_hats: Optional[List[HatType]] = [HatType.NONE] - hookshot: Optional[bool] = False - dlc_flags: Optional[HatDLC] = HatDLC.none - paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle - misc_required: Optional[List[str]] = [] - - # For UmbrellaLogic setting - umbrella: Optional[bool] = False # Umbrella required for this check - hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) - - # Other - act_complete_event: Optional[bool] = True # Only used for event locations. Copy access rule from act completion - nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) - - -class HatInTimeLocation(Location): - game: str = "A Hat in Time" - - def get_total_locations(world: World) -> int: total: int = 0 @@ -67,6 +44,8 @@ def location_dlc_enabled(world: World, location: str) -> bool: return True elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): return True + elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw(): + return True return False @@ -91,9 +70,23 @@ def is_location_valid(world: World, location: str) -> bool: return False # No need for all those event items if we're not doing candles - if data.dlc_flags is HatDLC.death_wish and world.multiworld.DWExcludeCandles[world.player].value > 0 \ - and location in event_locs.keys(): - return False + if data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw: + if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): + return False + + if world.multiworld.DWShuffle[world.player].value > 0 and data.region not in world.get_dw_shuffle(): + return False + + if location in zero_jumps: + if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + return False + + difficulty: int = world.multiworld.LogicDifficulty[world.player].value + if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): + return False + + if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT): + return False return True @@ -112,546 +105,546 @@ def get_location_names() -> Dict[str, int]: def get_tasksanity_start_id() -> int: - return 300204 + return 2000300204 ahit_locations = { - "Spaceship - Rumbi Abuse": LocData(301000, "Spaceship", hit_requirement=1), + "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), # 300000 range - Mafia Town/Batle of the Birds - "Welcome to Mafia Town - Umbrella": LocData(301002, "Welcome to Mafia Town"), - "Mafia Town - Old Man (Seaside Spaghetti)": LocData(303833, "Mafia Town Area"), - "Mafia Town - Old Man (Steel Beams)": LocData(303832, "Mafia Town Area"), - "Mafia Town - Blue Vault": LocData(302850, "Mafia Town Area"), - "Mafia Town - Green Vault": LocData(302851, "Mafia Town Area"), - "Mafia Town - Red Vault": LocData(302848, "Mafia Town Area"), - "Mafia Town - Blue Vault Brewing Crate": LocData(305572, "Mafia Town Area", required_hats=[HatType.BREWING]), - "Mafia Town - Plaza Under Boxes": LocData(304458, "Mafia Town Area"), - "Mafia Town - Small Boat": LocData(304460, "Mafia Town Area"), - "Mafia Town - Staircase Pon Cluster": LocData(304611, "Mafia Town Area"), - "Mafia Town - Palm Tree": LocData(304609, "Mafia Town Area"), - "Mafia Town - Port": LocData(305219, "Mafia Town Area"), - "Mafia Town - Docks Chest": LocData(303534, "Mafia Town Area"), - "Mafia Town - Ice Hat Cage": LocData(304831, "Mafia Town Area", required_hats=[HatType.ICE]), - "Mafia Town - Hidden Buttons Chest": LocData(303483, "Mafia Town Area"), + "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), + "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"), + "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"), + "Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"), + "Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"), + "Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"), + "Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]), + "Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"), + "Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"), + "Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"), + "Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"), + "Mafia Town - Port": LocData(2000305219, "Mafia Town Area"), + "Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"), + "Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]), + "Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"), # These can be accessed from HUMT, the above locations can't be - "Mafia Town - Dweller Boxes": LocData(304462, "Mafia Town Area (HUMT)"), - "Mafia Town - Ledge Chest": LocData(303530, "Mafia Town Area (HUMT)"), - "Mafia Town - Yellow Sphere Building Chest": LocData(303535, "Mafia Town Area (HUMT)"), - "Mafia Town - Beneath Scaffolding": LocData(304456, "Mafia Town Area (HUMT)"), - "Mafia Town - On Scaffolding": LocData(304457, "Mafia Town Area (HUMT)"), - "Mafia Town - Cargo Ship": LocData(304459, "Mafia Town Area (HUMT)"), - "Mafia Town - Beach Alcove": LocData(304463, "Mafia Town Area (HUMT)"), - "Mafia Town - Wood Cage": LocData(304606, "Mafia Town Area (HUMT)"), - "Mafia Town - Beach Patio": LocData(304610, "Mafia Town Area (HUMT)"), - "Mafia Town - Steel Beam Nest": LocData(304608, "Mafia Town Area (HUMT)"), - "Mafia Town - Top of Ruined Tower": LocData(304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), - "Mafia Town - Hot Air Balloon": LocData(304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), - "Mafia Town - Camera Badge 1": LocData(302003, "Mafia Town Area (HUMT)"), - "Mafia Town - Camera Badge 2": LocData(302004, "Mafia Town Area (HUMT)"), - "Mafia Town - Chest Beneath Aqueduct": LocData(303489, "Mafia Town Area (HUMT)"), - "Mafia Town - Secret Cave": LocData(305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), - "Mafia Town - Crow Chest": LocData(303532, "Mafia Town Area (HUMT)"), - "Mafia Town - Above Boats": LocData(305218, "Mafia Town Area (HUMT)", hookshot=True), - "Mafia Town - Slip Slide Chest": LocData(303529, "Mafia Town Area (HUMT)"), - "Mafia Town - Behind Faucet": LocData(304214, "Mafia Town Area (HUMT)"), - "Mafia Town - Clock Tower Chest": LocData(303481, "Mafia Town Area (HUMT)", hookshot=True), - "Mafia Town - Top of Lighthouse": LocData(304213, "Mafia Town Area (HUMT)", hookshot=True), - "Mafia Town - Mafia Geek Platform": LocData(304212, "Mafia Town Area (HUMT)"), - "Mafia Town - Behind HQ Chest": LocData(303486, "Mafia Town Area (HUMT)"), + "Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"), + "Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"), + "Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"), + "Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"), + "Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"), + "Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"), + "Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"), + "Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"), + "Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"), + "Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"), + "Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"), + "Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), + "Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"), + "Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"), + "Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"), - "Mafia HQ - Hallway Brewing Crate": LocData(305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), - "Mafia HQ - Freezer Chest": LocData(303241, "Down with the Mafia!"), - "Mafia HQ - Secret Room": LocData(304979, "Down with the Mafia!", required_hats=[HatType.ICE]), - "Mafia HQ - Bathroom Stall Chest": LocData(303243, "Down with the Mafia!"), + "Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), + "Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"), + "Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]), + "Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"), - "Dead Bird Studio - Up the Ladder": LocData(304874, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - Red Building Top": LocData(305024, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - Behind Water Tower": LocData(305248, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - Side of House": LocData(305247, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - DJ Grooves Sign Chest": LocData(303901, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Dead Bird Studio - Tightrope Chest": LocData(303898, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Dead Bird Studio - Tepee Chest": LocData(303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Dead Bird Studio - Conductor Chest": LocData(303900, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Murder on the Owl Express - Cafeteria": LocData(305313, "Murder on the Owl Express"), - "Murder on the Owl Express - Luggage Room Top": LocData(305090, "Murder on the Owl Express"), - "Murder on the Owl Express - Luggage Room Bottom": LocData(305091, "Murder on the Owl Express"), + "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"), - "Murder on the Owl Express - Raven Suite Room": LocData(305701, "Murder on the Owl Express", + "Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express", required_hats=[HatType.BREWING]), - "Murder on the Owl Express - Raven Suite Top": LocData(305312, "Murder on the Owl Express"), - "Murder on the Owl Express - Lounge Chest": LocData(303963, "Murder on the Owl Express"), + "Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"), + "Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"), - "Picture Perfect - Behind Badge Seller": LocData(304307, "Picture Perfect"), - "Picture Perfect - Hats Buy Building": LocData(304530, "Picture Perfect"), + "Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"), + "Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"), - "Dead Bird Studio Basement - Window Platform": LocData(305432, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Cardboard Conductor": LocData(305059, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Above Conductor Sign": LocData(305057, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Logo Wall": LocData(305207, "Dead Bird Studio Basement"), - "Dead Bird Studio Basement - Disco Room": LocData(305061, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Small Room": LocData(304813, "Dead Bird Studio Basement"), - "Dead Bird Studio Basement - Vent Pipe": LocData(305430, "Dead Bird Studio Basement"), - "Dead Bird Studio Basement - Tightrope": LocData(305058, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True), - "Dead Bird Studio Basement - Locked Room": LocData(305819, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True), # Subcon Forest - "Contractual Obligations - Cherry Bomb Bone Cage": LocData(324761, "Contractual Obligations"), - "Subcon Village - Tree Top Ice Cube": LocData(325078, "Subcon Forest Area"), - "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), - "Subcon Village - House Top": LocData(325471, "Subcon Forest Area"), - "Subcon Village - Ice Cube House": LocData(325469, "Subcon Forest Area"), - "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), - "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), - "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), + "Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"), + "Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"), + "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"), + "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"), + "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"), + "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"), - "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", + "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=1), - "Subcon Forest - Swamp Near Well": LocData(324762, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree A": LocData(324763, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree B": LocData(324764, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Ice Wall": LocData(324706, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), - "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1), - "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), - "Subcon Forest - Burning Forest Treehouse": LocData(325467, "Subcon Forest Area", paintings=2), - "Subcon Forest - Spider Bone Cage A": LocData(324462, "Subcon Forest Area", paintings=2), - "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), - "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), - "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), + "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2), - "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", + "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=2), - "Subcon Forest - Boss Arena Chest": LocData(323735, "Subcon Forest Area"), - "Subcon Forest - Manor Rooftop": LocData(325466, "Subcon Forest Area", hit_requirement=2, paintings=1), + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), + "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1), - "Subcon Forest - Infinite Yarn Bush": LocData(325478, "Subcon Forest Area", + "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=2), - "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", + "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=3), - "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", + "Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", + "Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), + "Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3), - "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", + "Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), - "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), + "Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3), - "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), + "Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3), - "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", + "Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=3), - "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", + "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", required_hats=[HatType.DWELLER], hookshot=True, paintings=3), - "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", hit_requirement=1, paintings=1), - "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", hit_requirement=1, paintings=1), - "Subcon Well - On Pipe": LocData(324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), - "Subcon Well - Mushroom": LocData(325318, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), + "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), - "Queen Vanessa's Manor - Cellar": LocData(324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Bedroom Chest": LocData(323808, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), # Alpine Skyline - "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"), - "Alpine Skyline - Bird Pass Fork": LocData(335911, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Yellow Band Hills": LocData(335756, "Alpine Skyline Area (TIHS)", required_hats=[HatType.BREWING]), - "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(335561, "Alpine Skyline Area"), - "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(334831, "Alpine Skyline Area"), - "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(334758, "The Birdhouse"), + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", required_hats=[HatType.BREWING]), + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), + "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"), + "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"), - "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(336497, "The Birdhouse", + "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse", required_hats=[HatType.DWELLER]), - "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(336496, "The Birdhouse"), - "Alpine Skyline - The Birdhouse: Hay Bale": LocData(335885, "The Birdhouse"), - "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(335886, "The Birdhouse"), - "Alpine Skyline - The Birdhouse: Outer Edge": LocData(335492, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"), - "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(337058, "Alpine Skyline Area"), - "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(336052, "Alpine Skyline Area"), - "Alpine Skyline - Ember Summit": LocData(336311, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(335448, "The Lava Cake"), - "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(334291, "The Lava Cake"), - "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(335417, "The Lava Cake"), - "Alpine Skyline - The Lava Cake: Top Cake": LocData(335418, "The Lava Cake"), - "Alpine Skyline - The Twilight Path": LocData(334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), - "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(336478, "The Twilight Bell"), - "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(335826, "The Twilight Bell"), - "Alpine Skyline - Goat Outpost Horn": LocData(334760, "Alpine Skyline Area"), - "Alpine Skyline - Windy Passage": LocData(334776, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(336395, "The Windmill"), - "Alpine Skyline - The Windmill: Entrance": LocData(335783, "The Windmill"), - "Alpine Skyline - The Windmill: Dropdown": LocData(335815, "The Windmill"), - "Alpine Skyline - The Windmill: House Window": LocData(335389, "The Windmill"), + "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"), + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"), + "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), + "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), + "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), + "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), + "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), + "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"), + "Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"), - "The Finale - Frozen Item": LocData(304108, "The Finale"), + "The Finale - Frozen Item": LocData(2000304108, "The Finale"), - "Bon Voyage! - Lamp Post Top": LocData(305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), - "Bon Voyage! - Mafia Cargo Ship": LocData(304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Toilet": LocData(305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Bar": LocData(304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Dive Board Ledge": LocData(304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Top Balcony": LocData(304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Octopus Room": LocData(305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Octopus Room Top": LocData(304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Laundry Room": LocData(304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Ship Side": LocData(304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "The Arctic Cruise - Silver Ring": LocData(305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Reception Room - Suitcase": LocData(304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Reception Room - Under Desk": LocData(304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Lamp Post": LocData(304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Iceberg Top": LocData(304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), - "Rock the Boat - Post Captain Rescue": LocData(304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, + "Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, required_hats=[HatType.ICE]), - "Nyakuza Metro - Main Station Dining Area": LocData(304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), - "Nyakuza Metro - Top of Ramen Shop": LocData(304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), - "Yellow Overpass Station - Brewing Crate": LocData(305413, "Yellow Overpass Station", + "Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.BREWING]), - "Bluefin Tunnel - Cat Vacuum": LocData(305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Pink Paw Station - Cat Vacuum": LocData(305110, "Pink Paw Station", + "Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.DWELLER]), - "Pink Paw Station - Behind Fan": LocData(304106, "Pink Paw Station", + "Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.TIME_STOP, HatType.DWELLER]), } act_completions = { - "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), - "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), + "Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"), - "Act Completion (Welcome to Mafia Town)": LocData(311771, "Welcome to Mafia Town"), - "Act Completion (Barrel Battle)": LocData(311958, "Barrel Battle"), - "Act Completion (She Came from Outer Space)": LocData(312262, "She Came from Outer Space"), - "Act Completion (Down with the Mafia!)": LocData(311326, "Down with the Mafia!"), - "Act Completion (Cheating the Race)": LocData(312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), - "Act Completion (Heating Up Mafia Town)": LocData(311481, "Heating Up Mafia Town", umbrella=True), - "Act Completion (The Golden Vault)": LocData(312250, "The Golden Vault"), - "Act Completion (Time Rift - Bazaar)": LocData(312465, "Time Rift - Bazaar"), - "Act Completion (Time Rift - Sewers)": LocData(312484, "Time Rift - Sewers"), - "Act Completion (Time Rift - Mafia of Cooks)": LocData(311855, "Time Rift - Mafia of Cooks"), + "Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"), + "Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"), + "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"), + "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"), + "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), + "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"), + "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"), + "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"), + "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"), - "Act Completion (Dead Bird Studio)": LocData(311383, "Dead Bird Studio", hit_requirement=1), - "Act Completion (Murder on the Owl Express)": LocData(311544, "Murder on the Owl Express"), - "Act Completion (Picture Perfect)": LocData(311587, "Picture Perfect"), - "Act Completion (Train Rush)": LocData(312481, "Train Rush", hookshot=True), - "Act Completion (The Big Parade)": LocData(311157, "The Big Parade", umbrella=True), - "Act Completion (Award Ceremony)": LocData(311488, "Award Ceremony"), - "Act Completion (Dead Bird Studio Basement)": LocData(312253, "Dead Bird Studio Basement", hookshot=True), - "Act Completion (Time Rift - The Owl Express)": LocData(312807, "Time Rift - The Owl Express"), - "Act Completion (Time Rift - The Moon)": LocData(312785, "Time Rift - The Moon"), - "Act Completion (Time Rift - Dead Bird Studio)": LocData(312577, "Time Rift - Dead Bird Studio"), + "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1), + "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"), + "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"), + "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True), + "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True), + "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"), + "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True), + "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"), + "Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"), + "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"), - "Act Completion (Contractual Obligations)": LocData(312317, "Contractual Obligations", paintings=1), - "Act Completion (The Subcon Well)": LocData(311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), - "Act Completion (Toilet of Doom)": LocData(311984, "Toilet of Doom", hookshot=True, paintings=1), - "Act Completion (Queen Vanessa's Manor)": LocData(312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), - "Act Completion (Mail Delivery Service)": LocData(312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), - "Act Completion (Your Contract has Expired)": LocData(311390, "Your Contract has Expired", umbrella=True), - "Act Completion (Time Rift - Pipe)": LocData(313069, "Time Rift - Pipe", hookshot=True), - "Act Completion (Time Rift - Village)": LocData(313056, "Time Rift - Village"), - "Act Completion (Time Rift - Sleepy Subcon)": LocData(312086, "Time Rift - Sleepy Subcon"), + "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, paintings=1), + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), + "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), + "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"), - "Act Completion (The Birdhouse)": LocData(311428, "The Birdhouse"), - "Act Completion (The Lava Cake)": LocData(312509, "The Lava Cake"), - "Act Completion (The Twilight Bell)": LocData(311540, "The Twilight Bell"), - "Act Completion (The Windmill)": LocData(312263, "The Windmill"), - "Act Completion (The Illness has Spread)": LocData(312022, "The Illness has Spread", hookshot=True), + "Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"), + "Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"), + "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"), + "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"), + "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True), - "Act Completion (Time Rift - The Twilight Bell)": LocData(312399, "Time Rift - The Twilight Bell", + "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell", required_hats=[HatType.DWELLER]), - "Act Completion (Time Rift - Curly Tail Trail)": LocData(313335, "Time Rift - Curly Tail Trail", + "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail", required_hats=[HatType.ICE]), - "Act Completion (Time Rift - Alpine Skyline)": LocData(311777, "Time Rift - Alpine Skyline"), + "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"), - "Act Completion (The Finale)": LocData(311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), - "Act Completion (Time Rift - Tour)": LocData(311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), + "Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Act Completion (Bon Voyage!)": LocData(311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), - "Act Completion (Ship Shape)": LocData(311451, "Ship Shape", dlc_flags=HatDLC.dlc1), - "Act Completion (Rock the Boat)": LocData(311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, required_hats=[HatType.ICE]), - "Act Completion (Time Rift - Balcony)": LocData(312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, hookshot=True), - "Act Completion (Time Rift - Deep Sea)": LocData(312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1), + "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, required_hats=[HatType.ICE]), + "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), - "Act Completion (Nyakuza Metro Intro)": LocData(311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), - "Act Completion (Yellow Overpass Station)": LocData(311206, "Yellow Overpass Station", + "Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, hookshot=True), - "Act Completion (Yellow Overpass Manhole)": LocData(311387, "Yellow Overpass Manhole", + "Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE]), - "Act Completion (Green Clean Station)": LocData(311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), - "Act Completion (Green Clean Manhole)": LocData(311388, "Green Clean Manhole", + "Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE, HatType.DWELLER]), - "Act Completion (Bluefin Tunnel)": LocData(311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Act Completion (Pink Paw Station)": LocData(311209, "Pink Paw Station", + "Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.DWELLER]), - "Act Completion (Pink Paw Manhole)": LocData(311389, "Pink Paw Manhole", + "Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE]), - "Act Completion (Rush Hour)": LocData(311210, "Rush Hour", + "Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.ICE, HatType.BREWING]), - "Act Completion (Time Rift - Rumbi Factory)": LocData(312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), } storybook_pages = { - "Mafia of Cooks - Page: Fish Pile": LocData(345091, "Time Rift - Mafia of Cooks"), - "Mafia of Cooks - Page: Trash Mound": LocData(345090, "Time Rift - Mafia of Cooks"), - "Mafia of Cooks - Page: Beside Red Building": LocData(345092, "Time Rift - Mafia of Cooks"), - "Mafia of Cooks - Page: Behind Shipping Containers": LocData(345095, "Time Rift - Mafia of Cooks"), - "Mafia of Cooks - Page: Top of Boat": LocData(345093, "Time Rift - Mafia of Cooks"), - "Mafia of Cooks - Page: Below Dock": LocData(345094, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"), - "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(345449, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(345447, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(345448, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(345450, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(345451, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Green Screen": LocData(345452, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: In The Corner": LocData(345453, "Time Rift - Dead Bird Studio"), - "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(345445, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"), - "Sleepy Subcon - Page: Behind Entrance Area": LocData(345373, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Near Wrecking Ball": LocData(345327, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Behind Crane": LocData(345371, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Wrecked Treehouse": LocData(345326, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(345372, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Rotating Platform": LocData(345328, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(345329, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Frozen Tree": LocData(345330, "Time Rift - Sleepy Subcon"), - "Sleepy Subcon - Page: Secret Library": LocData(345370, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"), - "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(345016, "Time Rift - Alpine Skyline"), - "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(345012, "Time Rift - Alpine Skyline"), - "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(345015, "Time Rift - Alpine Skyline"), - "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(345014, "Time Rift - Alpine Skyline"), - "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(345013, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"), - "Deep Sea - Page: Starfish": LocData(346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), - "Deep Sea - Page: Mini Castle": LocData(346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), - "Deep Sea - Page: Urchins": LocData(346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), - "Deep Sea - Page: Big Castle": LocData(346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Castle Top Chest": LocData(304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Urchin Ledge": LocData(346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Hidden Castle Chest": LocData(304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Falling Platform": LocData(346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Lava Starfish": LocData(346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Tour - Page: Mafia Town - Ledge": LocData(345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Mafia Town - Beach": LocData(345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Dead Bird Studio - Fragile Box": LocData(345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Subcon Forest - Top of Pillar": LocData(345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Alpine Skyline - Birdhouse": LocData(345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Tour - Page: The Finale - Near Entrance": LocData(345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), - "Rumbi Factory - Page: Manhole": LocData(345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Shutter Doors": LocData(345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(345892, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: 3rd Area Ledge": LocData(345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Green Box Assembly Line": LocData(345884, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Broken Window": LocData(345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Money Vault": LocData(345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Warehouse Boxes": LocData(345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Glass Shelf": LocData(345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Last Area": LocData(345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: 20003rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), } shop_locations = { - "Badge Seller - Item 1": LocData(301003, "Badge Seller"), - "Badge Seller - Item 2": LocData(301004, "Badge Seller"), - "Badge Seller - Item 3": LocData(301005, "Badge Seller"), - "Badge Seller - Item 4": LocData(301006, "Badge Seller"), - "Badge Seller - Item 5": LocData(301007, "Badge Seller"), - "Badge Seller - Item 6": LocData(301008, "Badge Seller"), - "Badge Seller - Item 7": LocData(301009, "Badge Seller"), - "Badge Seller - Item 8": LocData(301010, "Badge Seller"), - "Badge Seller - Item 9": LocData(301011, "Badge Seller"), - "Badge Seller - Item 10": LocData(301012, "Badge Seller"), - "Mafia Boss Shop Item": LocData(301013, "Spaceship"), + "Badge Seller - Item 1": LocData(2000301003, "Badge Seller"), + "Badge Seller - Item 2": LocData(2000301004, "Badge Seller"), + "Badge Seller - Item 3": LocData(2000301005, "Badge Seller"), + "Badge Seller - Item 4": LocData(2000301006, "Badge Seller"), + "Badge Seller - Item 5": LocData(2000301007, "Badge Seller"), + "Badge Seller - Item 6": LocData(2000301008, "Badge Seller"), + "Badge Seller - Item 7": LocData(2000301009, "Badge Seller"), + "Badge Seller - Item 8": LocData(2000301010, "Badge Seller"), + "Badge Seller - Item 9": LocData(2000301011, "Badge Seller"), + "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"), + "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"), - "Yellow Overpass Station - Yellow Ticket Booth": LocData(301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), - "Green Clean Station - Green Ticket Booth": LocData(301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), - "Bluefin Tunnel - Blue Ticket Booth": LocData(301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Pink Paw Station - Pink Ticket Booth": LocData(301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), + "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.DWELLER]), - "Main Station Thug A - Item 1": LocData(301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), - "Main Station Thug A - Item 2": LocData(301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), - "Main Station Thug A - Item 3": LocData(301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), - "Main Station Thug A - Item 4": LocData(301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), - "Main Station Thug A - Item 5": LocData(301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_0"), - "Main Station Thug B - Item 1": LocData(301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_1"), - "Main Station Thug B - Item 2": LocData(301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_1"), - "Main Station Thug B - Item 3": LocData(301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_1"), - "Main Station Thug B - Item 4": LocData(301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_1"), - "Main Station Thug B - Item 5": LocData(301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_1"), - "Main Station Thug C - Item 1": LocData(301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_2"), - "Main Station Thug C - Item 2": LocData(301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_2"), - "Main Station Thug C - Item 3": LocData(301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_2"), - "Main Station Thug C - Item 4": LocData(301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_2"), - "Main Station Thug C - Item 5": LocData(301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + "Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_2"), - "Yellow Overpass Thug A - Item 1": LocData(301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_13"), - "Yellow Overpass Thug A - Item 2": LocData(301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_13"), - "Yellow Overpass Thug A - Item 3": LocData(301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_13"), - "Yellow Overpass Thug A - Item 4": LocData(301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_13"), - "Yellow Overpass Thug A - Item 5": LocData(301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_13"), - "Yellow Overpass Thug B - Item 1": LocData(301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_5"), - "Yellow Overpass Thug B - Item 2": LocData(301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_5"), - "Yellow Overpass Thug B - Item 3": LocData(301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_5"), - "Yellow Overpass Thug B - Item 4": LocData(301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_5"), - "Yellow Overpass Thug B - Item 5": LocData(301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_5"), - "Yellow Overpass Thug C - Item 1": LocData(301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_14"), - "Yellow Overpass Thug C - Item 2": LocData(301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_14"), - "Yellow Overpass Thug C - Item 3": LocData(301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_14"), - "Yellow Overpass Thug C - Item 4": LocData(301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_14"), - "Yellow Overpass Thug C - Item 5": LocData(301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + "Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_14"), - "Green Clean Station Thug A - Item 1": LocData(301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_4"), - "Green Clean Station Thug A - Item 2": LocData(301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_4"), - "Green Clean Station Thug A - Item 3": LocData(301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_4"), - "Green Clean Station Thug A - Item 4": LocData(301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_4"), - "Green Clean Station Thug A - Item 5": LocData(301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_4"), # This guy requires either the yellow ticket or the Ice Hat - "Green Clean Station Thug B - Item 1": LocData(301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), - "Green Clean Station Thug B - Item 2": LocData(301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), - "Green Clean Station Thug B - Item 3": LocData(301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), - "Green Clean Station Thug B - Item 4": LocData(301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), - "Green Clean Station Thug B - Item 5": LocData(301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, + "Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), - "Bluefin Tunnel Thug - Item 1": LocData(301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + "Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_7"), - "Bluefin Tunnel Thug - Item 2": LocData(301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + "Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_7"), - "Bluefin Tunnel Thug - Item 3": LocData(301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + "Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_7"), - "Bluefin Tunnel Thug - Item 4": LocData(301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + "Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_7"), - "Bluefin Tunnel Thug - Item 5": LocData(301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + "Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, nyakuza_thug="Hat_NPC_NyakuzaShop_7"), - "Pink Paw Station Thug - Item 1": LocData(301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.DWELLER], hookshot=True, nyakuza_thug="Hat_NPC_NyakuzaShop_12"), - "Pink Paw Station Thug - Item 2": LocData(301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.DWELLER], hookshot=True, nyakuza_thug="Hat_NPC_NyakuzaShop_12"), - "Pink Paw Station Thug - Item 3": LocData(301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.DWELLER], hookshot=True, nyakuza_thug="Hat_NPC_NyakuzaShop_12"), - "Pink Paw Station Thug - Item 4": LocData(301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.DWELLER], hookshot=True, nyakuza_thug="Hat_NPC_NyakuzaShop_12"), - "Pink Paw Station Thug - Item 5": LocData(301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + "Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, required_hats=[HatType.DWELLER], hookshot=True, nyakuza_thug="Hat_NPC_NyakuzaShop_12"), } contract_locations = { - "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), - "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), - "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), - "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), + "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"), } # Don't put any of the locations from peaks here, the rules for their entrances are set already @@ -671,61 +664,248 @@ zipline_unlocks = { "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", } -event_locs = { - "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), - "TOD Access": LocData(0, "Toilet of Doom", act_complete_event=False), - "YCHE Access": LocData(0, "Your Contract has Expired", act_complete_event=False), +# act completion rules should be set automatically as these are all event items +zero_jumps_hard = { + "Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), - "Birdhouse Cleared": LocData(0, "The Birdhouse"), - "Lava Cake Cleared": LocData(0, "The Lava Cake"), - "Windmill Cleared": LocData(0, "The Windmill"), - "Twilight Bell Cleared": LocData(0, "The Twilight Bell"), - "Time Piece Cluster": LocData(0, "The Finale"), + "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "The Big Parade": LocData(0, "The Big Parade", + umbrella=True, + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread", + required_hats=[HatType.ICE], hookshot=True, + hit_requirement=1, dlc_flags=HatDLC.death_wish), + + "The Finale (Zero Jumps)": LocData(0, "The Finale", + required_hats=[HatType.ICE, HatType.DWELLER], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), +} + +zero_jumps_expert = { + "The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + + "The Windmill (Zero Jumps)": LocData(0, "The Windmill", + required_hats=[HatType.ICE], + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + + "Sleepy Subcon (Zero Jumps)": LocData(0, "Sleepy Subcon", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), +} + +zero_jumps = { + **zero_jumps_hard, + **zero_jumps_expert, + "Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + + "Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race", + required_hats=[HatType.TIME_STOP], + dlc_flags=HatDLC.death_wish), + + "The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio", + required_hats=[HatType.ICE], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Train Rush (Zero Jumps)": LocData(0, "Train Rush", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations", + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired", + umbrella=True, + dlc_flags=HatDLC.death_wish), + + # No ice hat/painting required in Expert + "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom", + hookshot=True, + hit_requirement=1, + required_hats=[HatType.ICE], + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service", + required_hats=[HatType.SPRINT], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), + + "Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.dlc2_dw), +} + +# please just ignore all the duplicate key warnings, thanks +snatcher_coins = { + "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw), +} + +event_locs = { + **zero_jumps, + **snatcher_coins, + "HUMT Access": LocData(0, "Heating Up Mafia Town"), + "TOD Access": LocData(0, "Toilet of Doom"), + "YCHE Access": LocData(0, "Your Contract has Expired"), + + "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), + "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), + "Windmill Cleared": LocData(0, "The Windmill", act_event=True), + "Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True), + "Time Piece Cluster": LocData(0, "The Finale", act_event=True), # not really an act - "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, act_complete_event=False), + "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), - "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), - "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2), - "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), - "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2), - "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2), - "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2), - "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2), - "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2), + "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True), +} +# DO NOT ALTER THE ORDER OF THIS LIST +death_wishes = { + "Beat the Heat": 2000350000, + "Snatcher's Hit List": 2000350002, + "So You're Back From Outer Space": 2000350004, + "Collect-a-thon": 2000350006, + "Rift Collapse: Mafia of Cooks": 2000350008, + "She Speedran from Outer Space": 2000350010, + "Mafia's Jumps": 2000350012, + "Vault Codes in the Wind": 2000350014, + "Encore! Encore!": 2000350016, + "Snatcher Coins in Mafia Town": 2000350018, - # -------------- Death Wish Candle Related --------------- # + "Security Breach": 2000350020, + "The Great Big Hootenanny": 2000350022, + "Rift Collapse: Dead Bird Studio": 2000350024, + "10 Seconds until Self-Destruct": 2000350026, + "Killing Two Birds": 2000350028, + "Snatcher Coins in Battle of the Birds": 2000350030, + "Zero Jumps": 2000350032, + "Speedrun Well": 2000350034, + "Rift Collapse: Sleepy Subcon": 2000350036, + "Boss Rush": 2000350038, + "Quality Time with Snatcher": 2000350040, + "Breaching the Contract": 2000350042, + "Snatcher Coins in Subcon Forest": 2000350044, - # Snatcher Coins - "MT Access": LocData(0, "Mafia Town Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "DWTM Access": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "CTR Access": LocData(0, "Cheating the Race", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "TGV Access": LocData(0, "The Golden Vault", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Bird Sanctuary": 2000350046, + "Rift Collapse: Alpine Skyline": 2000350048, + "Wound-Up Windmill": 2000350050, + "The Illness has Speedrun": 2000350052, + "Snatcher Coins in Alpine Skyline": 2000350054, + "Camera Tourist": 2000350056, - "DBS Access": LocData(0, "Dead Bird Studio - Elevator Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "PP Access": LocData(0, "Picture Perfect", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "The Mustache Gauntlet": 2000350058, + "No More Bad Guys": 2000350060, - "SF Access": LocData(0, "Subcon Forest Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Seal the Deal": 2000350062, + "Rift Collapse: Deep Sea": 2000350064, + "Cruisin' for a Bruisin'": 2000350066, - "LC Access": LocData(0, "The Lava Cake", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "WM Access": LocData(0, "The Windmill", act_complete_event=False, dlc_flags=HatDLC.death_wish), - - # Camera Tourist - "Mafia Boss": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "Conductor": LocData(0, "Dead Bird Studio Basement", dlc_flags=HatDLC.death_wish), - "Snatcher": LocData(0, "Your Contract has Expired", act_complete_event=False, dlc_flags=HatDLC.death_wish), - "Evil Flower": LocData(0, "The Illness has Spread", act_complete_event=False, dlc_flags=HatDLC.death_wish), - - # Zero Jumps - "Welcome to Mafia Town Cleared": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), - "Picture Perfect Cleared": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), - "Contractual Obligations Cleared": LocData(0, "Contractual Obligations", dlc_flags=HatDLC.death_wish), - "Your Contract has Expired Cleared": LocData(0, "Your Contract has Expired", dlc_flags=HatDLC.death_wish), - "Mail Delivery Service Cleared": LocData(0, "Mail Delivery Service", dlc_flags=HatDLC.death_wish), - "Cheating the Race Cleared": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), - "Train Rush Cleared": LocData(0, "Train Rush", dlc_flags=HatDLC.death_wish), + "Community Rift: Rhythm Jump Studio": 2000350068, + "Community Rift: Twilight Travels": 2000350070, + "Community Rift: The Mountain Rift": 2000350072, + "Snatcher Coins in Nyakuza Metro": 2000350074, } location_table = { @@ -735,52 +915,3 @@ location_table = { **contract_locations, **shop_locations, } - -# DO NOT ALTER THE ORDER OF THIS LIST -# This file is in here instead of DeathWishLocations.py to prevent circular import problems -death_wishes = { - "Beat the Heat": 350000, - "Snatcher's Hit List": 350002, - "So You're Back From Outer Space": 350004, - "Collect-a-thon": 350006, - "Rift Collapse: Mafia of Cooks": 350008, - "She Speedran from Outer Space": 350010, - "Mafia's Jumps": 350012, - "Vault Codes in the Wind": 350014, - "Encore! Encore!": 350016, - "Snatcher Coins in Mafia Town": 350018, - - "Security Breach": 350020, - "The Great Big Hootenanny": 350022, - "Rift Collapse: Dead Bird Studio": 350024, - "10 Seconds until Self-Destruct": 350026, - "Killing Two Birds": 350028, - "Snatcher Coins in Battle of the Birds": 350030, - "Zero Jumps": 350032, - - "Speedrun Well": 350034, - "Rift Collapse: Sleepy Subcon": 350036, - "Boss Rush": 350038, - "Quality Time with Snatcher": 350040, - "Breaching the Contract": 350042, - "Snatcher Coins in Subcon Forest": 350044, - - "Bird Sanctuary": 350046, - "Rift Collapse: Alpine Skyline": 350048, - "Wound-Up Windmill": 350050, - "The Illness has Speedrun": 350052, - "Snatcher Coins in Alpine Skyline": 350054, - "Camera Tourist": 350056, - - "The Mustache Gauntlet": 350058, - "No More Bad Guys": 350060, - - "Seal the Deal": 350062, - "Rift Collapse: Deep Sea": 350064, - "Cruisin' for a Bruisin'": 350066, - - "Community Rift: Rhythm Jump Studio": 350068, - "Community Rift: Twilight Travels": 350070, - "Community Rift: The Mountain Rift": 350072, - "Snatcher Coins in Nyakuza Metro": 350074, -} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index c8eced5836..7c0567a151 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -532,11 +532,9 @@ class DWExcludeAnnoyingBonuses(Toggle): - Snatcher's Hit List - 10 Seconds until Self-Destruct - Killing Two Birds - - Snatcher Coins in Battle of the Birds - Zero Jumps - Bird Sanctuary - Wound-Up Windmill - - Snatcher Coins in Alpine Skyline - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 @@ -674,6 +672,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "CTRLogic": CTRLogic, "RandomizeHatOrder": RandomizeHatOrder, "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, "CompassBadgeMode": CompassBadgeMode, "ShuffleStorybookPages": ShuffleStorybookPages, "ShuffleActContracts": ShuffleActContracts, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c3fbfe8359..b1d2293961 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,11 +1,10 @@ from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location -from .Locations import HatInTimeLocation, location_table, storybook_pages, event_locs, is_location_valid, \ - shop_locations, get_tasksanity_start_id -from .Items import HatInTimeItem -from .Types import ChapterIndex, Difficulty +from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem +from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ + shop_locations, get_tasksanity_start_id, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard import typing -from .Rules import set_rift_rules +from .Rules import set_rift_rules, get_difficulty # ChapterIndex: region @@ -881,6 +880,16 @@ def create_events(world: World) -> int: if not is_location_valid(world, name): continue + if world.is_dw(): + if name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + elif name in zero_jumps: + if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: + continue + event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) event.show_in_spoiler = False count += 1 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 5123250d85..dd8daedb74 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,8 +1,8 @@ from worlds.AutoWorld import World, CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ - shop_locations, event_locs -from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty + shop_locations, event_locs, snatcher_coins +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC from BaseClasses import Location, Entrance, Region import typing @@ -62,7 +62,7 @@ def get_difficulty(world: World) -> Difficulty: return Difficulty(world.multiworld.LogicDifficulty[world.player].value) -def has_paintings(state: CollectionState, world: World, count: int) -> bool: +def has_paintings(state: CollectionState, world: World, count: int, surf: bool = True) -> bool: if not painting_logic(world): return True @@ -71,11 +71,11 @@ def has_paintings(state: CollectionState, world: World, count: int) -> bool: return True # All paintings can be skipped with No Bonk, very easily, if the player knows - if get_difficulty(world) >= Difficulty.MODERATE and can_surf(state, world): + if surf and get_difficulty(world) >= Difficulty.MODERATE and can_surf(state, world): return True paintings: int = state.count("Progressive Painting Unlock", world.player) - if get_difficulty(world) >= Difficulty.MODERATE: + if surf and get_difficulty(world) >= Difficulty.MODERATE: # Green+Yellow paintings can also be skipped easily if count == 1 or paintings >= 1 and count == 3: return True @@ -264,6 +264,10 @@ def set_rules(world: World): if key in contract_locations.keys(): continue + if data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw: + if key in snatcher_coins.keys(): + key = f"{key} ({data.region})" + location = world.multiworld.get_location(key, world.player) for hat in data.required_hats: @@ -277,7 +281,10 @@ def set_rules(world: World): add_rule(location, lambda state: state.has("Umbrella", world.player)) if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + if "Toilet of Doom" not in key: + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + else: + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings, False)) if data.hit_requirement > 0: if data.hit_requirement == 1: @@ -402,6 +409,10 @@ def set_moderate_rules(world: World): # Moderate: Gallery without Brewing Hat set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) + # Moderate: Above Boats via Ice Hat Sliding + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + # Moderate: Clock Tower Chest + Ruined Tower with nothing add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) @@ -430,6 +441,13 @@ def set_moderate_rules(world: World): set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), lambda state: True) + # Moderate: Twilight Path without Dweller Mask + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) + + # Moderate: Finale without Hookshot + set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) + if world.is_dlc1(): # Moderate: clear Rock the Boat without Ice Hat add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) @@ -451,13 +469,6 @@ def set_moderate_rules(world: World): # The player can quite literally walk past the fan from the side without Time Stop. set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) - # The player can't jump back down to the manhole due to a fall damage volume preventing them from doing so - set_rule(world.multiworld.get_location("Act Completion (Pink Paw Manhole)", world.player), - lambda state: (state.has("Metro Ticket - Pink", world.player) - or state.has("Metro Ticket - Yellow", world.player) - and state.has("Metro Ticket - Blue", world.player)) - and can_use_hat(state, world, HatType.ICE)) - # Moderate: clear Rush Hour without Hookshot set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: state.has("Metro Ticket - Pink", world.player) @@ -473,14 +484,13 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.SPRINT) and state.has("Scooter Badge", world.player), "or") - # Hard: Cross Subcon boss arena gap with No Bonk + SDJ, allowing access to the boss arena chest - # Doing this in reverse from YCHE is expert logic, which expects you to cherry hover - add_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: can_surf(state, world) and can_sdj(state, world), "or") - set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), lambda state: has_paintings(state, world, 3)) + # Cherry bridge over boss arena gap (painting still expected) + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, False)) + # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_sdj(state, world) @@ -492,13 +502,14 @@ def set_hard_rules(world: World): add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_sdj(state, world), "or") - add_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), - lambda state: can_use_hat(state, world, HatType.DWELLER) and can_sdj(state, world), "or") - # Hard: Mystifying Time Mesa time trial without hats set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), lambda state: can_use_hookshot(state, world)) + # Finale Telescope with only Ice Hat + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + if world.is_dlc1(): # Hard: clear Deep Sea without Dweller Mask set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), @@ -515,6 +526,10 @@ def set_hard_rules(world: World): def set_expert_rules(world: World): + # Finale Telescope with no hats + set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) + # Expert: Mafia Town - Above Boats with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) @@ -526,8 +541,6 @@ def set_expert_rules(world: World): # Expert: get to and clear Twilight Bell without Dweller Mask. # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. - set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) - add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), lambda state: can_use_hookshot(state, world), "or") @@ -640,10 +653,6 @@ def set_subcon_rules(world: World): and (not painting_logic(world) or has_paintings(state, world, 1)) or state.has("YCHE Access", world.player)) - if world.multiworld.UmbrellaLogic[world.player].value > 0: - add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), - lambda state: can_hit(state, world)) - set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.DWELLER)) @@ -907,9 +916,12 @@ def set_event_rules(world: World): if not is_location_valid(world, name): continue + if (data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw) and name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + event: Location = world.multiworld.get_location(name, world.player) - if data.act_complete_event: + if data.act_event: add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index a62ad581cc..3e1d01ab61 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -1,4 +1,14 @@ from enum import IntEnum, IntFlag +from typing import NamedTuple, Optional, List +from BaseClasses import Location, Item, ItemClassification + + +class HatInTimeLocation(Location): + game: str = "A Hat in Time" + + +class HatInTimeItem(Item): + game: str = "A Hat in Time" class HatType(IntEnum): @@ -15,6 +25,7 @@ class HatDLC(IntFlag): dlc1 = 0b001 dlc2 = 0b010 death_wish = 0b100 + dlc2_dw = 0b0110 # for Snatcher Coins in Nyakuza Metro class ChapterIndex(IntEnum): @@ -35,6 +46,30 @@ class Difficulty(IntEnum): EXPERT = 2 +class LocData(NamedTuple): + id: Optional[int] = 0 + region: Optional[str] = "" + required_hats: Optional[List[HatType]] = [HatType.NONE] + hookshot: Optional[bool] = False + dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] + + # For UmbrellaLogic setting + umbrella: Optional[bool] = False # Umbrella required for this check + hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) + + # Other + act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion + nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + + +class ItemData(NamedTuple): + code: Optional[int] + classification: ItemClassification + dlc_flags: Optional[HatDLC] = HatDLC.none + + hat_type_to_item = { HatType.SPRINT: "Sprint Hat", HatType.BREWING: "Brewing Hat", diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 67a9fb0816..564cdbe4f3 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,10 +1,10 @@ -from BaseClasses import Item, ItemClassification, LocationProgressType, Tutorial -from .Items import HatInTimeItem, item_table, create_item, relic_groups, act_contracts, create_itempool +from BaseClasses import Item, ItemClassification, Tutorial +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options -from .Types import HatType, ChapterIndex +from .Types import HatType, ChapterIndex, HatInTimeItem from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld From 0e273dd17a6dd78d04ccec29ddda3aa7e5e84c7f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 17 Oct 2023 12:54:35 -0400 Subject: [PATCH 036/143] New client --- AHITClient.py | 205 ++++++++++++++++++++++++++++++++++++++++ worlds/ahit/__init__.py | 3 + 2 files changed, 208 insertions(+) create mode 100644 AHITClient.py diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 0000000000..844e84ac6a --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,205 @@ +import asyncio +import Utils +import websockets +import functools +from copy import deepcopy +from typing import List, Any, Iterable +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from MultiServer import Endpoint +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, \ + get_base_parser + + +class AHITJSONToTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(node) # No colors for the in-game text + + +class AHITCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_ahit(self): + """Check AHIT Connection State""" + if isinstance(self.ctx, AHITContext): + logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") + + +class AHITContext(CommonContext): + command_processor = AHITCommandProcessor + game = "A Hat in Time" + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.proxy = None + self.proxy_task = None + self.gamejsontotext = AHITJSONToTextParser(self) + self.autoreconnect_task = None + self.endpoint = None + self.items_handling = 0b111 + self.room_info = None + self.connected_msg = None + self.game_connected = False + self.awaiting_info = False + self.log_network = False + self.full_inventory: List[Any] = [] + self.server_msgs: List[Any] = [] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AHITContext, self).server_auth(password_requested) + + await self.get_username() + await self.send_connect() + + def get_ahit_status(self) -> str: + if not self.is_proxy_connected(): + return "Not connected to A Hat in Time" + + return "Connected to A Hat in Time" + + async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: + """ `msgs` JSON serializable """ + if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: + return False + + if self.log_network: + logger.info(f"Outgoing message: {msgs}") + + await self.endpoint.socket.send(msgs) + return True + + async def disconnect_proxy(self): + if self.endpoint and not self.endpoint.socket.closed: + await self.endpoint.socket.close() + if self.proxy_task is not None: + await self.proxy_task + + def is_proxy_connected(self) -> bool: + return self.endpoint and self.endpoint.socket.open + + def on_print_json(self, args: dict): + text = self.gamejsontotext(deepcopy(args["data"])) + msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} + self.server_msgs.append(encode([msg])) + + if self.ui: + self.ui.print_json(args["data"]) + else: + text = self.jsontotextparser(args["data"]) + logger.info(text) + + def update_items(self): + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.connected_msg = encode([args]) + if self.awaiting_info: + self.server_msgs.append(self.room_info) + self.update_items() + self.awaiting_info = False + + elif cmd == "ReceivedItems": + if args["index"] == 0: + self.full_inventory.clear() + + for item in args["items"]: + self.full_inventory.append(NetworkItem(*item)) + + self.server_msgs.append(encode([args])) + + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.room_info = encode([args]) + + else: + if cmd != "PrintJSON": + self.server_msgs.append(encode([args])) + + def run_gui(self): + from kvui import GameManager + + class AHITManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago A Hat in Time Client" + + self.ui = AHITManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def proxy(websocket, path: str = "/", ctx: AHITContext = None): + ctx.endpoint = Endpoint(websocket) + try: + await on_client_connected(ctx) + + if ctx.is_proxy_connected(): + async for data in websocket: + for msg in decode(data): + if msg["cmd"] == "Connect": + if ctx.connected_msg: + await ctx.send_msgs_proxy(ctx.connected_msg) + ctx.update_items() + continue + + if ctx.log_network: + logger.info(f"Incoming message: {msg}") + + await ctx.send_msgs([msg]) + + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logger.exception(e) + finally: + await ctx.disconnect_proxy() + + +async def on_client_connected(ctx: AHITContext): + if ctx.room_info: + await ctx.send_msgs_proxy(ctx.room_info) + else: + ctx.awaiting_info = True + + +async def main(): + parser = get_base_parser() + args = parser.parse_args() + + ctx = AHITContext(args.connect, args.password) + logger.info("Starting A Hat in Time proxy server") + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), host="localhost", port=11311) + ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.proxy + await ctx.proxy_task + await ctx.exit_event.wait() + + +async def proxy_loop(ctx: AHITContext): + try: + while not ctx.exit_event.is_set(): + if len(ctx.server_msgs) > 0: + for msg in ctx.server_msgs: + await ctx.send_msgs_proxy(msg) + + ctx.server_msgs.clear() + await asyncio.sleep(0.1) + except Exception as e: + logger.exception(e) + logger.info("Aborting AHIT Proxy Client due to errors") + + +if __name__ == '__main__': + Utils.init_logging("AHITClient") + options = Utils.get_options() + + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 564cdbe4f3..0ae5f0035d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -9,6 +9,7 @@ from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld from typing import List, Dict, TextIO +from worlds.LauncherComponents import Component, components hat_craft_order: Dict[int, List[HatType]] = {} hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} @@ -19,6 +20,8 @@ dw_shuffle: Dict[int, List[str]] = {} nyakuza_thug_items: Dict[int, Dict[str, int]] = {} badge_seller_count: Dict[int, int] = {} +components.append(Component("A Hat in Time Client", "AHITClient")) + class AWebInTime(WebWorld): theme = "partyTime" From 8b98cd7a0151434283f8c437760c91e6545f1c79 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 17 Oct 2023 13:32:06 -0400 Subject: [PATCH 037/143] More client changes --- AHITClient.py | 23 ++++++++++++++++++++--- worlds/ahit/__init__.py | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/AHITClient.py b/AHITClient.py index 844e84ac6a..390337108d 100644 --- a/AHITClient.py +++ b/AHITClient.py @@ -9,6 +9,8 @@ from MultiServer import Endpoint from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, \ get_base_parser +DEBUG = False + class AHITJSONToTextParser(JSONtoTextParser): def _handle_color(self, node: JSONMessagePart): @@ -41,7 +43,6 @@ class AHITContext(CommonContext): self.connected_msg = None self.game_connected = False self.awaiting_info = False - self.log_network = False self.full_inventory: List[Any] = [] self.server_msgs: List[Any] = [] @@ -63,7 +64,7 @@ class AHITContext(CommonContext): if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: return False - if self.log_network: + if DEBUG: logger.info(f"Outgoing message: {msgs}") await self.endpoint.socket.send(msgs) @@ -139,12 +140,28 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): async for data in websocket: for msg in decode(data): if msg["cmd"] == "Connect": + # Proxy is connecting, make sure it is valid + if msg["game"] != "A Hat in Time": + logger.info("Aborting proxy connection: game is not A Hat in Time") + await ctx.disconnect_proxy() + break + + if ctx.seed_name: + seed = msg.get("seed", "") + if seed != "" and seed != ctx.seed_name: + logger.info("Aborting proxy connection: seed mismatch from save file") + await ctx.disconnect_proxy() + break + if ctx.connected_msg: await ctx.send_msgs_proxy(ctx.connected_msg) ctx.update_items() continue - if ctx.log_network: + if not ctx.is_proxy_connected(): + break + + if DEBUG: logger.info(f"Incoming message: {msg}") await ctx.send_msgs([msg]) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 0ae5f0035d..ea8eb1b5b6 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -170,7 +170,7 @@ class HatInTimeWorld(World): "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], "BadgeSellerItemCount": badge_seller_count[self.player], - "SeedNumber": self.multiworld.seed} # For shop prices + "SeedNumber": str(self.multiworld.seed)} # For shop prices if self.multiworld.HatItems[self.player].value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) From d043c0d0f62c94d27f6faba11b6664ec2d20f5b5 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 18 Oct 2023 23:33:05 -0400 Subject: [PATCH 038/143] 1.3 --- AHITClient.py | 31 +- data/yatta.ico | Bin 0 -> 152484 bytes data/yatta.png | Bin 0 -> 34873 bytes setup-ahitclient.py | 642 ++++++++++++++++++++++++++++++++++ worlds/ahit/DeathWishRules.py | 20 +- worlds/ahit/Items.py | 4 +- worlds/ahit/Options.py | 8 + worlds/ahit/Rules.py | 91 +++-- worlds/ahit/__init__.py | 16 +- 9 files changed, 747 insertions(+), 65 deletions(-) create mode 100644 data/yatta.ico create mode 100644 data/yatta.png create mode 100644 setup-ahitclient.py diff --git a/AHITClient.py b/AHITClient.py index 390337108d..5ea3aff85c 100644 --- a/AHITClient.py +++ b/AHITClient.py @@ -3,7 +3,7 @@ import Utils import websockets import functools from copy import deepcopy -from typing import List, Any, Iterable +from typing import List, Any, Iterable, Dict from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem from MultiServer import Endpoint from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, \ @@ -70,12 +70,18 @@ class AHITContext(CommonContext): await self.endpoint.socket.send(msgs) return True + async def disconnect(self, allow_autoreconnect: bool = False): + await super().disconnect(allow_autoreconnect) + async def disconnect_proxy(self): if self.endpoint and not self.endpoint.socket.closed: await self.endpoint.socket.close() if self.proxy_task is not None: await self.proxy_task + def is_connected(self) -> bool: + return self.server and self.server.socket.open + def is_proxy_connected(self) -> bool: return self.endpoint and self.endpoint.socket.open @@ -91,6 +97,10 @@ class AHITContext(CommonContext): logger.info(text) def update_items(self): + # just to be safe - we might still have an inventory from a different room + if not self.is_connected(): + return + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) def on_package(self, cmd: str, args: dict): @@ -118,6 +128,10 @@ class AHITContext(CommonContext): if cmd != "PrintJSON": self.server_msgs.append(encode([args])) + # def on_deathlink(self, data: Dict[str, Any]): + # self.server_msgs.append(encode([data])) + # super().on_deathlink(data) + def run_gui(self): from kvui import GameManager @@ -147,13 +161,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): break if ctx.seed_name: - seed = msg.get("seed", "") - if seed != "" and seed != ctx.seed_name: + seed_name = msg.get("seed_name", "") + if seed_name != "" and seed_name != ctx.seed_name: logger.info("Aborting proxy connection: seed mismatch from save file") + logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) + await ctx.send_msgs_proxy(text) await ctx.disconnect_proxy() break - if ctx.connected_msg: + if ctx.connected_msg and ctx.is_connected(): await ctx.send_msgs_proxy(ctx.connected_msg) ctx.update_items() continue @@ -174,7 +192,7 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): async def on_client_connected(ctx: AHITContext): - if ctx.room_info: + if ctx.room_info and ctx.is_connected(): await ctx.send_msgs_proxy(ctx.room_info) else: ctx.awaiting_info = True @@ -186,7 +204,8 @@ async def main(): ctx = AHITContext(args.connect, args.password) logger.info("Starting A Hat in Time proxy server") - ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), host="localhost", port=11311) + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), + host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") if gui_enabled: diff --git a/data/yatta.ico b/data/yatta.ico new file mode 100644 index 0000000000000000000000000000000000000000..f87a0980f49c3cf346af8c288bab020eb77885c6 GIT binary patch literal 152484 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 HCB^>%Zp1Qi literal 0 HcmV?d00001 diff --git a/data/yatta.png b/data/yatta.png new file mode 100644 index 0000000000000000000000000000000000000000..4f230c86c727f4950a5c6e6b62b708b4d3d12f8b GIT binary patch literal 34873 zcmV)&K#aeMP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/setup-ahitclient.py b/setup-ahitclient.py new file mode 100644 index 0000000000..18fd6a1887 --- /dev/null +++ b/setup-ahitclient.py @@ -0,0 +1,642 @@ +import base64 +import datetime +import os +import platform +import shutil +import sys +import sysconfig +import typing +import warnings +import zipfile +import urllib.request +import io +import json +import threading +import subprocess + +from collections.abc import Iterable +from hashlib import sha3_512 +from pathlib import Path + + +# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +try: + requirement = 'cx-Freeze>=6.15.2' + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze + if '--yes' not in sys.argv and '-y' not in sys.argv: + input(f'Requirement {requirement} is not satisfied, press enter to install it') + subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) + import pkg_resources + +import cx_Freeze + +# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line +import setuptools.command.build + +if __name__ == "__main__": + # need to run this early to import from Utils and Launcher + # TODO: move stuff to not require this + import ModuleUpdate + ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) + ModuleUpdate.update_ran = False # restore for later + +from worlds.LauncherComponents import components, icon_paths +from Utils import version_tuple, is_windows, is_linux +from Cython.Build import cythonize + + +# On Python < 3.10 LogicMixin is not currently supported. +non_apworlds: set = { + "A Link to the Past", + "Adventure", + "ArchipIDLE", + "Archipelago", + "ChecksFinder", + "Clique", + "DLCQuest", + "Final Fantasy", + "Hylics 2", + "Kingdom Hearts 2", + "Lufia II Ancient Cave", + "Meritous", + "Ocarina of Time", + "Overcooked! 2", + "Raft", + "Secret of Evermore", + "Slay the Spire", + "Starcraft 2 Wings of Liberty", + "Sudoku", + "Super Mario 64", + "VVVVVV", + "Wargroove", + "Zillion", +} + +# LogicMixin is broken before 3.10 import revamp +if sys.version_info < (3,10): + non_apworlds.add("Hollow Knight") + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + +signtool: typing.Optional[str] +if os.path.exists("X:/pw.txt"): + print("Using signtool") + with open("X:/pw.txt", encoding="utf-8-sig") as f: + pw = f.read() + signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ + r'" /fd sha256 /tr http://timestamp.digicert.com/ ' +else: + signtool = None + + +build_platform = sysconfig.get_platform() +arch_folder = "exe.{platform}-{version}".format(platform=build_platform, + version=sysconfig.get_python_version()) +buildfolder = Path("build", arch_folder) +build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine() + + +# see Launcher.py on how to add scripts to setup.py +def resolve_icon(icon_name: str): + base_path = icon_paths[icon_name] + if is_windows: + path, extension = os.path.splitext(base_path) + ico_file = path + ".ico" + assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist." + return ico_file + else: + return base_path + + +exes = [ + cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name="ArchipelagoAHITClient.exe", + #target_name=c.frozen_name + (".exe" if is_windows else ""), + icon=resolve_icon(c.icon), + base="Win32GUI" if is_windows and not c.cli else None + ) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name +] + +#if is_windows: +if False: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + +extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] +extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] + + +def remove_sprites_from_folder(folder): + for file in os.listdir(folder): + if file != ".gitignore": + os.remove(folder / file) + + +def _threaded_hash(filepath): + hasher = sha3_512() + hasher.update(open(filepath, "rb").read()) + return base64.b85encode(hasher.digest()).decode() + + +# cx_Freeze's build command runs other commands. Override to accept --yes and store that. +class BuildCommand(setuptools.command.build.build): + user_options = [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ] + yes: bool + last_yes: bool = False # used by sub commands of build + + def initialize_options(self): + super().initialize_options() + type(self).last_yes = self.yes = False + + def finalize_options(self): + super().finalize_options() + type(self).last_yes = self.yes + + +# Override cx_Freeze's build_exe command for pre and post build steps +class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): + user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ('extra-data=', None, 'Additional files to add.'), + ] + yes: bool + extra_data: Iterable # [any] not available in 3.8 + extra_libs: Iterable # work around broken include_files + + buildfolder: Path + libfolder: Path + library: Path + buildtime: datetime.datetime + + def initialize_options(self): + super().initialize_options() + self.yes = BuildCommand.last_yes + self.extra_data = [] + self.extra_libs = [] + + def finalize_options(self): + super().finalize_options() + self.buildfolder = self.build_exe + self.libfolder = Path(self.buildfolder, "lib") + self.library = Path(self.libfolder, "library.zip") + + def installfile(self, path, subpath=None, keep_content: bool = False): + folder = self.buildfolder + if subpath: + folder /= subpath + print('copying', path, '->', folder) + if path.is_dir(): + folder /= path.name + if folder.is_dir() and not keep_content: + shutil.rmtree(folder) + shutil.copytree(path, folder, dirs_exist_ok=True) + elif path.is_file(): + shutil.copy(path, folder) + else: + print('Warning,', path, 'not found') + + def create_manifest(self, create_hashes=False): + # Since the setup is now split into components and the manifest is not, + # it makes most sense to just remove the hashes for now. Not aware of anyone using them. + hashes = {} + manifestpath = os.path.join(self.buildfolder, "manifest.json") + if create_hashes: + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor() + for dirpath, dirnames, filenames in os.walk(self.buildfolder): + for filename in filenames: + path = os.path.join(dirpath, filename) + hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path) + + import json + manifest = { + "buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"), + "hashes": {path: hash.result() for path, hash in hashes.items()}, + "version": version_tuple} + + json.dump(manifest, open(manifestpath, "wt"), indent=4) + print("Created Manifest") + + def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + + # pre-build steps + print(f"Outputting to: {self.buildfolder}") + os.makedirs(self.buildfolder, exist_ok=True) + import ModuleUpdate + ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) + ModuleUpdate.update(yes=self.yes) + + # auto-build cython modules + build_ext = self.distribution.get_command_obj("build_ext") + build_ext.inplace = False + self.run_command("build_ext") + # find remains of previous in-place builds, try to delete and warn otherwise + for path in build_ext.get_outputs(): + parts = os.path.split(path)[-1].split(".") + pattern = parts[0] + ".*." + parts[-1] + for match in Path().glob(pattern): + try: + match.unlink() + print(f"Removed {match}") + except Exception as ex: + warnings.warn(f"Could not delete old build output: {match}\n" + f"{ex}\nPlease close all AP instances and delete manually.") + + # regular cx build + self.buildtime = datetime.datetime.utcnow() + super().run() + + # manually copy built modules to lib folder. cx_Freeze does not know they exist. + for src in build_ext.get_outputs(): + print(f"copying {src} -> {self.libfolder}") + shutil.copy(src, self.libfolder, follow_symlinks=False) + + # need to finish download before copying + sni_thread.join() + + # include_files seems to not be done automatically. implement here + for src, dst in self.include_files: + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # now that include_files is completely broken, run find_libs here + for src, dst in find_libs(*self.extra_libs): + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # post build steps + if is_windows: # kivy_deps is win32 only, linux picks them up automatically + from kivy_deps import sdl2, glew + for folder in sdl2.dep_bins + glew.dep_bins: + shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) + print(f"copying {folder} -> {self.libfolder}") + + for data in self.extra_data: + self.installfile(Path(data)) + + # kivi data files + import kivy + shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), + self.buildfolder / "data", + dirs_exist_ok=True) + + os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + assert not non_apworlds - set(AutoWorldRegister.world_types), \ + f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" + folders_to_remove: typing.List[str] = [] + generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) + for worldname, worldtype in AutoWorldRegister.world_types.items(): + if worldname not in non_apworlds: + file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] + world_directory = self.libfolder / "worlds" / file_name + # this method creates an apworld that cannot be moved to a different OS or minor python version, + # which should be ok + with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + for path in world_directory.rglob("*.*"): + relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) + zf.write(path, relative_path) + folders_to_remove.append(file_name) + shutil.rmtree(world_directory) + shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") + # TODO: fix LttP options one day + shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") + try: + from maseya import z3pr + except ImportError: + print("Maseya Palette Shuffle not found, skipping data files.") + else: + # maseya Palette Shuffle exists and needs its data files + print("Maseya Palette Shuffle found, including data files...") + file = z3pr.__file__ + self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True) + + if signtool: + for exe in self.distribution.executables: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) + print("Signing SNI") + os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) + print("Signing OoT Utils") + for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): + os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) + + remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr") + + self.create_manifest() + + if is_windows: + # Inno setup stuff + with open("setup.ini", "w") as f: + min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000" + f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n") + with open("installdelete.iss", "w") as f: + f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n" + for world_directory in folders_to_remove) + else: + # make sure extra programs are executable + enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' + sni_exe = self.buildfolder / 'SNI/sni' + extra_exes = (enemizer_exe, sni_exe) + for extra_exe in extra_exes: + if extra_exe.is_file(): + extra_exe.chmod(0o755) + + +class AppImageCommand(setuptools.Command): + description = "build an app image from build output" + user_options = [ + ("build-folder=", None, "Folder to convert to AppImage."), + ("dist-file=", None, "AppImage output file."), + ("app-dir=", None, "Folder to use for packaging."), + ("app-icon=", None, "The icon to use for the AppImage."), + ("app-exec=", None, "The application to run inside the image."), + ("yes", "y", 'Answer "yes" to all questions.'), + ] + build_folder: typing.Optional[Path] + dist_file: typing.Optional[Path] + app_dir: typing.Optional[Path] + app_name: str + app_exec: typing.Optional[Path] + app_icon: typing.Optional[Path] # source file + app_id: str # lower case name, used for icon and .desktop + yes: bool + + def write_desktop(self): + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" + with open(desktop_filename, 'w', encoding="utf-8") as f: + f.write("\n".join(( + "[Desktop Entry]", + f'Name={self.app_name}', + f'Exec={self.app_exec}', + "Type=Application", + "Categories=Game", + f'Icon={self.app_id}', + '' + ))) + desktop_filename.chmod(0o755) + + def write_launcher(self, default_exe: Path): + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" + with open(launcher_filename, 'w', encoding="utf-8") as f: + f.write(f"""#!/bin/sh +exe="{default_exe}" +match="${{1#--executable=}}" +if [ "${{#match}}" -lt "${{#1}}" ]; then + exe="$match" + shift +elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then + exe="$2" + shift; shift +fi +tmp="${{exe#*/}}" +if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then + exe="{default_exe.parent}/$exe" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" +$APPDIR/$exe "$@" +""") + launcher_filename.chmod(0o755) + + def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" + try: + from PIL import Image + except ModuleNotFoundError: + if not self.yes: + input("Requirement PIL is not satisfied, press enter to install it") + subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) + from PIL import Image + im = Image.open(src) + res, _ = im.size + + if not name: + name = src.stem + ext = src.suffix + dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps') + dest_dir.mkdir(parents=True, exist_ok=True) + dest_file = dest_dir / f'{name}{ext}' + shutil.copy(src, dest_file) + if symlink: + symlink.symlink_to(dest_file.relative_to(symlink.parent)) + + def initialize_options(self): + self.build_folder = None + self.app_dir = None + self.app_name = self.distribution.metadata.name + self.app_icon = self.distribution.executables[0].icon + self.app_exec = Path('opt/{app_name}/{exe}'.format( + app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name + )) + self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format( + app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version, + platform=sysconfig.get_platform() + )) + self.yes = False + + def finalize_options(self): + if not self.app_dir: + self.app_dir = self.build_folder.parent / "AppDir" + self.app_id = self.app_name.lower() + + def run(self): + self.dist_file.parent.mkdir(parents=True, exist_ok=True) + if self.app_dir.is_dir(): + shutil.rmtree(self.app_dir) + self.app_dir.mkdir(parents=True) + opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + shutil.copytree(self.build_folder, opt_dir) + root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' + self.install_icon(self.app_icon, self.app_id, symlink=root_icon) + shutil.copy(root_icon, self.app_dir / '.DirIcon') + self.write_desktop() + self.write_launcher(self.app_exec) + print(f'{self.app_dir} -> {self.dist_file}') + subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) + + +def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: + """Try to find system libraries to be included.""" + if not args: + return [] + + arch = build_arch.replace('_', '-') + libc = 'libc6' # we currently don't support musl + + def parse(line): + lib, path = line.strip().split(' => ') + lib, typ = lib.split(' ', 1) + for test_arch in ('x86-64', 'i386', 'aarch64'): + if test_arch in typ: + lib_arch = test_arch + break + else: + lib_arch = '' + for test_libc in ('libc6',): + if test_libc in typ: + lib_libc = test_libc + break + else: + lib_libc = '' + return (lib, lib_arch, lib_libc), path + + if not hasattr(find_libs, "cache"): + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } + + def find_lib(lib, arch, libc): + for k, v in find_libs.cache.items(): + if k == (lib, arch, libc): + return v + for k, v, in find_libs.cache.items(): + if k[0].startswith(lib) and k[1] == arch and k[2] == libc: + return v + return None + + res = [] + for arg in args: + # try exact match, empty libc, empty arch, empty arch and libc + file = find_lib(arg, arch, libc) + file = file or find_lib(arg, arch, '') + file = file or find_lib(arg, '', libc) + file = file or find_lib(arg, '', '') + # resolve symlinks + for n in range(0, 5): + res.append((file, os.path.join('lib', os.path.basename(file)))) + if not os.path.islink(file): + break + dirname = os.path.dirname(file) + file = os.readlink(file) + if not os.path.isabs(file): + file = os.path.join(dirname, file) + return res + + +cx_Freeze.setup( + name="Archipelago", + version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}", + description="Archipelago", + executables=exes, + ext_modules=cythonize("_speedups.pyx"), + options={ + "build_exe": { + "packages": ["worlds", "kivy", "cymem", "websockets"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["worlds", "sc2"], + "include_files": [], # broken in cx 6.14.0, we use more special sauce now + "include_msvcr": False, + "replace_paths": ["*."], + "optimize": 1, + "build_exe": buildfolder, + "extra_data": extra_data, + "extra_libs": extra_libs, + "bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else [] + }, + "bdist_appimage": { + "build_folder": buildfolder, + }, + }, + # override commands to get custom stuff in + cmdclass={ + "build": BuildCommand, + "build_exe": BuildExeCommand, + "bdist_appimage": AppImageCommand, + }, +) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 7f6211f417..5a97518499 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,6 +1,6 @@ from worlds.AutoWorld import World, CollectionState -from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty -from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HatDLC +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData from .DeathWishLocations import dw_prereqs, dw_candles from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule @@ -169,8 +169,7 @@ def set_dw_rules(world: World): add_rule(loc, lambda state: state.has("Umbrella", world.player)) if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - add_rule(loc, lambda state, paintings=data.paintings: state.has("Progressive Painting Unlock", - world.player, paintings)) + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) if data.hit_requirement > 0: if data.hit_requirement == 1: @@ -303,10 +302,17 @@ def set_candle_dw_rules(name: str, world: World): and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) # No Ice Hat/painting required in Expert for Toilet Zero Jumps - if get_difficulty(world) >= Difficulty.EXPERT: + # This painting wall can only be skipped via cherry hover. + if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1: set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), - lambda state: can_use_hookshot(state, world) - and can_hit(state, world)) + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + else: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world)) + + set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player), + lambda state: has_paintings(state, world, 1, False)) elif name == "Snatcher's Hit List": add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index c9bb76739c..b4636a1d28 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -230,8 +230,8 @@ ahit_items = { # DLC1 items "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), - "Relic (Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), - "Relic (Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), "Relic (Shortcake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), # DLC2 items diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 7c0567a151..6353269a98 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -211,6 +211,12 @@ class ShuffleSubconPaintings(Toggle): default = 0 +class NoPaintingSkips(Toggle): + """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings.""" + display_name = "No Subcon Fire Wall Skips" + default = 0 + + class StartingChapter(Choice): """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game.""" display_name = "Starting Chapter" @@ -606,6 +612,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "ShuffleStorybookPages": ShuffleStorybookPages, "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, "StartingChapter": StartingChapter, "CTRLogic": CTRLogic, @@ -677,6 +684,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "ShuffleStorybookPages": ShuffleStorybookPages, "ShuffleActContracts": ShuffleActContracts, "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, "HatItems": HatItems, "EnableDLC1": EnableDLC1, diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index dd8daedb74..5d0377e235 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -62,25 +62,16 @@ def get_difficulty(world: World) -> Difficulty: return Difficulty(world.multiworld.LogicDifficulty[world.player].value) -def has_paintings(state: CollectionState, world: World, count: int, surf: bool = True) -> bool: +def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool=True) -> bool: if not painting_logic(world): return True - # Cherry Hover - if get_difficulty(world) >= Difficulty.EXPERT: - return True - - # All paintings can be skipped with No Bonk, very easily, if the player knows - if surf and get_difficulty(world) >= Difficulty.MODERATE and can_surf(state, world): - return True - - paintings: int = state.count("Progressive Painting Unlock", world.player) - if surf and get_difficulty(world) >= Difficulty.MODERATE: - # Green+Yellow paintings can also be skipped easily - if count == 1 or paintings >= 1 and count == 3: + if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip: + # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena + if get_difficulty(world) >= Difficulty.MODERATE: return True - return paintings >= count + return state.count("Progressive Painting Unlock", world.player) >= count def zipline_logic(world: World) -> bool: @@ -281,10 +272,7 @@ def set_rules(world: World): add_rule(location, lambda state: state.has("Umbrella", world.player)) if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: - if "Toilet of Doom" not in key: - add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - else: - add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings, False)) + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) if data.hit_requirement > 0: if data.hit_requirement == 1: @@ -417,13 +405,9 @@ def set_moderate_rules(world: World): add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) - # Moderate: hitting the bell is not required to enter Subcon Well, however hookshot is still expected to clear - set_rule(world.multiworld.get_location("Subcon Well - Hookshot Badge Chest", world.player), - lambda state: has_paintings(state, world, 1)) - set_rule(world.multiworld.get_location("Subcon Well - Above Chest", world.player), - lambda state: has_paintings(state, world, 1)) - set_rule(world.multiworld.get_location("Subcon Well - Mushroom", world.player), - lambda state: has_paintings(state, world, 1)) + # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell + for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: + set_rule(loc, lambda state: has_paintings(state, world, 1)) # Moderate: Vanessa Manor with nothing for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: @@ -484,17 +468,17 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.SPRINT) and state.has("Scooter Badge", world.player), "or") + # No Dweller Mask required set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), lambda state: has_paintings(state, world, 3)) # Cherry bridge over boss arena gap (painting still expected) set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: has_paintings(state, world, 1, False)) + lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: can_sdj(state, world) - and has_paintings(state, world, 2), "or") + lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") @@ -559,21 +543,29 @@ def set_expert_rules(world: World): set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), lambda state: True) - # Expert: enter and clear The Subcon Well with nothing - for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: - set_rule(loc, lambda state: True) - # Expert: Cherry Hovering - connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), - world.multiworld.get_region("Subcon Forest Area", world.player), - "Subcon Forest Entrance YCHE", world.player) + entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), + world.multiworld.get_region("Subcon Forest Area", world.player), + "Subcon Forest Entrance YCHE", world.player) - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: True) - set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), lambda state: True) + if world.multiworld.NoPaintingSkips[world.player].value > 0: + add_rule(entrance, lambda state: has_paintings(state, world, 1)) + + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, True)) + + # Set painting rules only. Skipping paintings is determined in has_paintings + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), @@ -648,15 +640,20 @@ def set_mafia_town_rules(world: World): def set_subcon_rules(world: World): - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) - and (not painting_logic(world) or has_paintings(state, world, 1)) - or state.has("YCHE Access", world.player)) - set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.DWELLER)) + # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) + and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # The painting wall can't be skipped without cherry hover, which is Expert + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player), lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player)) @@ -671,7 +668,7 @@ def set_subcon_rules(world: World): if painting_logic(world): add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), - lambda state: state.has("Progressive Painting Unlock", world.player)) + lambda state: has_paintings(state, world, 1, False)) for key in contract_locations: if key == "Snatcher's Contract - The Subcon Well": diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index ea8eb1b5b6..88904d8939 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -9,7 +9,9 @@ from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld from typing import List, Dict, TextIO -from worlds.LauncherComponents import Component, components +from worlds.LauncherComponents import Component, components, icon_paths +from multiprocessing import Process +from Utils import local_path hat_craft_order: Dict[int, List[HatType]] = {} hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} @@ -20,7 +22,14 @@ dw_shuffle: Dict[int, List[str]] = {} nyakuza_thug_items: Dict[int, Dict[str, int]] = {} badge_seller_count: Dict[int, int] = {} -components.append(Component("A Hat in Time Client", "AHITClient")) +components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) +icon_paths['yatta'] = local_path('data', 'yatta.png') + + +def run_client(): + from AHITClient import main + p = Process(target=main) + p.start() class AWebInTime(WebWorld): @@ -170,7 +179,8 @@ class HatInTimeWorld(World): "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], "BadgeSellerItemCount": badge_seller_count[self.player], - "SeedNumber": str(self.multiworld.seed)} # For shop prices + "SeedNumber": str(self.multiworld.seed), # For shop prices + "SeedName": self.multiworld.seed_name} if self.multiworld.HatItems[self.player].value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) From 8d3aa7e2d254bcf87a260e020e5d6f03e84e9ab3 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 19 Oct 2023 15:38:33 -0400 Subject: [PATCH 039/143] Final touch-ups for 1.3 --- AHITClient.py | 4 ---- worlds/ahit/Locations.py | 2 +- worlds/ahit/Rules.py | 18 ++++++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/AHITClient.py b/AHITClient.py index 5ea3aff85c..64d1e998bd 100644 --- a/AHITClient.py +++ b/AHITClient.py @@ -128,10 +128,6 @@ class AHITContext(CommonContext): if cmd != "PrintJSON": self.server_msgs.append(encode([args])) - # def on_deathlink(self, data: Dict[str, Any]): - # self.server_msgs.append(encode([data])) - # super().on_deathlink(data) - def run_gui(self): from kvui import GameManager diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 64f1074d7f..b398ffbfd2 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -494,7 +494,7 @@ storybook_pages = { "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: 20003rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 5d0377e235..d08c1411a0 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -372,6 +372,7 @@ def set_specific_rules(world: World): lambda state: state.has("Time Piece", world.player, 4)) set_mafia_town_rules(world) + set_botb_rules(world) set_subcon_rules(world) set_alps_rules(world) @@ -428,6 +429,10 @@ def set_moderate_rules(world: World): # Moderate: Twilight Path without Dweller Mask set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) + # Moderate: Mystifying Time Mesa time trial without hats + set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hookshot(state, world)) + # Moderate: Finale without Hookshot set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), lambda state: can_use_hat(state, world, HatType.DWELLER)) @@ -486,10 +491,6 @@ def set_hard_rules(world: World): add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_sdj(state, world), "or") - # Hard: Mystifying Time Mesa time trial without hats - set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), - lambda state: can_use_hookshot(state, world)) - # Finale Telescope with only Ice Hat add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: can_use_hat(state, world, HatType.ICE), "or") @@ -639,6 +640,15 @@ def set_mafia_town_rules(world: World): and state.has("Scooter Badge", world.player), "or") +def set_botb_rules(world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: + for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: + set_rule(loc, lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + + def set_subcon_rules(world: World): set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) From 6eaf9419430270e42900d469604eae9863706597 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 20 Oct 2023 20:57:21 -0400 Subject: [PATCH 040/143] 1.3.1 --- worlds/ahit/Items.py | 2 +- worlds/ahit/Options.py | 4 ++-- worlds/ahit/Rules.py | 9 ++++++--- worlds/ahit/test/TestActs.py | 6 ++++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index b4636a1d28..f877f9c372 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -262,7 +262,7 @@ relic_groups = { "Train": {"Relic (Mountain Set)", "Relic (Train)"}, "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"}, "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"}, - "Cake": {"Relic (Cake Stand)", "Relic (Cake)", "Relic (Cake Slice)", "Relic (Shortcake)"}, + "Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"}, "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"}, } diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 6353269a98..96c35c5fc7 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -306,8 +306,8 @@ class YarnAvailable(Range): """How much yarn is available to collect in the item pool.""" display_name = "Yarn Available" range_start = 30 - range_end = 75 - default = 45 + range_end = 80 + default = 50 class MinExtraYarn(Range): diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index d08c1411a0..3e7700ccf0 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -642,9 +642,12 @@ def set_mafia_town_rules(world: World): def set_botb_rules(world: World): if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: - for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: - set_rule(loc, lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) - + set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 3c5918c0db..7c2b9783e6 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -3,6 +3,12 @@ from worlds.ahit.test.TestBase import HatInTimeTestBase class TestActs(HatInTimeTestBase): + def run_default_tests(self) -> bool: + return False + + def testAllStateCanReachEverything(self): + pass + options = { "ActRandomizer": 2, "EnableDLC1": 1, From afdcde88ae6335eff437c13862299a20c5b81eef Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 23 Oct 2023 14:33:46 -0400 Subject: [PATCH 041/143] 1.3.3 --- AHITClient.py | 11 +++-- worlds/ahit/Regions.py | 94 ++++++++++++++++++++--------------------- worlds/ahit/Rules.py | 3 ++ worlds/ahit/__init__.py | 8 +--- 4 files changed, 56 insertions(+), 60 deletions(-) diff --git a/AHITClient.py b/AHITClient.py index 64d1e998bd..884f3ee5c7 100644 --- a/AHITClient.py +++ b/AHITClient.py @@ -3,11 +3,10 @@ import Utils import websockets import functools from copy import deepcopy -from typing import List, Any, Iterable, Dict +from typing import List, Any, Iterable from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem from MultiServer import Endpoint -from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, \ - get_base_parser +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser DEBUG = False @@ -148,6 +147,9 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): if ctx.is_proxy_connected(): async for data in websocket: + if DEBUG: + logger.info(f"Incoming message: {data}") + for msg in decode(data): if msg["cmd"] == "Connect": # Proxy is connecting, make sure it is valid @@ -175,9 +177,6 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): if not ctx.is_proxy_connected(): break - if DEBUG: - logger.info(f"Incoming message: {msg}") - await ctx.send_msgs([msg]) except Exception as e: diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index b1d2293961..0322f08e72 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -157,61 +157,61 @@ rift_access_regions = { "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], } -# Hat_ChapterActInfo, from the game files to be used in act shuffle +# Time piece identifiers to be used in act shuffle chapter_act_info = { - "Time Rift - Gallery": "hatintime_chapterinfo.spaceship.Spaceship_WaterRift_Gallery", - "Time Rift - The Lab": "hatintime_chapterinfo.spaceship.Spaceship_WaterRift_MailRoom", + "Time Rift - Gallery": "Spaceship_WaterRift_Gallery", + "Time Rift - The Lab": "Spaceship_WaterRift_MailRoom", - "Welcome to Mafia Town": "hatintime_chapterinfo.MafiaTown.MafiaTown_Welcome", - "Barrel Battle": "hatintime_chapterinfo.MafiaTown.MafiaTown_BarrelBattle", - "She Came from Outer Space": "hatintime_chapterinfo.MafiaTown.MafiaTown_AlienChase", - "Down with the Mafia!": "hatintime_chapterinfo.MafiaTown.MafiaTown_MafiaBoss", - "Cheating the Race": "hatintime_chapterinfo.MafiaTown.MafiaTown_Race", - "Heating Up Mafia Town": "hatintime_chapterinfo.MafiaTown.MafiaTown_Lava", - "The Golden Vault": "hatintime_chapterinfo.MafiaTown.MafiaTown_GoldenVault", - "Time Rift - Mafia of Cooks": "hatintime_chapterinfo.MafiaTown.MafiaTown_CaveRift_Mafia", - "Time Rift - Sewers": "hatintime_chapterinfo.MafiaTown.MafiaTown_WaterRift_Easy", - "Time Rift - Bazaar": "hatintime_chapterinfo.MafiaTown.MafiaTown_WaterRift_Hard", + "Welcome to Mafia Town": "chapter1_tutorial", + "Barrel Battle": "chapter1_barrelboss", + "She Came from Outer Space": "chapter1_cannon_repair", + "Down with the Mafia!": "chapter1_boss", + "Cheating the Race": "harbor_impossible_race", + "Heating Up Mafia Town": "mafiatown_lava", + "The Golden Vault": "mafiatown_goldenvault", + "Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia", + "Time Rift - Sewers": "TimeRift_Water_Mafia_Easy", + "Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard", - "Dead Bird Studio": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_DeadBirdStudio", - "Murder on the Owl Express": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_Murder", - "Picture Perfect": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_PicturePerfect", - "Train Rush": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_TrainRush", - "The Big Parade": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_Parade", - "Award Ceremony": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_AwardCeremony", - "Dead Bird Studio Basement": "DeadBirdBasement", # Dead Bird Studio Basement has no ChapterActInfo - "Time Rift - Dead Bird Studio": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_CaveRift_Basement", - "Time Rift - The Owl Express": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_WaterRift_Panels", - "Time Rift - The Moon": "hatintime_chapterinfo.BattleOfTheBirds.BattleOfTheBirds_WaterRift_Parade", + "Dead Bird Studio": "DeadBirdStudio", + "Murder on the Owl Express": "chapter3_murder", + "Picture Perfect": "moon_camerasnap", + "Train Rush": "trainwreck_selfdestruct", + "The Big Parade": "moon_parade", + "Award Ceremony": "award_ceremony", + "Dead Bird Studio Basement": "chapter3_secret_finale", + "Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement", + "Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels", + "Time Rift - The Moon": "TimeRift_Water_TWreck_Parade", - "Contractual Obligations": "hatintime_chapterinfo.subconforest.SubconForest_IceWall", - "The Subcon Well": "hatintime_chapterinfo.subconforest.SubconForest_Cave", - "Toilet of Doom": "hatintime_chapterinfo.subconforest.SubconForest_Toilet", - "Queen Vanessa's Manor": "hatintime_chapterinfo.subconforest.SubconForest_Manor", - "Mail Delivery Service": "hatintime_chapterinfo.subconforest.SubconForest_MailDelivery", - "Your Contract has Expired": "hatintime_chapterinfo.subconforest.SubconForest_SnatcherBoss", - "Time Rift - Sleepy Subcon": "hatintime_chapterinfo.subconforest.SubconForest_CaveRift_Raccoon", - "Time Rift - Pipe": "hatintime_chapterinfo.subconforest.SubconForest_WaterRift_Hookshot", - "Time Rift - Village": "hatintime_chapterinfo.subconforest.SubconForest_WaterRift_Dwellers", + "Contractual Obligations": "subcon_village_icewall", + "The Subcon Well": "subcon_cave", + "Toilet of Doom": "chapter2_toiletboss", + "Queen Vanessa's Manor": "vanessa_manor_attic", + "Mail Delivery Service": "subcon_maildelivery", + "Your Contract has Expired": "snatcher_boss", + "Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon", + "Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot", + "Time Rift - Village": "TimeRift_Water_Subcon_Dwellers", - "Alpine Free Roam": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_IntroMountain", - "The Illness has Spread": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_Finale", - "Time Rift - Alpine Skyline": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_CaveRift_Alpine", - "Time Rift - The Twilight Bell": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_WaterRift_Goats", - "Time Rift - Curly Tail Trail": "hatintime_chapterinfo.AlpineSkyline.AlpineSkyline_WaterRift_Cats", + "Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece + "The Illness has Spread": "AlpineSkyline_Finale", + "Time Rift - Alpine Skyline": "TimeRift_Cave_Alps", + "Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats", + "Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats", - "The Finale": "hatintime_chapterinfo.TheFinale.TheFinale_FinalBoss", - "Time Rift - Tour": "hatintime_chapterinfo_dlc1.spaceship.CaveRift_Tour", + "The Finale": "TheFinale_FinalBoss", + "Time Rift - Tour": "TimeRift_Cave_Tour", - "Bon Voyage!": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Boarding", - "Ship Shape": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Working", - "Rock the Boat": "hatintime_chapterinfo_dlc1.Cruise.Cruise_Sinking", - "Time Rift - Balcony": "hatintime_chapterinfo_dlc1.Cruise.Cruise_WaterRift_Slide", - "Time Rift - Deep Sea": "hatintime_chapterinfo_dlc1.Cruise.Cruise_CaveRift", + "Bon Voyage!": "Cruise_Boarding", + "Ship Shape": "Cruise_Working", + "Rock the Boat": "Cruise_Sinking", + "Time Rift - Balcony": "Cruise_WaterRift_Slide", + "Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium", - "Nyakuza Free Roam": "hatintime_chapterinfo_dlc2.metro.Metro_FreeRoam", - "Rush Hour": "hatintime_chapterinfo_dlc2.metro.Metro_Escape", - "Time Rift - Rumbi Factory": "hatintime_chapterinfo_dlc2.metro.Metro_RumbiFactory" + "Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece + "Rush Hour": "Metro_Escape", + "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" } # Guarantee that the first level a player can access is a location dense area beatable with no items diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 3e7700ccf0..ae7aa21243 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -466,6 +466,9 @@ def set_moderate_rules(world: World): and can_use_hat(state, world, HatType.ICE) and can_use_hat(state, world, HatType.BREWING)) + # Moderate: Bluefin Tunnel without tickets + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + def set_hard_rules(world: World): # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 88904d8939..1d3ca28cfd 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -26,12 +26,6 @@ components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) icon_paths['yatta'] = local_path('data', 'yatta.png') -def run_client(): - from AHITClient import main - p = Process(target=main) - p.start() - - class AWebInTime(WebWorld): theme = "partyTime" tutorials = [Tutorial( @@ -106,7 +100,7 @@ class HatInTimeWorld(World): or "Camera Tourist" not in self.get_excluded_dws(): create_enemy_events(self) - # place default contract locations if contract shuffle is off so logic can still utilize them + # place vanilla contract locations if contract shuffle is off if self.multiworld.ShuffleActContracts[self.player].value == 0: for name in contract_locations.keys(): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) From a3e928d7aa278b9c3f7785cda15a726d3feb420e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 23 Oct 2023 22:07:23 -0400 Subject: [PATCH 042/143] Zero Jumps gen error fix --- worlds/ahit/Locations.py | 9 ++++++--- worlds/ahit/Rules.py | 7 +++---- worlds/ahit/Types.py | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index b398ffbfd2..7825b1fe2d 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -44,6 +44,8 @@ def location_dlc_enabled(world: World, location: str) -> bool: return True elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): return True + elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw(): + return True elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw(): return True @@ -70,11 +72,12 @@ def is_location_valid(world: World, location: str) -> bool: return False # No need for all those event items if we're not doing candles - if data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw: + if data.dlc_flags & HatDLC.death_wish: if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): return False - if world.multiworld.DWShuffle[world.player].value > 0 and data.region not in world.get_dw_shuffle(): + if world.multiworld.DWShuffle[world.player].value > 0 \ + and data.region in death_wishes and data.region not in world.get_dw_shuffle(): return False if location in zero_jumps: @@ -720,7 +723,7 @@ zero_jumps_expert = { dlc_flags=HatDLC.death_wish), "Sleepy Subcon (Zero Jumps)": LocData(0, "Sleepy Subcon", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), - "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), } zero_jumps = { diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index ae7aa21243..42bf818114 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -255,9 +255,8 @@ def set_rules(world: World): if key in contract_locations.keys(): continue - if data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw: - if key in snatcher_coins.keys(): - key = f"{key} ({data.region})" + if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): + key = f"{key} ({data.region})" location = world.multiworld.get_location(key, world.player) @@ -929,7 +928,7 @@ def set_event_rules(world: World): if not is_location_valid(world, name): continue - if (data.dlc_flags is HatDLC.death_wish or data.dlc_flags is HatDLC.dlc2_dw) and name in snatcher_coins.keys(): + if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys(): name = f"{name} ({data.region})" event: Location = world.multiworld.get_location(name, world.player) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 3e1d01ab61..16255d7ec5 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -25,7 +25,8 @@ class HatDLC(IntFlag): dlc1 = 0b001 dlc2 = 0b010 death_wish = 0b100 - dlc2_dw = 0b0110 # for Snatcher Coins in Nyakuza Metro + dlc1_dw = 0b101 + dlc2_dw = 0b110 class ChapterIndex(IntEnum): From a20a9e89e7ec4a7ad25951d767d6d1ab89777a6f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 23 Oct 2023 22:30:06 -0400 Subject: [PATCH 043/143] more fixes --- worlds/ahit/Locations.py | 2 +- worlds/ahit/Options.py | 1 - worlds/ahit/Regions.py | 7 +++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 7825b1fe2d..0a43bc1367 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -722,7 +722,7 @@ zero_jumps_expert = { misc_required=["No Bonk Badge"], dlc_flags=HatDLC.death_wish), - "Sleepy Subcon (Zero Jumps)": LocData(0, "Sleepy Subcon", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), } diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 96c35c5fc7..863f940f0a 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -61,7 +61,6 @@ def adjust_options(world: World): world.multiworld.ShuffleActContracts[world.player].value = 0 world.multiworld.EnableDLC1[world.player].value = 0 world.multiworld.LogicDifficulty[world.player].value = -1 - world.multiworld.KnowledgeChecks[world.player].value = 0 world.multiworld.DWTimePieceRequirement[world.player].value = 0 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0322f08e72..8534a38ec1 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -832,8 +832,11 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: World): - min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value - max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value + min_items: int = min(world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + max_items: int = max(world.multiworld.NyakuzaThugMaxShopItems[world.player].value, + world.multiworld.NyakuzaThugMinShopItems[world.player].value) count: int = -1 step: int = 0 old_name: str = "" From b4fb763fecbcc14c24481336c57cd7673a3e2632 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 25 Oct 2023 14:29:40 -0400 Subject: [PATCH 044/143] Formatting improvements --- worlds/ahit/Locations.py | 107 ++++++++++++++++++++++++++++++--------- worlds/ahit/Regions.py | 26 ++++------ worlds/ahit/Rules.py | 2 +- worlds/ahit/__init__.py | 3 +- 4 files changed, 94 insertions(+), 44 deletions(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 0a43bc1367..00ec578624 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -166,10 +166,17 @@ ahit_locations = { "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), - "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), - "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + + "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), @@ -267,7 +274,10 @@ ahit_locations = { "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, + paintings=1), + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), @@ -276,7 +286,10 @@ ahit_locations = { "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", required_hats=[HatType.BREWING]), + + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", + required_hats=[HatType.BREWING]), + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"), "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"), @@ -373,10 +386,18 @@ act_completions = { "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"), "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), - "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, umbrella=True, paintings=1), - "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, paintings=1), + + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1, + paintings=1), + + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, + paintings=1), + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), - "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), + + "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", + required_hats=[HatType.SPRINT]), + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), @@ -401,8 +422,13 @@ act_completions = { "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1), - "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, required_hats=[HatType.ICE]), - "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, hookshot=True), + + "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, + hookshot=True), + "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), @@ -438,7 +464,8 @@ act_completions = { hookshot=True, required_hats=[HatType.ICE, HatType.BREWING]), - "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), } storybook_pages = { @@ -477,12 +504,24 @@ storybook_pages = { "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), - "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), - "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, hookshot=True), + + "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), @@ -496,9 +535,15 @@ storybook_pages = { "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), - "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), @@ -519,9 +564,12 @@ shop_locations = { "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"), "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"), - "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2), + "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2), + "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, hookshot=True, required_hats=[HatType.DWELLER]), @@ -722,7 +770,9 @@ zero_jumps_expert = { misc_required=["No Bonk Badge"], dlc_flags=HatDLC.death_wish), - "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), } @@ -798,7 +848,6 @@ zero_jumps = { dlc_flags=HatDLC.dlc2_dw), } -# please just ignore all the duplicate key warnings, thanks snatcher_coins = { "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), @@ -818,12 +867,20 @@ snatcher_coins = { "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, + dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, + dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, + dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 8534a38ec1..59186d68ff 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -298,11 +298,11 @@ def create_regions(world: World): botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) - create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) - create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) + pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) + tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) - create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) create_rift_connections(w, create_region(w, "Time Rift - The Moon")) @@ -310,7 +310,6 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) - basement = mw.get_region("Dead Bird Studio Basement", p) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) @@ -330,7 +329,8 @@ def create_regions(world: World): alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness - alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", alpine_area) + alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + alpine_area) create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) @@ -374,10 +374,10 @@ def create_regions(world: World): connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) - connect_regions(mw.get_region("Dead Bird Studio", p), badge_seller, "DBS -> Badge Seller", p) - connect_regions(mw.get_region("Picture Perfect", p), badge_seller, "PP -> Badge Seller", p) - connect_regions(mw.get_region("Train Rush", p), badge_seller, "TR -> Badge Seller", p) - connect_regions(mw.get_region("Alpine Skyline Area (TIHS)", p), badge_seller, "ASA -> Badge Seller", p) + connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p) + connect_regions(pp, badge_seller, "PP -> Badge Seller", p) + connect_regions(tr, badge_seller, "TR -> Badge Seller", p) + connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p) times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) @@ -403,10 +403,7 @@ def create_regions(world: World): if mw.Tasksanity[p].value > 0: create_tasksanity_locations(w) - # force recache - mw.get_region("Time Rift - Deep Sea", p) - - connect_regions(mw.get_region("Cruise Ship", p), badge_seller, "CS -> Badge Seller", p) + connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) if w.is_dlc2(): nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) @@ -425,9 +422,6 @@ def create_regions(world: World): create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) create_thug_shops(w) - # force recache - mw.get_region("Time Rift - Sewers", p) - def create_rift_connections(world: World, region: Region): i = 1 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 42bf818114..7eb09bedfc 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -62,7 +62,7 @@ def get_difficulty(world: World) -> Difficulty: return Difficulty(world.multiworld.LogicDifficulty[world.player].value) -def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool=True) -> bool: +def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: if not painting_logic(world): return True diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 1d3ca28cfd..64b3febc3e 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -10,7 +10,6 @@ from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld from typing import List, Dict, TextIO from worlds.LauncherComponents import Component, components, icon_paths -from multiprocessing import Process from Utils import local_path hat_craft_order: Dict[int, List[HatType]] = {} @@ -158,7 +157,7 @@ class HatInTimeWorld(World): set_rules(self) - if self.multiworld.EnableDeathWish[self.player].value > 0: + if self.is_dw(): set_dw_rules(self) def create_item(self, name: str) -> Item: From 8eb351e73488d10a0f506a90ff8dc2a3206a75d9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 26 Oct 2023 21:55:11 -0400 Subject: [PATCH 045/143] typo --- worlds/ahit/Items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index f877f9c372..869f998a9d 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -230,9 +230,9 @@ ahit_items = { # DLC1 items "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), - "Relic (Chocolate Cake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), - "Relic (Shortcake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), # DLC2 items "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), From e178a7c0a6904ace803241cab3021d7b97177e90 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 26 Oct 2023 22:04:02 -0400 Subject: [PATCH 046/143] Update __init__.py --- worlds/ahit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 64b3febc3e..ef4a45d6d4 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -54,7 +54,7 @@ class HatInTimeWorld(World): shop_locs: List[str] = [] item_name_groups = relic_groups web = AWebInTime() - + # a def generate_early(self): adjust_options(self) From f37a71850c86823640bb544979964ea03a4b00a2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 26 Oct 2023 22:05:37 -0400 Subject: [PATCH 047/143] Revert "Update __init__.py" This reverts commit e178a7c0a6904ace803241cab3021d7b97177e90. --- worlds/ahit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index ef4a45d6d4..64b3febc3e 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -54,7 +54,7 @@ class HatInTimeWorld(World): shop_locs: List[str] = [] item_name_groups = relic_groups web = AWebInTime() - # a + def generate_early(self): adjust_options(self) From 25a39a46ad79bea04b8b98bdf857e1f5b9539873 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 26 Oct 2023 22:10:30 -0400 Subject: [PATCH 048/143] init --- worlds/ahit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 64b3febc3e..047b788535 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -49,7 +49,7 @@ class HatInTimeWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = get_location_names() - option_definitions = ahit_options + # option_definitions = ahit_options act_connections: Dict[str, str] = {} shop_locs: List[str] = [] item_name_groups = relic_groups From 2ce7b526fd50958be0e0e560942f90bb4f424a4f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 26 Oct 2023 23:02:34 -0400 Subject: [PATCH 049/143] Update to new options API --- worlds/ahit/DeathWishLocations.py | 20 +-- worlds/ahit/DeathWishRules.py | 34 ++--- worlds/ahit/Items.py | 42 +++--- worlds/ahit/Locations.py | 24 ++-- worlds/ahit/Options.py | 206 +++++++++++++++--------------- worlds/ahit/Regions.py | 69 +++++----- worlds/ahit/Rules.py | 48 +++---- worlds/ahit/__init__.py | 61 ++++----- 8 files changed, 252 insertions(+), 252 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index f51d4948ee..f3e546f6c9 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -139,19 +139,19 @@ dw_classes = { def create_dw_regions(world: World): - if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + if world.options.DWExcludeAnnoyingContracts.value > 0: for name in annoying_dws: world.get_excluded_dws().append(name) - if world.multiworld.DWEnableBonus[world.player].value == 0 \ - or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + if world.options.DWEnableBonus.value == 0 \ + or world.options.DWAutoCompleteBonuses.value > 0: for name in death_wishes: world.get_excluded_bonuses().append(name) - elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + elif world.options.DWExcludeAnnoyingBonuses.value > 0: for name in annoying_bonuses: world.get_excluded_bonuses().append(name) - if world.multiworld.DWExcludeCandles[world.player].value > 0: + if world.options.DWExcludeCandles.value > 0: for name in dw_candles: if name in world.get_excluded_dws(): continue @@ -162,9 +162,9 @@ def create_dw_regions(world: World): entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) add_rule(entrance, lambda state: state.has("Time Piece", world.player, - world.multiworld.DWTimePieceRequirement[world.player].value)) + world.options.DWTimePieceRequirement.value)) - if world.multiworld.DWShuffle[world.player].value > 0: + if world.options.DWShuffle.value > 0: dw_list: List[str] = [] for name in death_wishes.keys(): if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): @@ -173,8 +173,8 @@ def create_dw_regions(world: World): dw_list.append(name) world.random.shuffle(dw_list) - count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, - world.multiworld.DWShuffleCountMax[world.player].value) + count = world.random.randint(world.options.DWShuffleCountMin.value, + world.options.DWShuffleCountMax.value) dw_shuffle: List[str] = [] total = min(len(dw_list), count) @@ -182,7 +182,7 @@ def create_dw_regions(world: World): dw_shuffle.append(dw_list[i]) # Seal the Deal is always last if it's the goal - if world.multiworld.EndGoal[world.player].value == 3: + if world.options.EndGoal.value == 3: if "Seal the Deal" in dw_shuffle: dw_shuffle.remove("Seal the Deal") diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 5a97518499..648549cee0 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -105,7 +105,7 @@ def set_dw_rules(world: World): set_enemy_rules(world) dw_list: List[str] = [] - if world.multiworld.DWShuffle[world.player].value > 0: + if world.options.DWShuffle.value > 0: dw_list = world.get_dw_shuffle() else: for name in death_wishes.keys(): @@ -124,12 +124,12 @@ def set_dw_rules(world: World): temp_list.append(main_objective) temp_list.append(full_clear) - if world.multiworld.DWShuffle[world.player].value == 0: + if world.options.DWShuffle.value == 0: if name in dw_stamp_costs.keys(): for entrance in dw.entrances: add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) - if world.multiworld.DWEnableBonus[world.player].value == 0: + if world.options.DWEnableBonus.value == 0: # place nothing, but let the locations exist still, so we can use them for bonus stamp rules full_clear.address = None full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) @@ -165,10 +165,10 @@ def set_dw_rules(world: World): for misc in data.misc_required: add_rule(loc, lambda state, item=misc: state.has(item, world.player)) - if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + if data.umbrella and world.options.UmbrellaLogic.value > 0: add_rule(loc, lambda state: state.has("Umbrella", world.player)) - if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) if data.hit_requirement > 0: @@ -184,11 +184,11 @@ def set_dw_rules(world: World): elif loc.name == full_clear.name: add_rule(loc, main_rule) # Only set bonus stamp rules if we don't auto complete bonuses - if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + if world.options.DWAutoCompleteBonuses.value == 0 \ and not world.is_bonus_excluded(loc.name): add_rule(bonus_stamps, loc.access_rule) - if world.multiworld.DWShuffle[world.player].value > 0: + if world.options.DWShuffle.value > 0: dw_shuffle = world.get_dw_shuffle() for i in range(len(dw_shuffle)): if i == 0: @@ -217,7 +217,7 @@ def set_dw_rules(world: World): for rule in access_rules: add_rule(entrance, rule) - if world.multiworld.EndGoal[world.player].value == 3: + if world.options.EndGoal.value == 3: world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", world.player) @@ -269,7 +269,7 @@ def modify_dw_rules(world: World, name: str): def get_total_dw_stamps(state: CollectionState, world: World) -> int: - if world.multiworld.DWShuffle[world.player].value > 0: + if world.options.DWShuffle.value > 0: return 999 # no stamp costs in death wish shuffle count: int = 0 @@ -303,7 +303,7 @@ def set_candle_dw_rules(name: str, world: World): # No Ice Hat/painting required in Expert for Toilet Zero Jumps # This painting wall can only be skipped via cherry hover. - if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1: + if get_difficulty(world) < Difficulty.EXPERT or world.options.NoPaintingSkips.value == 1: set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), lambda state: can_use_hookshot(state, world) and can_hit(state, world) and has_paintings(state, world, 1, False)) @@ -383,13 +383,13 @@ def create_enemy_events(world: World): continue if area == "Time Rift - Tour" and (not world.is_dlc1() - or world.multiworld.ExcludeTour[world.player].value > 0): + or world.options.ExcludeTour.value > 0): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \ + if world.options.DWShuffle.value > 0 and area in death_wishes.keys() \ and area not in world.get_dw_shuffle(): continue @@ -400,14 +400,14 @@ def create_enemy_events(world: World): event.show_in_spoiler = False for name in triple_enemy_locations: - if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0): + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour.value > 0): continue - if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \ + if world.options.DWShuffle.value > 0 and name in death_wishes.keys() \ and name not in world.get_dw_shuffle(): continue - region = world.multiworld.get_region(name, world.player) + region = world.options.get_region(name, world.player) event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) region.locations.append(event) @@ -428,13 +428,13 @@ def set_enemy_rules(world: World): continue if area == "Time Rift - Tour" and (not world.is_dlc1() - or world.multiworld.ExcludeTour[world.player].value > 0): + or world.options.ExcludeTour.value > 0): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + if world.options.DWShuffle.value > 0 and area in death_wishes \ and area not in world.get_dw_shuffle(): continue diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 869f998a9d..3a13b3e3c8 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -8,13 +8,13 @@ from typing import Optional, List, Dict def create_itempool(world: World) -> List[Item]: itempool: List[Item] = [] - if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0: + if not world.is_dw_only() and world.options.HatItems.value == 0: calculate_yarn_costs(world) yarn_pool: List[Item] = create_multiple_items(world, "Yarn", - world.multiworld.YarnAvailable[world.player].value, + world.options.YarnAvailable.value, ItemClassification.progression_skip_balancing) - for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent.value))): yarn_pool[i].classification = ItemClassification.progression itempool += yarn_pool @@ -26,7 +26,7 @@ def create_itempool(world: World) -> List[Item]: if not item_dlc_enabled(world, name): continue - if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values(): + if world.options.HatItems.value == 0 and name in hat_type_to_item.values(): continue item_type: ItemClassification = item_table.get(name).classification @@ -37,7 +37,7 @@ def create_itempool(world: World) -> List[Item]: continue else: if name == "Scooter Badge": - if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + if world.options.CTRLogic.value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression elif name == "No Bonk Badge": if get_difficulty(world) >= Difficulty.MODERATE: @@ -50,17 +50,17 @@ def create_itempool(world: World) -> List[Item]: if item_type is ItemClassification.filler or item_type is ItemClassification.trap: continue - if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + if name in act_contracts.keys() and world.options.ShuffleActContracts.value == 0: continue - if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + if name in alps_hooks.keys() and world.options.ShuffleAlpineZiplines.value == 0: continue if name == "Progressive Painting Unlock" \ - and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + and world.options.ShuffleSubconPaintings.value == 0: continue - if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + if world.options.StartWithCompassBadge.value > 0 and name == "Compass Badge": continue if name == "Time Piece": @@ -72,10 +72,10 @@ def create_itempool(world: World) -> List[Item]: if world.is_dlc2(): max_extra += 10 - tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_count += min(max_extra, world.options.MaxExtraTimePieces.value) tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) - for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent.value))): tp_list[i].classification = ItemClassification.progression itempool += tp_list @@ -90,8 +90,8 @@ def create_itempool(world: World) -> List[Item]: def calculate_yarn_costs(world: World): mw = world.multiworld p = world.player - min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) - max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) + max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_cost: int = 0 for i in range(5): @@ -99,13 +99,13 @@ def calculate_yarn_costs(world: World): world.get_hat_yarn_costs()[HatType(i)] = cost max_cost += cost - available_yarn: int = mw.YarnAvailable[p].value + available_yarn: int = world.options.YarnAvailable.value if max_cost > available_yarn: - mw.YarnAvailable[p].value = max_cost + world.options.YarnAvailable.value = max_cost available_yarn = max_cost - if max_cost + mw.MinExtraYarn[p].value > available_yarn: - mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + if max_cost + world.options.MinExtraYarn.value > available_yarn: + world.options.YarnAvailable.value += (max_cost + world.options.MinExtraYarn.value) - available_yarn def item_dlc_enabled(world: World, name: str) -> bool: @@ -141,7 +141,7 @@ def create_multiple_items(world: World, name: str, count: int = 1, def create_junk_items(world: World, count: int) -> List[Item]: - trap_chance = world.multiworld.TrapChance[world.player].value + trap_chance = world.options.TrapChance.value junk_pool: List[Item] = [] junk_list: Dict[str, int] = {} trap_list: Dict[str, int] = {} @@ -157,11 +157,11 @@ def create_junk_items(world: World, count: int) -> List[Item]: elif trap_chance > 0 and ic == ItemClassification.trap: if name == "Baby Trap": - trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value + trap_list[name] = world.options.BabyTrapWeight.value elif name == "Laser Trap": - trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value + trap_list[name] = world.options.LaserTrapWeight.value elif name == "Parade Trap": - trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value + trap_list[name] = world.options.ParadeTrapWeight.value for i in range(count): if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 00ec578624..2bc1c27080 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -12,20 +12,20 @@ def get_total_locations(world: World) -> int: if is_location_valid(world, name): total += 1 - if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: - total += world.multiworld.TasksanityCheckCount[world.player].value + if world.is_dlc1() and world.options.Tasksanity.value > 0: + total += world.options.TasksanityCheckCount.value if world.is_dw(): - if world.multiworld.DWShuffle[world.player].value > 0: + if world.options.DWShuffle.value > 0: total += len(world.get_dw_shuffle()) - if world.multiworld.DWEnableBonus[world.player].value > 0: + if world.options.DWEnableBonus.value > 0: total += len(world.get_dw_shuffle()) else: total += 37 if world.is_dlc2(): total += 1 - if world.multiworld.DWEnableBonus[world.player].value > 0: + if world.options.DWEnableBonus.value > 0: total += 37 if world.is_dlc2(): total += 1 @@ -56,11 +56,11 @@ def is_location_valid(world: World, location: str) -> bool: if not location_dlc_enabled(world, location): return False - if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + if world.options.ShuffleStorybookPages.value == 0 \ and location in storybook_pages.keys(): return False - if world.multiworld.ShuffleActContracts[world.player].value == 0 \ + if world.options.ShuffleActContracts.value == 0 \ and location in contract_locations.keys(): return False @@ -68,23 +68,23 @@ def is_location_valid(world: World, location: str) -> bool: return False data = location_table.get(location) or event_locs.get(location) - if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + if world.options.ExcludeTour.value > 0 and data.region == "Time Rift - Tour": return False # No need for all those event items if we're not doing candles if data.dlc_flags & HatDLC.death_wish: - if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): + if world.options.DWExcludeCandles.value > 0 and location in event_locs.keys(): return False - if world.multiworld.DWShuffle[world.player].value > 0 \ + if world.options.DWShuffle.value > 0 \ and data.region in death_wishes and data.region not in world.get_dw_shuffle(): return False if location in zero_jumps: - if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + if world.options.DWShuffle.value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): return False - difficulty: int = world.multiworld.LogicDifficulty[world.player].value + difficulty: int = world.options.LogicDifficulty.value if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): return False diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 863f940f0a..67bc5b56e6 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,6 +1,7 @@ -import typing -from worlds.AutoWorld import World -from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict +from typing import List +from dataclasses import dataclass +from worlds.AutoWorld import World, PerGameCommonOptions +from Options import Range, Toggle, DeathLink, Choice, OptionDict def adjust_options(world: World): @@ -594,119 +595,118 @@ class ParadeTrapWeight(Range): default = 20 -ahit_options: typing.Dict[str, type(Option)] = { +@dataclass +class AHITOptions(PerGameCommonOptions): + EndGoal: EndGoal + ActRandomizer: ActRandomizer + ActPlando: ActPlando + ShuffleAlpineZiplines: ShuffleAlpineZiplines + FinaleShuffle: FinaleShuffle + LogicDifficulty: LogicDifficulty + YarnBalancePercent: YarnBalancePercent + TimePieceBalancePercent: TimePieceBalancePercent + RandomizeHatOrder: RandomizeHatOrder + UmbrellaLogic: UmbrellaLogic + StartWithCompassBadge: StartWithCompassBadge + CompassBadgeMode: CompassBadgeMode + ShuffleStorybookPages: ShuffleStorybookPages + ShuffleActContracts: ShuffleActContracts + ShuffleSubconPaintings: ShuffleSubconPaintings + NoPaintingSkips: NoPaintingSkips + StartingChapter: StartingChapter + CTRLogic: CTRLogic - "EndGoal": EndGoal, - "ActRandomizer": ActRandomizer, - "ActPlando": ActPlando, - "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "FinaleShuffle": FinaleShuffle, - "LogicDifficulty": LogicDifficulty, - "YarnBalancePercent": YarnBalancePercent, - "TimePieceBalancePercent": TimePieceBalancePercent, - "RandomizeHatOrder": RandomizeHatOrder, - "UmbrellaLogic": UmbrellaLogic, - "StartWithCompassBadge": StartWithCompassBadge, - "CompassBadgeMode": CompassBadgeMode, - "ShuffleStorybookPages": ShuffleStorybookPages, - "ShuffleActContracts": ShuffleActContracts, - "ShuffleSubconPaintings": ShuffleSubconPaintings, - "NoPaintingSkips": NoPaintingSkips, - "StartingChapter": StartingChapter, - "CTRLogic": CTRLogic, + EnableDLC1: EnableDLC1 + Tasksanity: Tasksanity + TasksanityTaskStep: TasksanityTaskStep + TasksanityCheckCount: TasksanityCheckCount + ExcludeTour: ExcludeTour + ShipShapeCustomTaskGoal: ShipShapeCustomTaskGoal - "EnableDLC1": EnableDLC1, - "Tasksanity": Tasksanity, - "TasksanityTaskStep": TasksanityTaskStep, - "TasksanityCheckCount": TasksanityCheckCount, - "ExcludeTour": ExcludeTour, - "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + EnableDeathWish: EnableDeathWish + DWShuffle: DWShuffle + DWShuffleCountMin: DWShuffleCountMin + DWShuffleCountMax: DWShuffleCountMax + DeathWishOnly: DeathWishOnly + DWEnableBonus: DWEnableBonus + DWAutoCompleteBonuses: DWAutoCompleteBonuses + DWExcludeAnnoyingContracts: DWExcludeAnnoyingContracts + DWExcludeAnnoyingBonuses: DWExcludeAnnoyingBonuses + DWExcludeCandles: DWExcludeCandles + DWTimePieceRequirement: DWTimePieceRequirement - "EnableDeathWish": EnableDeathWish, - "DWShuffle": DWShuffle, - "DWShuffleCountMin": DWShuffleCountMin, - "DWShuffleCountMax": DWShuffleCountMax, - "DeathWishOnly": DeathWishOnly, - "DWEnableBonus": DWEnableBonus, - "DWAutoCompleteBonuses": DWAutoCompleteBonuses, - "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, - "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, - "DWExcludeCandles": DWExcludeCandles, - "DWTimePieceRequirement": DWTimePieceRequirement, + EnableDLC2: EnableDLC2 + BaseballBat: BaseballBat + MetroMinPonCost: MetroMinPonCost + MetroMaxPonCost: MetroMaxPonCost + NyakuzaThugMinShopItems: NyakuzaThugMinShopItems + NyakuzaThugMaxShopItems: NyakuzaThugMaxShopItems - "EnableDLC2": EnableDLC2, - "BaseballBat": BaseballBat, - "MetroMinPonCost": MetroMinPonCost, - "MetroMaxPonCost": MetroMaxPonCost, - "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, - "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + LowestChapterCost: LowestChapterCost + HighestChapterCost: HighestChapterCost + ChapterCostIncrement: ChapterCostIncrement + ChapterCostMinDifference: ChapterCostMinDifference + MaxExtraTimePieces: MaxExtraTimePieces - "LowestChapterCost": LowestChapterCost, - "HighestChapterCost": HighestChapterCost, - "ChapterCostIncrement": ChapterCostIncrement, - "ChapterCostMinDifference": ChapterCostMinDifference, - "MaxExtraTimePieces": MaxExtraTimePieces, + FinalChapterMinCost: FinalChapterMinCost + FinalChapterMaxCost: FinalChapterMaxCost - "FinalChapterMinCost": FinalChapterMinCost, - "FinalChapterMaxCost": FinalChapterMaxCost, + YarnCostMin: YarnCostMin + YarnCostMax: YarnCostMax + YarnAvailable: YarnAvailable + MinExtraYarn: MinExtraYarn + HatItems: HatItems - "YarnCostMin": YarnCostMin, - "YarnCostMax": YarnCostMax, - "YarnAvailable": YarnAvailable, - "MinExtraYarn": MinExtraYarn, - "HatItems": HatItems, + MinPonCost: MinPonCost + MaxPonCost: MaxPonCost + BadgeSellerMinItems: BadgeSellerMinItems + BadgeSellerMaxItems: BadgeSellerMaxItems - "MinPonCost": MinPonCost, - "MaxPonCost": MaxPonCost, - "BadgeSellerMinItems": BadgeSellerMinItems, - "BadgeSellerMaxItems": BadgeSellerMaxItems, + TrapChance: TrapChance + BabyTrapWeight: BabyTrapWeight + LaserTrapWeight: LaserTrapWeight + ParadeTrapWeight: ParadeTrapWeight - "TrapChance": TrapChance, - "BabyTrapWeight": BabyTrapWeight, - "LaserTrapWeight": LaserTrapWeight, - "ParadeTrapWeight": ParadeTrapWeight, + death_link: DeathLink - "death_link": DeathLink, -} -slot_data_options: typing.Dict[str, type(Option)] = { +slot_data_options: List[str] = [ + "EndGoal", + "ActRandomizer", + "ShuffleAlpineZiplines", + "LogicDifficulty", + "CTRLogic", + "RandomizeHatOrder", + "UmbrellaLogic", + "StartWithCompassBadge", + "CompassBadgeMode", + "ShuffleStorybookPages", + "ShuffleActContracts", + "ShuffleSubconPaintings", + "NoPaintingSkips", + "HatItems", - "EndGoal": EndGoal, - "ActRandomizer": ActRandomizer, - "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "LogicDifficulty": LogicDifficulty, - "CTRLogic": CTRLogic, - "RandomizeHatOrder": RandomizeHatOrder, - "UmbrellaLogic": UmbrellaLogic, - "StartWithCompassBadge": StartWithCompassBadge, - "CompassBadgeMode": CompassBadgeMode, - "ShuffleStorybookPages": ShuffleStorybookPages, - "ShuffleActContracts": ShuffleActContracts, - "ShuffleSubconPaintings": ShuffleSubconPaintings, - "NoPaintingSkips": NoPaintingSkips, - "HatItems": HatItems, + "EnableDLC1", + "Tasksanity", + "TasksanityTaskStep", + "TasksanityCheckCount", + "ShipShapeCustomTaskGoal", + "ExcludeTour", - "EnableDLC1": EnableDLC1, - "Tasksanity": Tasksanity, - "TasksanityTaskStep": TasksanityTaskStep, - "TasksanityCheckCount": TasksanityCheckCount, - "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, - "ExcludeTour": ExcludeTour, + "EnableDeathWish", + "DWShuffle", + "DeathWishOnly", + "DWEnableBonus", + "DWAutoCompleteBonuses", + "DWTimePieceRequirement", - "EnableDeathWish": EnableDeathWish, - "DWShuffle": DWShuffle, - "DeathWishOnly": DeathWishOnly, - "DWEnableBonus": DWEnableBonus, - "DWAutoCompleteBonuses": DWAutoCompleteBonuses, - "DWTimePieceRequirement": DWTimePieceRequirement, + "EnableDLC2", + "MetroMinPonCost", + "MetroMaxPonCost", + "BaseballBat", - "EnableDLC2": EnableDLC2, - "MetroMinPonCost": MetroMinPonCost, - "MetroMaxPonCost": MetroMaxPonCost, - "BaseballBat": BaseballBat, + "MinPonCost", + "MaxPonCost", - "MinPonCost": MinPonCost, - "MaxPonCost": MaxPonCost, - - "death_link": DeathLink, -} + "death_link", +] diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 59186d68ff..70f7ede3b7 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -270,7 +270,6 @@ blacklisted_combos = { def create_regions(world: World): w = world - mw = world.multiworld p = world.player # ------------------------------------------- HUB -------------------------------------------------- # @@ -311,7 +310,7 @@ def create_regions(world: World): ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) - if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): + if world.options.LogicDifficulty.value >= int(Difficulty.EXPERT): connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # @@ -397,10 +396,10 @@ def create_regions(world: World): create_rift_connections(w, create_region(w, "Time Rift - Balcony")) create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) - if mw.ExcludeTour[world.player].value == 0: + if w.options.ExcludeTour.value == 0: create_rift_connections(w, create_region(w, "Time Rift - Tour")) - if mw.Tasksanity[p].value > 0: + if w.options.Tasksanity.value > 0: create_tasksanity_locations(w) connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) @@ -440,7 +439,7 @@ def create_rift_connections(world: World, region: Region): def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = get_tasksanity_start_id() - for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + for i in range(world.options.TasksanityCheckCount.value): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) @@ -449,10 +448,10 @@ def is_valid_plando(world: World, region: str) -> bool: if region in blacklisted_acts.values(): return False - if region not in world.multiworld.ActPlando[world.player].keys(): + if region not in world.options.ActPlando.keys(): return False - act = world.multiworld.ActPlando[world.player].get(region) + act = world.options.ActPlando.get(region) if act in blacklisted_acts.values(): return False @@ -461,10 +460,10 @@ def is_valid_plando(world: World, region: str) -> bool: and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) if is_first_act: - if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + if act_chapters[act] == "Subcon Forest" and world.options.ShuffleSubconPaintings.value > 0: return False - if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + if world.options.UmbrellaLogic.value > 0 \ and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): return False @@ -484,7 +483,7 @@ def is_valid_plando(world: World, region: str) -> bool: if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": return False - return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) @@ -492,7 +491,7 @@ def randomize_act_entrances(world: World): region_list: typing.List[Region] = get_act_regions(world) world.random.shuffle(region_list) - separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) + separate_rifts: bool = bool(world.options.ActRandomizer.value == 1) for region in region_list.copy(): if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ @@ -511,14 +510,14 @@ def randomize_act_entrances(world: World): region_list.append(region) for region in region_list.copy(): - if region.name in world.multiworld.ActPlando[world.player].keys(): + if region.name in world.options.ActPlando.keys(): if is_valid_plando(world, region.name): region_list.remove(region) region_list.append(region) else: print("Disallowing act plando for", world.multiworld.player_name[world.player], - "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + "-", region.name, ":", world.options.ActPlando.get(region.name)) # Reverse the list, so we can do what we want to do first region_list.reverse() @@ -543,7 +542,7 @@ def randomize_act_entrances(world: World): and "Free Roam" not in act_entrances[region.name]: continue - if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): has_guaranteed = True i = 0 @@ -562,16 +561,16 @@ def randomize_act_entrances(world: World): if candidate.name not in guaranteed_first_acts: continue - if candidate.name in world.multiworld.ActPlando[world.player].values(): + if candidate.name in world.options.ActPlando.values(): continue # Not completable without Umbrella - if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + if world.options.UmbrellaLogic.value > 0 \ and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): continue # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \ + if world.options.ShuffleSubconPaintings.value > 0 \ and "Subcon Forest" in act_entrances[candidate.name]: continue @@ -579,10 +578,10 @@ def randomize_act_entrances(world: World): has_guaranteed = True break - if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): candidate_list.clear() candidate_list.append( - world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player)) break # Already mapped onto something else @@ -607,12 +606,12 @@ def randomize_act_entrances(world: World): continue # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled - if world.multiworld.ShuffleActContracts[world.player].value == 0: + if world.options.ShuffleActContracts.value == 0: if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ and candidate.name == "Contractual Obligations": continue - if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if world.options.FinaleShuffle.value > 0 and region.name in chapter_finales: if candidate.name not in chapter_finales: continue @@ -687,17 +686,17 @@ def get_act_regions(world: World) -> typing.List[Region]: def is_act_blacklisted(world: World, name: str) -> bool: - plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ - or name in world.multiworld.ActPlando[world.player].values() + plando: bool = name in world.options.ActPlando.keys() \ + or name in world.options.ActPlando.values() if name == "The Finale": - return not plando and world.multiworld.EndGoal[world.player].value == 1 + return not plando and world.options.EndGoal.value == 1 if name == "Rush Hour": - return not plando and world.multiworld.EndGoal[world.player].value == 2 + return not plando and world.options.EndGoal.value == 2 if name == "Time Rift - Tour": - return world.multiworld.ExcludeTour[world.player].value > 0 + return world.options.ExcludeTour.value > 0 return name in blacklisted_acts.values() @@ -714,7 +713,7 @@ def create_region(world: World, name: str) -> Region: if data.region == name: if key in storybook_pages.keys() \ - and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + and world.options.ShuffleStorybookPages.value == 0: continue location = HatInTimeLocation(world.player, key, data.id, reg) @@ -732,9 +731,9 @@ def create_badge_seller(world: World) -> Region: count: int = 0 max_items: int = 0 - if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: - max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, - world.multiworld.BadgeSellerMaxItems[world.player].value) + if world.options.BadgeSellerMaxItems.value > 0: + max_items = world.random.randint(world.options.BadgeSellerMinItems.value, + world.options.BadgeSellerMaxItems.value) if max_items <= 0: world.set_badge_seller_count(0) @@ -801,7 +800,7 @@ def create_region_and_connect(world: World, def get_first_chapter_region(world: World) -> Region: - start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player] + start_chapter: ChapterIndex = world.options.StartingChapter.value return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) @@ -826,11 +825,11 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: World): - min_items: int = min(world.multiworld.NyakuzaThugMinShopItems[world.player].value, - world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + min_items: int = min(world.options.NyakuzaThugMinShopItems.value, + world.options.NyakuzaThugMaxShopItems.value) - max_items: int = max(world.multiworld.NyakuzaThugMaxShopItems[world.player].value, - world.multiworld.NyakuzaThugMinShopItems[world.player].value) + max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, + world.options.NyakuzaThugMinShopItems.value) count: int = -1 step: int = 0 old_name: str = "" diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7eb09bedfc..f971526482 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -32,7 +32,7 @@ act_connections = { def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: - if world.multiworld.HatItems[world.player].value > 0: + if world.options.HatItems.value > 0: return state.has(hat_type_to_item[hat], world.player) return state.count("Yarn", world.player) >= get_hat_cost(world, hat) @@ -54,19 +54,19 @@ def can_sdj(state: CollectionState, world: World): def painting_logic(world: World) -> bool: - return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 + return world.options.ShuffleSubconPaintings.value > 0 # -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert def get_difficulty(world: World) -> Difficulty: - return Difficulty(world.multiworld.LogicDifficulty[world.player].value) + return Difficulty(world.options.LogicDifficulty.value) def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: if not painting_logic(world): return True - if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip: + if world.options.NoPaintingSkips.value == 0 and allow_skip: # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena if get_difficulty(world) >= Difficulty.MODERATE: return True @@ -75,7 +75,7 @@ def has_paintings(state: CollectionState, world: World, count: int, allow_skip: def zipline_logic(world: World) -> bool: - return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0 + return world.options.ShuffleAlpineZiplines.value > 0 def can_use_hookshot(state: CollectionState, world: World): @@ -83,7 +83,7 @@ def can_use_hookshot(state: CollectionState, world: World): def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): - if world.multiworld.UmbrellaLogic[world.player].value == 0: + if world.options.UmbrellaLogic.value == 0: return True return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) @@ -132,7 +132,7 @@ def can_clear_metro(state: CollectionState, world: World) -> bool: def set_rules(world: World): # First, chapter access - starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) + starting_chapter = ChapterIndex(world.options.StartingChapter.value) world.set_chapter_cost(starting_chapter, 0) # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale @@ -140,10 +140,10 @@ def set_rules(world: World): ChapterIndex.SUBCON, ChapterIndex.ALPINE] final_chapter = ChapterIndex.FINALE - if world.multiworld.EndGoal[world.player].value == 2: + if world.options.EndGoal.value == 2: final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) - elif world.multiworld.EndGoal[world.player].value == 3: + elif world.options.EndGoal.value == 3: final_chapter = None chapter_list.append(ChapterIndex.FINALE) @@ -185,11 +185,11 @@ def set_rules(world: World): else: chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) - lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value - highest_cost: int = world.multiworld.HighestChapterCost[world.player].value + lowest_cost: int = world.options.LowestChapterCost.value + highest_cost: int = world.options.HighestChapterCost.value - cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value - min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value + cost_increment: int = world.options.ChapterCostIncrement.value + min_difference: int = world.options.ChapterCostMinDifference.value last_cost: int = 0 cost: int loop_count: int = 0 @@ -213,8 +213,8 @@ def set_rules(world: World): if final_chapter is not None: world.set_chapter_cost(final_chapter, world.random.randint( - world.multiworld.FinalChapterMinCost[world.player].value, - world.multiworld.FinalChapterMaxCost[world.player].value)) + world.options.FinalChapterMinCost.value, + world.options.FinalChapterMaxCost.value)) add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) @@ -243,7 +243,7 @@ def set_rules(world: World): and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) - if world.multiworld.ActRandomizer[world.player].value == 0: + if world.options.ActRandomizer.value == 0: set_default_rift_rules(world) table = location_table | event_locs @@ -267,10 +267,10 @@ def set_rules(world: World): if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, world)) - if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + if data.umbrella and world.options.UmbrellaLogic.value > 0: add_rule(location, lambda state: state.has("Umbrella", world.player)) - if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) if data.hit_requirement > 0: @@ -288,7 +288,7 @@ def set_rules(world: World): # Illness starts the player past the intro alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) - if world.multiworld.UmbrellaLogic[world.player].value > 0: + if world.options.UmbrellaLogic.value > 0: add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) if zipline_logic(world): @@ -356,9 +356,9 @@ def set_rules(world: World): set_event_rules(world) - if world.multiworld.EndGoal[world.player].value == 1: + if world.options.EndGoal.value == 1: world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) - elif world.multiworld.EndGoal[world.player].value == 2: + elif world.options.EndGoal.value == 2: world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) @@ -551,7 +551,7 @@ def set_expert_rules(world: World): world.multiworld.get_region("Subcon Forest Area", world.player), "Subcon Forest Entrance YCHE", world.player) - if world.multiworld.NoPaintingSkips[world.player].value > 0: + if world.options.NoPaintingSkips.value > 0: add_rule(entrance, lambda state: has_paintings(state, world, 1)) set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), @@ -630,7 +630,7 @@ def set_mafia_town_rules(world: World): add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: state.has("HUMT Access", world.player), "or") - ctr_logic: int = world.multiworld.CTRLogic[world.player].value + ctr_logic: int = world.options.CTRLogic.value if ctr_logic == 3: set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) elif ctr_logic == 2: @@ -643,7 +643,7 @@ def set_mafia_town_rules(world: World): def set_botb_rules(world: World): - if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: + if world.options.UmbrellaLogic.value == 0 and get_difficulty(world) < Difficulty.MODERATE: set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 047b788535..8dd16d4a90 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -3,7 +3,7 @@ from .Items import item_table, create_item, relic_groups, act_contracts, create_ from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id from .Rules import set_rules -from .Options import ahit_options, slot_data_options, adjust_options +from .Options import AHITOptions, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events @@ -49,7 +49,8 @@ class HatInTimeWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = get_location_names() - # option_definitions = ahit_options + options_dataclass = AHITOptions + options: AHITOptions act_connections: Dict[str, str] = {} shop_locs: List[str] = [] item_name_groups = relic_groups @@ -58,7 +59,7 @@ class HatInTimeWorld(World): def generate_early(self): adjust_options(self) - if self.multiworld.StartWithCompassBadge[self.player].value > 0: + if self.options.StartWithCompassBadge.value > 0: self.multiworld.push_precollected(self.create_item("Compass Badge")) if self.is_dw_only(): @@ -66,14 +67,14 @@ class HatInTimeWorld(World): # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock - start_chapter: int = self.multiworld.StartingChapter[self.player].value + start_chapter: int = self.options.StartingChapter.value if start_chapter == 4 or start_chapter == 3: - if self.multiworld.ActRandomizer[self.player].value == 0: + if self.options.ActRandomizer.value == 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) - if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: + if start_chapter == 3 and self.options.ShuffleSubconPaintings.value > 0: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) def create_regions(self): @@ -83,11 +84,11 @@ class HatInTimeWorld(World): nyakuza_thug_items[self.player] = {} badge_seller_count[self.player] = 0 self.shop_locs = [] - self.topology_present = self.multiworld.ActRandomizer[self.player].value + self.topology_present = self.options.ActRandomizer.value create_regions(self) - if self.multiworld.EnableDeathWish[self.player].value > 0: + if self.options.EnableDeathWish.value > 0: create_dw_regions(self) if self.is_dw_only(): @@ -100,7 +101,7 @@ class HatInTimeWorld(World): create_enemy_events(self) # place vanilla contract locations if contract shuffle is off - if self.multiworld.ShuffleActContracts[self.player].value == 0: + if self.options.ShuffleActContracts.value == 0: for name in contract_locations.keys(): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) @@ -111,9 +112,9 @@ class HatInTimeWorld(World): hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] - if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0: + if self.options.HatItems.value == 0 and self.options.RandomizeHatOrder.value > 0: self.random.shuffle(hat_craft_order[self.player]) - if self.multiworld.RandomizeHatOrder[self.player].value == 2: + if self.options.RandomizeHatOrder.value == 2: hat_craft_order[self.player].remove(HatType.TIME_STOP) hat_craft_order[self.player].append(HatType.TIME_STOP) @@ -137,12 +138,12 @@ class HatInTimeWorld(World): self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", self.player) - if self.multiworld.DWEnableBonus[self.player].value == 0: + if self.options.DWEnableBonus.value == 0: for name in death_wishes: if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): continue - if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + if self.options.DWShuffle.value > 0 and name not in self.get_dw_shuffle(): continue full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) @@ -152,7 +153,7 @@ class HatInTimeWorld(World): return - if self.multiworld.ActRandomizer[self.player].value > 0: + if self.options.ActRandomizer.value > 0: randomize_act_entrances(self) set_rules(self) @@ -175,7 +176,7 @@ class HatInTimeWorld(World): "SeedNumber": str(self.multiworld.seed), # For shop prices "SeedName": self.multiworld.seed_name} - if self.multiworld.HatItems[self.player].value == 0: + if self.options.HatItems.value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) @@ -187,7 +188,7 @@ class HatInTimeWorld(World): slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) - if self.multiworld.ActRandomizer[self.player].value > 0: + if self.options.ActRandomizer.value > 0: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] @@ -198,14 +199,14 @@ class HatInTimeWorld(World): if self.is_dw(): i: int = 0 for name in excluded_dws[self.player]: - if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + if self.options.EndGoal.value == 3 and name == "Seal the Deal": continue slot_data[f"excluded_dw{i}"] = dw_classes[name] i += 1 i = 0 - if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + if self.options.DWAutoCompleteBonuses.value == 0: for name in excluded_bonuses[self.player]: if name in excluded_dws[self.player]: continue @@ -213,19 +214,19 @@ class HatInTimeWorld(World): slot_data[f"excluded_bonus{i}"] = dw_classes[name] i += 1 - if self.multiworld.DWShuffle[self.player].value > 0: + if self.options.DWShuffle.value > 0: shuffled_dws = self.get_dw_shuffle() for i in range(len(shuffled_dws)): slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] - for option_name in slot_data_options: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value + for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items(): + if name in slot_data_options: + slot_data[name] = value return slot_data def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): - if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0: + if self.is_dw_only() or self.options.ActRandomizer.value == 0: return new_hint_data = {} @@ -250,10 +251,10 @@ class HatInTimeWorld(World): new_hint_data[location.address] = get_shuffled_region(self, region_name) - if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: + if self.is_dlc1() and self.options.Tasksanity.value > 0: ship_shape_region = get_shuffled_region(self, "Ship Shape") id_start: int = get_tasksanity_start_id() - for i in range(self.multiworld.TasksanityCheckCount[self.player].value): + for i in range(self.options.TasksanityCheckCount.value): new_hint_data[id_start+i] = ship_shape_region hint_data[self.player] = new_hint_data @@ -281,16 +282,16 @@ class HatInTimeWorld(World): return chapter_timepiece_costs[self.player] def is_dlc1(self) -> bool: - return self.multiworld.EnableDLC1[self.player].value > 0 + return self.options.EnableDLC1.value > 0 def is_dlc2(self) -> bool: - return self.multiworld.EnableDLC2[self.player].value > 0 + return self.options.EnableDLC2.value > 0 def is_dw(self) -> bool: - return self.multiworld.EnableDeathWish[self.player].value > 0 + return self.options.EnableDeathWish.value > 0 def is_dw_only(self) -> bool: - return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + return self.is_dw() and self.options.DeathWishOnly.value > 0 def get_excluded_dws(self): return excluded_dws[self.player] @@ -300,7 +301,7 @@ class HatInTimeWorld(World): def is_dw_excluded(self, name: str) -> bool: # don't exclude Seal the Deal if it's our goal - if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + if self.options.EndGoal.value == 3 and name == "Seal the Deal" \ and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: return False From eb6f373e01cbceb17b50c1c293bd06af2ea12739 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 13:36:49 -0400 Subject: [PATCH 050/143] Missed some --- worlds/ahit/Options.py | 84 +++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 67bc5b56e6..9c19ae1fa3 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -5,64 +5,64 @@ from Options import Range, Toggle, DeathLink, Choice, OptionDict def adjust_options(world: World): - world.multiworld.HighestChapterCost[world.player].value = max( - world.multiworld.HighestChapterCost[world.player].value, - world.multiworld.LowestChapterCost[world.player].value) + world.options.HighestChapterCost.value = max( + world.options.HighestChapterCost.value, + world.options.LowestChapterCost.value) - world.multiworld.LowestChapterCost[world.player].value = min( - world.multiworld.LowestChapterCost[world.player].value, - world.multiworld.HighestChapterCost[world.player].value) + world.options.LowestChapterCost.value = min( + world.options.LowestChapterCost.value, + world.options.HighestChapterCost.value) - world.multiworld.FinalChapterMinCost[world.player].value = min( - world.multiworld.FinalChapterMinCost[world.player].value, - world.multiworld.FinalChapterMaxCost[world.player].value) + world.options.FinalChapterMinCost.value = min( + world.options.FinalChapterMinCost.value, + world.options.FinalChapterMaxCost.value) - world.multiworld.FinalChapterMaxCost[world.player].value = max( - world.multiworld.FinalChapterMaxCost[world.player].value, - world.multiworld.FinalChapterMinCost[world.player].value) + world.options.FinalChapterMaxCost.value = max( + world.options.FinalChapterMaxCost.value, + world.options.FinalChapterMinCost.value) - world.multiworld.BadgeSellerMinItems[world.player].value = min( - world.multiworld.BadgeSellerMinItems[world.player].value, - world.multiworld.BadgeSellerMaxItems[world.player].value) + world.options.BadgeSellerMinItems.value = min( + world.options.BadgeSellerMinItems.value, + world.options.BadgeSellerMaxItems.value) - world.multiworld.BadgeSellerMaxItems[world.player].value = max( - world.multiworld.BadgeSellerMinItems[world.player].value, - world.multiworld.BadgeSellerMaxItems[world.player].value) + world.options.BadgeSellerMaxItems.value = max( + world.options.BadgeSellerMinItems.value, + world.options.BadgeSellerMaxItems.value) total_tps: int = get_total_time_pieces(world) - if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: - world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + if world.options.HighestChapterCost.value > total_tps-5: + world.options.HighestChapterCost.value = min(45, total_tps-5) - if world.multiworld.LowestChapterCost[world.player].value > total_tps-5: - world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5) + if world.options.LowestChapterCost.value > total_tps-5: + world.options.LowestChapterCost.value = min(45, total_tps-5) - if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: - world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + if world.options.FinalChapterMaxCost.value > total_tps: + world.options.FinalChapterMaxCost.value = min(50, total_tps) - if world.multiworld.FinalChapterMinCost[world.player].value > total_tps: - world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5) + if world.options.FinalChapterMinCost.value > total_tps: + world.options.FinalChapterMinCost.value = min(50, total_tps-5) # Don't allow Rush Hour goal if DLC2 content is disabled - if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: - world.multiworld.EndGoal[world.player].value = 1 + if world.options.EndGoal.value == 2 and world.options.EnableDLC2.value == 0: + world.options.EndGoal.value = 1 # Don't allow Seal the Deal goal if Death Wish content is disabled - if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): - world.multiworld.EndGoal[world.player].value = 1 + if world.options.EndGoal.value == 3 and not world.is_dw(): + world.options.EndGoal.value = 1 - if world.multiworld.DWEnableBonus[world.player].value > 0: - world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + if world.options.DWEnableBonus.value > 0: + world.options.DWAutoCompleteBonuses.value = 0 if world.is_dw_only(): - world.multiworld.EndGoal[world.player].value = 3 - world.multiworld.ActRandomizer[world.player].value = 0 - world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 - world.multiworld.ShuffleSubconPaintings[world.player].value = 0 - world.multiworld.ShuffleStorybookPages[world.player].value = 0 - world.multiworld.ShuffleActContracts[world.player].value = 0 - world.multiworld.EnableDLC1[world.player].value = 0 - world.multiworld.LogicDifficulty[world.player].value = -1 - world.multiworld.DWTimePieceRequirement[world.player].value = 0 + world.options.EndGoal.value = 3 + world.options.ActRandomizer.value = 0 + world.options.ShuffleAlpineZiplines.value = 0 + world.options.ShuffleSubconPaintings.value = 0 + world.options.ShuffleStorybookPages.value = 0 + world.options.ShuffleActContracts.value = 0 + world.options.EnableDLC1.value = 0 + world.options.LogicDifficulty.value = -1 + world.options.DWTimePieceRequirement.value = 0 def get_total_time_pieces(world: World) -> int: @@ -73,7 +73,7 @@ def get_total_time_pieces(world: World) -> int: if world.is_dlc2(): count += 10 - return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + return min(40+world.options.MaxExtraTimePieces.value, count) class EndGoal(Choice): From a028b9aac380d8e8959fbfac042b4f73b33ee270 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 14:15:28 -0400 Subject: [PATCH 051/143] Snatcher Coins fix --- worlds/ahit/Options.py | 16 ++++++++++++++++ worlds/ahit/Regions.py | 13 ++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 863f940f0a..f3dd2a8c66 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -28,6 +28,22 @@ def adjust_options(world: World): world.multiworld.BadgeSellerMinItems[world.player].value, world.multiworld.BadgeSellerMaxItems[world.player].value) + world.multiworld.NyakuzaThugMinShopItems[world.player].value = min( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.DWShuffleCountMin[world.player].value = min( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + world.multiworld.DWShuffleCountMax[world.player].value = max( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + total_tps: int = get_total_time_pieces(world) if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 59186d68ff..9245efd937 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -826,11 +826,9 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: World): - min_items: int = min(world.multiworld.NyakuzaThugMinShopItems[world.player].value, - world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value - max_items: int = max(world.multiworld.NyakuzaThugMaxShopItems[world.player].value, - world.multiworld.NyakuzaThugMinShopItems[world.player].value) + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value count: int = -1 step: int = 0 old_name: str = "" @@ -877,6 +875,7 @@ def create_events(world: World) -> int: if not is_location_valid(world, name): continue + item_name: str = name if world.is_dw(): if name in snatcher_coins.keys(): name = f"{name} ({data.region})" @@ -887,15 +886,15 @@ def create_events(world: World) -> int: if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: continue - event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) event.show_in_spoiler = False count += 1 return count -def create_event(name: str, region: Region, world: World) -> Location: +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: event = HatInTimeLocation(world.player, name, None, region) region.locations.append(event) - event.place_locked_item(HatInTimeItem(name, ItemClassification.progression, None, world.player)) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) return event From eb2bd1c4d70dbd104344e2d7f0ee11e60f879977 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 14:20:08 -0400 Subject: [PATCH 052/143] Missed some more --- worlds/ahit/Options.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 9c19ae1fa3..4e8738b5ce 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -29,6 +29,22 @@ def adjust_options(world: World): world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value) + world.options.NyakuzaThugMinShopItems.value = min( + world.options.NyakuzaThugMinShopItems.value, + world.options.NyakuzaThugMaxShopItems.value) + + world.options.NyakuzaThugMaxShopItems.value = max( + world.options.NyakuzaThugMinShopItems.value, + world.options.NyakuzaThugMaxShopItems.value) + + world.options.DWShuffleCountMin.value = min( + world.options.DWShuffleCountMin.value, + world.options.DWShuffleCountMax.value) + + world.options.DWShuffleCountMax.value = max( + world.options.DWShuffleCountMin.value, + world.options.DWShuffleCountMax.value) + total_tps: int = get_total_time_pieces(world) if world.options.HighestChapterCost.value > total_tps-5: world.options.HighestChapterCost.value = min(45, total_tps-5) From 311966ca3a23f9f119e8f2be20a4bee4e03e119b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 20:46:42 -0400 Subject: [PATCH 053/143] some slight touch ups --- worlds/ahit/DeathWishRules.py | 6 +++--- worlds/ahit/Locations.py | 10 +++++----- worlds/ahit/Regions.py | 4 ++-- worlds/ahit/__init__.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 5a97518499..c448484036 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -321,7 +321,7 @@ def set_candle_dw_rules(name: str, world: World): elif name == "Camera Tourist": add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) - and state.has("Triple Enemy Picture", world.player)) + and state.has("Triple Enemy Photo", world.player)) elif "Snatcher Coins" in name: for coin in required_snatcher_coins[name]: @@ -408,8 +408,8 @@ def create_enemy_events(world: World): continue region = world.multiworld.get_region(name, world.player) - event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) - event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) region.locations.append(event) event.show_in_spoiler = False if name == "The Mustache Gauntlet": diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 00ec578624..bf31c8cba8 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -4,6 +4,9 @@ from typing import Dict from .Options import TasksanityCheckCount +TASKSANITY_START_ID = 2000300204 + + def get_total_locations(world: World) -> int: total: int = 0 @@ -96,7 +99,7 @@ def is_location_valid(world: World, location: str) -> bool: def get_location_names() -> Dict[str, int]: names = {name: data.id for name, data in location_table.items()} - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(TasksanityCheckCount.range_end): names.setdefault(f"Tasksanity Check {i+1}", id_start+i) @@ -107,10 +110,6 @@ def get_location_names() -> Dict[str, int]: return names -def get_tasksanity_start_id() -> int: - return 2000300204 - - ahit_locations = { "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), @@ -848,6 +847,7 @@ zero_jumps = { dlc_flags=HatDLC.dlc2_dw), } +# noinspection PyDictDuplicateKeys snatcher_coins = { "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 9245efd937..807f1ee77f 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -2,7 +2,7 @@ from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ - shop_locations, get_tasksanity_start_id, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard import typing from .Rules import set_rift_rules, get_difficulty @@ -439,7 +439,7 @@ def create_rift_connections(world: World, region: Region): def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(world.multiworld.TasksanityCheckCount[world.player].value): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 64b3febc3e..0ed14c6376 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,7 +1,7 @@ from BaseClasses import Item, ItemClassification, Tutorial from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region -from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem @@ -252,7 +252,7 @@ class HatInTimeWorld(World): if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: ship_shape_region = get_shuffled_region(self, "Ship Shape") - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(self.multiworld.TasksanityCheckCount[self.player].value): new_hint_data[id_start+i] = ship_shape_region From a262b13c5fb1b5fe53f1ae6f567a604d22eeb83a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 4 Nov 2023 17:14:03 -0400 Subject: [PATCH 054/143] rewind --- BaseClasses.py | 218 +++++++---- Fill.py | 45 ++- Generate.py | 17 +- Main.py | 7 +- SNIClient.py | 4 +- Utils.py | 64 +++- WebHostLib/options.py | 6 + WebHostLib/static/assets/faq/faq_en.md | 109 +++--- WebHostLib/static/assets/weighted-options.js | 291 ++++----------- WebHostLib/static/styles/weighted-options.css | 6 + WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 10 +- WebHostLib/tracker.py | 19 +- test/bases.py | 28 +- test/general/test_fill.py | 4 +- test/general/test_host_yaml.py | 4 +- test/general/test_locations.py | 3 - test/general/test_memory.py | 16 + test/utils/test_caches.py | 66 ++++ worlds/AutoWorld.py | 36 +- worlds/__init__.py | 42 ++- worlds/_bizhawk/context.py | 63 +++- worlds/adventure/Rom.py | 6 +- worlds/ahit/Regions.py | 9 +- worlds/alttp/Client.py | 3 +- worlds/alttp/Dungeons.py | 3 +- worlds/alttp/ItemPool.py | 1 - worlds/alttp/Rom.py | 6 +- worlds/alttp/Rules.py | 28 +- worlds/alttp/Shops.py | 3 - worlds/alttp/UnderworldGlitchRules.py | 2 +- worlds/alttp/__init__.py | 46 +-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/archipidle/Rules.py | 7 +- worlds/blasphemous/Options.py | 1 + worlds/blasphemous/Rules.py | 8 +- worlds/blasphemous/docs/en_Blasphemous.md | 1 + worlds/checksfinder/__init__.py | 4 +- worlds/checksfinder/docs/en_ChecksFinder.md | 13 +- worlds/dlcquest/Rules.py | 4 +- worlds/dlcquest/__init__.py | 4 +- worlds/ff1/docs/en_Final Fantasy.md | 3 +- worlds/hk/Items.py | 45 ++- worlds/hk/Rules.py | 15 +- worlds/hk/__init__.py | 16 +- worlds/ladx/Locations.py | 6 +- worlds/ladx/__init__.py | 13 +- worlds/lufia2ac/Rom.py | 5 +- worlds/meritous/Regions.py | 4 +- worlds/messenger/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 7 + worlds/musedash/MuseDashData.txt | 11 +- worlds/musedash/__init__.py | 2 +- worlds/noita/Items.py | 84 +++-- worlds/noita/Regions.py | 74 ++-- worlds/noita/Rules.py | 13 +- worlds/oot/Entrance.py | 10 +- worlds/oot/EntranceShuffle.py | 52 ++- worlds/oot/Patches.py | 2 +- worlds/oot/Rules.py | 21 +- worlds/oot/__init__.py | 344 ++++++++++-------- worlds/oot/docs/en_Ocarina of Time.md | 7 + worlds/pokemon_rb/__init__.py | 17 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45570 -> 45893 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45511 -> 45875 bytes .../docs/en_Pokemon Red and Blue.md | 6 + worlds/pokemon_rb/rom.py | 6 +- worlds/pokemon_rb/rom_addresses.py | 10 +- worlds/pokemon_rb/rules.py | 38 +- worlds/ror2/Options.py | 42 +-- worlds/ror2/Rules.py | 3 +- .../docs/en_Starcraft 2 Wings of Liberty.md | 22 +- worlds/sm/__init__.py | 19 +- worlds/sm64ex/Options.py | 7 + worlds/sm64ex/Rules.py | 7 +- worlds/sm64ex/__init__.py | 1 + worlds/smz3/__init__.py | 8 +- worlds/soe/__init__.py | 2 +- worlds/stardew_valley/mods/mod_data.py | 8 + worlds/stardew_valley/stardew_rule.py | 16 +- worlds/stardew_valley/test/TestBackpack.py | 49 +-- worlds/stardew_valley/test/TestGeneration.py | 39 +- worlds/stardew_valley/test/TestItems.py | 6 +- .../test/TestLogicSimplification.py | 91 ++--- worlds/stardew_valley/test/TestOptions.py | 35 +- worlds/stardew_valley/test/TestRegions.py | 4 +- worlds/stardew_valley/test/TestRules.py | 2 +- 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 +- worlds/terraria/docs/setup_en.md | 2 + worlds/tloz/docs/en_The Legend of Zelda.md | 14 +- worlds/tloz/docs/multiworld_en.md | 1 + worlds/undertale/__init__.py | 2 +- worlds/undertale/docs/en_Undertale.md | 19 +- worlds/wargroove/docs/en_Wargroove.md | 9 +- worlds/witness/hints.py | 4 +- worlds/zillion/__init__.py | 33 +- worlds/zillion/docs/en_Zillion.md | 10 +- 104 files changed, 1545 insertions(+), 1190 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 d35739c324..a70dd70a92 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]) @@ -303,11 +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) + + @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) @@ -329,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) @@ -424,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] @@ -467,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 @@ -608,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]] @@ -620,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()} @@ -668,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 @@ -712,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 @@ -736,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: @@ -749,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 @@ -816,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) @@ -855,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. @@ -866,6 +931,7 @@ class Region: if rule: exit_.access_rule = rule exit_.connect(connecting_region) + return exit_ def create_exit(self, name: str) -> Entrance: """ 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/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/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/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/Utils.py b/Utils.py index 5fb037a173..bb68602cce 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 @@ -73,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]: @@ -90,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)) @@ -146,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) @@ -257,15 +289,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): @@ -905,3 +935,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/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/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. diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index bdd121eff5..3811bd42ba 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); } }); @@ -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; } 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: diff --git a/test/bases.py b/test/bases.py index 5fe4df2014..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) @@ -284,7 +310,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_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/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/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/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/AutoWorld.py b/worlds/AutoWorld.py index d4fe0f49a2..d05797cf9e 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: @@ -400,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/__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(): 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) 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/ahit/Regions.py b/worlds/ahit/Regions.py index 807f1ee77f..2cabef46ab 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -431,9 +431,10 @@ def create_rift_connections(world: World, region: Region): connect_regions(act_region, region, entrance_name, world.player) i += 1 - # fix for some weird keyerror from tests + # fix for some weird keyerror if region.name == "Time Rift - Rumbi Factory": for entrance in region.entrances: + print(entrance.name) world.multiworld.get_entrance(entrance.name, world.player) @@ -631,8 +632,8 @@ def randomize_act_entrances(world: World): candidate = c break + # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) - # print(region, candidate) # Vanilla if candidate.name == region.name: 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) 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/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/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 65e36da3bd..d89e65c59d 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) @@ -585,27 +586,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) @@ -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/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/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/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? 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/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/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/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/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'} 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) 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 6c89db3891..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): @@ -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..eaaea5be2f 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 @@ -518,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 @@ -526,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/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 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/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/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'))) 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/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 diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index ca53c96233..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] @@ -84,8 +91,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,43 +102,46 @@ 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, 1), "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), +} +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": 10, - "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, + **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/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())) 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) 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/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] diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index fa198e0ce1..529411f6fc 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 @@ -223,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 6af19683f4..e9c889d6f6 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 @@ -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,8 +198,10 @@ class OOTWorld(World): option_value = result.current_key setattr(self, option_name, option_value) + self.regions = [] # internal caches of regions for this world, used later + self._regions_cache = {} + self.shop_prices = {} - self.regions = [] # internal cache of regions for this world, used later self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -489,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) @@ -526,6 +534,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,8 +567,10 @@ 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 + + # 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'}] @@ -585,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 = {} @@ -610,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', @@ -622,7 +640,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,9 +654,46 @@ 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 + + # 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, @@ -658,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: @@ -671,7 +728,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 +739,13 @@ 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)) + + # Create items, starting item handling, boss prize fill (before entrance randomizer) 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 = [] @@ -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,8 +828,9 @@ 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.get_state_with_complete_itempool() 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] @@ -781,7 +841,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: @@ -792,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(locations=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(locations=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 @@ -836,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) @@ -861,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: @@ -883,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))) @@ -896,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': @@ -954,48 +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': @@ -1032,30 +1059,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 +1138,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 +1146,7 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } + def modify_multidata(self, multidata: dict): # Replace connect name @@ -1156,6 +1161,16 @@ 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): er_hint_data[self.player] = {} @@ -1202,6 +1217,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 +1227,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 @@ -1218,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) @@ -1242,24 +1284,29 @@ 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.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) - 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_entrances(self): + return self.multiworld.get_entrances(self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1294,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) @@ -1336,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', @@ -1355,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 + 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/__init__.py b/worlds/pokemon_rb/__init__.py index 11aa737e0f..b2ee0702c9 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 @@ -467,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)] @@ -559,7 +559,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/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/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/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/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, 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), 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 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/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/sm/__init__.py b/worlds/sm/__init__.py index f208e600b9..3e9015eab7 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 @@ -294,7 +291,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) @@ -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 @@ -390,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) @@ -563,8 +562,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/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): 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/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/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/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/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/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_()) 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/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)) 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] 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 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/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 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/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/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_) 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 bd8698e1fd2b8433e3ce1096b7e6cb352c6022aa Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 4 Nov 2023 17:55:48 -0400 Subject: [PATCH 055/143] a --- BaseClasses.py | 218 ++++------- Fill.py | 45 +-- Generate.py | 17 +- Main.py | 7 +- SNIClient.py | 4 +- Utils.py | 64 +--- WebHostLib/options.py | 6 - WebHostLib/static/assets/faq/faq_en.md | 109 +++--- WebHostLib/static/assets/weighted-options.js | 291 +++++++++++---- WebHostLib/static/styles/weighted-options.css | 6 - WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 10 +- WebHostLib/tracker.py | 19 +- test/bases.py | 28 +- test/general/test_fill.py | 4 +- test/general/test_host_yaml.py | 4 +- test/general/test_locations.py | 3 + worlds/AutoWorld.py | 36 +- worlds/__init__.py | 42 +-- worlds/_bizhawk/context.py | 63 +--- worlds/adventure/Rom.py | 6 +- worlds/ahit/Regions.py | 9 +- worlds/alttp/Client.py | 3 +- worlds/alttp/Dungeons.py | 3 +- worlds/alttp/ItemPool.py | 1 + worlds/alttp/Rom.py | 6 +- worlds/alttp/Rules.py | 28 +- worlds/alttp/Shops.py | 3 + worlds/alttp/UnderworldGlitchRules.py | 2 +- worlds/alttp/__init__.py | 46 +-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/archipidle/Rules.py | 7 +- worlds/blasphemous/Options.py | 1 - worlds/blasphemous/Rules.py | 8 +- worlds/blasphemous/docs/en_Blasphemous.md | 1 - worlds/checksfinder/__init__.py | 4 +- worlds/checksfinder/docs/en_ChecksFinder.md | 13 +- worlds/dlcquest/Rules.py | 4 +- worlds/dlcquest/__init__.py | 4 +- worlds/ff1/docs/en_Final Fantasy.md | 3 +- worlds/hk/Items.py | 45 +-- worlds/hk/Rules.py | 15 +- worlds/hk/__init__.py | 16 +- worlds/ladx/Locations.py | 6 +- worlds/ladx/__init__.py | 13 +- worlds/lufia2ac/Rom.py | 5 +- worlds/meritous/Regions.py | 4 +- worlds/messenger/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 7 - worlds/musedash/MuseDashData.txt | 11 +- worlds/musedash/__init__.py | 2 +- worlds/noita/Items.py | 84 ++--- worlds/noita/Regions.py | 74 ++-- worlds/noita/Rules.py | 13 +- worlds/oot/Entrance.py | 10 +- worlds/oot/EntranceShuffle.py | 52 +-- worlds/oot/Patches.py | 2 +- worlds/oot/Rules.py | 21 +- worlds/oot/__init__.py | 344 ++++++++---------- worlds/oot/docs/en_Ocarina of Time.md | 7 - worlds/pokemon_rb/__init__.py | 17 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45893 -> 45570 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45875 -> 45511 bytes .../docs/en_Pokemon Red and Blue.md | 6 - worlds/pokemon_rb/rom.py | 6 +- worlds/pokemon_rb/rom_addresses.py | 10 +- worlds/pokemon_rb/rules.py | 38 +- worlds/ror2/Options.py | 42 ++- worlds/ror2/Rules.py | 3 +- .../docs/en_Starcraft 2 Wings of Liberty.md | 22 +- worlds/sm/__init__.py | 19 +- worlds/sm64ex/Options.py | 7 - worlds/sm64ex/Rules.py | 7 +- worlds/sm64ex/__init__.py | 1 - worlds/smz3/__init__.py | 8 +- worlds/soe/__init__.py | 2 +- worlds/stardew_valley/mods/mod_data.py | 8 - worlds/stardew_valley/stardew_rule.py | 16 +- worlds/stardew_valley/test/TestBackpack.py | 49 ++- worlds/stardew_valley/test/TestGeneration.py | 39 +- worlds/stardew_valley/test/TestItems.py | 6 +- .../test/TestLogicSimplification.py | 91 +++-- worlds/stardew_valley/test/TestOptions.py | 35 +- worlds/stardew_valley/test/TestRegions.py | 4 +- worlds/stardew_valley/test/TestRules.py | 2 +- 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 +- worlds/terraria/docs/setup_en.md | 2 - worlds/tloz/docs/en_The Legend of Zelda.md | 14 +- worlds/tloz/docs/multiworld_en.md | 1 - worlds/undertale/__init__.py | 2 +- worlds/undertale/docs/en_Undertale.md | 19 +- worlds/wargroove/docs/en_Wargroove.md | 9 +- worlds/witness/hints.py | 4 +- worlds/zillion/__init__.py | 33 +- worlds/zillion/docs/en_Zillion.md | 10 +- 102 files changed, 1190 insertions(+), 1463 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a70dd70a92..d35739c324 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,15 +1,14 @@ 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 Counter, deque -from collections.abc import Collection, MutableSequence +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 @@ -48,6 +47,7 @@ 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: RegionManager + regions: List[Region] itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -92,34 +92,6 @@ 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()) @@ -128,12 +100,16 @@ class MultiWorld(): self.glitch_triforce = False self.algorithm = 'balanced' self.groups = {} - self.regions = self.RegionManager(players) + self.regions = [] 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 @@ -161,6 +137,7 @@ 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') @@ -222,6 +199,7 @@ 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]) @@ -325,15 +303,11 @@ class MultiWorld(): def player_ids(self) -> Tuple[int, ...]: return tuple(range(1, self.players + 1)) - @Utils.cache_self1 + @functools.lru_cache() 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) - @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) - - @Utils.cache_self1 + @functools.lru_cache() 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) @@ -355,17 +329,41 @@ 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.regions.region_cache[player].values() + return self.regions if player is None else self._region_cache[player].values() - def get_region(self, region_name: str, player: int) -> Region: - return self.regions.region_cache[player][region_name] + 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_entrance(self, entrance_name: str, player: int) -> Entrance: - return self.regions.entrance_cache[player][entrance_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_location(self, location_name: str, player: int) -> Location: - return self.regions.location_cache[player][location_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_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) @@ -426,22 +424,28 @@ class MultiWorld(): logging.debug('Placed %s at %s', item, location) - 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 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 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) -> Iterable[Location]: + 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] if player is not 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)) + 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 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] @@ -463,17 +467,16 @@ 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 = relevant_cache.get(location_name, None) - if location and location.item is None: + location = self._location_cache.get((location_name, player), None) + if location is not None 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(item.player): + for location in self.get_unfilled_locations(): if temp_state.can_reach(location) and not self.state.can_reach(location): return True @@ -605,7 +608,7 @@ PathValue = Tuple[str, Optional["PathValue"]] class CollectionState(): - prog_items: Dict[int, Counter[str]] + prog_items: typing.Counter[Tuple[str, int]] multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] @@ -617,7 +620,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 = Counter() 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 +668,7 @@ class CollectionState(): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = copy.deepcopy(self.prog_items) + ret.prog_items = self.prog_items.copy() 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 +712,23 @@ class CollectionState(): self.collect(event.item, True, event) def has(self, item: str, player: int, count: int = 1) -> bool: - return self.prog_items[player][item] >= count + return self.prog_items[item, player] >= 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[player][item] for item in items) + return all(self.prog_items[item, player] 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[player][item] for item in items) + return any(self.prog_items[item, player] for item in items) def count(self, item: str, player: int) -> int: - return self.prog_items[player][item] + return self.prog_items[item, player] 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[player][item_name] + found += self.prog_items[item_name, player] if found >= count: return True return False @@ -733,11 +736,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[player][item_name] + found += self.prog_items[item_name, player] return found def item_count(self, item: str, player: int) -> int: - return self.prog_items[player][item] + return self.prog_items[item, player] def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: @@ -746,7 +749,7 @@ class CollectionState(): changed = self.multiworld.worlds[item.player].collect(self, item) if not changed and event: - self.prog_items[item.player][item.name] += 1 + self.prog_items[item.name, item.player] += 1 changed = True self.stale[item.player] = True @@ -813,83 +816,15 @@ 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.EntranceRegister(multiworld.regions) - self._locations = self.LocationRegister(multiworld.regions) + self.exits = [] + self.locations = [] 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) @@ -920,7 +855,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) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> None: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -931,7 +866,6 @@ class Region: if rule: exit_.access_rule = rule exit_.connect(connecting_region) - return exit_ def create_exit(self, name: str) -> Entrance: """ diff --git a/Fill.py b/Fill.py index c9660ab708..9d5dc0b457 100644 --- a/Fill.py +++ b/Fill.py @@ -15,10 +15,6 @@ 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: @@ -30,7 +26,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, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False) -> None: """ :param world: Multiworld to be filled. :param base_state: State assumed before fill. @@ -42,20 +38,16 @@ 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() @@ -160,15 +152,9 @@ 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, []) @@ -212,8 +198,6 @@ 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 @@ -263,12 +247,6 @@ 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 @@ -304,7 +282,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, name="Accessibility Corrections") + fill_restrictive(world, state, locations, pool) def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): @@ -374,25 +352,23 @@ 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, name=f"Local Early Items P{player}") + player_local, lock=True, allow_partial=True) 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, - name="Early Items") + fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True) 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, name=f"Local Early Progression P{player}") + player_local, lock=True, allow_partial=True) 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, - name="Early Progression") + fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True) 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 " @@ -446,14 +422,13 @@ 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, - name="Priority") + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: - # "advancement/progression fill" - fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression") + # "progression fill" + fill_restrictive(world, world.state, defaultlocations, progitempool) if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') diff --git a/Generate.py b/Generate.py index 8113d8a0d7..34a0084e8d 100644 --- a/Generate.py +++ b/Generate.py @@ -7,8 +7,8 @@ import random import string import urllib.parse import urllib.request -from collections import Counter -from typing import Any, Dict, Tuple, Union +from collections import ChainMap, Counter +from typing import Any, Callable, 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) - return callback(erargs, seed) + callback(erargs, seed) def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -639,15 +639,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - 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." + main() # in case of error-free exit should not need confirmation atexit.unregister(confirmation) diff --git a/Main.py b/Main.py index 691b88b137..0995d2091f 100644 --- a/Main.py +++ b/Main.py @@ -122,6 +122,10 @@ 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: @@ -229,7 +233,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: @@ -263,6 +267,7 @@ 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/SNIClient.py b/SNIClient.py index 062d7a7cbe..0909c61382 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, death_text: str = "") -> None: + async def handle_deathlink_state(self, currently_dead: bool) -> 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(death_text) + await self.send_death() # 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/Utils.py b/Utils.py index bb68602cce..5fb037a173 100644 --- a/Utils.py +++ b/Utils.py @@ -5,7 +5,6 @@ import json import typing import builtins import os -import itertools import subprocess import sys import pickle @@ -74,8 +73,6 @@ 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]: @@ -93,31 +90,6 @@ 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)) @@ -174,16 +146,12 @@ 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", "data/lua"): + for dn in ("Players", "data/sprites"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - 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) + for fn in ("manifest.json",): + shutil.copy2(local_path(fn), user_path(fn)) return os.path.join(user_path.cached_path, *path) @@ -289,13 +257,15 @@ def get_public_ipv6() -> str: return ip -OptionsType = Settings # TODO: remove when removing get_options +OptionsType = Settings # TODO: remove ~2 versions after 0.4.1 -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() +@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 persistent_store(category: str, key: typing.Any, value: typing.Any): @@ -935,17 +905,3 @@ 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/WebHostLib/options.py b/WebHostLib/options.py index 1a2aab6d88..785785cde0 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -139,13 +139,7 @@ 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/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index fb1ccd2d6f..74f423df1f 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -2,62 +2,13 @@ ## What is a randomizer? -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 +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 game, you might first find item C, then A, then B. -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). +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. ## What happens if an item is placed somewhere it is impossible to get? @@ -66,15 +17,53 @@ 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: -[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). +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). -There, you will find examples of games in the `worlds` folder: -[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). +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). -You may also find developer documentation in the `docs` folder: -[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). +You may also find developer documentation in the docs folder +at [/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. diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index 3811bd42ba..bdd121eff5 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-options.json`)).then((response) => { + fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => { try{ response.json().then((jsonObj) => resolve(jsonObj)); } catch(error){ reject(error); } }); @@ -428,13 +428,13 @@ class GameSettings { const weightedSettingsDiv = this.#buildWeightedSettingsDiv(); gameDiv.appendChild(weightedSettingsDiv); - const itemPoolDiv = this.#buildItemPoolDiv(); + const itemPoolDiv = this.#buildItemsDiv(); gameDiv.appendChild(itemPoolDiv); const hintsDiv = this.#buildHintsDiv(); gameDiv.appendChild(hintsDiv); - const locationsDiv = this.#buildPriorityExclusionDiv(); + const locationsDiv = this.#buildLocationsDiv(); gameDiv.appendChild(locationsDiv); collapseButton.addEventListener('click', () => { @@ -734,17 +734,107 @@ class GameSettings { break; case 'items-list': - const itemsList = this.#buildItemsDiv(settingName); + 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)); + }); + settingWrapper.appendChild(itemsList); break; case 'locations-list': - const locationsList = this.#buildLocationsDiv(settingName); + 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)); + }); + settingWrapper.appendChild(locationsList); break; case 'custom-list': - const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options); + 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)); + }); + settingWrapper.appendChild(customList); break; @@ -759,7 +849,7 @@ class GameSettings { return settingsWrapper; } - #buildItemPoolDiv() { + #buildItemsDiv() { const itemsDiv = document.createElement('div'); itemsDiv.classList.add('items-div'); @@ -968,7 +1058,35 @@ class GameSettings { itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; - const itemHintsDiv = this.#buildItemsDiv('start_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); + }); + itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); @@ -977,7 +1095,35 @@ class GameSettings { locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; - const locationHintsDiv = this.#buildLocationsDiv('start_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); + }); + locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); @@ -985,7 +1131,7 @@ class GameSettings { return hintsDiv; } - #buildPriorityExclusionDiv() { + #buildLocationsDiv() { const locationsDiv = document.createElement('div'); locationsDiv.classList.add('locations-div'); const locationsHeader = document.createElement('h3'); @@ -1005,7 +1151,35 @@ class GameSettings { priorityLocationsWrapper.classList.add('locations-wrapper'); priorityLocationsWrapper.innerText = 'Priority Locations'; - const priorityLocationsDiv = this.#buildLocationsDiv('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); + }); + priorityLocationsWrapper.appendChild(priorityLocationsDiv); locationsContainer.appendChild(priorityLocationsWrapper); @@ -1014,7 +1188,35 @@ class GameSettings { excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; - const excludeLocationsDiv = this.#buildLocationsDiv('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); + }); + excludeLocationsWrapper.appendChild(excludeLocationsDiv); locationsContainer.appendChild(excludeLocationsWrapper); @@ -1022,71 +1224,6 @@ 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 8a66ca2370..cc5231634e 100644 --- a/WebHostLib/static/styles/weighted-options.css +++ b/WebHostLib/static/styles/weighted-options.css @@ -292,12 +292,6 @@ 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; } diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html index 8eb471be39..2b943a22b0 100644 --- a/WebHostLib/templates/lttpMultiTracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -153,7 +153,7 @@ {%- endif -%} {% endif %} {%- endfor -%} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} + {{ 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 1a3d353de1..40d89eb4c6 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -55,7 +55,7 @@ {{ checks["Total"] }}/{{ locations[player] | length }} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} + {{ percent_total_checks_done[team][player] }} {%- if activity_timers[team, player] -%} {{ activity_timers[team, player].total_seconds() }} {%- else -%} @@ -72,13 +72,7 @@ All 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 %} - + {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 55b98df59e..0d9ead7951 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1532,11 +1532,9 @@ 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] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 - ) + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / + len(player_locations) * 100) \ + if player_locations else 100 activity_timers = {} now = datetime.datetime.utcnow() @@ -1692,13 +1690,10 @@ 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"] = len(locations_checked) - - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 - ) + 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 for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: diff --git a/test/bases.py b/test/bases.py index 2054c2d187..5fe4df2014 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,4 +1,3 @@ -import sys import typing import unittest from argparse import Namespace @@ -108,36 +107,11 @@ 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) @@ -310,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 = list(self.multiworld.get_locations(1)) + locations = self.multiworld.get_locations(1).copy() state = CollectionState(self.multiworld) while locations: sphere: typing.List[Location] = [] diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 1e469ef04d..4e8cc2edb7 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.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") + 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") def test_correct_item_instance_removed_from_pool(self): """Test that a placed item gets removed from the submitted pool""" diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 79285d3a63..9408f95b16 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 Settings(None).items(): + for option_key, option_set in Utils.get_default_options().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 = Settings(None) + utils_options = Utils.get_default_options() for option_key, option_set in self.yaml_options.items(): with self.subTest(option_key): self.assertIn(option_key, utils_options) diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 63b3b0f364..2e609a756f 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -36,6 +36,7 @@ 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()) @@ -45,12 +46,14 @@ 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/AutoWorld.py b/worlds/AutoWorld.py index d05797cf9e..d4fe0f49a2 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -4,7 +4,6 @@ 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 @@ -18,8 +17,6 @@ if TYPE_CHECKING: from . import GamesPackage from settings import Group -perf_logger = logging.getLogger("performance") - class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} @@ -106,24 +103,10 @@ 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 = _timed_call(method, *args, multiworld=multiworld, player=player) + ret = method(*args) except Exception as e: message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}." if sys.version_info >= (3, 11, 0): @@ -149,15 +132,18 @@ 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.") - call_stage(multiworld, method_name, *args) + 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) 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 sorted(world_types, key=lambda world: world.__name__): + for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: - _timed_call(stage_callable, multiworld, *args) + stage_callable(multiworld, *args) class WebWorld: @@ -414,16 +400,16 @@ class World(metaclass=AutoWorldRegister): def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: - state.prog_items[self.player][name] += 1 + state.prog_items[name, self.player] += 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[self.player][name] -= 1 - if state.prog_items[self.player][name] < 1: - del (state.prog_items[self.player][name]) + state.prog_items[name, self.player] -= 1 + if state.prog_items[name, self.player] < 1: + del (state.prog_items[name, self.player]) return True return False diff --git a/worlds/__init__.py b/worlds/__init__.py index 40e0b20f19..c6208fa9a1 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -5,20 +5,19 @@ import typing import warnings import zipimport -from Utils import user_path, local_path +folder = os.path.dirname(__file__) -local_folder = os.path.dirname(__file__) -user_folder = user_path("worlds") if user_path() != local_path() else None - -__all__ = ( +__all__ = { "lookup_any_item_id_to_name", "lookup_any_location_id_to_name", "network_data_package", "AutoWorldRegister", "world_sources", - "local_folder", - "user_folder", -) + "folder", +} + +if typing.TYPE_CHECKING: + from .AutoWorld import World class GamesData(typing.TypedDict): @@ -42,13 +41,13 @@ class WorldSource(typing.NamedTuple): is_zip: bool = False relative: bool = True # relative to regular world import folder - def __repr__(self) -> str: + def __repr__(self): 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(local_folder, self.path) + return os.path.join(folder, self.path) return self.path def load(self) -> bool: @@ -57,7 +56,6 @@ 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]) @@ -74,7 +72,7 @@ class WorldSource(typing.NamedTuple): importlib.import_module(f".{self.path}", "worlds") return True - except Exception: + except Exception as e: # A single world failing can still mean enough is working for the user, log and carry on import traceback import io @@ -89,16 +87,14 @@ class WorldSource(typing.NamedTuple): # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.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): - # 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)) +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)) # import all submodules to trigger AutoWorldRegister world_sources.sort() @@ -109,7 +105,7 @@ lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games: typing.Dict[str, GamesPackage] = {} -from .AutoWorld import AutoWorldRegister # noqa: E402 +from .AutoWorld import AutoWorldRegister # Build the data package for each game. for world_name, world in AutoWorldRegister.world_types.items(): diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index ccf747f15a..5d865f3321 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -5,7 +5,6 @@ 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 @@ -22,13 +21,6 @@ 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""" @@ -43,8 +35,6 @@ 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 @@ -55,8 +45,6 @@ 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 @@ -73,41 +61,10 @@ 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 @@ -152,13 +109,12 @@ 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 and not ctx.server.socket.closed: + 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.client_handler = None - await ctx.disconnect(False) ctx.rom_hash = rom_hash if ctx.client_handler is None: @@ -180,14 +136,15 @@ async def _game_watcher(ctx: BizHawkClientContext): except NotConnectedError: continue - # 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 + # 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() - # Call the handler's game watcher await ctx.client_handler.game_watcher(ctx) diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py index 9f1ca3fe5e..62c4019718 100644 --- a/worlds/adventure/Rom.py +++ b/worlds/adventure/Rom.py @@ -6,8 +6,9 @@ from typing import Optional, Any import Utils from .Locations import AdventureLocation, LocationData -from settings import get_settings +from Utils import OptionsType from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer +from itertools import chain import bsdiff4 @@ -312,8 +313,9 @@ 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 = get_settings()["adventure_options"]["rom_file"] + file_name = options["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/ahit/Regions.py b/worlds/ahit/Regions.py index 2cabef46ab..807f1ee77f 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -431,10 +431,9 @@ def create_rift_connections(world: World, region: Region): connect_regions(act_region, region, entrance_name, world.player) i += 1 - # fix for some weird keyerror + # fix for some weird keyerror from tests if region.name == "Time Rift - Rumbi Factory": for entrance in region.entrances: - print(entrance.name) world.multiworld.get_entrance(entrance.name, world.player) @@ -632,8 +631,8 @@ def randomize_act_entrances(world: World): candidate = c break - # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) + # print(region, candidate) # Vanilla if candidate.name == region.name: diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index edc68473b9..22ef2a39a8 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -520,8 +520,7 @@ 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, - ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "") + await ctx.handle_deathlink_state(currently_dead) gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a68acf7288..630d61e019 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,8 +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, - name="LttP Dungeon Items") + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 88a2d899fc..806a420f41 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -293,6 +293,7 @@ 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 e1ae0cc6e6..47cea8c20e 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(player): - if location.address is None or location.shop_slot is not None: + for location in world.get_locations(): + if location.player != player or 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 = list(world.get_entrances(player)) + all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == 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 469f4f82ee..1fddecd8f4 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -197,13 +197,8 @@ 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 - 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_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) 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)) @@ -1531,16 +1526,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 and region.is_light_world + return region.is_light_world def is_link(region): - return region and region.is_dark_world + return region.is_dark_world else: def is_bunny(region): - return region and region.is_dark_world + return region.is_dark_world def is_link(region): - return region and region.is_light_world + return 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 @@ -1608,20 +1603,21 @@ 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 region_exit in region.exits: - add_rule(region_exit, rule) + for exit in region.exits: + add_rule(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(player): - if is_bunny(entrance.connected_region): + for entrance in world.get_entrances(): + if entrance.player == player and 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 c0f2e2236e..f17eb1eadb 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -348,6 +348,7 @@ 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): @@ -618,4 +619,6 @@ 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/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index a6aefc7412..4b6bc54111 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[player]['Moon Pearl'] += 1 + fake_state.prog_items['Moon Pearl', player] += 1 return fake_state diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d89e65c59d..65e36da3bd 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -470,8 +470,7 @@ 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, - name="LttP Dungeon Prizes") + fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True) except FillError as e: lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt) @@ -586,26 +585,27 @@ class ALTTPWorld(World): for player in checks_in_area: checks_in_area[player]["Total"] = 0 - 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 + + 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 multidata["checks_in_area"].update(checks_in_area) @@ -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[player][item] >= count + return self.prog_items[item, player] >= count diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 8ca2791dcf..94c30c3493 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 get_dungeon_item_pool +from worlds.alttp.Dungeons import create_dungeons, 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/archipidle/Rules.py b/worlds/archipidle/Rules.py index 3bf4bad475..cdd48e7604 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -5,7 +5,12 @@ from ..generic.Rules import set_rule class ArchipIDLELogic(LogicMixin): def _archipidle_location_is_accessible(self, player_id, items_required): - return sum(self.prog_items[player_id].values()) >= items_required + items_received = 0 + for item in self.prog_items: + if item[1] == player_id: + items_received += 1 + + return items_received >= items_required def set_rules(world: MultiWorld, player: int): diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index 127a1dc776..ea304d22ed 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -67,7 +67,6 @@ 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 5d88292131..4218fa94cf 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -578,12 +578,11 @@ 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, 2) + can_cross_gap(state, logic, player, 1) 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), @@ -703,11 +702,10 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player), lambda state: ( - can_cross_gap(state, logic, player, 2) + can_cross_gap(state, logic, player, 1) 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", "Ranged Skill"}, player) + or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or precise_skips_allowed(logic) )) # Doors diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md index 1ff7f5a903..15223213ac 100644 --- a/worlds/blasphemous/docs/en_Blasphemous.md +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -19,7 +19,6 @@ 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? diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 4978500da0..feff148651 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/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index 96fb0529df..bd82660b09 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -14,18 +14,11 @@ 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. - -## Unique Local Commands - -The following command is only available when using the ChecksFinderClient to play with Archipelago. - -- `/resync` Manually trigger a resync. +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 diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index 5792d9c3ab..a11e5c504e 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[player][" coins"] >= coin + return lambda state: state.prog_items[" coins", player] >= coin def has_enough_coin_freemium(player: int, coin: int): - return lambda state: state.prog_items[player][" coins freemium"] >= coin + return lambda state: state.prog_items[" coins freemium", player] >= coin def set_rules(world, player, World_Options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index e4e0a29274..54d27f7b65 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[self.player][suffix] += item.coins + state.prog_items[suffix, self.player] += 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[self.player][suffix] -= item.coins + state.prog_items[suffix, self.player] -= item.coins return change diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 59fa85d916..8962919743 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -26,7 +26,6 @@ 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 commands are only available when using the FF1Client for the Final Fantasy Randomizer. +The following command is 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/hk/Items.py b/worlds/hk/Items.py index def5c32981..a9acbf48f3 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -19,43 +19,18 @@ 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 = ({ - "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"], + +item_name_groups.update({ "Dreamers": {"Herrah", "Monomon", "Lurien"}, - "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"}, + "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"}, }) item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index 2dc512eca7..4fe4160b4c 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -1,4 +1,5 @@ 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 @@ -38,12 +39,14 @@ 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 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) + 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) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c16a108cd1..1a9d4b5d61 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[item.player][effect_name] += effect_value + state.prog_items[effect_name, item.player] += effect_value if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: - 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) + 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) 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[item.player][effect_name] == effect_value: - del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + 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 return change diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index c7b127ef2b..6c89db3891 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[self.player]["RUPEES"] + return self.state.prog_items["RUPEES", self.player] return 0 elif item.endswith("_USED"): return 0 else: item = ladxr_item_to_la_item_name[item] - return self.state.prog_items[self.player].get(item, default) + return self.state.prog_items.get((item, self.player), default) class LinksAwakeningEntrance(Entrance): @@ -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 eaaea5be2f..1d6c85dd64 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -231,7 +231,9 @@ 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(self.player): + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue if r.dungeon_index != item.item_data.dungeon_index: continue for loc in r.locations: @@ -267,7 +269,10 @@ 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(self.player): + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + # Set aside dungeon locations if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations @@ -513,7 +518,7 @@ class LinksAwakeningWorld(World): change = super().collect(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] += rupees + state.prog_items["RUPEES", item.player] += rupees return change @@ -521,6 +526,6 @@ class LinksAwakeningWorld(World): change = super().remove(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] -= rupees + state.prog_items["RUPEES", item.player] -= rupees return change diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index 446668d392..1da8d235a6 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -3,7 +3,7 @@ import os from typing import Optional import Utils -from settings import get_settings +from Utils import OptionsType from worlds.Files import APDeltaPatch L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d" @@ -35,8 +35,9 @@ 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 = get_settings()["lufia2ac_options"]["rom_file"] + file_name = options["lufia2ac_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/meritous/Regions.py b/worlds/meritous/Regions.py index de34570d02..2c66a024ca 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/messenger/__init__.py b/worlds/messenger/__init__.py index 3fe13a3cb4..0771989ffc 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[self.player]["Shards"] += shard_count + state.prog_items["Shards", self.player] += shard_count return super().collect_item(state, item, remove) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 187f1fdf19..fa992e1e11 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"{self.multiworld.get_out_file_name_base(self.player)}.apmc" + filename = f"AP_{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'))) diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md index 7ffa4665fd..854034d5a8 100644 --- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md +++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md @@ -72,10 +72,3 @@ 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/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 5b3ef40e54..bd07fef7af 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 Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10| +Crimson Nightingle|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,11 +488,4 @@ 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| -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 +Shun-ran|66-9|Miku in Museland|False|4|7|9| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index bfe321b64a..63ce123c93 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 = 11 + data_version = 10 web = MuseDashWebWorld() # Necessary Data diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index c859a80394..ca53c96233 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/Items.py @@ -44,18 +44,20 @@ 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, weights: Dict[str, int], count: int) -> List[str]: - filler_pool = weights.copy() +def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: + filler_pool = filler_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=count) + return multiworld.random.choices( + population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=random_count + ) def create_all_items(multiworld: MultiWorld, player: int) -> None: - locations_to_fill = len(multiworld.get_unfilled_locations(player)) + sum_locations = len(multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() @@ -64,18 +66,9 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None: + create_kantele(multiworld.victory_condition[player]) ) - # 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)) + random_count = sum_locations - len(itempool) + itempool += create_random_items(multiworld, player, random_count) - itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) multiworld.itempool += [create_item(player, name) for name in itempool] @@ -91,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, 1), - "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), + "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful), + "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), @@ -102,46 +95,43 @@ 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, 1), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful), "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, 1), - "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1), + "Kammi": ItemData(110028, "Items", ItemClassification.filler), + "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler), "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] = { - **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, + "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": 10, + "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/Regions.py b/worlds/noita/Regions.py index 561d483b48..a239b437d7 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, List +from typing import Dict, Set from BaseClasses import Entrance, MultiWorld, Region from . import Locations @@ -79,46 +79,70 @@ 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, List[str]] = { - "Menu": ["Forest"], - "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"], - "Frozen Vault": ["The Vault"], - "Overgrown Cavern": ["Sandcave"], +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"}, ### - "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"], - "Lava Lake": ["Abyss Orb Room"], + "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"}, ### - "Coal Pits Holy Mountain": ["Coal Pits"], - "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"], + "Coal Pits Holy Mountain": {"Coal Pits"}, + "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"}, + "Fungal Caverns": {"Coal Pits"}, ### - "Snowy Depths Holy Mountain": ["Snowy Depths"], - "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"], + "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"}, ### - "Hiisi Base Holy Mountain": ["Hiisi Base"], - "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"], + "Hiisi Base Holy Mountain": {"Hiisi Base"}, + "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"}, + "Secret Shop": {"Hiisi Base"}, ### - "Underground Jungle Holy Mountain": ["Underground Jungle"], - "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"], + "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": {}, ### - "Vault Holy Mountain": ["The Vault"], - "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"], + "Vault Holy Mountain": {"The Vault"}, + "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"}, ### - "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"], + "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": {}, ### - "Laboratory Holy Mountain": ["The Laboratory"], - "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"], + "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)": {}, ### } -noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values())) +noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 808dd3a200..3eb6be5a7c 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -44,10 +44,12 @@ 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())) @@ -153,12 +155,11 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: def create_all_rules(multiworld: MultiWorld, player: int) -> None: - 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) + 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) diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index 6c4b6428f5..e480c957a6 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -1,4 +1,6 @@ + from BaseClasses import Entrance +from .Regions import TimeOfDay class OOTEntrance(Entrance): game: str = 'Ocarina of Time' @@ -27,16 +29,16 @@ class OOTEntrance(Entrance): self.connected_region = None return previously_connected - def get_new_target(self, pool_type): + def get_new_target(self): root = self.multiworld.get_region('Root Exits', self.player) - target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root) + target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) target_entrance.connect(self.connected_region) target_entrance.replaces = self root.exits.append(target_entrance) return target_entrance - def assume_reachable(self, pool_type): + def assume_reachable(self): if self.assumed == None: - self.assumed = self.get_new_target(pool_type) + self.assumed = self.get_new_target() self.disconnect() return self.assumed diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index bbdc30490c..3c1b2d78c6 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -2,7 +2,6 @@ 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 @@ -26,12 +25,12 @@ def set_all_entrances_data(world, player): return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool, ootworld, pool_type): +def assume_entrance_pool(entrance_pool, ootworld): assumed_pool = [] for entrance in entrance_pool: - assumed_forward = entrance.assume_reachable(pool_type) + assumed_forward = entrance.assume_reachable() if entrance.reverse != None and not ootworld.decouple_entrances: - assumed_return = entrance.reverse.assume_reachable(pool_type) + assumed_return = entrance.reverse.assume_reachable() 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): @@ -42,15 +41,15 @@ def assume_entrance_pool(entrance_pool, ootworld, pool_type): return assumed_pool -def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()): +def build_one_way_targets(world, 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(pool) for entrance in valid_one_way_entrances + return [entrance.get_new_target() for entrance in valid_one_way_entrances if entrance.connected_region.name in target_region_names] - return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances] + return [entrance.get_new_target() for entrance in valid_one_way_entrances] # Abbreviations @@ -424,14 +423,14 @@ multi_interior_regions = { } interior_entrance_bias = { - '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, + '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, 'Market Entrance -> Market Guard House': 2, - 'Goron City -> GC Shop': 1, - 'Zoras Domain -> ZD Shop': 1, + 'ToT Entrance -> Temple of Time': 1, } @@ -444,8 +443,7 @@ def shuffle_random_entrances(ootworld): player = ootworld.player # Gather locations to keep reachable for validation - all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state = world.get_all_state(use_cache=True) 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 @@ -525,12 +523,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, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, 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, pool_type, valid_target_types) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, 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)))()) @@ -540,11 +538,14 @@ 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, pool_type) + target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() - none_state = CollectionState(ootworld.multiworld) + 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 # Plando entrances if world.plando_connections[player]: @@ -627,7 +628,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 = ootworld.get_state_with_complete_itempool() + new_all_state = world.get_all_state(use_cache=False) if not world.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world @@ -699,7 +700,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=10): +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): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -744,6 +745,7 @@ 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 @@ -753,7 +755,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran if entrance.connected_region: original_connected_regions[entrance] = entrance.disconnect() - all_state = ootworld.get_state_with_complete_itempool() + all_state = world.get_all_state(use_cache=False) restrictive_entrances = [] soft_entrances = [] @@ -791,8 +793,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(locations=ootworld.get_locations()) - none_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_events() + none_state.sweep_for_events() 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/Patches.py b/worlds/oot/Patches.py index 0f1d3f4dcb..f83b34183c 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 ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']: + if dungeon in ['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] diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 529411f6fc..fa198e0ce1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -1,12 +1,8 @@ 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 @@ -154,16 +150,11 @@ 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 in {'song', 'dungeon'} and not ootworld.songs_as_items: + if ootworld.shuffle_song_items == 'song' 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: 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) + add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) @@ -185,6 +176,11 @@ 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 @@ -227,8 +223,7 @@ 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.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state = ootworld.multiworld.get_all_state(False) 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 e9c889d6f6..6af19683f4 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): - 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} + 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} def copy_mixin(self, ret) -> CollectionState: ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in @@ -170,19 +170,15 @@ 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) @@ -198,10 +194,8 @@ class OOTWorld(World): option_value = result.current_key setattr(self, option_name, option_value) - self.regions = [] # internal caches of regions for this world, used later - self._regions_cache = {} - self.shop_prices = {} + self.regions = [] # internal cache of regions for this world, used later self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -495,8 +489,6 @@ 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) @@ -534,10 +526,6 @@ 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(): @@ -567,10 +555,8 @@ 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'}] @@ -599,8 +585,6 @@ 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 = {} @@ -626,8 +610,6 @@ 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', @@ -640,7 +622,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards'])) + boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] 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] @@ -654,46 +636,9 @@ 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 - - # 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, @@ -713,9 +658,7 @@ class OOTWorld(World): location.internal = True return item - - # Create regions, locations, and entrances - def create_regions(self): + def create_regions(self): # create and link regions if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL world_type = 'World' else: @@ -728,7 +671,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.get_region('Root')) + start.connect(self.multiworld.get_region('Root', self.player)) create_dungeons(self) self.parser.create_delayed_rules() @@ -739,13 +682,16 @@ class OOTWorld(World): # Bind entrances to vanilla for region in self.regions: for exit in region.exits: - exit.connect(self.get_region(exit.vanilla_connected_region)) + exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player)) - - # Create items, starting item handling, boss prize fill (before entrance randomizer) 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 = [] @@ -768,16 +714,12 @@ 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: @@ -815,7 +757,6 @@ 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 @@ -828,9 +769,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 = self.multiworld.get_all_state(False) 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] @@ -841,6 +781,7 @@ 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: @@ -851,63 +792,35 @@ 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(locations=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(locations=self.get_locations()) - # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - 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) - + world_items = [item for item in self.multiworld.itempool if item.player == self.player] for fill_stage in special_fill_types: - stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items)) + stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_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.pre_fill_items.remove(item) + self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), 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.pre_fill_items.remove(item) + self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) # Place songs @@ -923,9 +836,9 @@ class OOTWorld(World): else: raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") - songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items)) + songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) for song in songs: - self.pre_fill_items.remove(song) + self.multiworld.itempool.remove(song) important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions) @@ -948,7 +861,7 @@ class OOTWorld(World): while tries: try: self.multiworld.random.shuffle(song_locations) - fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), 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: @@ -970,8 +883,10 @@ 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.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_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_locations = list( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.multiworld.get_unfilled_locations(player=self.player))) @@ -981,14 +896,30 @@ 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) - self.pre_fill_items = [] # all prefill should be done - fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, + 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, 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': @@ -1023,6 +954,48 @@ 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': @@ -1059,6 +1032,30 @@ 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 @@ -1138,7 +1135,6 @@ 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 { @@ -1146,7 +1142,6 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } - def modify_multidata(self, multidata: dict): # Replace connect name @@ -1161,16 +1156,6 @@ 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): er_hint_data[self.player] = {} @@ -1217,7 +1202,6 @@ 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") @@ -1227,32 +1211,6 @@ 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 @@ -1260,16 +1218,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[self.player][alt_item_name] += count + state.prog_items[alt_item_name, self.player] += 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[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]) + 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]) return True return super().remove(state, item) @@ -1284,29 +1242,24 @@ class OOTWorld(World): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type) - and (not only_primary or entrance.primary))] + 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))] 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): - return self.multiworld.get_locations(self.player) + for region in self.regions: + for loc in region.locations: + yield loc def get_location(self, location): return self.multiworld.get_location(location, 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_entrances(self): - return self.multiworld.get_entrances(self.player) + def get_region(self, region): + return self.multiworld.get_region(region, self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1341,8 +1294,9 @@ 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.itempool + self.pre_fill_items: - self.multiworld.worlds[item.player].collect(all_state, item) + for item in self.multiworld.itempool: + if item.player == self.player: + 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) @@ -1382,6 +1336,7 @@ 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', @@ -1400,12 +1355,21 @@ 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 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 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)) return locations - diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md index fa8e148957..b4610878b6 100644 --- a/worlds/oot/docs/en_Ocarina of Time.md +++ b/worlds/oot/docs/en_Ocarina of Time.md @@ -31,10 +31,3 @@ 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/__init__.py b/worlds/pokemon_rb/__init__.py index b2ee0702c9..11aa737e0f 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -445,9 +445,13 @@ 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 @@ -463,17 +467,13 @@ class PokemonRedBlueWorld(World): locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - if not self.multiworld.key_items_only[self.player]: - rule = None + for loc in locs: if self.multiworld.fossil_check_item_types[self.player] == "key_items": - rule = lambda i: i.advancement + add_item_rule(loc, lambda i: i.advancement) elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": - rule = lambda i: i.name in item_groups["Unique"] + add_item_rule(loc, lambda i: i.name in item_groups["Unique"]) elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": - rule = lambda i: not i.advancement - if rule: - for loc in locs: - add_item_rule(loc, rule) + add_item_rule(loc, lambda i: not i.advancement) 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)] @@ -559,6 +559,7 @@ 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/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1..b7bdda7fbbed37ff0fb9bff23d33460c38cdd1f3 100644 GIT binary patch literal 45570 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

RP#AGhVah-Ib>@Av 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ΤiKQQ=5s|y*F=q&V0P=^;47ONdt(6PDCsbIz;RCrkINddU3T*Oc*xEjI>3AUt*#FP+?zOfY z1{%!6@R>ELZROS~W!*&oH&$+ks-M133L9{vB(pGctWqU`2)`=E>xk-){1k}IHVp#J zx@#;!woxRCVZcy)Nb=)-#*12Kn4#=rf7PDq!OF)%c%)oRQqf#<#ERvg5B=*mj_bGA zWt9v>{0!K4OS8-d4@OdEm@TRKhrckWl#{+N2gGok4?|B&2W^7*VW@YIu(4aNM^V-% zBG!1j+_>mEM|$7jy!4_mOoEAr7TsNf`wWXbE?V)*%zpx z0)pU#NI;YWkkmZC%IV$~r^7EAos?Xw_CIUKG@CHMlmU?Vz3;-w)#2lwC+8*t#tI6g z766fmqXB+0O0LLm=XIyIv}>P}wh-9(Y?cgt8bYF|uoglK6a__Ku@&DeEuLBm;iygUA)s-ZjV3l7B8QvpPQ7|L7bR6+|#DA#+zfyl?a zU@t!YzPsV>F2)pEt*RzEk zNZ?#0r)FF$F?!W2iC~O_5o!J zdPPMkA9}$^O~YXWfByQhZC3E~Z4A-n=0@J|ip2tZ71%*9D6+3cJ z*u;zq0Z&oP^FJfjLDiq=pu2yW_*6!Y7QAClj+Q^hUE03H6}n%1*Q2@zsD%{gKnuoN z4ELC(KLTb?GnaXhcX90{Qya=?m}z}#q=Cda&d!W3SL?3Qx!Pl7u$$+xZ6>+t z<~|pauj=@(>SYfE^Ft`6VAKn%g(n8xJ?KwTSy(`mvied*Ia$oE*QQvRNS+*Iysx(_QKZ znf0{w-&qgL;a=wapJIk8nxH++KG8JKj*y=n13a{>1KTp9|C80o#Zc`@SRccF*^uDL z1lNBti^+v#p2(h&&YFJ~(f=QH1O{1vS2>tz4&fl7L16>SmmdZauc0v7pmz4A=SkSS z_8bmIw}PvcD(gp04)wko_%Cf`t35g&5tDIeA5so&_owH_N2h%C=X~RA{|Cd*`mLu_ zd0g`~!$pimx@RyOVXAc6>(eF8xrSws?9jR<99$lUQqE=*IgKyV-)LGYM9P+F6KgRM z*?}f0)02p6hhg5?*27#@MF&uWGZrR*WM#ZDc$9UxX>=;2e&F0O=GWvwva2eT``SHy z!!fv^n_HSs6X_qVxNX@vt=)!9R+-EpAp#vjQIDd!(N8fm`I6R)(5T4x_pkcRr}6Z^ zZxfnFN6y#zu9HS<3@CI7L}mdNf+A33+)xEpDpk|blU%@!eTv&w^y*h2suAOkGM;Fn zMv95({-u6LqKmZkBk8#cmmf9ZsO^qpVnh(6kZ_P0*Md<810)zggp`~W=D0FnK3_ds z*P#`Z&{2+#s@_YVd)7|_{(I?hPjz#kYB1)tQ^BS_uMz(PlxVm zo6#$4qPvZV7P-l1v$p*1&DE3V9#^&to z;Ja0&OO{fEO~J(3Sr+%@=@)R*+xi{NR7VF}-XO1f{qoHx$i9}DOtNdzVuL3pkuv@z z_iGnYL*&dG3n!grPM^Khz_-94mzqKwhIU(?fc4Cc3-)h6wY=MHv8u6wO;W@w***od zUfL7Pci>P`nktPhnjyD7puQ)+m_g&|{Irjw?X7&9j5L+F5Z^IL$f?nr&dVOS4R6%~Hp=6#>u_xrlUbhn)?ad zu5}86O?J}SpaAR7)$A`crDvCFwY$~=@~&gHL}UA`fo)LWfg=1h2&d{ivIsb2qtQK! z`}%X10|y}OP7}!2%bu6qM4T!#6@JPEjq(VXJ=yWzKCL0(VK|@2*$;AudhibM8PkEs|-R)jVqk*lz;Qs!5Dv>g>`B+vOY=M1cYGc0Qbj&OaX+-4Sy7abXBqvrnGK160zes%Q6$&p3!4fW-4*FM^vmsJ&_IC-EIKe>R?fxc1rZ zA4MleokQTD@bKzOYRxEZ_Wp9ncl(+4?l@A=7k`nVlUiE=PXhV+*9g8JGOSpE1%k!@ z4^k7D#qY8m^y8{4#)pe6ld%urWs{GeWKsu0X_9^V1oy~%_8upLzg0VX)TtBq?EPF0 zmJvz& zDd(9<@5sVz==&yffQunE|GOA4ApYX7Py5Wn;+dP|C*Bkcd@m7rVS)l|2Z zs5p%ZoOUEyNdRBIx8-R?ea>pUSTVL}1`^BcrC<;!Q}R$%;>8rI%IXYzlJ<#0w^M(+ z=Yh%DI?{Ha{F@D#=O>=O2WdjF;Pj=3sz`h zTPO?!A~hb$u5(AbMI{H(89LUIRK8N9c*v$wk*ySYHVrJSlrCvL!W@kJZDmbWO5rs` zl+xQEp23F_Uy)3`8rx}==C0^>otp>{F&l_1snRH?hHvk8^Km>fwZfD{mte9YqZ#VK zUIqj}si98h#dXBU+KVl&I{~1NL8}%fC2G8u5wCYrEKUx0g^>%JojmJ@YxtAYwt>1z z&6{?**delEj^1~RCjA{hS^oClyP*gxXrBZ|fO=V9DZ4aJg z)U$-#9DS=Np}eW~~f$kFM|VZRKbltJTMsDcGQO1@)l8qPvCwOd7c@n=w;dfk7S zms1kMAk2UWFoWb<_|L(Vz|%4W(YL5Y$Kjg>r@VyBlM@h8z-eXr)%2bf?JGaZ7Gs!mx`LI4h6W` zu6`T+GK=+PPU|*9tT~uH@!t1-&&ywXogI?gNHvf%gL>uYke;|6o5*-wTY!ROCxdbY zIajDH%~Zk;cJrn?nW>MVbL*kp<*XYN?r|%y^!^2xs*Hx0!J}FxoXUBM&W9B6KC|il z#`u0yK2m-=HOiOR^RV|Lr{FStRzUx?bhwLU68{u!B8WH0Dw(_m6jLxvhm7I_5Ep^4 z7vo3Q;!VhEO~`N{ZSgkkvAn*=0J)Qe0v$Jd*S%&O;fm)3pPO%7>n~e%&;uhbjH}_d zZ%gey%ZHusO&gBR=i2SN4`-)W4Zth8Akg4enUVt-^B2gObukbl^b?>{_vQ%=rRKcL z$1urC8OO_QE-tU-aA#eWIUb^zk4F#o+{U#DRT|Ul+S%oo%XgemDNnfk_BG0%fZDv9 zrtiz1w+jz;!)aFU4{Yk^GluRmR}|ZQ7Ob|+SpH<)I7G&D63=7KJjR87LwxaytjY5| zx=43a`hAVLbGBdD*Xs2t*E$^iGp`08Sjk+RxEJc=VX6U`$Fq>^3+4~J=Yy`k5Zilu zvCC;y(v7C)3x~CHq5;7;Yr8%M*UQ zz(){{2kj{M+N+N8*F_ZW?rdJWAF3STO?#`mO%_>N^@eOF+PiOXiH zAzpF<9zSa(mLqk|BV{61_s*H&nG%~T=UxIYB3O8ctM5pCsKxnI1Hv5949HzoNEy^` zWbr-+V7cnZ zel|8g=)(=uZ&!g5y35A!ZcS%5BU#{lrH^>C^sU+E6Z_>crb~@CFv+QWm zu7}`%T{Qk(oydMtEP0t(`&sfT9LhShM7db%_8mvhJ$9C;KtZGJ`#+dof=hVLp3D7pGaFUc<$h?Hn9sqK;?0!>x*P`H_&e2KLgofhTj1U$4yG zG=(E`ij8@f-!YzJ48kQ`q!!7v3lvgF2-%{;*YVT-}-TC{MY|4%vywdvlHD!mX1#eY8^rq7Fb-06qNJie3B78g?GA2 z;DTl8nHc}cOlLs@T%ZyubmGHiVmFzcg>R$Tr(5~)w@ihy5{u!4Uz_SA7s`;fPVfM5 zDr1O@fzTas?Y$VVea~!wK!q%VNe>V}5)i3`zu$)&expa3 z)Ucti4#8jzPp#9!L}(}@SPB2c08Z*FmJLKRcn3XpYw0%7L0ch2s6w@}cC)cQV(b#g zgFHv2Kw?ASAiL#Kj%^XBFf%vUL;(%zD!>)wl;dv{LMoWdzFzVUccWl&Ie*37b2PQT;BxM+$fP0TaomE~L^Zo}YP& z(U3Wzi+5QABsLFkex}gY&89#PZRjsDT{!TzE~l``L2oHD%omj;PbJSC_=&O$$$k$L zOxNRN<;t#C?|582T@SqF>udA+_BHuRJ<|>JC7} zkE!iox%Oc}`lO0S04OxbL|@!Hv|W8r!Yk&Yr_aG|Il0r1&9NQA_)TG@|nz2n3V%nnjjDh zTk$ZJI3Z25v&rTgba^VrjRU7N;oOERvl5{k@=yu`@*p(hhlL{+l-C8+HHV4^&gb6B zB8pT0{x0N-aG@Z(21_VHT4*^jL0KkKS^h7Y%>XA{fB*mg|NsC0|NsC0|NsC0|NsC0 z|NsC0|NsC0|NsC0|Nr1SA2rRYgX^oUwys?q0g-?k?`GCv_a4dhe=snbDPFK=F| zUf$_)Y?X?b~;J@EcJQB4TK0 z000q_O*CnVf-nhy69mCAG--*1zy!q82*3d{Vqiv?np4J&7!yVbsj)O@!5TCT42%;5 z(*)HBgdw1404ACYnrWjTX`?1aMkbRdq|?v_O#n>PG{IE&lOQrpG+@w}spHhu9%-t6 zQht-f(wj-*j}s~7AE`X1L&m45+EdDUo{6;&2zpVyG@#TVnFPQAgv4kB!X`9nG;Jy3 zG--t}BLSp(BU54-Q}s{8(-L}5(i#RPRQ!rO8f{M$Jx^20HZ?_G0E7hc8i%1Unl#ZgYI{`mJO(2XN9m%Tr?pL{B=tO}q93W~ z8jPWtN0ju%AEKTp{ZrJ_)G(entGa^r>0Treu`};rfQxj&>CWTk5QoTrbEgA z(?dYfpa1{>&>0#7)Myxz37`QQ8U(;5hD=O`L7}0cqGdeL(W#RKJv726i9IyH6Go$G znwWYuJxoT_XwzyYnqbOkdYK-kntDuW4I4zr(Sl+$4@tD1rH+mVWuormV&XuJOqUX5 zq6$S{f;4`g)ViJQ#w0Ej@HbC7MWdRNKn(etdfM^Z@3#`;`!Lf2%+?3r{^Ci66WhE< zdwoDK22m%fixp5Gro4lCZm81&iHpm^N=#fr0jtCrSrD)oznq+yQq9E1d2Pe$+|$0q zlSf8IAnQAYKnU6D`lp~3QQv*wqXsCH8IuZ11JTYY2nCbhJ&n0u9L}DT$H2n9M+*A+ z%dWBhUCBPnDc#9>o0nD|qdHBmb^U|E2yd>ju{gJk=Y6M3V#$Us2}0*RzKI>`HYE2s z8hZEHU=Qn0SqkqEHy5`RATa=D|F#*HA5nqa;3TPCIu|S?-WS0^!3d`>dEBivxZsQ) zexf5lLlOxP(TjAc_mUg91$t z0>fC%`Jujl16i!->>r?OIA|`FR(LTY5G;lvB%_U=-B&ts72rE-z-s4-js`ok61UR3Vpn zIJuHZ$*YIliXf?GfCNXTi8E_xH#R^qwZdlzi+WLesp|L03=-5aA^PG+TRa9Rq1*1IR)~#R55=CN?LjrqpJo-`igdC+5#Y1PdJ~ z(ar@iOXD&sfF!|ABwBa#8%f;vXzb2%4t)^d0;>ebEPw#KV;~GN7=fkB8y_0BLxEs5 zU$`^wdQzn9*jnyp{xX^9Fy4V?45EMG=uaohVHo3y)ckHY1qJbY9HQnBkP%Pk|1t75 zwr#7tbC$o}-~wK44P;go9IwWWEY$2guR68G1+MR#Jc^3@OsxDwa5fpVCv*qU8l7^4 zV0F2V=avaMWX^XpXL5LFX@o;r%-4JI(lKrQNy_K=tYLo;~?YU3wELz`prS7 zQmbY76W(2?JDIago*-+hV(_GrJx5mrAfOBiL9paAnIjqOi)?t5QPGm+O;2`LZV)gP z;9ErE+mJwX1ddxk;%qydeSW(QBbx5vyh|`8yE8HR{0+Ywr{hw%JKy&JM(h?tBuMgOdt6GFk%7Gk}42d7WQ8m#N?Gy5@Bk# zVTOYulavUFJwrM4RxgDqR_Ux%DL%6xB3&k*WYTQQsaPs=auTMR8|Wm-OHGVn1~VWU zW+pMjk9ggR11${Cy7a;Kcj}P2y36@j{CxS&@AL2R<;b2*UgPq38Q6`8F+(mPTFJ8Y zL@=*=6gYDh$YKU|QBUY{5+xq_tvP6^7ckXm2?Z;P0h**i3A(g~{gw8qC3d7wAOKJ( z>86ab8Nju|5-Mi~JzO2L*jJWUuM3EwW#D{!u>q!g2E+8;>&ekSNwM%Xf}&Iv`zg+J zy?2XTHJ38svBk*b7|&Z*#-Dfu00M#SVz6+?U>PF}p&YO}j-OjWDdFVd>Mo{oF{Wms zcLzSd`;E;*A7V#ypKj_9(})I+ z@%@<=d^_}l_r55CuUN3e64=x{APF6*U&LhzDqI-Y7w4RLjSfKy8zpg(o;7793zUMq zxi5_$CnW>;o6#;eS5zkQPu8uIDzXRRr`%C$Q9~pErLBv)dT>W)O%=mUG}W5un@m^r z*`@(uKBo_<0!S`_0ubdPtE4_zWD5xgV6TBdBjPauCsEGj*o&;_`k0uSeT1fNo+$*7 z$OsA{%9K?wS6kGQiD{weB%V?(!>k+E|8sG`a|YDb;vuOQUtwEb4xe#%Lr;NF;XL_s zGhTDs$5wMBAo1`~$b2{s~_%Ct>jyfY(lDMY;Ce-SW z;d6+t(`@MKDpbBWLOV^G6m+l@fI$I@(5i-k^^P3csFD}o_Ga;da4Wu08jwSOJF%!` zKY_$|U6gk=ZuL=q9xJyDO2$Q2Df}LpF zq-@lEOBfJA5RjUg5y4dw*cu4GEr)jdJcti8=P*eSf+Zx9+DBmEjmt0_L;#TD)uTxk2^CP&IWS=2(Xl-3$N> z0Mu{ibS(ubL^k8Vq7J9ih^49U8{5fW(E9IsyJ#U@(iOZfYzHdYtGlb4R1=0t$H{aN zFVzMKVVawHmATrs<1yh6tzbp0Nle0>9Fr3{p>^BjW!nnPoryK=U)Dmf0Dys|BfzM7 zjDsKv?+_FodVYw=oDWV9CG7eYlLDgQ?d`7a1czOo7k!R(QmOJwZp=##Id&AdmBAvNsoCnW~2>U zvjaD_|8JBZHQ_P5fkTC`*jU@o8v|3U!teK8F97ZhSP3FPKqO6A6c_BZ@M?h{s_QuB zvB<@%$oN#R{C3P%<|}B7N_SRd4Msw$q*)Y+7F}L z=4r#u+c`uc!!ZfG7N>EG6#VCJthwB^-HIP}hRe*t9;;`0!~AAp!B5w!o};OwH;jl9 z5TPwqDdqqlThDw2isxNbEG=yj6Q8}&@ao{TIE7a}SXa8!iUJ_l-nIz_Xhx&M3r9^* zwBg7c2JG>R5gmb%{-ru*m=g|cm=ZsCn{Uat=mjlAAels%u3YOU69v=_DGirS2&G=2J0#;8y3=R8%ix+;}kIyR0eX)`y3JR znQO=B4eJ-hZ8>G9*MhOC<(l+T_J5VJVwkmKfJ^^P@K>Ec*O zKzD!vwMQ#-6`6Or-B=%@TWQJHgA(RuR<(*(?m!!#06{au6{QOxpNWixl^Vj(9aqAM zwVsd^)|{5Xp8P=L<6tlEqpoQ+gW-Twn!bTVRO(+&k#i)tE-DOzqL!ou=)K#;bwTd?N zd}_}r)Delgbv}GDLrrH+`vqP=NaR}a_d6*)_RB&*Kr;FL1BRFqxmz69#SB8DhTu4c zQ!n7(B^9M~WlQiLtuGcSd|jakPk5u+TQ@6i_0B#gPR1q&1ZDU zO0l*|YD{Bg8DZK>?#fWe-ifW5(}!G#IGZIr>#djCpSW<$rMH_=+%yFvYD2G`Ah`^?>;ZBoPKg2ZgvW#8d_f8xmL2>OhWW7_i7hZ;6sy{Fx7+*m8d6(W3cS| z5@g8jA2T>e1Hl49?*Ew6BgHqpb~(!(RDK@2Blbyqb`d+g?}UjrqZB@2}j6@#2?`I(o`BC2-GSgINa zl%jDd#a05Ng2twbXt_sV<0sP$K}O(BKSNDj6(>oNnRpKo1u@jp&@~OUnXh0+RSV5K zVDer%8Y?ciWu2yvagCGsb(M`%B z6obL3nHj+%8>y->z=BpMQz%CzVnTF+LQ;cBbYga3u8I{Ok6YGIf$M)Wl~!&Npwgzj(1V&Yr^bQSf_2+rMxo zMUBzzG_8aVzvF$Y!D{Q zq2An*Ecq(>_k)CN3Ecdy&aXYoVrAD5=`LJOPV~u3sYZGpS%InB)2&f?r}k_4ox?x0={(Bn+d!^ZBYzfOl`00^k!aGj^e5|99n?E*PECL%1% z+g*!p_nMoWY)Jg9fqOoh7l~XLO&WLMKI_;y8)oWdCR?r=b0rLdMnBbgKd<(b;irqR8oe)ZBuJJK z`3X*YpxD(Qev$1DIY{R199cd`QmO;{yw4p;RI;h! zZ_IwL!yPkfR9ym0S3cWBSpn^dZkVzmAMIw+X0FVJ9l1QWx2_(H~P85E27_2nkAjUw* zAvNA6)4$r*ZAB>6QSYFxfBO-9_zcMc2p;+e4FL}UbQ6(30TBh+%L||)ga{y!~ETuG2o z7|n##VzGd57~^)-x9KJy-dUXBWTFLcFO)au7d!2Gz4#m09v^@Eu7B2r8(5qMX$YXASly8E+H$Rx4^+lR05lV(S*yFiB*l8L@H;O+o zn@l;l+?pbxk(pkfq|W@ESC`q8x(#g~r|=t&IC$R1hQ~vjiKC)0`PoOOvIqPbSxi|l zs4|e~>a4pDYHsZnJF>aMW~)QA){RJ|Q_zY|A@7cG^E;a-Wh<=^*ol_WvFv|8&h=%u zNV<3Vw04>5e8BVS zo(K}J=+S&My{c|5`?uDg7@14=O%4n{Z^nl`pk(^p74e;*G*=L-rXY8pbqz~T>a*Cv z$~}@HoJlM}N-nw6LX_Cxf{=b*20!aw5-4+q59itKtcrPj0Rb8;`A7pJ0EY+7{232+ z{1`|c5Z|yO5nQNGTj2Y~iWo6*^(ORk=4rKTY-1i1<~7|nebiz(q{H&6CyECH z)9{}#-l5l`-5DT+E5U$55K{~w01eVe6oN?KzjOBeo#)2GxZD`InjSXD zSs*%>I}8fzc`0N_nB@3}a>!HG9`k(xPJz3XVZDSG&gzsD_`oLg&YxXA@1uh*J_{?m zp%@SS$nnmtX`ZL^Peow-E8$Z)GMi+bmQ?c z024F#SRg?Cu45q8Y<)`=qjny+d%J8bAOH`z&s5`pRo4_CmC|zI5mIG>8Gg+xCbM?5 zi+a<*=K(w}-9VDfmqN7ZI2Ht+3L9?#^EyuH7_YVTb65LWESyQr{IAt4K>VF{E4&Y> zB%82X?EchGx7@qc^nnE+pBCJ2?rMAYN)>As^nvc^Vh6m|mvy6TuTwI(?(M(mU4?BU zx%i{U5QG7sF<4C5!lV-^?~*U+iL|4)xSVj~_aU1&%2ub0TlJilMYJ?2I3>Q;4L_kP zXyuILTsI)%?tAJGsSO;AP$>qV+|I51{xF+13TX=JXYf-$G~P=Y1^l*Fc#?oPEfb5! z0tf*h0BUq!OH0|%ON~jR~ZZZwb_pRi+$B1f* z87jNr8A(PIK}=`BfGHz`%VeZr|8oO}jsiYJ{-Ud28_mFVlvU5{HxrcWJs)+Z?uxI= z>*L2Vu2j^*NJq?8&5HA zx31~W!nNpPewX~KSg2at4cZKKmcEaUf78nm!O7BRMoc9{;s6x0HNNMlr&*@NXvHrq z%epLaJRAt*>&KM_w#T_BnC6OsF+wA9IJd?;qLN{LXxitViwCI~;kq!@MX8sF{ z+H-^TN!XU^NYwhX>?1}(7flE}j1J@&4ehZsg^J9d(bT1UcYzRqPeq)ZN=t312u0v< zAaWDozevojRcBs*?E{sM7oX2s!;ATs!D~7Aa9w?V?sVR6^#4iMwHMm`MXyruxeqT- zrm1lh>#G6aFdxt2Cr<^>G@;p6;D_!(&(%`vhKw^lBOpJ^yL{n8&2y((FCn64{=88&3 zeB{s+34&SPL?<(uLc=IPW{aHlc;DMOT)9*b3t2JEO-T_5r4^zX8kWWGBUVb1GNKw1 zA*9g|RAy2`BL+lxiCVqQ2&|Q?BqOSbL_{4}sKTW&atxAah({AJieSUCMR1W7AVLTb z#T8PbIhX`zGHRibKrXj8u95c|p}eZwPt%#%>QnCjX>aq9^9YI(78n$~Am^9Ilr#pTnXeh3Gr_u~jE zJ2CgS>N`xWt)_bmki3tvK@rwglH8?qW8U_Z7wJ$?z)DBP`x?E#e|^h-lcp`gQI?nR zGNO=<^!TvpDSa;d46J%< zgC`7rEvDyD&Ry~nOWJ@1c=CT%-{w2=&z+OGZ~K{=|C)WhpK_0}NpjD)NQ9z(`r4@o z6@}Ks8+7*`6AdGQt2(}|;z)XUigljKL>*$zEYuc$3;Ag552ENOaaWg%SJ8HVKSzaL z8m+wA*7J(2`ID_MOK#+_5F7r^(qLl!;^%vOzAQ}(;^rRcZoU4$IPtIg^Irn~l2?m= zgv*<+&yIU8|4`+p2phz(-=zgFsB%KY$muTE%A7V6f$k6^UuYlAK?C65k0PXu4yW63 zyN$n*rabmqy?aGD;Et59}W%+d9^)kHL6vZdE6nqKR94U$9HEE;c8ZiM6T9K5kNB zzvJ6&I^_5T!J(&E#)|QZO zLL%+IDzFJV>ArS%8mBK#uU}osc}E_Lpg#vsA{WD+xT+j}$NS8hrRKI&s`%u~9>Ny( zzfQKaLcK8I!g9{>up!L6{9`8ex#Kf`?G;D2)8TisFYU623C5DX-QTfK*Km=S^i`Js znR;3{Q94GfSn@w#4%E^WRsbJQ`|a!g(9n0yJ};K!1eTSb%J1gV&*Oleu$gPu`XEfs zOz?T1Tb2Kd?7JWYph`z|d>a*Iy2;&Ory4YRft?7BA_PVtIR0_RlzQo(qyH#>6-7hZ zdkNCXukOFb_=V@=9S97Mc2xOU&A5CL*V3uN07G3k_N_1)+)ckQotW&L+if>$CDd4O zDqZoSJP8mK@;0^&4=MFSD*<nb$y=pVP!ge7xa9?o>`eKLse2XDWsn*3sS!qBNoS9H)h?iIKUszZ_ znqRa)((wNz@Kx|Ro!A3?rz6VEXKLytX>N6Fzh4r?HY_`#Y2a9ckh0KyvYL@OPur`8 zj-~0_S}|?e+rP^iqMkFMof33?=*mp%Jju8&s+=KR;ceahoF%sYe9U&n`uN)uPQ~p2G3(iO5ip5nbr2DKZpfk& zb#A}@(n-t8=2-d=aExF?EGr!39T*z>It`8~HC$Mtyvc*5vv|&%#56-E`)gNB&m-Xb zou|SzkvJxsjVJ!?bS!NC|C|8}g8PpRE&0pMig>Pkiy@lNjjczuw_wkUiw#+Dh*(CD zVHiv~<#Z1BwY{tFkZETB!?92H{gJ=StvW_puQs~wR;=tkr{6pGn+HRg74`67bXs_g zHJkiNXrF(*xEBNpk)H3L4`X66FI;ClBPz8VSnCw-i!q&2M+n@cM?qNHf;Xg+^l)R} zcdNI}m4*6)OFVkbTe-ZPob)Uhgp*%hXAM$av*&n2H*a`f_1$*Q7rg&0lnYnLcxH;5 zKL+6_NY)z(7YK;RiKbGrqa*mqFPDqKWKPKQtv*a{dyp61+H@FN_+CCvuZ7at>b}qY zS7>DPe`!Pdfk&3L2R3_G6-xkt3lQJBCCphgVp1+NG`_q)HyPPEw<8DwC^!c&&H5BnL<*t<=kU95c_gp2F?2FEEwHvNx`EA?2=5(`lfZzZh>=YSC596N(2$NUFGkvRQ_G(5#`n;a4 zkrh`v{xPSmbEZ#2-PLdL<%aBN0i~izV71pvojUSWc+omPuKYhY=2-~5aCUjap#11k zLPQsW_FaUV9Cmh*FUkc~>`o#C3b{4DDEc{HujQ9ijUFCQWYQ;B0(h=bgN zW#hh1aG&I|usYQ|?qiLsVkCT;+NjLkz8$?&TWys}f6B<0a?KZJZj~;PUj2f?n?A`) zPuWh@GvVniBeP`f?FY;@860<@-7(2c6ashyU6Vgvd(>syqgEbS-6^XURZrbf%8vN% zB_LG1R583ETIEX>?wrJ7r>p4BSo)DNGP2AcKRNpoI(yw#X`}6*DzG`2ItT!VDUKr3 zmIDkH^b0-O@2nRQo4R4=JJfR6u5;x2^FO5z)TtF?$t5B*luYR%YJ&A{@RA_jg}jB~ zuFTiWr1fobxNg;ncWw80+ERyfT9j?``iDr#|K-d@CN3{nT4Ml~2C*1X6Jv`gWAj%Z zR@`(w+9TYd`&PM7{7wD8Ma~h>c5#Lupv%?P{q6(f-ltWtTp8jJ5qUwz7!I4<3<_M^ zAC~XJ{!uTb%>GzHE<{XbwE6c1vU(-^8=p4U z6(2a!@xCZAsT73%9db>08fd<&6Eey)XiTH|ESJR8y*bNO@KnnVtRj3YYf39HLH zOw`4C+-VX&$ML{HcWLKl^WI>dzE)cZO#&C6`)@s3{@(5Azld}og*~UMgW+#hW%2`a>uQ10vQ-41NCUKF8-#B$U~bRmekRJKbM9Jp0RK9zF1w#d zWOX_5=$NQG^8ct800ai{Mu4vuf_R4)yNuA`iN0@L1i*f8 zj#ARz)rbKc46O{I?NS!`-e|)L|6E)WOsBJJ4sm%VVg)Il(mi7pW>bMTF1E4&*uN!f zjgpI^c5@CPg~S%_S4TFG_n}~lyyV=|%kM=1A6U_JNu!yMo|I=X?+p@teeK8rhD5t+ z-S+nhqbVu}swzMSVRLp}GV4<;WoF1h5Psfq(hxVpmm;q(0_z(2J=hI5Npv7c>SAM4 zv)iI}J9xXz0^kFmCQ>Hhz4#09`lJvG#2>YsK)V=98%CYkqM{hkbNCz`8wT5>gn)hg z9-k|FPn{?hfk`D+*fqdnN~7lG%^QdW0hvrN2iI*)55|bLKs#b^;uH)$c0SW_3~o0y za7Busso^P-3D-l060hZp=&rWLg8tQErMLCBv zCf1PLcUyYwZ+;y=iB}G@Q;^aRgX&zHaQ6Y7FR>Vo-kZJ}okH=Z&>TQ#XQBT_XYF|T zvqmej)r6*B!S7DhY%C-lmH;7{>dx{gj?Xu&!Ztu|efnmq^ zDXZr!x;o^Zj8AWuv+30}XE4Jqrsy_M?avqG4!e&(EI#zQzSF_^K^OL>hUYs_zAx`b- z+4B0ZB3)^&Ezt-FazVjPu&SKcROR-K9G=ux>ta1x7GXAs1%9t-)!HjC=;FFV+OKu!r%u`^Hz*ZzQL{hBVF>qV4XJFN>4N~#Y5ZJZim5Y9??09UN z`D%0+Um^oB-gzRbmK3VFg*$~YIty+sXhB#W1fl~|mB2!RsX!&9GNhc~=#C{N6Piey z>*44+z#Z@A4u_-c?rK+=nNSY)fm@mrBn)`xf`Gx)>p8v-`{0ia#l}Mu`25}8A489MTU`(Ybe;g95{Ue#;e=ZOs??$0G)SX_;XYPp z3;12V+iT+q{WN4>3=(4bT~r;(!yI>^M_LSON+}*lsiVKOHZ#4zkA)Kj0FXe(dFDwI z%81(#m{#`xFG%b%ksNtZE3h~n8TG0bKaCx0d6Bw_p7UY4stV>z2ugLxMlf^|`Oq#$ zvaTi<_t-s7amd7%-c0ato;K9*|CKX3f$lTccPhd=Z?bU;1xejwUe-r8HWuY^aS;Wh z*E^AW3cF3o|$R|rHI9L~P z^?KRVbxKC6sL?OR^cZpQYxBw!|KZq z#wR|C>u_v|1#hG=;v;4<*AzUAZ8n7>Z#OF2Uym%5hu@4~U;00ho>>l%)j^ zLT+ltjT(Kr1v44w`I|I_JtZ~tQ>j*u3HkS|ni2)OMCS^Y<0oO>pwzHNSSu#?r?0W# zQJVz9i4Kf(Qv%=ph|)9YTb(3?46-fy$;twXEhl>69}E^Q##diCr{SfdXD1gE$-7T4!2Vn=|(uN>L&lOl9 zB`C93w<*oC&mOr1_V+cqvg$M;K%CzpxPlIsN@^_15kpL6vh$VbgWAO50_MYCGsfcR54ws6r6W9-oJK(%|LPn15XoR!o3_^i+Q!+ z6SBXiV}N4$G77Q;@yuU`W0_$4RC1`+N~#7h5h6qLMKLx@tFxP3b_NEiZ574NH*Jdz z-7p1m28uQwRa<56!{!vw5-VK-x@Hj}Q6i@V7Ed$T@~|^+AT>0alg&~(DycHkLdB5Y zAmkRcfPwagUA+dqzGiuu3#k)iBIX{3!Lqi*3WGMJBq34e6hmZl*QZV!q$!o{40+>X z>Ej5jLMqE8woKLcxftb8 zsA^QSy3hSAgsPb7!>fnt?6SJcqUH*&Zm%vOxep5Y9AK#BrCE$-K4dExdotR9u9YjZ z8sLCrmojv&il+14POy^i89@Y4WJMEzX;eF#?B_e6#=Dqii4E%8*kC^620M_I>Jbec zjy8@!#DUk#Qmf^G@Yfs*Z8C!B-bjG1ODhca)tsxC6d)43o9Q|}Wn^;>Y?}YHT z1isAm(6ui*!T@4a+|Q_A!s-1lv*r)V2QYiGZxmwv!aSuSn*Ucz=+Ju%!bQ4^1weZo zD=L&&&VF}gav1<8LdbzRlk*?+gKV$`;GuD~;w`3=Rix;R2D;Q*h8hH6&^?-DhzCkR z$~LSJr6?d33~8MJ*M3W|VjyNjNxnm_U zA)Sex#qX(+%jA*ms~{@_R(~tb=y~233Yar%!v%47 zq~Vw$xI(Agb#sl zA+ZZ6F-VUY5R?(Evhe{CSS%`#R6&3#URaQ82tym)tgy&jyTG(oC`Epeu^!_O7v*~X zw*kH03w^d5`mjt!DwujQ5I`QOMb&a#qNTh%^|VaTJpsioE_APBspD*W!$I$(q>U#0 zzrKqH05(FnMSUhG3rG7fT+?8TfpVIJu9Yq+hum3L{KFy}9W*Zr{^CD1`wlgDA6|_;FNW^gFIiye+91{SwjfC&) zZa72&2HCt=J^=DI_%6M@nT|&-!WSuhqI2B>^jnVMxc+~0aCMitb{=7XOgQcm=;Sz0 zMz10;NWg+2m=~Jb$>}bJlxKoLI^@F_Iy*g3h%1URJ-Svq*x^0#uxqxsMg~$KLdzr$ z#%sAV+5~{_r!|Am(VWkuREX|FkUbdB49X3HEaw_A-^*w7vmKk>@OVBx3a=2ml%!^0 zDkiANHP0@N1^(blkR4?#VM;RVSlgflQdVrpvK#c}1nBPSv~V7f{_z7IFFqN%0ReqE zK^{xOglYE-3{swPxz9rBuZsYMTlYp+yaP~=r@X&}TU$F>#MGc)LwzwX*T03-OC_;? z(`6x_nKudvGmQ3m=^-qsW@jSUf=Z+xgkfD6-52f+^@^#_BY9W9>x2{}-;MpLbC$kd`H1OvbUkcQs!k^#mB-V$@7 zNx@NVBGCMWk|bIg@c@`9k{6aDkMQHQ;-978q^+&X-S0V0pmIEZ4*PwKTx@U+7%mI_ zse!)SOA4$MQMf7)8@Ux7aSyv=MT_2Zio<>4O?&TPKtI1cN}}VJUV%KyAfO+1F)_D877y_oVyaH*@3{G#DVKFGYbZK z_E&pVj9K)Fdx6C1!tmnpWyL0P;brC!%rghb97{F#c!r-A`~7TtE{o2>k(}LkfwzJ5 znP%bM-t_kK6nv_sDZ_*7T=O(PiaEVJpH0C^L1w$0#t-7G&C`yg4uYffM&PidM@nr} z`SW&FT@};Xei%p}et!G?eN$vVFYxzX!HUbX7Nyok(`<83L!=4S&smL2p#Uckx|e;P zS`ZdMIkWQt2QZr!FqgKr`8f~pw5XcWJLW5j zEU6f(-@}woj&9!DBq&+%r@fpukovguxmiO2jAL*#?h*3W(cZF!MSKu600GqoV8N9H ze3LR<)mM1=V-HpzZNJBQEv_A02AMLo*Z=_d1Ltlx6eoApN)l$@9=QE4UpxjyIfp6zYtrzTXMWIvHVsq0o&&%4R%cj{cPv*L4)?+b5Q69xOx zuQBS`xA}^bw-Fun-S}u?p(0|i*}BlfuEEcv(&*t z@V#QLN41&u!f#IV_KbSEwBpw7<};_DUZ&DGLBa0(gm+rcI~H!aHTC?`2wixdBmO|O z5=(qu6%7bzdVv(A5)7K}$=N0tkFE%tBskF-YL1K_r3EU4cu|b;ZlQ!62@4hH3-zWHtt=23Ht2 z{b2$hxVE0z*$RRCon|{tkX69h(pKV?b#snTa3GL467NcSxqxfM)uN9>(jjvCTmE)8 z{NbsbO(&Fi+sKn#wo9ZF;R5e{tN`f=|9M3hCC!V!T5IuzeA0UkJ8KU)`=U=zKv%BilU^l`hp>^n~4x0 z8|W4xnf3WT7G;>*aL6DK(0zPB_#Q$R+W^q7oi)ZTK@rS|d|JCjpvKf=vOp$Aom}!g zZ4GXRffMSfT}D%ShY?trkr>R45pVwdyDsO;@w-vT#Ln>ZsQ*{(-0uV^bliWv=0&$H zaZ?vOVgqcj)wjm(x-i^PN}5XAh}G$9^EFmUS9lxgx!{k|zn_=x`@nZp2A;*tV;47; z4dbZY@C)%fedzHm zd%B%3>;cRhhhVO2n1_d)t;yvCC|Y(>JXq#a@8)*>RV-+1kat8g7KX`_ewx z$l_(e%|!~Z%@NQZzI(M>2RfFhx){v8BCMk{we3-{%&sIKFBRXDFtrdqfs)R|b9t=) z8lKu7pM3H@C~jn;V)9c1-~PRa*y9bfKymht9XgpZ=ndnz09)6Qz9d@q>eosP3sMY( z7~`M=&6o~{M4rxwU3%gO>mggPnwoCO4pHG+U|XPEuP`wV34ZE&mcz(Mkwyd17dDvy z$?!tI_(G8F4II3)Jb(zie-~L?sDviy8b*y0q^m?Ql)+4Raey;oWFZLa2`F&~fK?AI z@GMb17=w!v?c>nMw{T(F2sk*q??17{)>6P9wpV;1$q@DPV?p5K1G?Q?V(z5M8Jy5X zBb>W~NQB`WKJ+jF{7tb5=5L$?2j@l2Nq3i7nal_1XD+st#cc(y=WrCymbKp{WCTl@ycBj-r!gt1@r4)amqB;3%x67JJ&N^;MO8vd~sqGOH~k$X#{Tre!kBrkZJn+Ltzj+RLuGiYl#DmRVb6mVC0x$yHUR zuDtrSSz()gEZQ}D+7sx~sM~3vn{Br)T)C4TTUDdIDpsp+UcDcabtw7FH#Tgk zOf=NFZ8FnMGQ%<0Wovd>X5yT>ROiyra(h2>XZpRrXP>~@`u4Ct`sM0R9&YWPXaAb= z+d&WeTH^O#5=Whp=_HZ~7gr~%EUR2=ja=Uooh-P|%Lj{14Bjm1)2420=EqVx%G2=| zqYfM?UrU$FK*fLXtpQ{elf+i}NIqK7)Pkf6c3?kMSu$iue(dSfpTnktY!E^JwK6*F z5v_5mH#N7CI_#23BoIdX>vEmPBl3MYWtG=_SzdZhT(Zj)Har_pk~y!xMftEzn>K2d zBW>8pmXd4E!45L~-~cl?I3RMhPwkUwla-CDhkz^jHSZ9Yt{+W8cUcYC-K)$*^<}?>R+>v6TKo$rOzV7PN+>es=y#iMher-Q{Bn^rl4V`3R$@Tl5 zu3uODTgn!<^nu#0A|{tY_a0vBQyNRy(LRM$JfYP&?Rl1!Fel5)FPw5(T!c9l8~c^Pv^lFxe9XYC;L$Q7-AV-+qi`Uav+~(=o(xG z_&zQy*`#m0HM6nHocO4?gigblCMrr#VFDC8Pm8j!ysOdZY2w?>o1SPDKw}GdCf`S< zqxjzmbJT85VjuR*1y`;g65g=)M`B*T^A*og6rBP;bS%I77rvYXNsJ&5QmBA*e@Raa zfC_N@`=yjbh^U4rjI{i+TnOo@3FI1wnnlOhJ9r(O70f$nSi1S~v%^WG(LpkD(9{8| ze@z6rOqaP?S`WeUfgsobjRU_xN9JZ{n>`P*6RQ1a`C>Qtd4I6sH}^R85);#SAH$Z)0cc)VJh5K ze}GQ|(?CWv%_WFj5vlu~WzU`SY0+=6S6H_nk%g`Y>Hw`ySBTc01%F4!kUvFbrclx{ zz=I^M76fOgNZqiucx$~E`FYPx=0ABOVnde*k>Y>Zlm((NglG+?L;%n^tBH!*zi@POf< z2NeO__8a8rmft$m;F?|I#(hcGSAZQe61 zzfRpaz*0gL2KSM`ewXVf zN2RJSR#|p^&%yszDj;I~DY+bX$??;NrU1ZTFdTYjP9SE^6^p^f=K!Q2xR&(ZN;*hG z-kOdxtDUq04A$IZC!!cHH;+)@_zCf&ykfZ2a=!_1d%LoR%M`sB(BjLCyg2tWJPDkl z9%BQD9tJovumtQgJBsz?)G&DP?E9=g!@w1M{BC|)uZc~rc(n$~V5~CxLsvUoJc5IRxFP_q%p=g3V3p9z8>bgi zU;d9^*O2(b=^dUAVf8gm&x{}?lW=yl6vda!rTg;02L&=F6BHPc)Hn0*=Dw0x$lVp5k>nbwg2f-MD z9PY{ttfE-OC5jvpo<5zooB$v&3#lR$+y&j_fozHTPcit}#YXf-F^AfCP39}s2^;jE z0E+aDeW*3F`S8G(6)5PCyN9tUD>000j) z6L0D<`eerEzl?iLtJKSbYgUuWoB=BeyVRGp@W3@(&`#7{L6Bi_f{+SKsP!9-$3Uyr=Ffz?>2mzCJlUu96m@ea%#fqkYRBSut%&;S zU&nnh3zCe5eh*6g5HJ9ELBTfiq>9o10zi)lITx>;fQru#^f`ZPyzPK;8A*`jA;@uS zwqXhzlptL)K*zj$_n#4v(kScMJjDedA6b>R;5-^X48y=!Z6X5#Y^hs(TwVVa&THzQ uM5qI9g7t38A3lul&NbBm>g2t>P<1xy^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^hlg8_?|Xz-DUj&xB~fQ1G41o(JS00;nB zEcw3&4}HZo0~=sI;vlRU%G=>QeeD&|;|s-GAFa0|6F`{Bod* zi=f)cR1vED!bHgulwj-tHd|pUoB&P?)kz3^uwTNaUM)cd_>MwDuM(zLergV-J!{y* zfF53#v;E%u)T}}ay{3HgR7t*7{=__|S(k`qi&cZKr@GzJwgY7ZGSe7e4NJ%`Vq4^fzVQ71d{5o>JEaVO3JPuv4%UX(sx;?GVQ{ikTU3XI3g zf(YI~beXIDdj>{{8_LLTqT@%7>nIK;6M;lvIYbf*0XI0y!2n1kamc}cnJ_@xXJ@FT z?_9F#hV@&~fW#0H;uALr3 z8Z)wSa*3ENz_uKd@N<9u@?Q8r-!Z-1G}6YNBsK}v{YY)h00`)Z%ucPw|}vxxUv6XQFV&%XNc`|lZ_E)Us^tg$nIx^6~P z3JMC4r4)1samkraDJ?CXE&3wX&n@3!Ro%6`I?CdALiLh1g#M{_qi8y`Pp#!Qy=sV>`UT1Z%-NqUHpp!LCId zSkbN>=71oPFVWYTY5OF&%g%K!$$Xg7&?-zQ%iOAmqFIywlq+dZocn7dg`iNmvm&9= zqD@u51c*+E(&n@tEqPEM=jZ$Vn!yF+f2zja;hZM4)Nt(dR|o!79>z8U;84K#B%44~ zBb~*c3c8Po&>Q~Y7PZG+g6QdPdlcYC@GCEkDDV+%ivj;`Q^s}-56Vt zA+ok)8=+EHwu-}M5@WM8#bTXVM%KoaT%BE)79)Bxwri#qGYd3uT$KC2B4^L6Mt^$+ z@L@(8GjU;YdG?Ffm>aRQYN_0|=N(H4UX~gVZVP!@o0t3F^91V|JAOIB*(o+s)-V{d zRmp@;&vpWjCtD%}`_=R=>5Z#Y+6{R`MWGaozkldV%9D78-aaW3y{e!gy(rm54^|}c z_PX~E^}2?na7>#}Wg*JC*gdW=21|T+1xnjDwrNdzhPZV~YfBA&PDLa#BFvWsz@X(W zF3#`vOTarsA`8K@nylcg#tMNPe#_1CI}7j^gm|^!z#Js3G2&1*1^!yxUEwrZ+Vqss6nc$QGtFW zD`A6}L+pAg62)}KMy+CTks}u^q&z~nW%VAxt^AbIme&*Su_nd4$xIKlx_;@jlG1Uh z5f4tC{9y97+8$h(u(@VZ*7y7G(Uewpi%T3$2WUIy%rcc26V7r14GyQRj9rlwdPwZ{ zlE`~?GuL%4q$w+o-uA1(hI*f>#d9~Ooo*A^sDPgeAD`-=8eg=c+hSXxW#ao9+0FI zbFQO-ltZ+aNm#tFEn&HEHDu?qI+*hh2b96& zn$Cr9cJm)^K?{L|x2763`{WG_m+>_z@yE;UGU~aXr&TD+HRZNvWa(*}Gd)=Ak9oyI6UjE)M3oTebGX*&t z*CwTh@k?sMx(x+XXyo>E2KJCVjYo++@dxU@^th|Wc&sNB+C4gP$3~P?hjb2f?y%K7 ziP$x*pse|7U9TAohB)F0a1A5||DI)?tVFiwu2M>CU98G?1?Qlfe2o<;*oq$$PhA=c zn$xNt%O8tQ$GitrXR%ho|+#Zf&TVP_WIM(R$7wi@fqk*UpRZAID@+%y95f zi#CYg@T;ux=XKQ()~)TEO7+>9d%{~yrf~+YH!{Sln)2QhI(D-a#bU;v4VI@|TG(`e zc`}47$bcP_w}I@}M6KDg+~S%u_2(7K4Z)!zQ|>=MIom2>pVVCqY+O35v`wjEt(GIC#XKlG`^g3vy5XtSVv&dkEW>dAo>@lVi zNe`|ab_sBK^bBq}6oi=Ni%zH6JQqjOR7Wb6MyL-$eQ(4DAAIHi6h1@1FdZc#SJz%i zB2UwDKc%Npu;CyJWu;{OXAtlg;&pj{-cLr2X2F*R_5Jy>=;WoCvZ=mkF9P?jQJv8d z%RZ@FYPB#~P3TkOk^a!G9Br65Ht(m{#X3%#Z$EUz^mk@P9XW1E8`40H^>0!NhJgUSI_yOlt=T z@x>hwSwS;=;#{%vx)|&U>0F(eE$@WiXZ*o@q=YmlHJYaVDP5pvVb+luj)}0#Zn>a+ zEBVIN=F#UPg%?|45~We0+8f%DI3;##()@AdiH2*Jo1h-Y5^BZQyQ#dSBadV!Ph7H? z`gy;+r)M?oEk)<8{ssj)cD7YbPa9EdeOUIIx%}PYJzmbW#=_1i0{+2N^DpFg`>B@r zqlMNAn`TR&a9}P$j%7!k%ee`MjU)$B8VQ~@9MtsVakF#B)9I8jaO`-!KV{I$k*^LBqd@CzE{5~hqcK6(@tyjZ`nfB39E z+yDg3PyrUe5h{Qioy1$?hm~U$*m= z*7g;KbM=`=`!veIVg*}VN&^xXDXfTGneHFMtCZsXZ~$dI3WOS|SU~xXhLR?D=(Z2R zAbfGl`!8ubLAd$uzT(86{XKSojUqss!U{GEDV5vYij<2T)x04{P!YcWK#V&!(^u_l zRr~?9OrT5S>Wi5>?$GB>fiNE;03Z$kq5@2zp@NrbQfY=wfsuS zeH+%szi?{ONOzncFZ0ya*Y5b)$X9(+Q{{57DOiqjm>5X;u!t(283irMtY;i#y99yg zaj^?q=dA+r?iWvl?*);59k40jkQ6JqFk0*Qb9-0nP^b&q^6t4qX|{$@sE-&VSV=i{ zSureZ6%_Wxh{qDOUI@SAHn158Ek_GWNmYq1St6%QL^5rYB4@g-ev&L(6}|nbut$ZZ zCXNZTk66reUH?1Q7y|7`gf@W`?V>n?1T9K)Az+hbiba?uYv1$nRh4lm9~!i>Ag(D{ z#uuDSDESOiVO2fT<(+j-K!Bt=%&9nIpf+!{&<$;49wI>s%!`3p)~2Ne%OLpb%V!~q zXS*_}ofGFgFSX1p^+-jFPX634+|-Kj^qo}`Pa(odhN7hu-GOX}?{bCD`Xph?W{BIh`w%yjQBb$a*RBL|BL?_40cWP_DQT=% zM3-4>Y&ogI+mulc>W(O(cRuL9`P@i*qghdgS&NXaA&aNu3A-nYWrR$WV9`Z|Lw`@v zBho}W2%=m5+}qkcXB2IZjBh{1#8y%xVjZSjj!7GgH#G1Vir}J|0wT_=qy-_{tw@L> z3jV}kQZ|?B*s4>uZ%4_XZ@za+|Ma@j7GIg;W4H-WeE*%|{%>{P46dTY0O9xtAY0tA?hIUzcM z2(8&mQ`9vtyKbV_ZD-H(OW;O0CLq0n6&MhsLnUl3p@lf$N*PXL za|L=$zmHBeXl*-C&30<;vy^Yf+%Zt6cjBxrk&uI(u+UX!z zbN^OmFf0>GEqNG+%<=t)^NanU2c8Z$Oos-h-dH&jg+WmJjt=V~35X>!yiQ!(y~(rj z_-%3wWQA(A$44eJtxZiM%Eg{x>zT8(t$pD|9N>1H>NA*S^mMFqv~%=RTwjtlnP|?r z;-uPX-(IM^cEKvlj})ib^J^w8a~6vgLn6B+Qh;lWZo)YBjC(qX)$*UI*`8HJ+vJho zks19n>|Rz!28s4-+&NL0HPW}M?Yk2{aUD<>uxq;G@A?^CXppm&CQ)CCB@+o zO&2I!4q$^NY(vF?6hJ_DFccyft%NArWm#^>!Nx^GA~@tkjuArHhSU+h0pO^}bwmqw z2}j!lC$oza5j%PLM=hJ#(e9R_sZDykb0u(}4=wL_XSaVlw2)plm&4cJQF3}Ir>&Q( zk%qf!H*1VNungF~d9gs)I6P^^r)6Z5= zNvY{Z$2_n|hsC*MiPF5+@aYs7f1Jql8Qf0uyTCu{B+zknblk>{Df5-fMi2sDTzNun zlnJx6!iug#f6LK**6gY}(zYCW_KnvBy3s=1UL> z2f?n0|F`drpKK?wF2xj#>I?AyKQ`e|jv)jj=lnmY$^T6q==R#*@o&(`%VSvhO)_f2 z0aW1_82+n85I`PVzo3>Y0Pw+3n&p@sEfqzqPhCp9^%|nqp3mLu)s9%R9T>&9M1Uf| ze;NQlJs6qz-b4#Dd;%a_ec8pJmv)MBQ-}-j-@fhc>?`o|TCpJA_t)+c$?EZ2UcHw; zC-?3<+s&=;ynB92=-Qp{S@w0gUf&nWe*)ZHmh$fK?mhp0J(ix_CMDhFtGzz=-rBt3 z#TjNTAv(3L}2h@Ou2BXcwwnxOit=gWXGDI z>O-ZA%Crxql!WRd#e!APPZfsef#i133z^2s$O{<~=sDy{DB360QPs{IAc{?)1b_m- z4&=bcq-{_uCSTSB(F&Jp?nUM>JmloAfZ*g(^yC~?|h$OjdJo~MxB z3&J#HYD^1C?Etfs$(3bfsVZv8kpDyr3xNNCkpu;&&Hu;%faR6{(f@bxbYsyMoju47K-EvWg;38)6krbDPCW)W{rP zr79r^Pi0k5brB)JPYFZ_f|(>MT|u?OeC|6M{};fM@%7W}FRo(H41*0)A6M5t*^iI@ zm=1BIw0TrBYWBsSdc3EdPoMPvelTlNTHfo{!e#SEbs+Ur|7IpXoMv!)wwNbBe*!kv zK|vp8u|NSo>VzuS*fF(dU(s@%HL7 z-z<0Ti8ps$*EBLlrF|UKQ=+Aad)Iwv+@qn>FQeFg7n$mXwx7J8(sn0n$YG}am87vg z3T-_72o}Z&yDZB8mGJAv!)`V8@~SBCjTAamb;LAm;%im;u04x3^{mRekG`ttDZ`HW zJL`;*sjmKt5{y8ph6@<*M7Rc&t^S%m6Iux>x; zohvb8Q6b}RMtNkp6sBxGjJac-J3xtv_4iId?I+6a_qQ8CQYsS|)ZW$69@G`7fz^ez zjIkrp9HK>e3kfn&HLYPpL7b0p7}`RK!wPTt%f3f3>NFr&>@h5*2osh<>nU&_yC_w!8uI!z8=8V~`+R#=V{{Hy^53K)BKMuOGpg?u69l}t$Q?c=3XCXhHrL^TJCa} zU-!rEFH4akHrzWwjwWj; z5M|I=LBR{v!#N{CfI^BV8f_iU!wQuz-4`ky(?f(}5MPMpul`=f_mO%(xh$52&)Z3G zfEd|G#C^0nkW?Xig%rW?|6iLvb&-ha9dHu2y}atIl!>Z07t)k-7Tvc1~ji~c^ZUflx* zIQa=-lLB1SzqlHr{$hQ9_&`m z5+I9&GcGzcl_$qSKy#!$x%5(w0AbO7csd86R?&0zb4YRbWxCHj)+&iU=#T z(Ty2{{9&0SZB13h)aDq2V(9topE%?Eg0_okf}(i;^0HpNR{nOLJH5#hk;=Wtmmq|T zZb~yWwXNmnlsXyCMO(x@f<)$xDP#-U~ywhKv<|g@HY?&I;nQ;Jsr4pPcf&O}Va=C%U%evvfu5 zT4V#$2-Bn}$Ednj12iRV420s`BI{tKaF<%NTP?;Czw;czT+Fb^F*m;MO2t64x*<0? zJ^^%xx4$;3zx=?Qw!g{!{hLfYWk+;mgKBRxxVOj)*zhGbcgPKH)&oLvp9entga?r!w=0D8)&Kd(3=btr40^D@Kzle|0<03)>%3obL)2Jp>NN+ zab#GGbx=ORNQX%sRbn-UlK06d+&oG4u4T)9)A_mypGclKJ5LSsMsZqh!HZi5@J`!Uo4UKJ zQVOYL*%#K)T?KN0jV7d3+Rt5SR;h>0g4O+|*pGC_=zCb97HXNXB%-;v6wM~x(ll}h zXY-hyx9hQW^bTkD1{+V(8i!|>HEqh;@l<+n_w|;;7$Vg;?c9%=H&6ouUr|TroWozQ zN}bx8QN&5}a>)}1Br8g2j$JiwUC=Koo!P7Ss=b6ntLp7yTvPipx~2bM!waUnouDZT zXGM~E#K&)RH471rb4W45W$Us@%bsLi5DQwIN+#4X3mW=Y%QGIo#rq?+<1X*G!D%l1 zi%h0igI-fdrbq8%z4b;$CG+YOV$}G95^6467)~*>)}4o+!c&L8ehvBH!U{NhkZe!& z9S4urG`Zn1>u-|E*7ucO+NCW2+!s_{lrjKs4#7ptp{WQ*NBI52ZxNNxfxOf zRrdbf76JU^ZYSaXbn={5P03f4;ROqR25{X2tuz&I9xhULUdcL<$kkSGP;^3-7S{5- zBa|XX@9}8WlP7L^u|Uhmo2R!_?GY zTgoEf=D^&@ljT=ex{9pKaBEhexQ#)igZq*R83v%RD>es)Z2nKx_%}O{ghFBo3NszU z9w}{Ka=WwsA1tAJe({4xSY3pFW;O9dvXmoq4T+Pr16H?Gk-j8%pzl}_QnUgkLyD<5 z#c>~3|5k#p>JPELt!n*Xs^H12Frp5Ps+s9RcsvszsBg7IK^229)9K@~%S*Fp`^(HTrDElosNRl0|Zp6@YZ2YBjLZG{x$5K{1>qb+*R!>5IgHWEgcphFp&j;YFUbt$hI z1?3I~9SkDwPt*Y43Fdz;Zf52q^eqwoWWD+}8hBy4MeGSL?Pi+=e@&3^bUnhdyR0!< z58W6OtA^tAW#Plz@gE-bGju&MeXS)Q`+RLC^CR>a_z>>8TEm$4Sgs)M9vobR{?&m>~V_7vEBF<+mS)GTxKoJZ&cK6(?n$k2-Qz1mX(f$_`e#ZQ}^0Q z!R^XZnfDBFc{3ajIbcPW6tKPArhgOgy%~2o)W|0C9w*NfnP{% z!>NXCGwe_C)=u7yYIiT?mT*1UvYDyYNeqV`Oo6y(GZ(2C^x>Iz0lZJ9j0b*94KkP*PDz1u$^LlMp^7)7gbQpsXcU=`NVeiiu@u~2?0Twwtiqpz_Z0%li zxew8`q1oWTy#}yV$wunR@CxD#_;Bt;*MLRaGx>+EB!5)c!LF)R&xy^Nk{KL`emgfRf`neG7tu`1 zlhT&)^cO0IKQZMG~sm(cB zUxU6ZTT4$KmXy)Y!mBRWx}<`192)$-ozx&zG=-P)^J?W!e+Nr2fnE^@+U?RW|PQ)ostr zCoKAVc6nk-=XH4G@q`yJcgoRt`8mo3$(a+W0bq(tbn7?-b5RZ0-j$_2 zP??3l;BRLaeGMTUXFh68FYnkl$d1z5-ti;8h($1<56cunihs$yo!{LJIbDvtLAR0n zBGtRxjtcGr`MM8J?{}P>T*0A90CLtX0J2xt*tQhQR$$2+99F)JbrTpCJ#5D+d_TfX zGw7yoUzGpQn@qbEI@hxcCy8}R2aCw>r4>9e5`@FVfv@k*rIUz-ZuPEj5#}om;y1&E zd^aI8ziuw4cXop<-S&uC@Oa6>bZ6}KCGI+vm1J^tpv_ehGaSa< zMWMYYvRu`}gOCxNL*;b?^d;Ya{ia5ABl6N6k5O+tVRF={6@7>R%S1@`H_0u>U$+fX zcluZP)V8w9$B4j0wT3+h8feLAQI-f-t)OY1XSRN*T;i>?F0)9Kg`sCGDFySgx!~-D zrO>@tqMl;WOVe^miB3rs3tVDJF7x7SAc`bRXzUhkNih{l3boBmNx}l~F&TDepbhPv z8M~2hS#P0d z$LikHP2+qz9%&_&jtE6eu%c6p9B7MUh~DKr;kpcQYLc9sQ(O zY`>(Dm{gkKNL`YSC>i2Q+X8TR8jG;%WSZ&-ZHlppJG>QzT;J9e$57;ygihjNIc5Nx z37FbM;UKIfkdybFnJup=ry<5xXbYN7JqmL#CSdP|e2%tUSFmRnGqh2|gi=D6Vydjt zkzWivHYp`?FnMJg3;_w!<>Ox`)eHH*=|#j0H2Y=pTNUiPj6uw3oc?8pG$MziCzHZ% z60OCi&8DH{JV(P8Qv3V|Bu9gO4rnDsb?Q`2#yME6W=ja7oL)V0%SW^A*ZaOD(d-vy z%k%Lsw*P(0PLdl~mqp;dCS>i|&gQ=AdT<#Dp1_)7V|n|x3_MhHwMmgStok&~7xm4Y z3~ZuHG!I3y=F(|oy&&8>j2@MXo|x@B5hXD$ISMq5t`WT0Oa;ff1T^M%RFwAXI^Ei~ z{0Ev`vnXfGc0@Np(YHiG0^kvT0YwSm@dYd4ff9tr63+dLjBB(Afiot)HVXp`ZfGbo z6E1cDe=}dD%T3pCW52Zy@fYd(?-zBZgOXqz+%93gdW?TO!?+>(Ax5`wdYr5E+TP~n zTAo2VFlje^F!yiwX6iAZ-?52x50pC+nR@2U$vQkujp_kGb(__@cSXzkWj@8T1*LK5 z;TppUna6&dB!NnezIiFG=(!xaM;|it{RE|`H=_ia?^79==21my1sY-eNXJ9{%*bIF z5f_G%FZm{z^<pXHrV|ry%!qR^F`s2_1+exmgk+PGm}kkf`ae-j!VI!_srW4G;}kLkQ6O-{BDCaEqv=Q zHrjj0!grLFu-NuE9HY3E1?8f}8>&@yM=%ss*Jm|H!AM&v@K+;Jp`lVzq1-fRird7F zgKm^T$fpvmvtDEAJ~qbpnzhj}DOj6`W#Kq@n53NI+DxhA5vt)sO9;ug&GU-qxoMZ{ zAwsA~lh3SZV^EC((Kx{fFtcE^(ted1(X6MxIRi&>%)M1si?U@=ftxfZktIdSWwamP zoP}joK!-(2-_rqlZOHiEE1yw zKoKd3Lo89%-hXe=uHiT|{bDfmAZJD1)zt%17B7G8SWZk}+he65+q1g%W&6WRz5cCw z7AZY>HEeX(jLaNU$A)^TRZg1uX(yd@%7TODDf{GJ)>D+HAm8@=IKhl8;rMmI9q(#2 zNT5Ppi}AMVt4&^UKsA3fobjdtN;cclT?SEal0e316c}gaQgWh zzB1QVhgb+bYTaojYhZ0&dm`nDdn=Y37^#(xQ*Q9Cj?T7540al?Lf0aCsAK8g%j{_$ zsVRLK?O6>+ZMWJHS6Ruc*pQZ1xx$6(=)Jlv{;`Mm+-u98?kcz>uB4$gb-TmILEpqz zayple{9C{v%woVBc6k&@ihm_-d^H>NF>S9PbHl7+A5wZaPSnVOX<#9>Z-q-Z+i1M{ zge)fo6Hy}1Eu3wzE}f_?Q;~zVDBvsuYER;w{2JiEZ!NG`;WxOoHUl3ZhWyHn*loa618~h>NhW24|>@hl={#ZX@sd)U>(X;Axs?|JiDZ=@b*2 zs;n+o@`!r1USPv-W3*Z-V!uqVFuw76 z5^C3eBVvdZyCL`vYTg#_g;Nk%CJFSydT8G-s@QemdQUHV6@iCC0AtAxACFIofRm zP>wi=qpuVRO$TNc?83pT_kPr`R9ro?VO1ZqSHgi0qJ@MrGCAnyu?98`tx{@A}cPd|5Sr6N_OhJu6#sq%Pd_ zvvDERc1K>d`N#_e=f$63-~O~R<(HA1qR>=8pA?h9)j_VZjG!ReAPst=ZKoEfVCFUq zQg);xP>XGL@nXgfHQ%|?7hI)7w3DLfZdq~D(Mb}`uG&#izRF5rp-;P;z>>wDfAtRX z-nh@fSC4<7&zU|kVKK>d1F0?}7s*PE_yE_aWpDPwXro}$fSNd>(N%(+`>q1dm5rPCc|ul}29_^4nXEAfHWZ^VZbyv#-R(U@M&I|jhD$r$1HQYu>sL%JH=8?5 z(+yLqhI+mlUiH5SV)>MK{V;vGMM9RfqX~_WGY=Mxrl9~-$sR@{b3?UpH=&a;m2qF! zK~@8Fp#W8Z=HJT^h=ysRmKscy~y_1}Rxk0cv0}>Z_(E+G)lDllGl+a%*v9;n!U$ z%o!fKRCY=r5}=_C-P>C4)fT zS90EZd<%y5+%Of~Ui&^0wRH>P?x zb!hFt=GCo`)?8KYEb6*#$LPyc+rZ4yqIXw4UQA`V-hmt$M$*e+v`xCxW{J7CmOlzA z???+1WFUGZ<~3A+03j%txk^_pJXw^YOrRc#0kRuxfr!jdGGoS22h=8_AM+I<(a>zx zWC`Ddyv^J-v8$l;F0fp7MU>N5SkqZ;ANGi1*IeVYzzcx$qC;6I!ZBl8s9K%Ax9i^A z*;#f<4_(pBkPLQW%TchzGF35}Gn!OvO<>@ppPW4-Tvx$~NmwbhJhs7cC+saO;f5p2 zcxlF>2$|%yiln4@V-+uD4J@PkRctD5K~}3KgMPS+bj}v}EQqX9PTbVgBKt2s+W14fN`2BL+%V+)vE)rAObJr;Gp*n{-~|SIo|HOP?QGGi z^+9jSufuNHxk0Gybze;3Qg3t_{0o$nP(%EAh2__Bu!5io8;Vlrf9dqvs1T9Wqhp)V zK5S~_AOzO*vXRc}D25Zn6AFzHk2yh>hd za6Bnp7P>6M1d5{K&K6%Y#FB1N9#Og*2+vT4a*_<}^f*qTHdiVf#`k;hSsXbW^v&jZ z8LF&e2h{RvzR_)w?XqNQF)aZLebzt@cmxMYqbs#g3WS&bgnhkXORq6YQ-adhtWAn` z{a0GaV{;Ni?DS#^jp#xEY0auDwi>7wN1~ioYS+d~5l~}J5W*vuYX*^K+{4!Ez#f_8 zaw!#rE|K60$Z~N3&sMwJhGr!Y>2AZWOyFkGPMNMNa}4EJwQbeG=;%9D#j>u_&r;UO z<1H{Np~$lv)tZHx)*;6wW~q&UhPA8B4Z?$> z$4+CJ4bSV`rUkoQWbBo0ku|5;9W|FsN97{A)N>XUhqCzVxQJcWn%8Q!B^8G@)zdlP zP@>OFn_0JYNF5bNl=u}KV;nLTqgyuA(Cw&I)e4by#VH8eSUxwl?er*Q0>Ej?ny8mNR^#EGU^2U_vU z=QDcmyVt8yh`qm@RV3_ypwYy0K~w>+)^AJ@DKcV!xNUw;CEfS!?DJngV7}2_&@-;|VBkI8`Q!LkT9;DPmA$ol=0hfPnA; zg)KII?GWKBMa6_$xlqLcjZO*+fG*47rj&I=M8NpSFzKy^2eOOmbBcfKg~Qv$clJ%1 z!w3 zzh!Zmuvm1p7njqgE1nK|1mSA&Fg?Ro1Op*7x8qR}>AI zLe7G0Lc8t7ReC@-aiAIGs+PiqfPz_c$=RkLy7#6w=5bJ9C{eUjdEsrEVG(3v8XRM# zVAR67>(K$q+T;8EJ^|tTx~1(v5r=2qs;m4rA^ZT-_vLJOo8Z^}BE?EW9z0bFRkRqy(HqFr#PxsWmFs0gjI9h zy5UQn)vn?{;EI3S?LAzb+l5EF+P?}B#2Va32V_}MUR`e);P;hD8tdSR71krzP)PD0 zaUy-%Npq}Z!*6#~cE0A|Vhn!8yDryCN2n!tk&ZkMay;iWuC2V zh5U35v8K;g$ik@{XtIL7Igf24vs!-BBE-|X`S^T`U*{>+{AyrMA7nV*Fto>Pu(!Bc z?D&&I9|?HtcWlIImK*u~-QlZ5VKLJS>^>5A)=w6S_|6B+PU^nWn2RA@#_4TK=Mc3NL%SW9mw#mcI8}xr5e>vOuT=|b-UVnB0BJi3Lqkwkz zryMyxHu>7xmZ#+kA3T2uRD263^`9N(ay%v^SPIVdIw{bat*1;lfEabBoqZ~~JvO52 zMkG=PLw!DpgiTysvK^uQ&pUle0i4VH(|ITQw}@+}d#llbH}Bcy!2y&p;1>e=a03iy zJ@!{5-)-l}e@%z%-7`5Ha)`EOG`g4o;crNY!7vBxq7m4XQyDkDYMKhX*H79lzj)e1 z9j8SU%8zaK)`qB_3)Nwzsf56iGT$)G#3eG~=7m|cYQ}oH-i_v(KYX0N_k23H1J>f1 z_Ou^Vrr+W{Uvc?##J|8>qE&qS9shFFIZepv8|YQ~<6lzT?saE=X5;(r3|V1c{cE7I zcgW3+SHZpG)2pH555cR0S0j;{d;ynt$2+ID8)vVt7AHUbb{o(3U4*~RFZuuTKYjZQ z_|x0lQ~2%TZ=VeJuT_ijBr>6I*XLe^3Z4?hrT#GnlCi}ByY&3t{{4v*GR#r6Uu68Ae8>4)hjdQ5 zgxn9am-nOfbNgD1-uG0d$>`G|2yvo6!ff6!d*t*$;vNAec18CXLs+8zUAr^m4`KCA z^Pc4$&&RLDVzcnnk;9`$PqOs=flsCch|rgxznRrtARM%UU)#&k$&o)a*uKXd3Mx_Y z`EezUb7UH6a3*?0v$Znl_l`{9YK?c5kJaDSjHD9NV{WPHEBPmvu1ZH7Ew**wZrKYI zFt1cAM9~-C9`7D_&z@>u=eTv>3PV*lmqvWk)-DqKeEp_vHxlvl86u{qC$KjRM)y-K zd*M30cJkXP&qWNeXEh&<<%urWh96N=|Hr@Es%=1P$*S#%!&!e^BjisD?E*zB$*Gt@ zpN+JEC`Kc7$!0f~)Z$&*tOr$usuN4m2P=~n0{tZ~a(b1~Fq`rs(RgeXpbJDxc|AKg z_F$3?Vg@{JFy`J}{PB2NvOW$X4Hx`Q?{I64N0Bg3G3=dwK3>{wP(qvnB*v4s#} zwV(qqI7hnqTWV1S`h1 z7@624$_psa7lIH}0acu_D^K2xRbF0#IyYYezEL%@^mTgvDVR>5nVS5&-wFPL#fFdn zQ|Zjta>+9kiK0lg^~X#?_rtAO7p2X5Nk~+7?%>c%t^@zZ1d3i#sx#kpFv;R(e7sYO zuTLLIW`{Ec`~L!LK$E|gPtTd1?Ki;^5KA)cf6a32tgY(ZtU=mtaHc%*7^gX9XZU6d zcPzlKuv9S1dbIpzgF*fxrduQ8%?I^Gsk}+iINk$FdO}%96Wv_1q#TD^S#HpfUs9gJ zT1*`CgcrRkY1SsRu!Iu^1-!Y`zMfMBFXUfla@!I(5p2UbIt<+@|3ep4$CuqMCHHG* ztA-y(mpaRa648^t34WV0K3l-np;mq%^qQ{$2s9sy( zBRXgZ06p7%*6otUP6QEvD3<~}ojh_6k1R6Ohtsa-zD;AIE$N%NRm}SKGWhqH~#(4bN_vMe>cy}yXIqaH8>sbLG$GOIRa*sJ9#;= z4&X$9Sm;D#Y)3DYx9rry!w&$Gb^QzYI73D2U2N-RwITeBd}+eKuyoOg2(Ss!38Nkd zztH@;+Sr@@3St);#55p68Ul=MO)UYaZ);NlIpri8il@Xgi%MIGu)mCSI>v|~gw z6}j;7aLWw(d z%svLYmI32+>bdm*G9;Zv6RM}s1sps1`u9Q~Y(r|5mZ{VZU^L3^n?NM_!vLKh8#s0S z)b&z5lg$4sGl88 z$NN{g-=NL(R;^FRSMqJEm5?>}N3nV1hf@fztZj8432E{O9uEYYF)@2R^qvxvVQ>77 zTU%R1$f4D~gy&oA)i|kJ)%a0PKds=5POBW$4YSb!l2mvt(Maw6kBWwbVQdX#JRq;RtdqVqL{*JkU)5Dz@e4 zbyx#rwR31%mD!lmnm316alhc>KE4X*wx@$(vUf9SFRx;uSq5xdQgX|gXtO3Nd)Xl) z^68Zco{0gXVlf=L1q;^Mw`-Xk;8HO2hLLr`T}Daw8iok40QMLQ3k`vW0S%j!tq9^2g?VKUvMW z;e1bZr$^}#vIrWsQVu-h>C=yUa!XWRQ(Q*qrwGJj|YPja7F?!5sJs*$id|k3%IKt-;eLN zcKW;jFM@xM1LS?a7o1RF8g`0=B5Y+TAfIi3IPuViNhBwA*A$}Nf{YhZE0+;Ac9(F# z8%r`K|IS7Q)D+n@I=CVvvdo4dj3(_3-q({eZ(Yk;6GBc8U75DW+3^ddL#ypDkPM|R zN50zHnyX=SO zY4F?CeCj)iG$ia%G0c0q1^rr70tb?cy+;4+w5|;PR-BnsFH;Zzg3=;FNRI6AAC|~D zW+oXl`OtvPsG}~*qMFE*V-@-KpM~_!vQ2~e-fe7TYM$xAN^vOim=ZdySYaqZ0!3ha z=h*kXpFNH^vHNNPjKn(K=}g|%D#8&3Hx+WIpQc)xL5dR?7rgN8xEd!21n4cZGs6`A zNqr^lTRHaA-u1X(H;~17#p8hu`rWYtlcievP?Baq3>iNugwUH$<#Ov|t-p$(Zy{^y9nyfgH0wX9(95`U z^swk%Qb4R$v6U+YBJprlZ*6hx5Qf&e*@sW5cc*m(J}W-WC)&i3U`@x6nv+{3`OK0A zP6Cb;-DjhU)(ymvmSM3lOYLMN@G#P7@Ff9277|4!?L-lX9d01LrYHV^bk;;ux5Z8; z4JbB(l#2-uRm#nHtMb_c_3VMwN!7_lbzPKJI@4;!v~e47!*ioZK~J_i4p!pLKU#CCECfGlu=FX&d_V#@5a?e(QrhGIc{1k^R*^BHnd)^}mZ!p-HylRv4QU=TFnGIH_dWuPt z_<;zfgCHlj+uLNbALg)j#e-&KMH>zzhYbyYPA~!S6Y|sa?Bt=?d!)(T)P>ABC|SKT zw8C|tFX=0Rz1S=yMIE;18G7AN4s?|R%ZOMwAbC>U6{3}-fEHV}Ku&|kI)%tmCSpO~ zst!Luw3K6zC*ud^Mn0xl`X_nJk7bi4?>TF*_8|;I0YjEgFH(j>u;sr%V4(~fl ze0*MqnT|%claH>=aqn5g+Cye)URKKUwzH)>QY{M6 zv8bu(T|m54d+#@pm#&&aVhG4=%95lg$*sOWUmdHi!>N?_FH5aNElUvqsE$Vzqma+Z zkOlhNkC(6D#4fo9x@;Rs<1%bMGfjt#S|O@#m@u&vc3RzX;p&{?x*$2#5*n9Ei}J`J zBbd0kCL%7uAV%+upbb)kAIt681iAgr9xg~mf}QE$YpQby7qBV?$fr?n!*drED5DEH zADu$*wCbusNsWxUBpE{y9OqQFEW^i=t82vXZDU|!kW3ORl8A^w?7^{dwXIs(0Ow8x-C|c&;}7e@?PPq`NSAb zm;3T8YjjS2uX`2IztV+yL0O9*?u#3=P7G9BVq2LmT9vd?RdvG;q$O1Ln~tieq%0JI2H%mJI59LIWVe`W zU&fPQW`r*dDku*_nLx6_@~k(w{MEq+9X}sCX3AFT+cLMQbrO*iq+*0iJ);N`1 zObaHWF4>_0aG0Qq;Q3`1y)8b91*GxyU1d2pMVWlxU@XkUtrIJj(F@|lBSC4NqOc&XY$W5>oBr!&|*+()UR ztcM$vKm5;%X4*UXlN8H`OB>x0?JOKN3KOa#<*`dea8Gj;%%Ai5P_J{)<$8*dFlrIq z2CTq?)njbLCy_s(domu)X6%Y5)i98%g!OLuojU_xM>-u&ym2}OAcO&#D+^%tfiO)n zKll|ml`SwK6S_5#W2r)oAip&3^||R`m#d~y!BM0rRiLAt44`Zga)k&K7X(yA1pt#O z4{42I+7$ud3i--2PjH694JlX=pmtp{jt)-eq)MtwAyYIb9qhMkC}Ot z=d+mFSf@K%)!abhWwxgoDzZ^kp(ZgfvLOO7g&&6s16l<{p!Qc{-Cn{f4KYDPh>pJv zCmSIKD?H7sHWK`EP@)S)$iv87K~>1Z$wHVKsYswy%ZQXKj8G1&O-iJNWd%o8wHFEj zAXA`~<^eMs^%sl7vg6ZCCv|~g)0#}~vTU9Pb^H2lwdmTK?|3je413N+oS9SXymtq3 ztB*wx553!?x5~c2gB=w-Br4oC4Gz8T<^|jgI8g6dGwx!YEd}BH_bfq!&APs0*<|v! zewxAC+d*h_PIwbK8CK6V9O(W$NM1RX!#%JhNve8A!7vagh7@E%`M!`t79IH==qV1~ zhD#4Ki4YiyRCYD%!N&HU6A6?qpeBi>+*R)Mt%wg4@gX26rdTg2Kns1<1uV!RfjRU7 zLdc~uT~)%DwoPbv;&?cUigkLlS>~pr3NSqL-p59|%gVJ;j{~bw*(*)O%AAQh<#}w( zTKCn-jD7{S-Yg8Tucs4X&S|55MzLIlTUSH3X;eZ=SDdD&3F_mX0gbVUMozWyC@BfP zDYQzJ1M3v&Na0$v#<%$J87#iY1!rfWdfg-l6D5538oETGuYnj8f}*pV%>Bp1LD-}B z=r7*peF|ew170z!t<}ftuKi#3g11ZOwdn4d>LE>8&;s(7gFZtQN7S1ro0pKtxs>$H zcD0$Vl@UKy?Mx&`5?XBo!28@fk7QJ99CiB`JWM$vkA?O%kC9$NdTF-gVaH~c7ODwy zcUmb$;;Ni(Dz785aa|+)1m~)LIC~!S!rgc8#`7-W?q`y+iCV)3L7? z*U-gNR0qD(=2J}e$qDU{GtQ-89z!V?Orwx^Xcz<&oXuUzQyvk?h5Zztg#(9 zc#ZNG!I8EWto3MoMpoj^XJ!s=Oo{n%(d%TQ%&ngb&(v5VCw2L(|A?C$x+A|HLDy}#V?W4Eu-+WUTEt>l7jZfQJE zs(!}dw`Avb>@u-hXfT9?2zALu60=i`N!-ZkgV3nN`CGUDS+##hL-w#a$m#vAubk}I z92<52Mo$5as27PbFx~HH4B9GY@G@F*+tB6nDs5Y{rCfrjM~pa%dE$v06%*6zzRD0a7<8qzmWbQC{BqrG50w3MIZ0l{aCKa_%E<92LWs4YN{8_K zb#nAv>}ry1Fy}qpg`>kn>JJ#p2Q}-MAiK5V-aG|><}-s4ZZb+@;&_04iC4(R4vabYpVhVjI4r^If_uo*s(<|pb5(jdR*xT^a6Tr?pN$FUo0Bj&B1e4ZvJy(~(!IX&u*4Co|z%(Jb zuX2u~O2=1@UX!QO#eau!_}f%oRQsP-<5&Zn!t^k;Q=nv%ulYDwif$p=emghY`|1pc zc_zOcM-UO{=jWcJQck{q=q84A|*rH$(9g!_Bv1IH){k*r+b? zD^gsA{OaKN)r}9Gwbr|XxZoMPs*}KZ2|RbWz|Fr$6EfRXjCxhha-fq{lvzvw9WnHJ z%gQNH<5;a`y8&@mDcunm{ANJ5C~!cLefoq`-FFItqKBLYR>YwEgg$CmhQhO&^|*(Y`6f$UfW z#YhJ?f`}Zi?o`c%lvs(lR3`xpSf>^eVYx6*URGb-fs(V}_TE=55Ha)5NiZ`? z%dt$iGMdNO?Od1H-$$$W|7oaq-j*Zv6r%y_@-M2jO|(t)?Bg;dNa-SPuSV&JpyA1G zn+jBCx>0oToqs%lrrD|%3%a>0@POr;q$eMGqW4Fo02S$e35T3F(D^?x(}g8UGimQrc!^8NHQ7cZ#zR)OU0-d$?ZeC zdmOmtTq<$Dzg{!353F!TB5wDc^PD# zh<}GHoV?p2kUCRLljzVVutVIi@*W-f>D%6=NS`I2kzmfkDL(xEDOuiIF=0&<3td$A zjk&3KJ&v#!;5c~odH(BBr>ieOE?)qj@>|?!270v(UH7HANHtY4b@lYa#Sm z0QU@-tcK`p&eyg46~6UPs;BT!*B9z=v$)R^b$cAcW_2^XWhb;F0js6=tmg3*!fpR` zuwX&>s=WutW*^3qPj%+UXW3ehD49_xysP=;gx3mTp}}o(g;#3QQ`e zAZbwE>C`@nKXmk1V1@bWWXqP_`hDY{AGY{CJP`w$`Hg+lfms}zoQ6coK#3$#($MZW zTOBBao#E}nz-sa`ngHx;X@VX`+k2RoBaH%51n|` z>6}^G&TnCnOoby-DDvzYSsAT*>Yeg8x_fbj*rn9kRHUR%xee&lNVY77x6PkjvSL*7 zL$T(tfdc`wg36tFMLY9%UuJG6epWbAh?42nL{eit5LbbL5GrUX*4D1L85=swbDDr? zA@X%K2%1M4lF~Kq>P3nH(D1S$dvm9}ym&ueitfNU%;DkZYu9;`nMLjVTh^Fn&)NA` z|5<^)fu$5MVbY!?Ii}9c4sDF-^laaXK}9)JeEQysUCw0~jdWWr6oz5zFsA-5;$e1_ zFmh(79exgFwX+M}UR$q9cbH{C_BBjFT+Kb_IzFYRBR_d>VY$_^dP|C)0S{dsRhEWbAGO*hGD?u&7GB%XQHA|&Rs)3=mmAZHyB7HK#I0~O9WkaGI_8*9Q z-*xc6J$>^;Ci!l9OvrH#)|&E?e>8gjf7JHw2xCIs#vq3^Filv+cjY>kJO)+>qdWv=!rl0vyw!>-6duq^zG~9c~o$fHP`$l;Q>ay&VR^K|ti7uRE zUCdDZG2-Ga_?#3G&x#=9f~l{7qKbc0319Wg11t-`YlABSJtkUf5p?SjoPnETGh)(% z4CTz6C=lnnKBLmYvmRK(alt3-M#)+?%nR!z(8 z@L#on0?07X;8z)v0~qs{$2Hc(K#$N)fl=caBsQ0x^Di*NB`P$>eH)TIpWfQjd-r71 zLs?wg-{F)a7AbXIzMcr#R!;g|iJ;X+~ts_D$pWc@#we6^b#^IU)u=7bpb4iSR-;SSBsBR76CyqaAHse2$n%0z^sg zHxdh@qEtnvdche~^%9;g?y{g2wOpI1IoU7nuGebHmS7WFbkn2j(Eq9l1IZ245RGRP z$eMHBH4$)sr!tp!x#?*5Q@l#U9Y*#vaU{yT_#8y)q1!o87tKJAAGeU(Qafi8MpVte zHh0!CCBiHCj{yI7SRNuO+u|OKg8wCe@QYp|nG4HR0XYRG*3tDjX`=$iwRqGsBtIbhGDAkaZLb$1!usVFS~|K>3?=>s6|O-EfPaLnC9TX{*>K_nbaz z!$Dh2FFKt^+SiTa zQNx?cHw2EP4aFVqe#MtN?tLgjE&R2qZZp=*gjISOHlD&NNZyuxSntsA{FzXQaS)kX z_y#)~%T56p)wiL#{x!U%#FYZ``}z1k!Rez%rSmfsE9cnJqc$&t>{D0c(Yg`lC6B3* ztJu$#W^<_Iq-&-Q28UVBp36-lAVW__jq&fYew?=PF84}_JvZM|>**KWNCZXd=MRRw zwzu*K2UY1Gb#4J}E69OE>dbh?L^%+LiD&^n;`X6Ob&Fe{m$h*gn@lv%GWHB=%wU*F zmr4b$go4E>CK#&2$|R7fwugSp~wMa!1l36WrBvuU7;UFT!MF^BF*OrUi+R z4Vgx9r3y2XYnQ{f?Ynhmf37D;*kfTyd@u{O{lx`-Vg@+600O;v%2burJ{XTL{D4Mx22cDg)qiA=0*dweY;g?) zpxFSg{-6SfQ)IAeA)CNDEVo}twvr0j3ME1oEtEB#x-%DSmP8rkJu3qe9{mOW%}D0c z8p8uKf8h`oD>Z%ZSFnN(AYgQ|o@%K<-_|?KRzQVPHB5bGVgDYXbPFYfyo(V}e%Br_ zQ4o?)bZa~2iiEv6v@@?m8}x7?GQ{Upq@gk>qyubNTr?_=JHJs0QEJry3HD@l%oJ2= zW45A-N{DQ5VA_2u-Zy*&WGP6cY;6sO7vkzgd{ykCG|opDR9#>wEdZ+kSDz`t)KLhk zVKV^a`YZX=tHZK(&Auuv$l6hg zD>$<+3D{y=T4gCWN&;riO&c<21$@ZLmnhntF20nh=11fHxeSWS2 zR`r~qA9?8BWx4hVY<*tdS_^q@hYQM99zTRmIS6k8Ciy<5#leGSowBRI`JV$`_7wN@ zZ9lEwxFd|!bc_b_wR8-yO zKTDLFW`vMss)W*5YaCp*$QghP&N4B4UV;}X=Z5rs!N(Xaw%sjhfGRxDK)z861!66QtRN8UkkkiZ zwV`H76o5f$uuMRQ#o|2#2NVUQvkHOV_0ZuYpw~jB{L}^m1bEC5NRa_V8s=Pr6=hOT z@O9oREQlJ0vcO+AV2?G@9 zk&r@KXgM)KSte6iad0|Y04G`h|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0|NsC0 z|KK|xHFj!%^|`dy)1!ini~!z`XLAmk8rEmI;9I=+o$mp?jg&`V-gS3x8|v~6Pze_M z!1rE(Ly!Of01W}q4Fm2z_nz+6?e~Q;k2P;Psny-?#ce~osk`3rQ117;dCD5D_aAuf zJ%zyT;^*7Nb@c9T;2%?4?_V`tZF%j=p+EJdO5eU)%0GMWz35WoiG#ZSJ7)Gawl=Nz+`c0;j${v9;N^KPXQ_(VQ zCZ_dIDtkr=sP>6HPePxfo+!liAE`W1rqk5a@}8lmwMVFVMo&XiLue57JrU_N8kz}+ z8UTr=NXdw3)XbSQ(KOT1JyUH=RQw^5pQf5*X{M7?PtiS7YJN$wr|D1Bo}Nun?NeZ$ zgHzE@)eqD?Oq!Z$>IbN4`lIwn@`3tL$OF_5P(4p5$akr=)E(6Un4v8$mQ=4XE^LRn@}lX!z1K*-)chEyS5xgVfEF?L|-vgJK|q`^hFGPf+qh_%wq7lxb*3k`P%%d0Bdw z1gt|no1^g98yghCCqx-r8r_4)Z=Y<=Z?xIUZJUYSUsS`&mszVLD0pt`sR1KTX-)8o za$tesNDNUZGbR=3nxmXj5DJ6Xs^>CMw8v)Gp;cW>jdeXbYpdB@>PfHOIo(V05K8`acDe6oux3(!?FVadV$brO>Prro}S$su1C?=b$7_@8WQf(@1Zj)bOSyOA;E{ zhsg&x6o`yirWPe36rPcfzq54$EdX}whSqN;NE;_;=5T~c<!mOvvw=W}!)T_n;1EJ6;9@$KwS=@GnZb-o$;P1WioQ5FCwq@7v#4DhtLI+-mu*Mv zSsPNV9ff>Ih8_kTkXR!(0&(|*z8P4fZ!#sin-C^|tU~$e#mtgUO=LYt;|jZ?01+m| z5@**_=1n310O@M+?yj{y?^&CPlgOD;g0TUEGpah3r4M9k;Rs#XxfA9~`X884sxHtf z%Pr2=Rd#+{tBa#%?G;NIGG>8c!|ECdk}x|-Q;$RxhQL5Tg>+ak^>k-~9{nH$hY4sA zle@d?sT-Ss9)v`6$Pvr$Gj)I2t?4wAaW3%2L=)(u1O;W5ET9p{yxY;D09u{3SE1@} z@EHzz7q@79B1(_|w{SGV^Z*OYgaL|E5H*&@uYYAJz_1$sd@Urja1_W*uS;duuLfOQfd)gP8pFdu@@>rkGh zP0gK9<1sv;ZsUq@fxc9Ka01M;pToC}zL!$I-m<`p@Goj=+>vAhBj~SNUP%KOlv+R| zbND{~-_VtfS@AX8IkI9)>$WPa0maD*vX^*PO&Z#D8D4AIE6Ua}WfXEA z(&>??JlR-ejSygn-uO2B3muH>8M`{@HU|=>sJ&f}=S zyXlHE*H5{3!IugX1MLBt(1Nl9WfJoJ{uHY&R8{z;JgyGayq zK!gBLKObHfJBEBGu)V;oxaV2Lf>O)? zWbp|S+jfg|Tgwxu92m{9=wUdVQ4n)GU<#0u#`5B0(|AMOkN{zSI&|zO^Shg_jNEKZ z;n~gs_6V6K*n*uj_GPyr7;DX8lHqOy?Wg_yArYYfY%QQzkz<>xZVB9K1SGZQ3*cQ% zo527#J7}Mhsbr&OLS1T)IPwxCfeaWGP{gz)L}dkL;Fm^A@<-Whihh=WFB|n_13dHg z#@R}!Q}0}OQ`t-Y)CVG1CCKro3aP|?<=Z&peJpkJun z#wrfM*0$mYp}*m|g?P&mOSB7K^()UKAAeqMJ=G(EVe8AzdDUYHCY2C_@rq)@K|5ps z|H_kd5HD@SJU*CDVGRl&6ug$e{=LvP?HHYlIKmai-ueHnff5wYzG}WcPD_Wf3$e`2 z7>8ELow_s$>Lqt~7Cf{%u6nps+Y+ygxhF=dI&>0hjjGH(VVhd)s1=-{VapY&?f|k-Sr4sbt8glM%o|0-8*d#;1kjwr?y7Kym@! z?Ji%tm)nfw{OX?SpswX9v}?p^8?tFGvu$C-Y3b-75=&&vf@0Aks%-mXEAaF5E29glZ%z- z70#H6Xz3c6aHQt3g#-}^3F(m?YOR{U z(nblaIM6fE0qsG|5=0=0NhG&2SU4km%m(EEB{;Q`1(HT;!a>CVl0_f_S=r28rDoKk z!deUxMtUWY+@^K&oRSu323fUKE|D#O5TKlc6p|C%$;^;}m;{w;G65-w20$cLYf+Vg z;Q|8tT+Z`2+v_UBXO3rwU6IxB_-ePmXhCRRu|?r@N=wb!tR>m9%nm)aJ$;An@s;J= z8-T1{H=|qMnVh0cov2o3ah^mLHP+RIWDHVE82 zlM4Z9uP{8yeSVXhf)igpcJ%#_Rj#qNzLHI-e1wdQngJI5sKPvEvEJovZf3a8IA!X@ zGlzzvcSgQK3}C;Fk2d3`Ej#n+&sEH1Ut0iPpxY(UYWu!$;8V*?W8J%-+O+Wn+(4vX z`~R+-3WkebhjXN{nd9)iT+K3&Yvde=X?}xrhzwiz&hnzvbGF*Zgi*{HJRA5DuRmb7 zgsVKrCzy|wMc=AkAAdG42e-fP@bSD3*GIXyrR<~_a1D#JW24>wzsnFCK$zgdqR88A zs@_o>V_lt*+Hd=gaq3Ng2_is16r>}d8W;IYaU6mVs&`azpIukFy6Cvn^9?;EJtWjt zJe__(F0J0ZT32(#=sS8_Nv;Yy1foCNzG}32eog-Y{bHM#Pv{C+HfLKw5LD7*3XZAI3g?~mw(z!t%{Ly26s{hXHk)v~w@VmUG@}SCMF^J_JZYPq3?9MLP_PhtSjK zxQ2r~Ebl4DqrXVZw`x{K$(E~7i zOK9r)OPL*E~mAWvIp0QVP&!9(nLL?UOWEYeUQr4r1U2Jul(vFsU)7Lp&pf>;{Y z$%Vh9oF>kYABe7KDEwLY-UjhQ&MXJ|)CM6$ET@2jT*ptrMOp>olh^^s`RTT^0mN&$ zjNBkw#HI`^ms==R0j_QFJe1vFu>R6oLc}D5Z;pM6r-23*P^h$kNTqZ;yHAQ3Pwk305aE0QVe(Qk=ub z>?y!~xx@J2T+%6hsmyB@wdsx9n%Ji=GfSHcEf-~fQbJO)0BuTOk%AlaT-LL}=7smN z(~dLZ%qQ%yPdc?4*;zWqSq?_)vBAMm-wdW{bnxda=h`nUoF1!Nw1bN!-v;+eAxfFh z*T+U`l)^qD`pW4jN32kzl2*AjRxJhF^^>{_)99cm$z_hK0r#Pe}^?c~?5TbR#zFl(nvKss=&|wqWV#AP_?Fdp!!ep2mA!vT!5CA-lNw~HPZ^s}4nq=^z#ygIefOQgy zj8>rnSG+Y{4vkD?F$sucFQ+z1^z7h0bb?LHLN;b>;|+cPGEA6$HS?IlKppf63wsWY zrbmKrcdi=*uF?Fs)$oaFtE9Y0*Gt0JJeuz5RssLH?GK`T#T;v=F{gp<+8@aoY4y9!)wMfmKq}y-at7WU7L}+#79z+!(NT z(LBAG$YA7?^51OmD8h0xwl+dUF)Cay#>2vRBUAr=heJG%yF(*nTOUi#iBYZh<%&gG zo=Iq(u)#cieu9b>pc@_1(x2tp6?3bVveVG}y)BKt3%i4#F`5#rFwT>fvN{Sjd3N&K zau8>2_HM7*31A3;z#wmbOWeJ9=iKq3&^g4bne56i(GLN}a`$+JID|VKAo$1>;-_e3 z22TcP6R!OK>+G#ipXftWC6K4DY--FJIj84oI(L$;RBsN!9Cxzfk(MlciSQtWSu7ar+h;dQwN z)Sd6k_?sO+Er~mHG3{DS&alZ#W{mVbQKwI_$bbd>$nbNW9U{K7Jw0(f-hj^0>x|Mh zBQGmb_*NY6uvWKMbW5=eX726?p;xJyhMrtAO?xUcPIW~nW+o=~WkfD7b2+)28QNP- z-hnA9yOWn+;>l#KO3lVoR30?)iU1&OwaYp!uuz}^4Y5H6?x@5GPqCPeY)o9+?S53A zR(Qg!mDN)Kg63X|Ih2KCX>OX_FVBgr1Q6k?s?;9*ssX=b~ zen^fEJ=`9vj|}o9RR_B;wLguN+fK(2dFl^HAt#!w!>qs-%Apvgnm*~AIxvKsTy`A z1@2KkJ(xPtGE~nzYt?Q%a>o`|D5F6J8_O6^y=?A#nVVV^vgW8<^wYM$icdD1Q4hzF z*KC;g8nHUhI)0uit6&#xv;DDltUmx~VgJ=6;@OkeMuSRssCEg|adgm!JEOrN>s=av z?jGpUtcmCq#;DcgM7xFc>>TrV7j%i<@bM(9UgN}*uYVqGMO_d9BvKIKADUP@>7XT> z22kNav!uzK3+TKD;u3JB17qF>pm8#-ZTQqR7-BeZoyU&UT>!G1S%i8sh1K@@-IS1P z(F)NpB%p#p6p%?E5Je)8DFBcGB9cg?5eX!|?8gevpO7p~NC+|zGKfuqmAUOVRy(0z zBAlE!Y~cGyz^1^7-x_^60*4Y0ogIDm#3UL*Ivb)hH=mj7$d)#BIb za&LX@81SY<@Bj)HA`6b+%L4>wNYquN;Ur7qIMov%Vp4lf1(6RYma+5jB`qoIjaT6C zb3|Dm=dZS;Hn^-%(Ue`Or%F639v~W7R{pq;)+Cy*WJ*V`q z-E>~m%B6M^G<2E3_c7mlo*hWd4xZmt>)`tNc%59(_nIaaA&L9MgJRiVru!bZ|CQbC zpg`}KDY6D}0A3AyQ~RRe93S98(%&u^Zr=)Z8$uGkf)X!4TvVUG`yQNGjl=>3T(BQP zZ4Z9RuUT1|avx8&;lezfz<)gr6Vj_&zb~9ZLje@Wzq>d9aLAcRUA95P0|-3%@SxxP z^kIx1BgEPMZ-cXRzQxx?KHc75GFpcQLe`*cJADN21`0{!+?VAj@j zW`Z663u5Rg)B1uTH~>Ug;wRG8=D7B3nF>Nikq&xs-2dhzj1?`94#`T;b8)IPMMEPp zyFXaY`<+Y6wkc`#dJi|yw<_}Cu6{k;*8 zYL+-E^@DdhJak)pOh5n+xZN6Iz$&Xs5J>8{sF6}+f*E?vBQmWOWeWA9i24NaJM`j7 zHCqb9gy2^acqnc>1I+0=q%!Yw;O39^vRRnZoH<{rNP+n}>{oanQb_k;wcY=CyY#Mg z>hdsxkWXt(_x_5H9nytTy~QAVAzgz76*DtF-L{V2sb1E%vgZ!oKf&roj6x6$;*uuM zSzA6wvfr}9;frxSt=D*0w_bmFuD4RPI$`0gW-+ZGAIZTjcd)6wX&YwjM&jeRg68+a zoc@r_#K88DY5nZ_&up-f(ZM!5ZA<(nFw#qq@;Uz%jh-$590_(r<%19f02)43Je9NW zpEZ9?-H*dfq0~(Ev%hM{;3O(%`e$!_qEtPH+;8eOGhVim@f{#>lrt3)fMqT$D1)?R z#DFOyh0bH7U)|@2A0!38LH?mC-y2Q9QSnueEH{&s>ivIpmdMplpVr$X#>(rPLf6)36{DZ-6Sk}+)U#|(1g1yC!Q=U0obV==0JVByK(viD%pwWB-K+Q|&hwNW44sKyyp2W< zg@ka&;_DxWnSI=X`L(__n6cSY<8^6X-QYwZ7euRzc}Z=>0SL4=!UqvOn{`Z!s%ral z|7aZCn7s~?&MaP8Y!5h;WONbMgp-hDMoTcps-}$!>$k_2J`2^yE4=ZQVxwt$W($|6CLapija^Ie&p6|WbM+viG*gjuE zkB)W(`M8@?&J=@=N$7GS7)FSxD`zlq@z|VUpCyny&IU&Qvm)bfGFxRAYi^c1+>V^q z0&q=IP-x1P;I}sYzX#ruW&R9)g{xi20ifP?jA?u>WB3{TagltIpk#1Z{_beFc6D-| z*R;W@@)mxPmX%m}4NCERM+x8!DwC4G_?%CS6)O!(f}ZHaCyKj}K%6jGb?vN~l<~L3 zK66Rmame3uTrTp0^(pcI2}^!$v#sC<_s`4ppJ=L&7Fu4pkqCr;v&)M~ zOYG>WWhOW%3zjg2)M^|G>vhnyK8)1!;4d0V-|>7F!QcwU>*(;Q$5gnRS>UcJ zz2K-}&Vr$}C)kyO2@fkC=I#KUM zo3uZa;R9%u+jQWi^-aiFn4Bcq`O}8PZac&TJM9z6s33bA@#f=5!0P?Cn^m}6FQ=Fh zss;t$0^4^cIy{%-8-2g3Fewoj>}=mH%g34edW-JwKh~kz>a#XlqJr8ha^2lKq*AOO zF=;tg?GR{f5&Q3|dz+SS8nQl#;kU(7b{Ygq+v0jew<6nMaBwtrIY;!xOZWJqUV zTODLvRK3;KG!7n-j(+>y$z~jm$}(}?!~vTR@@>r@)ZhmGf8lXo_J)JLX!88`pd_^H{&#t84A#&S))PFvkD=_$*!wj zK1;EHN(7{TV!LLjtr~S0Qw585dtF+9Dh*b%5-|ih@bTj;-t7e$8eoo%Kr=e zca&bPQNV!2?`cn)p4W<+R{B*qKnM$a?{dodrOey&0m+8Zy}sLKs!ctI5~1HBBDop@ znnu>ao#pT~^&##qVWQ)(b$VZT_+Of`xbLKXEVMmDcj##aieEGYix;B)6Do>Zj!bS@ zG%RykWs)`pod4^iomefUTW04B;s27nHC-pw60f!>RD*MSxrcrr|wllN7D7J?HG3~ z?jPli5RRFU4hcEFM#@a{2pgTlECc|9Sz%emn2Zskx}yz@aSx21M_=8I!Tx0`wPVi0U{gaxsgy}xXLgQn7et9Nm%ncrw5ui` z;_CBLZle%ac$=4hX6bFrzJ5znZG3f!pK|tq7`80Ch&Y75opaPIH)J6Rva@6VX`}vg zdG_Z3<3lhgX`$`7B&Vyxenx;HacRpa@XxcNZ@eL5iySJ^VWtH)K&(u*gk>3+c z$dldgbnI;Y|EvKCHS;DKTlM+Qig>Miiy@rNk+0d?+^}ZH$A+u9K`J9SFpMT0BJC5N z6=!<;<{Ek5;OsN~|7>^to2f|2Q|Z`S&DNcV)%)lFxzKb!$giz-1ZzhjqOXUIEfetg zHU}g@#*Fvn@%DCO5_Sf1#4)Pk37xh%ssVxhlXdUv##DH2^_&; zBHa-g5j4nMDl#RpCF0RnSrf84qn{=>NyH0o?K*56wHs$mZzG+*M!wH>s?f)xd1*uY zfkc+I2PSJ){xyI=g@|z6(q=4~@u@j1?GKL+&4yM^O^9LyHwcEGkP6b>AV4Bz^>?;b zs|LQ;>uFI3Kxr;Y=HU%H%g-;f&PRQIMHpbVg=%pq`?!}8Bl72U-jJPpSB_cKp9Qb# z3%|7oQA?WLHEAUO!o4dag_fCo+jpOAQArfn4l{NUr0t0%JtOOOPtv>Z8#g=cw~Sdw zJI~`8TO$r+P?W6sXE14Gcv^D_iCr?>GN8EAg_DgjimNLhA5%%wXFpi$^}pE*r4@zT ztH;w`%dhb%Y1_u+aItlO+yEQ6N79Yo^O9^4CZCjIR<)zip&q66c|B_)DvnqDqdY!O zSe}o+u-k3<1W}*{mWd^W%dM9qe3iAhZ{MHWL;o zM3&w}Yu0H?_FL9yJ9Il_Ub2l&XFnc|2|djP%FhSSFNy%%#|yvw9;ez!Iq(;Bd6XY` zUfcJX-x{m3oQ@z<4c~($*T6eSdMa%U9nJJo0;@e^#y@e_6Bi_dk7nTJH}}AAIY^*| zdZv~A%Ba)lXqlT=bvM2|CwpP{9E&c>ceHMqjq_*HvYt{z8As(_FfV^ViQ=@QZaz{5M`0@{OOf+25AH zOl>T&!GHn{^fJ8Cue8Tyh6f3UiHAnKPW#_C)BS(* z>*+CU-g^lMqno0Gn?lUBcljc+@pD`Zqo9F-NbgbuGF#;pYV!3N39dz?{!lFh)Q zsD0X^j_8qMYB`r^FGfdBusvMW;9h7%nIlJT<>q zE<-PbztnT84*IOlsrTVN zXL(lyPy(x+%mUa#%M^<(>Mx32yn%z^y&rnVQ6#+s>wg?TN^oU5Nv82=P? z@9n7Owmpcpc~GyNjk#ahI>Nu%+A@-Y5g3AyDU&_SA6uBGWGNi}^rG3L)iadOEkyem z9?LHf8>LxN4s~*;#QLBe;}G5>d5;5jc{B1gR3oA2XxRhI`lOoPl_815=gKs}LEoMI zJ-GlNHva&8(F_{K{kc4>0?5E2h6Q2kJFcd&>UUG@o$s_wjgI-rL5zv-$LD55fBM*1m0BV~58&8i@nb_BK9N<==0+_!gkrDUy0IkcZ(z*sbt8uZw;A4mGz_{f}7v%#{Z{@Bg#X^7)?68$%m~-gmcmLhW4? zXpaulAYuX6m^16x?5by4f5bXzMttusg%<>ofQSLfyB`2(>OQ#VuaQCavi=2}4W9{P zr`$--D<9jaFsP6FKU49C!}KxxIZnz#dSwDv8RaV@Nti}>WuYcU8{65317VeZ_RGc} zbM-avX*6cI3mdveMsKQ)XbGN%0n}9sV@5WMBJO@r$Z$Ylh0iR6X_J1D(J^0KnQ;@bNt*<+A^^~wb8KYKxQM39Tz#d*>J;+#k39q#{v%`@+Wj|Bj?3> zHG@G9Kj$N?1ajpk7Vl}QdHsIxFR^m^4bR=3lNvDRw_xUVT_S|s-F3EUe9?=^_1vsh zx0e1~_>+jI#*ghkh34)689&TEnhuW_UVTUSdp-qi6^)@>@VfsHD`#D~_rG>dQ5*%T zix11p#O%<{zU`gUrMn)*V%Fa867{&6A_1^W%f-L z@NkvyFRv9@AoIi?dPfA1li`Y~t62cXX>x`0zYF7(QdIA1f7{>c*S&>`&sipq$8U#$ zrKeM3v@FEI`q`?^CCSrVgTRH>NW(7}U9WWT4ORVbdrig1Lg3)s+rw*vLdReAL5pjL zXNu@RN6!{j3g=>R^_W$^GT(cbcQ?z{@;BP=1IJbt?dEQQM4d@of#+6cCdZG zX}_Rp9OH63o&I#^T`zrNM@(Fd1J?U}rLSlaARc^*B`6Re93LaOZwG(ieJ}L1&G*)J z*s_D~-2V^5%@^de{$Er!rTKoPuH8<}xb(=Od*;a)rK!k<++D&O4?GXwAVb)MjsAF z(Tz*dvQ}u~SulvzDDwO-tk?pE$iy6W=aZQ%t0um9&Wa&-taf?i$&8=9W)K^og1fL$ z%~vx%*dkefv00j>1e&E#gp>zWwBKCEXJFN>4N~dQ5XWn!D;D;wY}7U-d$l?X&zk|5 z?!1tcq@^?P%E(E9jSht{Ac9m6IvRj9eh`6*ch6O)nO@y%nOur0A!B zxZ`a;=ynt06%wSd>sD2T<>B=(m>dXF6YvZIlNG*b94rn4{r;aC=iotd1`-=KW;>BVU*zTb5P?;?&mqDggKtn`$Vr-Fjc97(4u^8>2p^{54*10B{f)K4lSD1@$q z2j%w8ic%x2eBO>!Z+zGkR0WO2jN%4emzu_#nO{K&RXuiNQMpW|6ax#JQ*>KOqhek} zrevzC$iwq7)s?D~FW?9r33_u+ZE0SzP|n`>Euq1r*1d1%#OX%YXQmc$N@(>?=U~IL z2QSB%pm=F(f+%8o^TeviYkQKF;A?3dKsod7G$33AF|Q4*HrU>h*6vK!68Ki#M{VYq z0{pYSlXGH#u#}APLRx#FuP;=B0Cs9e0T(%4v;#?8t3OzM!O)AiI3)I8?G-LaYmd;U6pnrB@Xgn-Lnj9?$L8V;EUK zk*wb1&t9{A?Iwi?xN^|q#X^lDIFkkwruD?wF-mx@dncxo=cIa)R)g!g5XwkC$Ft;g zy9Lx|*bqFgA6(7{=&p0VYlPx7mzq3F?Kee#>i*7>?(Y?8vbSe6W@d+ALKbFw-sQ^? zVGN!aG-3s|7=)`AidNboV`Ik^XG5+HY4IMkxv5E=|# zDK)O0I;9%}0lZa6+i6~eWZg z_6t+Qr(=@_k<|#14SQqvuvl17Kx^zy?YWw{*XE~@z%RY?2K}H~M8yFpp%Nn&+B&#* zkty{?eZ*O{uL{6EeEKY_@V&Z&DfYJM?_*WFIaMfDPLL^0vN5^_X~~XJ1OgpuQNL77{T^O@|jYAkRl+t;25eYwAx! z455c-EIQfks%}>~v$u@0TyTgeffK!Pi2<^mw?JYLZ%5P)BT**K_+)h}dmxWA)%JIT zYh!DrkrJl&gM_Z`7~=`acS83vXH;4y3*g$nV6uGE?EURv&<&C0uaFzgwWyC=l}af1 z(<@HSu9(0+?#y#mf|SUId=TFM*$9E9^*lbs-w8aES=Bg4E9k;n7JT0vd^j5>I&g1j zOkN$gO2n^-e;T8=UapQ+LlwG#NyhT!+uL*v*#`<)`WpHUOs|B2hVJlwOPH9jmvopNy<#+hF4Zw0|>~Xf{@c( z$e@c(38l0T|J2STvU@ZRd&3}HMu?y|Bh-yB(&3`4u^WqKuX~4@DuqK$QqtQ$=42&Q z%_hnGzoUa|Ul@jW;l`0Po#Gwr#C6X3&4B^MYCipGQNz()si31OS7sHV0Ld+4WZM-@ z=g3a5lINL01W;r}6M$({JDu$F85lU!yl|x~!%&xPr#|j`4-hnsA{Y17-`NzbZB2&! z#XHm{zz%WAWu|z@KqyUWgXmy6pkNIk5e3x9V_vrb9f9YFM#I2Sxbs3BgPsTp#*Kq` zVMQg}Mu4qR)?oUz0Oy91L!5e0L20H`viGPMK%2T-qwSN%p_cFSSkaFv9hT}jOWEV+yL3Yqncek@%X z_>~h$RU9m*P|T+SoIC}MEDcJ5Pk*lm&IPZ8^}&}wj)9C;x`7{JA-AKR^0KZef%$=SD5pm=?oxcoj{Zh)VmHfYdHP^(*4W6#qMcex|d z)<9MVenSyVaoa~c-YMxJ^Lcx~al)#$S?kCk0~y05ai?8E9Ex(gx;Y&U0U9A1A|+1O zLqd)IL8%Jb1zkNAbO5Aafp@tV{9_oaJ(<|*IDIb0m)Y9y!CGBmIA#cWq=s2R$r15! z9Y;b*o;o0taTfiuZ$!3(ftQrxy>+CQDVpRCG`jw*)UkJbJKDv(Ttkv*$dyot8I0p~ zpagE$5NOn=3>bz$Ibb8hdq9+iXk95CsthA*c?>eph?v5OGDQM|f{-J32JRswlG>)2 zVY9=1ZzeeLjf`MvGkZO6EpI4#ohDvgWI*sFc-+|O!YGK`Pmo!0RdVY2s!tkVV2~iN zgFP9t6BCn-U_!v2dpa*6dBio87-I3|Djyox8vYcl<;AkWgb#eTP}qf(7$ir8h)M|7 zRnQ=5HqtC*~j^e06>r&y+N% z8Zbp$^9y_ALB)=J!P3-6>gaILjm5-XEtuaZXQlwC2*(BuZjKkFEIwjjicUbJ48)Z3 zbL(j%3Tg;}CWcW?vZi{W5LXmrdzm&n*x@}Xuxoc5BLgWAA!U*WV>P|i>A4Ucod&gh z6_gqTij^hEa!1mT@zEgI!s3CflLfXunscpqS$TY_QfHwyqti10_d5(&Gwqh1AxC}; zaR^>bc#b$9kyh~lGelVCWg{WIBqzM7J0qwCt+~qE(j3F@?>`b8Vo204#$ zzSG3%@C#6dgcs&#?SYt&pTNY3UERAoi<;p6_f;StN{tM|V+D8Vydv7pL?Piit2Onr zh&2tJS3YBbd&v6ZFBP2_Kti@D)PHzYe*$YOqZSG^2RIQJ^tO zjv8$imZpmvxd$2E`@WL*DXMB#X`*OH#y+iUEu7$)FSuFi>k9?e0I}IrlswMn_qV)M zsbNQ~-DL-Au2ITBz4RG`;m9z9F_K96g$wZl1ny{R; z$X9D?4sNpU*$ZblKE!54smG+~^(L2bXHfxaYouwhRp2o2AIV&tYm1M2do35~Br%*E zPhPd(_QofxiRz#+h!q}m>tDj^h1z9YMt|NU5W%qr0aompD{|jVyeqGbVF2y$@xFi1 zNmBFu4%IDOSH6^8L>!BKin9HGwiz~-vMWjg0LX;uUHV*CJE{I#oH;+paQG*YY zlFB8jCap(zL(0^qw0B#=4q1)4Qn%gPr75<|nF?)MR+P^htJ{CQfRK^ctqGD$h5px}y0ApHh%4riu8$|uNl4odEQ7zEf} zoCFKd3D9pHH90Z?)Q08KkTPIk{w7x9*AMWrTTbJdO6P5J7O=%_s@LTc{LPDrx(1W+ z75h1h>-`S#P2TKaF^qGE0&~7>dF$7;vzNlBasZ&2+mt<-jgp2;7*$>#*rV33_uzK# zNpp!wQe{<=IuHOL0QB}728_hvJmQj|o88Ziz97(}0KpM5y;*EbrMSFF5?Ks9;?~Z3Z z4@N{N!r$CaUD^@${?Ofckn%g&5Z`ZN$Ue4**P_yrjJ%WF9}49I^&k^*Wb46u__toi zRS#==tgYo_5+B}y>VGA@g{?PVyYkkf9>-bAjM{}*M} z%hV&TMIbvgMvYb?9?W-$ITWot$X|1(^KAGwa%Q6;7+MPXuzP-c0SZ4!M7r5*AV9u0 zE=2yOU;;qw*@{U70w?>tMg_uy-0I&UI04W^2c%F1B09871dtvY$H+x5e1$~_fRNSA|#H2S0o_fMDnO6*#Q zaClEHEd?b`vVNl&>$|LjhW@a_e}j5;G@*chobj5?_o*UT` zYL2nn87-TD(IaFlm9i62K&XZA#Pd;G_eGmtkff4WK@*N6AbqL21_8-Vb}j0HrFm+L zt!I@h0x>C;NS~{^E$d^FK}9IR`BL=^jd32#Dhhr!+tAT*jB0JsN+E(h87haLbZD+P zVT}^qgalO06!z)3+J8oa*}5L3JYiEvSAUyTg!ZqSDx?!+r}^YMJLw2jFlYqI7y-8B z6yP7F<=rIUzdU~aP1t;`cYWV^`+NVE!IzEl`#nE1MTdy@{9@7MUl~F|$jQqd=k)YB zKIdSsJC-1%AYT+f2kxPVwha>b)7)d^ktE8CzOcDkoqlG?k^wR`EGVYPxyZuSMI&iS z>N17Z@3bc$4|@sb@Q<~(T?6muOtS{ zR~B1W8UALzbljv~vo*wbw|ovfU9?Zw4orK{iT^t_YwmYpTkwRjE1MX;k4!G;uS0;4 z%pe1mFSJ|A>r6ykoTENJ3WvhPHpOl2H|!pHZHFc`tz6z7OLa767G$J^svT&(3h6go zdS{kJ&LOjB<;bYU@bo)L$h^gHn~p5t58lz1tN=|dDiY+UwuHJ+`i4K-z=`@U?YBU4 zM%j2ObY>yo^P!%2z(WHyiiaXO5$tC0cornuz{_&4F2?4{*f9Fb{@18~tnwc(Co0t4 zbzf`-Pi|tH)4=K6=<{^Hy4e5EooUC3GVUsHRxa!iWrZr^jV&V~xOsgSKeKv0 z@NU*&G5i$3_MXPu>+%NfFgp#)hd#Po+CzZuV17O}zwb)$HLaZB4TZ6aV-&HLgl3(V zZ#>@4c)Rt;BR8$qyONTC^*pC3`2x3@6RxLxZ_LxJU*d>l2}|yxZC6Z?oF^ zh)v{{R)HFcaE{KuAI|4y+PH)}s%M3lp=XOkA@MAMF!1JU@Ykp(;DV5)CaQs?@d>7I z^xFUd`dgt3&e3cI4~Yjfm$tRd(@-E6O*3}Qs$Jf3LN0$dNE1!}Mn|IK=#P%^xb$Q> z2ya=+;dpH_8@NqNk#E*qywnH;5J+yO6Wn}Z?d|SPR|6}5DldoFNd`L@P#=>ChZ-I~ z60OjttrI3v3Do-IkErKpYqG7qbJ477H+_T4=OfAQ|JGt}yDLtbUHJYGyOk8OATYm) zea}Hg8>uH=8}p?n;v|jhucP)j z7oKkakFoG6c0a!dp`FU$@3~&rSLXXZKPQvfUzK~`-x^Vq@Dg&7o}~pTJh#auB@|@z zqoRu|r7fRrW~NS0hKf>=Gw+y_XWt3rM*_zEn(@hjoN|}sgo|w8hf%c?`~{ z%9}ED`b;q3t{7p3*Irz=k8IgteqzhA#I>c%UR#VBFEUJpfQBzS> zRcz=lK7`5leEsLSneuyn*V5tcdpDO~uCwOv9(V6vYW}^m+qhq$>)3pbtXJ-;N~)@A zqs>+D*IaSa9WeS|OY^I*mo0O87N@i1U0q#R;pptyyR&lMhsnIci4q*Ijmz~Q-@y0{ z1jR~Dl2<6g>zX1kk%L0S7zrgNOo-+WBTb<}vB$ackuDU0~!i`EmOI z2T?*P>iNH8j?pJ89aRqiUHUZf5SOhVQ9^fJ0odHlk?|m@B1{iWMX4molP6SAnj%pm zMTDRUvY2vYg5^W zHbsV*2U>Gx%5veCD$KgCE_#z4I&kG*WCVFgdUG3~?V(_btAKe-%|~{BN1^I=*)rF1 zTW^txu*B;;9gl?Mmt))?ahNmvGaTp@Ei9Z^u(4IUWV>msX^C%u&6+<=Qs!%!;WSaA zn7-BYcKvAYy$?h1V}3tbJw!!ROouk^X%lwqx#n1g=pTX5$4ol#-2az-&)&01!lHZ) z`1gbexbp0I9oI1n?t3JDqDc_cWu^}rwqU_BL3Syuz572VdK0xxLu~~e=hE7~{w*E! zh;O?hMO6|2ut0x{y33z)G;_ASB!uMS-)m2e98nhgyhtxs_mu7B<9=(ea41apJ6?8`& zR^G%YY9|TGy-VmILW^_c?d@=EHF-5P*OTYzZUV9l!i*wl$DoV|-vI3=RyjzYZdeU1 zsD1N#;o1)HeaHKa&%T3qK!3t-_4YmAr&J#jAb)N-4RQ6Pe-CgEfl|kv_@49c)Qfzs zF0KJ_gLj7Rn0W+V{d3d8cBnltw^F|0n%YrYTU3gyi&EGNL@SYCp^9GsvLXJzL>jUn zBDQ%8o;tpV3innJNe~N9nBWu7x9pDe=9EF#g<$ zb!H6;Md#TlUjOSCu8qi{6iBS+qd}{klw=^4`fKeR0xm=@H|P_~A3Xq$Gp9kREnvg` zU3@pTOKN(6Coxqb;v z^OeNqH<4y6-XQFJkKrt@3ra%(t1qEqn)8+~DW`{Hkz?l}b_iynh$n*JfbQZh`D|_@ zG-udpwA^R=ymHTTU;2=rcyBQnp7os}Um-KtS6WM>P!RfT5 zC-`UZY;0avKduC!Hj#2u)y8+YsBJ^_VJ@Yj{OOReIp@Ci%UExm-5q72uXmB?%l@m} zoI!@8LTV>F<7W?}PaZ3NOO9a1HtO=1$77f4eBf~e0Q|I{%^}<2{^-ca*K%DcXsAX{ z&8r0?(%W-=3;Xa{+HLXsLCzq74B}yldqP#5(;E?6LEf(()W7l4K1Do%?wu<$)R@rF zw(P7&_LzT7)!#Rb)Z6dax_a`@(%WmSn>Cu00tDeoPTR@SzCP@;-siOHHCYey^Xa;m zMUHyakz$U*6X(gDa&j+m;EWSlTrEmQzvA6Ry70?Zj_k$&M?boKD=3mNNn)GgQ``C5 ziNFE_0J`!a+TtzndWCYQ={+ZdNsLKQ4u>2*Tt@G@*JjOQ-b}`~>>p3qpC=Yeo+g7s zb1FgEcag~lK%Os9hYx!84;!Ub6trUpA~5+)EOWXO@jw812%FDEkJTneZ@j_vtf#~5 zEW9s~U$(tNctJ1CE-Vh!{J68L2o2F$TU&paQlg%ol97}dHRL~A@Q{p{p#T+#g2@$4Os0f;M@6wbr*>Xj@^9o`;MY%3>x_)0Iu#{^%0#~hD%v@_+X!i zqAQCQ%W{&igFi1KRjh~#KG~+gC5&xhYYXbGmpOS!gGq8$Ma!BxO^w9rc0u8_ba&@Q z`&@iXjxur?-@th6`*g550iD2(V*Gbtto~$#H!m!K@)iJPcKx!$Y2yO9za2kL5SZoDk0?WilIzVuEKZ+ zKZb<)+utbcnT{3=y{=wgP?hkYda!*A^^4oV{#kN<<#>`uwV(B_WtOnQ-S2Pvp@Ioe z0)!jWXHYw$b@ZWX8=3^ literal 45875 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 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..daefd6b2f7 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -80,9 +80,3 @@ 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/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 096ab8e0a1..4b191d9176 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -546,8 +546,10 @@ def generate_output(self, output_directory: str): write_quizzes(self, data, random) - for location in self.multiworld.get_locations(self.player): - if location.party_data: + for location in self.multiworld.get_locations(): + if location.player != self.player: + continue + elif 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/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 97faf7bff2..9c6621523c 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": 0x75c, - "Option_Blind_Trainers": 0x30c7, - "Option_Trainersanity1": 0x3157, - "Option_Split_Card_Key": 0x3e10, - "Option_Fix_Combat_Bugs": 0x3e11, + "Option_Pitch_Black_Rock_Tunnel": 0x758, + "Option_Blind_Trainers": 0x30c3, + "Option_Trainersanity1": 0x3153, + "Option_Split_Card_Key": 0x3e0c, + "Option_Fix_Combat_Bugs": 0x3e0d, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 21dceb75e8..0855e7a108 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), diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 0ed0a87b17..79739e85ef 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 = 1 + default = 0 class TotalLocations(Range): @@ -48,8 +48,7 @@ class ScavengersPerEnvironment(Range): display_name = "Scavenger per Environment" range_start = 0 range_end = 1 - default = 0 - + default = 1 class ScannersPerEnvironment(Range): """Explore Mode: The number of scanners locations per environment.""" @@ -58,7 +57,6 @@ 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" @@ -66,7 +64,6 @@ 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" @@ -86,7 +83,6 @@ class ItemPickupStep(Range): range_end = 5 default = 1 - class ShrineUseStep(Range): """ Explore Mode: @@ -135,6 +131,7 @@ class DLC_SOTV(Toggle): display_name = "Enable DLC - SOTV" + class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -277,8 +274,25 @@ class ItemWeights(Choice): option_void = 9 + + +# define a class for the weights of the generated item pool. @dataclass -class ROR2Options(PerGameCommonOptions): +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): goal: Goal total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment @@ -296,16 +310,4 @@ class ROR2Options(PerGameCommonOptions): shrine_use_step: ShrineUseStep enable_lunar: AllowLunarItems item_weights: ItemWeights - 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 + item_pool_presets: ItemPoolPresetToggle \ No newline at end of file diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 65c04d06cb..7d94177417 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -96,7 +96,8 @@ 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(player): + for location in multiworld.get_locations(): + if location.player != player: continue # ignore all checks that don't belong to this player if "Scavenger" in location.name: add_rule(location, lambda state: state.has("Stage_5", player)) # Regions 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 18bda64784..f7c8519a2a 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -31,24 +31,4 @@ 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. - -## 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 +for more information on how to change this. \ No newline at end of file diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 3e9015eab7..f208e600b9 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -112,12 +112,15 @@ 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 @@ -291,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 not in self.multiworld.regions.entrance_cache[self.player]: + if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache): 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) @@ -365,7 +368,7 @@ class SMWorld(World): locationsDict[first_local_collected_loc.name]), itemLoc.item.player, True) - for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) + for itemLoc in SMWorld.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. @@ -373,10 +376,8 @@ 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. - 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) + if (SMWorld.spheres == None): + SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] self.itemLocs = [ ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type @@ -389,7 +390,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 spheres if itemLoc.player == self.player) + first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player) playerItemsItemLocs = get_player_ItemLocation(False) playerProgItemsItemLocs = get_player_ItemLocation(True) @@ -562,8 +563,8 @@ class SMWorld(World): multiWorldItems: List[ByteEdit] = [] idx = 0 vanillaItemTypesCount = 21 - for itemLoc in self.multiworld.get_locations(self.player): - if "Boss" not in locationsDict[itemLoc.name].Class: + for itemLoc in self.multiworld.get_locations(): + if itemLoc.player == self.player and "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/sm64ex/Options.py b/worlds/sm64ex/Options.py index 8a10f3edea..a603b61c58 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -88,12 +88,6 @@ 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""" @@ -116,5 +110,4 @@ 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 27b5fc8f7e..7c50ba4708 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -124,9 +124,4 @@ 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)) - 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) + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 3cc87708e7..6a7a3bd272 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -154,7 +154,6 @@ 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): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 2cc2ac97d9..e2eb2ac80a 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.player][item.name] += 1 + state.prog_items[item.name, item.player] += 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[item.player][item.name] -= 1 - if state.prog_items[item.player][item.name] < 1: - del (state.prog_items[item.player][item.name]) + state.prog_items[name, item.player] -= 1 + if state.prog_items[name, item.player] < 1: + del (state.prog_items[name, item.player]) return True return False diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index d02a8d02ee..9a8f38cdac 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 self.multiworld.get_locations(self.player): + for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()): 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/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index 30fe96c9d9..81c4989411 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -21,11 +21,3 @@ 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/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py index 9c96de00d3..5455a40e7a 100644 --- a/worlds/stardew_valley/stardew_rule.py +++ b/worlds/stardew_valley/stardew_rule.py @@ -88,7 +88,6 @@ 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] @@ -113,7 +112,6 @@ 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) @@ -141,8 +139,6 @@ 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_ @@ -155,14 +151,11 @@ class Or(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - self.rules = frozenset(simplified_rules) - self._simplified = True - return self + return Or(simplified_rules) class And(StardewRule): rules: FrozenSet[StardewRule] - _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -187,7 +180,6 @@ 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) @@ -215,8 +207,6 @@ 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_ @@ -229,9 +219,7 @@ class And(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - self.rules = frozenset(simplified_rules) - self._simplified = True - return self + return And(simplified_rules) class Count(StardewRule): diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py index 378c90e40a..f26a7c1f03 100644 --- a/worlds/stardew_valley/test/TestBackpack.py +++ b/worlds/stardew_valley/test/TestBackpack.py @@ -5,41 +5,40 @@ from .. import options class TestBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla} - 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_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_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) + 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) class TestBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - 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_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) - 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) + 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) -class TestBackpackEarlyProgressive(TestBackpackProgressive): +class TestBackpackEarlyProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive} - @property - def run_default_tests(self) -> bool: - # EarlyProgressive is default - return False + 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): - super().test_backpack() + 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="is early"): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + def test_progressive_backpack_is_in_early_pool(self): + 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 46c6685ad5..0142ad0079 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,8 +1,5 @@ -import typing - from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \ - allsanity_options_without_mods, minimal_locations_maximal_items +from . import setup_solo_multiworld, SVTestBase 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 @@ -10,11 +7,11 @@ from ..locations import LocationTags from ..mods.mod_data import ModNames -def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): +def get_real_locations(tester: SVTestBase, multiworld: MultiWorld): return [location for location in multiworld.get_locations(tester.player) if not location.event] -def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): +def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld): return [location.name for location in multiworld.get_locations(tester.player) if not location.event] @@ -118,6 +115,21 @@ 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 @@ -193,17 +205,17 @@ class TestLocationGeneration(SVTestBase): self.assertIn(location.name, location_table) -class TestLocationAndItemCount(SVTestCase): +class TestLocationAndItemCount(SVTestBase): def test_minimal_location_maximal_items_still_valid(self): - min_max_options = minimal_locations_maximal_items() + min_max_options = self.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 = allsanity_options_without_mods() + allsanity_options = self.allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -216,7 +228,7 @@ class TestLocationAndItemCount(SVTestCase): def test_allsanity_with_mods_has_at_least_locations(self): expected_locations = 1246 - allsanity_options = allsanity_options_with_mods() + allsanity_options = self.allsanity_options_with_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -233,11 +245,6 @@ 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")) @@ -409,7 +416,6 @@ 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, @@ -522,7 +528,6 @@ 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 38f59c7490..7f48f9347c 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, SVTestCase, allsanity_options_without_mods +from . import setup_solo_multiworld, SVTestBase from .. import ItemData, StardewValleyWorld from ..items import Group, item_table -class TestItems(SVTestCase): +class TestItems(SVTestBase): def test_can_create_item_of_resource_pack(self): item_name = "Resource Pack: 500 Money" @@ -46,7 +46,7 @@ class TestItems(SVTestCase): def test_correct_number_of_stardrops(self): seed = random.randrange(sys.maxsize) - allsanity_options = allsanity_options_without_mods() + allsanity_options = self.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/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 3f02643b83..33b2428098 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,57 +1,56 @@ -import unittest from .. import True_ from ..logic import Received, Has, False_, And, Or -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_true_in_and(): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + assert (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_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_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_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_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_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_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_true_in_or(self): - rule = Or(True_(), Received('Summer', 0, 1)) - self.assertEqual(rule.simplify(), True_()) +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_false_in_and(self): - rule = And(False_(), Received('Summer', 0, 1)) - self.assertEqual(rule.simplify(), False_()) + +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_() diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 02b1ebf643..712aa300d5 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,11 +1,10 @@ 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, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods +from . import setup_solo_multiworld, SVTestBase 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 @@ -18,21 +17,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: unittest.TestCase, multiworld: MultiWorld): +def assert_can_win(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def basic_checks(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def check_no_ginger_island(tester: SVTestBase, 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(): @@ -49,9 +48,9 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestCase): +class TestGenerateDynamicOptions(SVTestBase): def test_given_special_range_when_generate_then_basic_checks(self): - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange): continue @@ -63,7 +62,7 @@ class TestGenerateDynamicOptions(SVTestCase): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not option.options: continue @@ -74,7 +73,7 @@ class TestGenerateDynamicOptions(SVTestCase): basic_checks(self, multiworld) -class TestGoal(SVTestCase): +class TestGoal(SVTestBase): 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), @@ -91,7 +90,7 @@ class TestGoal(SVTestCase): self.assertEqual(victory.name, location) -class TestSeasonRandomization(SVTestCase): +class TestSeasonRandomization(SVTestBase): 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) @@ -115,7 +114,7 @@ class TestSeasonRandomization(SVTestCase): self.assertEqual(items.count(Season.progressive), 3) -class TestToolProgression(SVTestCase): +class TestToolProgression(SVTestBase): 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) @@ -148,9 +147,9 @@ class TestToolProgression(SVTestCase): self.assertIn("Purchase Iridium Rod", locations) -class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): +class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue @@ -163,7 +162,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not option.options or option_name == ExcludeGingerIsland.internal_name: continue @@ -192,9 +191,9 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): basic_checks(self, multiworld) -class TestTraps(SVTestCase): +class TestTraps(SVTestBase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = allsanity_options_without_mods() + world_options = self.allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.option_no_traps}) multi_world = setup_solo_multiworld(world_options) @@ -210,7 +209,7 @@ class TestTraps(SVTestCase): for value in trap_option.options: if value == "no_traps": continue - world_options = allsanity_options_with_mods() + world_options = self.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] @@ -220,7 +219,7 @@ class TestTraps(SVTestCase): self.assertIn(item, multiworld_items) -class TestSpecialOrders(SVTestCase): +class TestSpecialOrders(SVTestBase): 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 7ebbcece5c..2347ca33db 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 SVTestCase, setup_solo_multiworld +from . import SVTestBase, 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(SVTestCase): +class TestEntranceClassifications(SVTestBase): 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/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 72337812cd..0847d8a63b 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 = {1: Counter()} + self.multiworld.state.prog_items = Counter() def test_sturgeon(self): self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index b0c4ba2c7b..53181154d3 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,10 +1,8 @@ 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 @@ -15,17 +13,11 @@ from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Frien BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods -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): +class SVTestBase(WorldTestBase): 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) @@ -42,73 +34,66 @@ class SVTestBase(WorldTestBase, SVTestCase): 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 -@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_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 + 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 9bd9fd614c..2cdb0534d4 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: unittest.TestCase, multiworld: MultiWorld): +def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) -def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): +def collect_all_then_assert_can_win(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) collect_all_then_assert_can_win(tester, multiworld) -def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld): +def assert_same_number_items_locations(tester: SVTestBase, 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 36a59ae854..b3ec6f1420 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -1,17 +1,23 @@ -import unittest from typing import List, Union from BaseClasses import MultiWorld -from worlds.stardew_valley.mods.mod_data import all_mods +from worlds.stardew_valley.mods.mod_data import ModNames from worlds.stardew_valley.test import setup_solo_multiworld -from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase +from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase 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: unittest.TestCase, multiworld: MultiWorld): + +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -24,7 +30,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.T tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestCase): +class TestGenerateModsOptions(SVTestBase): 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 3634dc5fd1..23ac6125e6 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,14 +1,13 @@ -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, SVTestCase +from .. import setup_solo_multiworld, SVTestBase -def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): +def basic_checks(tester: SVTestBase, multiworld: MultiWorld): assert_can_win(tester, multiworld) assert_same_number_items_locations(tester, multiworld) @@ -21,7 +20,7 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestCase): +class TestGenerateDynamicOptions(SVTestBase): 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 e22c6c3564..0145f471d1 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, SVTestCase +from .. import setup_solo_multiworld, SVTestBase 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: SVTestCase, multiworlds: Dict[int, MultiWorld]): +def check_every_multiworld_is_valid(tester: SVTestBase, 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: SVTestCase, multiworld_id: int, multiworld: MultiWorld): +def check_multiworld_is_valid(tester: SVTestBase, 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: SVTestCase, multiworld_id: int, multiworld assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld) -class TestGenerateManyWorlds(SVTestCase): +class TestGenerateManyWorlds(SVTestBase): 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 bc81f21963..0265f61731 100644 --- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py +++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py @@ -7,40 +7,45 @@ class TestBiggerBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.Mods.internal_name: ModNames.big_backpack} - 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_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_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) + 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) class TestBiggerBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, options.Mods.internal_name: ModNames.big_backpack} - 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_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) - 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) + 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) -class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive): +class TestBiggerBackpackEarlyProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack(self): - super().test_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) - with self.subTest(check="is early"): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + 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]) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 9bdabaf73f..02fd30a6b1 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,17 +4,24 @@ import random import sys from BaseClasses import MultiWorld -from ...mods.mod_data import all_mods -from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods -from ..TestOptions import basic_checks +from ...mods.mod_data import ModNames +from .. import setup_solo_multiworld +from ..TestOptions import basic_checks, SVTestBase 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: unittest.TestCase, multiworld: MultiWorld): + +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -27,7 +34,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.T tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestCase): +class TestGenerateModsOptions(SVTestBase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in all_mods: @@ -43,8 +50,6 @@ class TestGenerateModsOptions(SVTestCase): 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): @@ -98,7 +103,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase): self.assertIn(progression_item.name, all_created_items) -class TestModEntranceRando(SVTestCase): +class TestModEntranceRando(unittest.TestCase): def test_mod_entrance_randomization(self): @@ -132,12 +137,12 @@ class TestModEntranceRando(SVTestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestModTraps(SVTestCase): +class TestModTraps(SVTestBase): def test_given_traps_when_generate_then_all_traps_in_pool(self): for value in TrapItems.options: if value == "no_traps": continue - world_options = allsanity_options_without_mods() + world_options = self.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] diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md index b69af591fa..84744a4a33 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -31,8 +31,6 @@ 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 diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md index 7c2e6deda5..e443c9b953 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -35,17 +35,9 @@ 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. -- - -## 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 +- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index df857f16df..ae53d953b1 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -6,7 +6,6 @@ - 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 diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 9e784a4a59..5e36344703 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/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 87011ee16b..3905d3bc3e 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -42,22 +42,11 @@ 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. - -## 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 +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 diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md index f08902535d..18474a4269 100644 --- a/worlds/wargroove/docs/en_Wargroove.md +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -26,16 +26,9 @@ 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/witness/hints.py b/worlds/witness/hints.py index 8a9dab54bc..4fd0edc429 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(player) - if location.address + location.name for location in multiworld.get_locations() + if location.player == player and location.address } always_locations = [ diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index a5e1bfe1ad..1e79f4f133 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -329,22 +329,23 @@ 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(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(): + 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) + ) # debug_zz_loc_ids.sort() # for name, id_ in debug_zz_loc_ids.items(): # print(id_) diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md index 06a11b7d79..b5d37cc202 100644 --- a/worlds/zillion/docs/en_Zillion.md +++ b/worlds/zillion/docs/en_Zillion.md @@ -67,16 +67,8 @@ 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 b8948bc4958855c6e342e18bdb8dc81cfcf09455 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:38:45 -0500 Subject: [PATCH 056/143] fix things --- .gitignore | 3 + AHITClient.py | 236 +++++++ data/yatta.ico | Bin 0 -> 152484 bytes data/yatta.png | Bin 0 -> 34873 bytes setup-ahitclient.py | 642 ++++++++++++++++++ worlds/ahit/DeathWishLocations.py | 262 +++++++ worlds/ahit/DeathWishRules.py | 539 +++++++++++++++ worlds/ahit/Items.py | 286 ++++++++ worlds/ahit/Locations.py | 977 +++++++++++++++++++++++++++ worlds/ahit/Options.py | 728 ++++++++++++++++++++ worlds/ahit/Regions.py | 900 ++++++++++++++++++++++++ worlds/ahit/Rules.py | 944 ++++++++++++++++++++++++++ worlds/ahit/Types.py | 80 +++ worlds/ahit/__init__.py | 334 +++++++++ worlds/ahit/docs/en_A Hat in Time.md | 31 + worlds/ahit/docs/setup_en.md | 43 ++ worlds/ahit/test/TestActs.py | 31 + worlds/ahit/test/TestBase.py | 5 + worlds/ahit/test/__init__.py | 0 19 files changed, 6041 insertions(+) create mode 100644 AHITClient.py create mode 100644 data/yatta.ico create mode 100644 data/yatta.png create mode 100644 setup-ahitclient.py create mode 100644 worlds/ahit/DeathWishLocations.py create mode 100644 worlds/ahit/DeathWishRules.py create mode 100644 worlds/ahit/Items.py create mode 100644 worlds/ahit/Locations.py create mode 100644 worlds/ahit/Options.py create mode 100644 worlds/ahit/Regions.py create mode 100644 worlds/ahit/Rules.py create mode 100644 worlds/ahit/Types.py create mode 100644 worlds/ahit/__init__.py create mode 100644 worlds/ahit/docs/en_A Hat in Time.md create mode 100644 worlds/ahit/docs/setup_en.md create mode 100644 worlds/ahit/test/TestActs.py create mode 100644 worlds/ahit/test/TestBase.py create mode 100644 worlds/ahit/test/__init__.py diff --git a/.gitignore b/.gitignore index f4bcd35c32..b9ca4b8d28 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage +/oot/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -196,3 +197,5 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini +A Hat in Time.yaml +ahit.apworld diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 0000000000..884f3ee5c7 --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,236 @@ +import asyncio +import Utils +import websockets +import functools +from copy import deepcopy +from typing import List, Any, Iterable +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from MultiServer import Endpoint +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser + +DEBUG = False + + +class AHITJSONToTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(node) # No colors for the in-game text + + +class AHITCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_ahit(self): + """Check AHIT Connection State""" + if isinstance(self.ctx, AHITContext): + logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") + + +class AHITContext(CommonContext): + command_processor = AHITCommandProcessor + game = "A Hat in Time" + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.proxy = None + self.proxy_task = None + self.gamejsontotext = AHITJSONToTextParser(self) + self.autoreconnect_task = None + self.endpoint = None + self.items_handling = 0b111 + self.room_info = None + self.connected_msg = None + self.game_connected = False + self.awaiting_info = False + self.full_inventory: List[Any] = [] + self.server_msgs: List[Any] = [] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AHITContext, self).server_auth(password_requested) + + await self.get_username() + await self.send_connect() + + def get_ahit_status(self) -> str: + if not self.is_proxy_connected(): + return "Not connected to A Hat in Time" + + return "Connected to A Hat in Time" + + async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: + """ `msgs` JSON serializable """ + if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: + return False + + if DEBUG: + logger.info(f"Outgoing message: {msgs}") + + await self.endpoint.socket.send(msgs) + return True + + async def disconnect(self, allow_autoreconnect: bool = False): + await super().disconnect(allow_autoreconnect) + + async def disconnect_proxy(self): + if self.endpoint and not self.endpoint.socket.closed: + await self.endpoint.socket.close() + if self.proxy_task is not None: + await self.proxy_task + + def is_connected(self) -> bool: + return self.server and self.server.socket.open + + def is_proxy_connected(self) -> bool: + return self.endpoint and self.endpoint.socket.open + + def on_print_json(self, args: dict): + text = self.gamejsontotext(deepcopy(args["data"])) + msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} + self.server_msgs.append(encode([msg])) + + if self.ui: + self.ui.print_json(args["data"]) + else: + text = self.jsontotextparser(args["data"]) + logger.info(text) + + def update_items(self): + # just to be safe - we might still have an inventory from a different room + if not self.is_connected(): + return + + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.connected_msg = encode([args]) + if self.awaiting_info: + self.server_msgs.append(self.room_info) + self.update_items() + self.awaiting_info = False + + elif cmd == "ReceivedItems": + if args["index"] == 0: + self.full_inventory.clear() + + for item in args["items"]: + self.full_inventory.append(NetworkItem(*item)) + + self.server_msgs.append(encode([args])) + + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.room_info = encode([args]) + + else: + if cmd != "PrintJSON": + self.server_msgs.append(encode([args])) + + def run_gui(self): + from kvui import GameManager + + class AHITManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago A Hat in Time Client" + + self.ui = AHITManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def proxy(websocket, path: str = "/", ctx: AHITContext = None): + ctx.endpoint = Endpoint(websocket) + try: + await on_client_connected(ctx) + + if ctx.is_proxy_connected(): + async for data in websocket: + if DEBUG: + logger.info(f"Incoming message: {data}") + + for msg in decode(data): + if msg["cmd"] == "Connect": + # Proxy is connecting, make sure it is valid + if msg["game"] != "A Hat in Time": + logger.info("Aborting proxy connection: game is not A Hat in Time") + await ctx.disconnect_proxy() + break + + if ctx.seed_name: + seed_name = msg.get("seed_name", "") + if seed_name != "" and seed_name != ctx.seed_name: + logger.info("Aborting proxy connection: seed mismatch from save file") + logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + + if ctx.connected_msg and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.connected_msg) + ctx.update_items() + continue + + if not ctx.is_proxy_connected(): + break + + await ctx.send_msgs([msg]) + + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logger.exception(e) + finally: + await ctx.disconnect_proxy() + + +async def on_client_connected(ctx: AHITContext): + if ctx.room_info and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.room_info) + else: + ctx.awaiting_info = True + + +async def main(): + parser = get_base_parser() + args = parser.parse_args() + + ctx = AHITContext(args.connect, args.password) + logger.info("Starting A Hat in Time proxy server") + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), + host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) + ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.proxy + await ctx.proxy_task + await ctx.exit_event.wait() + + +async def proxy_loop(ctx: AHITContext): + try: + while not ctx.exit_event.is_set(): + if len(ctx.server_msgs) > 0: + for msg in ctx.server_msgs: + await ctx.send_msgs_proxy(msg) + + ctx.server_msgs.clear() + await asyncio.sleep(0.1) + except Exception as e: + logger.exception(e) + logger.info("Aborting AHIT Proxy Client due to errors") + + +if __name__ == '__main__': + Utils.init_logging("AHITClient") + options = Utils.get_options() + + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/data/yatta.ico b/data/yatta.ico new file mode 100644 index 0000000000000000000000000000000000000000..f87a0980f49c3cf346af8c288bab020eb77885c6 GIT binary patch literal 152484 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 HCB^>%Zp1Qi literal 0 HcmV?d00001 diff --git a/data/yatta.png b/data/yatta.png new file mode 100644 index 0000000000000000000000000000000000000000..4f230c86c727f4950a5c6e6b62b708b4d3d12f8b GIT binary patch literal 34873 zcmV)&K#aeMP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rh0Tu%u0}Iuo@BjdS07*naRCwC#{dbgP*LB~E zf6lp~a_F3Ux~C`S8DM}x1_2NRzyJm@h>|EOS$fKT&;D8V^Ln=IXZcB9vaOs=C5sY8 zikTn?fXEq>bLM%VRq@nt*Rcm^xtE+C^d%kDy{oQ+iH>@73 z$Lg_qtRAb!n>oblCAjA6CB1)bAm%;lfQa|J0vuo-NCR154oCqxAP>0S>sF6dfZR~m zp}1%z5CB3z6fl7XU@OoAGk;r= zrDYE(3=C-l5Vj)NRuqAtHntH{Y9nLV352w~KQKWWzEDP%%*W{TQD#z?5hzq4L%xv0 zaWWK~6&VHB3-ITBF4=3qK`*A6UUkJ*cKk*k7&fFv5pEt_7bOJzm)-w-021}$hueS; z0(-sqLHYveMPmmkOu}}ONT43e2oaBT5e+sWfMlqiWVj84AQWgqN(+>NP#u^7{j(j> zS0Cl!)ABB~U@qOqe0Ch==E>)$nM?PRnjd6j{4lxvG;`@bvbiZ-H+Qw~SBzzHz-bRK zF9CwA6rkzuB9&4LoC>YWkv|twSYn(unY@OC4()HVY^sluiQ^28 zzRJk>Yvgj%WU}KZmArl#~cn+}uSK$16Q>RM;i&#R7_pUP*0#gpUUh0&o!s zZ}k>IrMxJG5=c=BAq<2tNyOF>47SnIco%c&ex_&6Ff{rK^Vu;bXHJoxAJU(rT!nH2 zlyfz!$3HU`t4P=04*V?e06Lf9Uxf$)gafs7*56E1aszEOJBbAv9K#4Yj&co95+McN z;&+AA0Pz;F^et+?DRLK$>xEvNLV%P$Yhsk2FJYnj?}hKw%kEa`B_%dY;3`V|g6kGM zcdNkA_^a$c@!y$ET>|AgIQbm#Ux5GV#U-o9KNAZgJ!a~2z>lC4dxjqtrV*qjx`w9s zHo6=3(VX0jAuSAHqCmRJDLFkTrIa*;@>z(*4z=WdBC2t@^c#rkck@@axZeA?RWZxL zvnaY$hGAowjewvt@F1g;hdFuSZ*U9Q;)?^TF4jLUfCPaL06z`f1;q6_GfIR`BHYQk zrh90w-9uA+1BMZx=pZS*z)BWF$H_vdiuEgeP+OSxUa;Q$ZkF)CQVj;BJQE}qLQX6O zl4?+?Mc27fF0Lwo5C|lO2!Jb!Y=Ym6tsegj08;B&jGsqm!9{6-879#{1MM}tS=V?s z4Y3}ARvd)z*e9iG5wQ_5N>oLcCwIDxu04ciuQQ)({zee|7lvbm}pe4SA zjjiuzUGx2zMg&(mxXRVk>{4(LORld)t%zERt&)p?Zh;52qGc8vehOi?#frl;TaE?CNoY0i@dt#D9Wv{S*KJE6RrE_p_tpqr}6ls8YS* z@_Y5loEBNZW2u#x8?ls2wO9|;5<&f<=OC_mc`IC`G?9i~Wa~iZhE`py9}s{@bcx8X z0FU~?A%!85kq)+Xe1y#{AHXm|6w0(MQZ8IeMbfnB)f<3RrL_3MumIPqrIIB(qOP_= z)zxYMec%vs1+O(cmbQw+QcK~`E&Xl5NfQY*5RY{;IeWJF3fps4SC1PUAOg4*_z%GQ zyc{VQ1$j=&aj|WrT2id!$n9+mq^dYA<>g0K8ZT4@(V~jh6-#XU6}gh7 zFM(R-ELM6Mg;@2feh>lD4g3;tw>}ro$q@!E$*t_{`V`#_w;@z{Im;EI?y7sdD%MF| z|y_sE| zpP(hV8ITlwDLwxh^1CC>0;i>V`8gHwhGH>yt9W+RSwA0;l=2D^vuTvwGZs08waBgr7(EZ-k0VNeF{A^|!F6^M`1Nu15+J zrQ8*%Bdo+l5lWRRam9*oyo?p8$SjKG7r$7Z>2k&OUCf0=kVuy3hgm(|`v3`{OGiGB zuBr0wv81bhFE@98in?$YhOl`18sfxN-dQZoU@dA7mx8cjY3jGSq3X?>3Swp4UwEb)m;WVx@J z7%avsivi6aO$mQVGS`w-SAZc*f`&i}8!#2*i^60t&?Qei2SDlL9>HJ7UTlTq9-+Jlb$z-+MCnCHnrT(uFj7WFe9bn5ykb9 zx^kH%vp{)TR0h*Z7V{b_thlXiqZNLe=ZjfX5hS9@l~Uz;DkZ(dpIGGFuHi-+L2NTj zHlHo#Nu#SV$a^0k_X0nLuk0ZaYGGZ|y+i`F6+!v60z|QLdG+Oc%Zow#RRH2IP*ha_ zDS<>RVNpCw;+Ca_`~`>ldF4a8TzLTI9fmY94O?XMDDMrD6m?HmkM|@%)_LGqQ+y(1 z*RZ$e^E4;7T{mNOtxicvy`-9=6>?>Kn7{t2SOvTw04V0=sw)}&R>jc**A?&;*X{MH zg)UYMU-9Sa@!p6*ig!8$d=hwn(GLz-F}j=Xp|y4oN*JYO^k%gC-=6&X64s-*hF#YS zpmi;lP!7T^c+Y$FS<3N#F8-bJy@4uQBIQ;*uK-s`ug~SpToHk3hOn%dAC;k1fV{UX zh=0r7)F`$2&Ngq^%j75TNnI73vz9?XSpC-_&VQ(*VR{i`0^Oi?+p4B zPE_Ud!V6PeB`_*tnH64m0hSfP)&Rk`Nv+mny~iwwLgzew6g`W>1qcV~>1ny2y7+pE zq6*?Vk$COy)gqrNFTVGyG-PD?N>(#gIKt;`sv~f#nt0I zVnLK=(m#Nf>9GN2NQ-qX_tR0k2Scg1gwR}3nxMKtN~kJLT~Q_8RuymKE7C1__~qBH z_?xJzH!WHY_p$d~3NXrmqBuyNCg<+Wo7WPK3|Za((|7 zj@O1Fz%6&ONTd7)OSj)TfP;_OJLRnP@GmJBsfsg+X`YoixQZROCCZ z$8xO9ddLMXmg{G$p50XBc*1s!?xs74 zhMRCJIc^sgjb79ub|Ju$MDK!=>gA*D1VQ-Ur(iKRWFbpb$z4SRM`zH%4+-F zEvZU>U+HccfQb+V48zt_t5oUU)_SqvYQf^W>|%AIY26w?DHMrFJ8kthVH&p2db~Bk z`O59hBGQSjVfx2WTRwoS9`7PROms=f zp5mgkj1X%Y?<5)S^l}7(w?dJB#eXeiMTWd$MwIeDSom0kMJPi+T?0tevRJK*Ig0P= z_`u@lGaY4kisNjC8{d}60Ay)zUwYlJMaOJNw5VdqQMTD6Fad8m1ktUMHXa5 zZ{;gx*Sl7x=1qNXJ7pwkyl5wT;?20afS~7vH;4UB$O?Rz**Xz zR04!lm{tgc>A$zFTw^Ze$G!}~Bpm!(M0yk@O*r=TRe)U2u@Tr+Tna)MwAbB4O}K-1 zZ3^*H^;N};_k!R1s%fhS*|4xKab=Lb+AM_SUb9$@mbmKnA*8Kmx0jD7*N{#BEeB!} zP9H+9dl+&K%CO+v*CDeCkT-RN(29s%#V3;CHahEX#x~#j9M#GV(IQHUN-bWjSiNY4 zTam1LJI-$T)2(vj7Bpuq0S;keo3UzutTK>93$R=%%^Z)|3k!;H&Fka zgP7|D$Xc{2Vn7P1OleaW>7XgT7AcgT+3_ZrE5B!=szOy!MV(#M#ZurRy_R0DP*BLC z^Im4R{F!d)6Pa@>GwUmN0E-1yD~LmujA@KTteiyRmRvJeNn9jSnpkG6x&h5Z3t3-P zY;x0X*tA=>>WVDnlDKj^h&Q0_b{KqVHAsImTjpL$oq~d(6{V};R)S{K2ab1iFJ6@O z5m$Bfu1;equ>zLtE&diOyH@4%4p{N(g2jNxWL_`JQE>9g3)0tluGZ}8a)0LlQi~R1sL?U5q&c~bx@h-OBf;L`V_~JO zKp}Cgyb}QF@PhMFhBgsuqc+m@*3wn#YSF-o6nHD&);}La z`Npt`B|hBetGI6}YN~SIg!HK>mfL-GFfLI_VoHa)a3A%NEA-UuXMNop)Q9>(WDy<_ zWs=A6I{TH9D5cper37wel2d`I2RdPH|7zg<_5(zq720nCOq7do^CUyf*oIxPPr7d* zR)kOe!|_p>vXnd%D||17RC;Kq62i-_Fa5W|bFb7@G3!F9iq@B;EGfb3;z{KZ0h;kG zf>0q1{XH6^19aCMWpmTZw8qX8Ge=5RO(>msbd`gne74bbQLck4Tx?ttb^+T^WCb|- zBW$3Bl})BwJ+2cVL0~t!z7N7zo1`jIW7RcFa%JzO z=UJ6pEoVs$U6uxjQdNPz@9#zRR9&_3A0d#kKr}c@d(8!!V*PAyd6i`7A^|h4d*}-i zp&Sh1pb7<)>sdXoV;AL=);nfllVefvMFc|Mml@>7ebzQY13)5RLAREd@a%gPfC z&akfeBB6o7Q;wf^lu2m{xj>nrxO_*_m`DZ1+gv7lf)UAkr#a3@-2 z=A~6Da_s^4wH%ITvbUl$w4OWfs_FSbF+O*hkX=JG)P&(Fwl9}2Tk81*qX|TjKN1z7 zk&9NmUQ6^WiVdsU)0`Y;XV)Qa zUH?40x?ZO#F-X{+!Y~|!bde&D#6>6 ztR{hJU_UUwy6oRRfVjX#;EZQUI{bc3Iy)ssX3i1~Hc%63#qj*{r9g7608v%e;!OiY zpcixz5JEd!3+99f;eEC$!oc^MGRsxm3WP82P)Z<##4@3?sgHFn=Xu|@r`fmRRW`Mq zq&7N)WfU-F9wBlV(!nwc801S`kA~2VT}pZ4a;i#C%P)EjSW+>Y)&QAIL#9yrrR8mC z=Yg}V_Nc#Y00CXoa{+iAoe$}tG~?V9*}^PCQ^%1)QXlWZkd?zY-IV}DEe4QkZ7P9W zG31samqf{WxS|~GR07E#2g!vX76R$@x+|sWTGMg}2j=N&>EoWAFY~}n&$6}i1TFDC zV!=72aBv+5Q{=Hk7OC=>(m@Ir;kqRcPU50`01-lXAn^mFd^Q3_O$Y|36$kqi^Ofar zbzZPO1WZsCbWp2+d20buT!F*D%U(Rvt^d^yLEcF*K6?QvO_GrgOlenn;|nR$#U>>e zOPqw?&-U%(@=ejMKFZbiL88PyiDj&wZ>FrM9Dwe+0?R7UQa{R$btkxY&&%Ao?KOH@ zE)cLXm@=oWfzY0+5P5Aqq~}fv*SF~1(oRvd8bxxU1Q6wyMs>X_Yv39-;ef%hOL_XH z7ku$fkAb`yU1+)!=1=SBV6E=?1sX?Ve!$*D z#vyjBJI9vpbJQfJh=x-*1wo-uj3E@tb;=^wt^;mC|KG`bVsZujaC9*4I1a8`Kq(hl zP~f__ZXTsvl=7})@o!M%M3A(IQOG&`)i;Luqt_IvoLVlPoCnhAhQTb50>*&Lz$Ks; z81VjonH3g1{&Qxr7F%gU!2beH1OM6EnGAq*Zi2&?zl0Eq9UULVGJ>V0cD;xe*T`Be z&Wu&EPmA@96mum4g(HkoU6d3qiP#)XwG*su>1Xezlhnp$u>xt7aJ||oKUJ(M)l?RI zkPoy{g6rxyNyvr8pQqQqr zl*>a+e1(7y0l$iF%e6qy(QB{S&AxR%MO|#|l9j*J#omhnr0fZ> zEJ%s~x!RqsCJ8(;3ENWCCT3XId6_+%Ptj02K`@ZRFr9J{rP5_96@qa7e7{n-jvG1TgT*VJtt4MFfkKiV$NnR z9Rg(&wgZdaXji4k=B7OGX31nmQA#m6eU7Qwi_B+6nV306p^#!Wb;*l^mIceKMviA^k);aC{3FYaRLT~1YiL{ct7 z)UiV)Kq^bu)dELMdG3gm1?rP?Y+ier?d#9cR5wl}obu>gFfB)ygOmW`ixaA12Bj3P z>!2JLg?5{8^mlSy*DE7&^f#~q7?C(K7RL%lu^O6j2r@epCX=-&I0lE0Hge%&ExrA% z-fe{K}#^VI)l1Nh^q#~7#F*I7s#mkLcINQM3 zSe(h}7&Dm&dDrx|03Fu|;is>!!tD#kXH8_8ol~wa?h+n{UT9|OMV#VV;)fGmA90iOYW4%ko(F=C-+Zd>=$tZ%-zav@g) z$JY)J!Y*EFG76q$~N{gY&)w#u= zwdGhGAP5&ToB%6`iZ)?HB1lt`O}SjS*uaI}X0D91aH+46sp%-f+d+g-xI+3!g{q;U z*9Z`&`g<&5$E8pRp)*hjNhUK!CO66W^ce<5-(YOwAd^$aaNPw$9eLoS2a><@sNx&y zc&7%#dl{UfhDXUgC8R0#)EhK_+K2G@RtM{asuhY@lzb zj`VyI$2E|`#T3rlauXc?xD>CFY{6^XqI;?kQde1Ef9r3PRUSZ;5`+R-y4wf2>*gcewDk;~E#nxnGWoU;99Op>%2!M&gozQ1 zVb(Qa#F7Y8H%(1WN9h}>;hC4V^4QaRIB;qmBNH`b@bau5YsI`i2V#5Qa-j z{TR1yKf&gnOVlQ3F$9=WEU;LMA4Ln|TmX0!$wjv;It<+zNG+nnib z=HQ7gE)Fy>Jenk5FpySR4#M)4uoyUR{oPjJO8=ZkS>ea6R6`0v`|z|VU@yjlgw@<$B#81T!$ z9_?NW6}D^S){c*g&7B`3WJWPcnci~HyX@Kf^cD(A;wpg@iex;+=AJ9;*>Z-S&MTN^ z9_7kXQ`E{2w-Q9k)R>n7v!(_k97Y%tmjKypfXV4FM^3hK>TD|)ue30i4v}-Do*K0v zZt($Ks>=LIDUR}4MOEH31WLE!3W3DYRmNV^m!AXy&+QZnL#on%Q>83~kVr!yObA9z z?0_U*V_^pkq||*cMNJ4Hbh5VOmN{N|J|LxyQi|_A`I|ib%x~coGQ~Ud4d9nNI`};g zkat@zY1U&nM!kH=KBbh*=BL&1%U>1+H_!T(2dE9TAca|32fxx~>}t86_T~}Zck2;$ zZ0sdqd!Kp#144?-RArXd!utONJI5HdnDVUiFGBFk7_~~{I9qZt7exK)47B%GXH= zgey==F+V?n4e76t-m)!67k&zi14Vur+^uB=KX#U zNyp7`^2%43oV&n|&L5&RxfOv#m3sTG7VHQdS5j9q&4#Wk?A&;cwH+e_1G!QF{^r^s z%8OQ5VT@o55s4v#A%tx)Gi7l3a)M(g+ql%%z{S2sa(Sb)NJNE{yXkd`S4h72Ncn{d>|3Y!=KiDfUOiu3t-GMQOYb7Q3ECYYTaVr1kb z6O+B(_cAc;C{2wUS=+UbNTh{;9VMHY^WUw!7lY=wLCR6yZGaRnP!8QM-3R;&M>(H! z9k(5nx;*xpaNIn(whytU{#FcWRxn0oW~Lm{T1FmIy0p|zbI(mj*t+37k#I)WWGQi# zds9UYR4|{i6y#WS&B#CqVOS{BB$cu__IfQRPd9OlTW}uqtp=ZlV zmzs#CZg1Xpmd3gdP(0t3epRu#urKMqI}kt}B_Fu{e3MmIDWxICmyV+KG`< zL660fo*axw_VSUqx`ql@?0m06*dV+1B7D%f)Sfg*zzjtMai<|at^bzW6Wo! z8R$RA>671OYU&D?`wo!J&*L~b9SN?$)in$oDGd*3`657OfL{lGpK_g$TD2h8c1!|) z>BS|#25eHw4W+XaynOc0$>io(*L*jzU_+_5q#C0$q|FIXX5PNU(fk-b!0NS?#Y$Hm(2DkG-2qvcOk1y!*%RY zOROuUv#f??Uo(=j%7T}DdfYg(#SMY-A~(Jq)U49sLLmwuNohvMqFv=~xo;<6zBECl6s+Qer1H=+l(1s)!31ZnMkz|l?JV+=K zKuU>}eyddgTt-KG>AP~6OPAlE_tI;OPxLZ9-H&ozU6xbqxb&6DNCBo?COY#@t*~@> zHw65-N6wt3GOMmu0dj4Jf$mWJm%t-2A1{y&FG3Yu#oHso2;)z-@a@(%n9c z5c!H`tZUwwg`u*+lgXEqhVaZO!A-WqBe&2!S-D217JRZBqg(U?L6Sxlf|>5w7cy%cYr}8{qKKZ_?LygmY(~ zB{ernzK~sNY=treDqtd7qllmd&2fm?kauA&SH5h+b1*y$c?ah6C1uCOjw#>?;J=~8 zKjeGW-FmOf;yb{L==z-$@JS%(xH--bzrcKcn7tc5!PYff_{amVml~MVoAL2W#fB0i z7Q=|fArxVHLUQ&@4Tlaia`Jc$*{tD>Sr=7xP-UWZ;r_E&Yp_?;=$9;Ac71rScU_c_ z7?z104iHO*iNu2hf)=4@5aH3X_@+!33OUm0NqR57!l~0wa=HHySNaZX$2?vvWaNk}0+t26O&=K(AYDx33z?G6#02vH>+hveKxDH{O z^j!|~+*3`QI$lpYZC13CyV#yGSJ4u90uLq2QFN=yBGn762tGmUrS=DZ2P_z-AQTG{ zt%(t1uuNm$E&M+|dLZ8=y9_IIkvs#xj+IzG*l)1XreE+$(l>Dr5XC@Fn2?@#x?8qziMS z923BCFE&{ZgcOQgVV;qhbAUrfW0;0yV1;`^F&83`!6;Uu9vKYb8WFAx*7C|rO+5ac zPR^XJC7rf$U5Sudm0fva)Fladhh2EDqe&Sm^^W5)p;NX|Ia`^zM)HwNkrX);M zIIe3jHyh-Q1Fd}Xn_V1uy@AQeAj&QK@KXBbBKp+Y^)mO5H}C;ORF$CnS5ZYRS8+B; zvDV8$358)AgcD(!IvZ*3Y#>$>!4BC-LzeWjrG$JwM>;dbh29r=<)uI6g%|#iV<*4E z`1l3#xp|aZ>E}oRK?~}``Yf`J=D%tZvD_y={{E@8YufFp>A7GomluxqkwnhXamHL0 znxYU1fMIwOa>c^03Sb-17}KvGO~KUsf+_Gt9j;f$*yw z>ziX>Y@SVPqBPb7C^)XS!FzYjv~<;-VSpV*gljNjadJ+Sg9n><;;9~9dA*g9!4SBj zI2yUM-$_wyQS}AnP-c8eD`W|Ph^jzbR9OrZhEPaVz_KKfnke-h^)z%g5J^TfTPzfg zo2O97qflfrGYpNK;qc+F^85>b$g8h@flC)(VLm-YE|)IJ_!{2FM{I}(VO<^E+y>Gx z8J=~?yNGbm zx*tdZ$*{IOgEJanRckQnc?T!nYk~TWIJ-`HrueVWwOqdhY%dhFHTlj!2~Z1UP($Wx$$R{n~+2KvEJIl^#F3( zqYT#r!jy=S$sAL2d3LO8p*9&u*Z!|JF5!8XYEV~0zD$g4d zl_Sr6E|ObfqbR#qC6`K;7)$ z`=4ncETFHqk=OQja{hdRu?ZVTfo(ZdO4TWEwA;e{Ln(zT7T0~OAdicA1*xS-xqiOi&JoP2*%T3+a}RakWYW?BRulpeHf-uY3Y=~ z$CTB|$>v`Z=NR^Qyf20%bByy8U(^wK{E%CXbo4CQ?3@m`7dfQROw_tM=&94YcrvJ; zHx}{#Cg$OdKDaXFeM8q}eRn4h-g_J2P_UeV(M{=0P0uoyPEk{r;Gz3&1LgAT|Kra% zak{s(E4T^{4QR`CXE#Ixz73mG=)RF1sZ^Net|Qr8iyaDhlvr^zo5R%fAQvyc&Y^=} z=HkUy$mQlqrABbwg_~+I&(Iu)t{Uh`LL#J}D;a>QoTx&xM+f@h^q3&0JA+x%-on58 z)t_ebrXF&+9HuF_(m%|1pLmgrmj^g={vxUQEJ%Z0+c$9gP22dz&;1a;{pVj)=P&e? zE=48}hx+x`x~m5Qwr{qh=Vs*#aCQV{G7B!{Fglx_TLsA5cZ7lW18euY| zM8n>W>yj_z5JECN)z5|VFL3b4S2=(F1?E$uOLk650f~SPa@W;CXAyj;2L=VMVG}U*FF)~-2YBGY+p%qniQyr>|I90V<=fBj((A_<9iP-wbiMa@>iJjs zQd={3-nxrhc5P>TY?`TQEu82m7)!(9L1;+8&bCT*87ZJ|3NB16&=tsfwEda);-dON z0?1Z$tyaAFL~UdZo0{*(Hp8q`Gpd@w!gYPgkjht<#!46C0Hw3uLI?th81dEyV$BU6 zFe)jY+1U{;UU-p12mg`t7hhv`ZiMnc!i7go5W14Eu}+tIB*S{qxnArq7YrRL9|ffY zLxQ_gjT8Nxtp`ye$^TUt?V^JbawL}{02t>k|w&hXGx;|=h;tJ=^KgaQ7 z-{Mm5%gm-m$rr)9#3{DN;m!_dis;O=X%&-}MJtET;`xlC(1n8i)A0Hg?f(l;`LSzF z8$b8?Pq1^_Mn*=)`5(Xgm;CR4{5E5g(-jl6Lcsvxa0u6N$rlP_vpLf9^Bg#GlBW7P znj7l4+@B*~C|9iy&A{{L;r)Gb?wm076JIxq0T((|}-lx}S4rpW*ni@6g-(61iO3D=!zzW>i8Z>}-S$^<@@H6)9;d zt%j^h;5%rS=GDtEFs)r4LkQM&wD52L$};vXB*o#^e{a&%hdENy;u5q{}G~CA9u801ff=?CtqcVxgw0>n=;? zK!~M9QpAd$(^kWj&g+@AML$|7hgZZ-+%Q94jef}A|553$(8_8 z4B8L(>r#}1SK$0uRm;4NJ)S}fT)$_Bh~CHmQRw;KpGS{b03Z^mXI<0X#KWyi{E6bt zk8Y#nE=j{6TvtQ9xshOFjozL;O&?bk7$3jFxeG6H;NX|Ja%DerGowqoP(cH_5>OX} z)02>yF9)_=30>&tDqlIe=U(|$?4`f<5x6|9!BGr~4F!$$HT>8oKgef4^+8%%n>c;? z0$=;iGvso4*0i_q!TWFLGavl`n>Vf_o{TQuC!(OewUN($>_K|gwDIqL;}1D~?lSRc zgv@-Ff>U<22B+a$M>Ts^1r8T2((%XWDs5H)^7gn`_X3ZC2w-T@vvn=^)6;YhC~%ac zGvq~cQ=#;(N-T?3Z1fSgW$|UCmlfA*Um`7&P;G)lR|C=7229H$r}ADHqL`T(;mn!u zbKuZdxp3hHGU>@hJN1fGZC3(z)I)0&_7A~KuF`rm$06p6@hhe-cv=wBm&A7zoF0XP z1KOq0ZWV;0K_30cgZ#`-{RjZD=o@Q7S4{r3zuLvL#dLtCK=;nAN>IT=P&#u&CQJ{ zm1S^vloww)NONNy|Mr)Enh!sC55g2U1wE@xmXzuJk(%-bi|PQ#RB-#=o&3lr9${)` zmTgsGW_~`i+%@e}@Q3J@+f`kxx9O0;gJ@9)1qvx`y6W~)6Y0QJg(X}o|4@ai zw?MnDiy5#8Bw|Ec8b~xXV1>)sX4i3;of+c7#g{p-|BGC{^cs`1BRV$%UVlj0t1yHj zY8ljqglGu6a!7G>H)EH#T)8S}psNu&y z{SkihCq7F{b0e{f)8<}XfL>gd5=}@{-6Lvpl7pxs+jW-2->2XS>Jd!mJz~L zPN@e(h{XrHxk{EsdoD<_8G2zGwoS6Tm3Tt~fmjr2YQ=ZuD&|wuoILSe4j=jo=XzgY zX0}M<`wCK(R#Y?un1&=|YYQ@4a5*(zbfW}0*I+v1P;kq;bu6I!EsFC=q|Y6+WjW|h z=QT@oWDw4b>E8!*Hik7loqXmK5Azd0@=;nE8}*c30bo!UPw<(KK1?7G0$1Up5X#lp zM0jJ_geYZv%A-O&_D?~;w5W+gZL$lbB(0{jdEUXHGvwCNoR6kn!rY%KnaRC;|p_#9X@K4pIuwvM36UOFE|) zckhg1Dpx#bQHp;;mM$L=i^yjgdWtlMqThrOhPyEQo`0S@YNOMy? zWDAu;iChQ=g5Ern;^rWHTE$DN!W6Hs5W3hA50DrZ55Dhq{^=V}@X8y97q`j^AqBgD zM=}MDp@)600^}_`T7gfa6&N*jncPBq-5$S4vJVr>aJkC&Ql&6MK_c}@lI<;oYm*pO zx!Ec;GtH&`mpOjq>s-3{D#OEPy^&)AAq_7PDF{eKZBWq_cW92dw8b<~q!1XgY_ZaL zczw9C61a3zB(%kK;i3O@J;TOWN@t~er*vP%e4(5aN+x1#+0?@)9{m6hKXRW2hpQI9 z3`$i}-Pe_>)3e|=^sM0%k3PupQ)ii-OD_oNT!rf@;)dW+rFdFts#mQ7V8c=1LpKlaHF^2tXZVqMP~tbh&q z#lTVCNThN~A0ifGD1;!J&ymgLJ&_iPVVJtsOb7y=Y>^OPdBY{|zw>6k{Iwol-G6jZ z-osV6reuTXChteLPp<;xEjSuAtxIch<8Q6mMN@JMDB+`1Jq`FO&q|ehmNZ3bNg|DP z)U>ydXl%r^Ob=TQnamvL&OOhWGtYDS^b?GXoF<=}r@A@Gl#mR-mRem^8x4SMQlU## zqLNWa0V65Js{;-b^V-SlXs%&RdkZJd_A)+~#+2H{GNsN+56?mG3|yGh`3)yeiF!5+ zK`a{N{=07Fvp@7f?!9da$o3TCRH^4=)4EO`eBW&xJaUR$KED7UN{4ZlN7LVpo*AN60doDZ$3B1T5weqX z*569NjNw+aoGvdiYS<>Bx;XKUM#A+q*j513)R$s@ZkFCl&vW9~w>f+EX$D8m;W&i_ z_b*^TeF%CIu(3v459twC%3r}!wK~S<;iZ0;p;-;)mYN8k{K)$m8J*zB$+O_e>R3{{ zPp3z9Mtm?0S+{IPg@RDfroFX^hwi_VM<00~x9r|QzzX<8@ZJmsBEYgp&!xC@sgK9L z_cSlpP|M>MMICZvn!6zuC zM?@lML-8=$&6-;U$n_lo;C9c7AcPP#kq#1}W=wA+f)GXW)-B0wxXLYA7AXyau_(#b zCgN?4n30g@hdaz?=NKRB<;a1rvj6bkGCMPZQp#&-EgwMxS|Wmun4&wOfngQZPt+ph zvMR>JlCg~BrGDt0R+!SEsiBUa`^-ap=)({5pa1)xGndX`7zXosmxDtvkv5o^FTYoj zxiST8Slht|9=M%{@4t(ic5Wpc3hGhOxQl?JdUN(iKC)u$Vy=+q%<&UE{p2$|_Ux;i zzi^qUxhzxZT**aM3Z8!cHNv4FU;EDY`S>IE^VyF-Ok;gb>9ZT^>u7IlS_~j1ch|%E zJ-|)qir&?Z!t0=RcX;tfPy@uECAp2p_UGKxBxA?DWIea;w;V_gTvW`w$E4~DcBlyWFvqEDIz@z!SIEsezL>#zelPf;l3 zn425p*op6O;J}x-aNz|Cg-k_Ryg>JKH+rtrmKr=pN?Qzd74k<2%~qYBgae~6mG_Zr z0?wYl$Z!AtU(nRl$c2mjNGT{d!Yk`iAYCGn5V1&@JNND3?mKRxtFw)pc5NUY3u>v} zLZNC>?X|N{2uU`Z<(X%m=J$W^4;UF4!EqoOvd9)(OzDzy6nR&ds|$R@n1q?RIsW8t zzRbmoSNKo=`WM)~eG|56($&#MA|6`~Ab1VXIRz6dQ(R`@KDiu0Mtd-(wx}r zYe^Lq5r}eKg3$z=eC=6aNDiBiN&Mfx|n8p^qRchE($4d3I(2j>M{Q05B`X$v2koE!B9n! zRWhpVm=(+I4XvY%;XR+r<$3D)*Ae26_`iPs$GP>^opiQ0(zB+WOBV+gV+fRwZ0|%@ zMjS+|XRZR|8jlWiGn6R1SG$OZS}7E3u6zn4hG7z}i&N9pM!Yo%QtKh+3Rx~*Im(F> zPjKkqR~Q*O?d38m$(#BRw1i<@LeUV?nQOnSu0Mw+s{m4TLHg5hY*e?oYLPVmIBP*( z7#_+TO-p%+I>R(s*RzJtee#3cea9}E8|z5K<5;FQdd?MZQQkryj&gYA`KS2C7r%fr zH$qJias>xVKtV#rTf&(FSl&ck=~WiHLjUY2a9zb*I>UFLewmAXL;S~I`xo4G`)<~+ zY3CXFMs*pBLeB*;y&D(vVv(<-3mM-tqY!QsfNVvVwrc=Lo0ghgi+c-(B-&I*ZC4xN z`Wl4P3{*as=E9{n`1-g01A_y{$!BLP%DJUiUAsOGn`*UN6*LwPas7W&eH2`{Fb%H` z!c?YOLC^wWfwV#8fqWSxKI=CPQ!G^V6*ug+?^5!*9`Mv647|c$L^X%i_VsvPT zNKle1xY~tuy~0T^xv!SUHRj#2))qh}o8!odv;4(hf1R~mZEW4#LpT&cWnO5E8Q4 z8G0|i%G1yMK2LrBcNiKxf$R7bM}&5tdg5?<6YQ?n!U&5_QPSe>3nmsQZ-Go?bkSk4 z@X(cElo2P9tf#K2n?QUm!FV%S$3ifVVVD?}>FH%@vt&rY^zuh_HEG7s_?s z%yh-8a2{GN_4&uY$qSEv6M=&iE{4=?0L6ZjV!Y-0STyC$3{oCIMAZyYAp|qCbBvBn z5($MkcJdrEbE%3muq47V5UyJapbXE2e(JrHIJ^-oNGE!(oi_#3q#?GRSTKQ7d96Ak z6lPtBy6#SrEv-0ihP=u%H$BGT1OLRc&;23e|37w4QJ z2`tGK6hV(2%z8Y#kdP~QS#vZAp(%9jZYhsN92_0vxmOMlkOs+kl>WgHDr!Onb}WFJ zR{Hw+n92u%Khx%Q)q+@I0@5|2yP!S>4la=;~o10K-HHiR-xJl%OCCq?Clh zQKT6pm(St2x(=*pMk@jb2{WlQwq=t}&oeezNu3)K>LUnOAu;w@ERV=)RTZs zNv#y(xQo-YEHVJf5HMGO^OL$@@Y0;n3q?o>wL+v5q>U052aaCgz|jksfe2rF@(jsD zn7j6_O5yJ4RQX;Fxh;eG?Y-Gt&vl3xY##rg!Q!n%Xwfx@8-}kj$pXIQjZx96s`O&YypQxzyG3O*J8C411JrmFRj& z2Fs}26$MamU^pu{F{U^@0ke6%+hUp!YKK6(#8x#VVjn5jG>F$h%oFH$O^4vIiNG^ba25g_r(-S6}-pE?s_uTs~c~(@UY_jQXIi zhAr0bg)QiaLT#X;^1T$4N~7|5SHVbHaD3e4z_7`=X}7FCA}t7aK&T6(XYhmt(!{iE zKJnq(_@$qEh>t(AhX?Q8gk@Ru4QF}&^?pXi)9l&aOfnXrp*G6)T|3F=3miRi3@4vu zK9ePph;rZEw-AfzUhp?Lzqi7}8tLoh(2GxynVT-TRmug+4zPC1PVV~9r?~B*kJ7zm z2a#w3Q(72?iIfJ0VPM-fi9~|6YrD8<_jY=^Iyrao3Iju<;7Jalw2I2MjXeyFjx(RB zoSc&g!Nw*nCO4PWa}m*_5DegX-UDKCHv&LX=+41A5h9E6see}oJy6;}X@r2eIUM?%46yZpij*fO3YLi^N+)w|&sGc1S?A*MLbzL2t>+NT@ ziph!xVQ0HXa!i(p58<&{=RL~TSp~?-T)~Gy)#17V1cM61LXB zEe#qdQB&(p4W#vrD7d;zy)9Y}$cu!04n5L%IAd{OwvmIw1{Y@}X&2_R8C>O*Khugs zWCIv65GI%*F!Uz4W%D|I^WT1kJzLrdScX>#D~N_HdO8zqT-QkdjKPx!rf6;qv#C2q z$TDbdYydODD=$37%+xs1Sd91GzKgETwvx)qoAz0i!pUS9zI>5WufBwno7dynTxja( z5|^*qy%Y+ApTP$0mOW2aG4k%&e3)JGp?+vbgY>+xq7 zSP)Y{k2mqEcMRcEKnZ&r;|u61V(;1^ZxDd2L1$Y_+nG|TA^^8-i`M1_Lcu^Ofi;~k zC%tV6YoZz~#o*@BEKFw0>{O}%GkHzbzBU9;U2-`&m}WZTQYi8LWzhnJgg_J6wVu^5 zOF0G%i)~xF>1auMuA8XX`GpW{?vC@zpIJ{^W0?Q;H)nbF;J8Pb3hur0CO-Jk`?ael zAfL;j+_xrCsC*W|Qz9i8j&k$+KE|4jyEHi7L?T-V!M%6xg8!Cvhc#7e$FyrC=GLY7@LRbtPV5- z0f>bIbT`KtO63_G$a%R6z=&vo$bg=UE)4BjnKnq9`nn|HkiG1e+r1&lum9W@{{0`F z;Gdou;GP||gaZb3wMicSz=J&T5ov3d;^)+a{I`^M<6kXA8GH{AnUJNpK1)7%b!`8J)^Ig`l~mp62#OEkI{T z+S{9mMFQAHxnv$7d zA1eILZ~lAnnNNP0`nn``z+%_-O?>>r_mfO$?fuJh@bZW*wv*^sWM>^ThsyOuIYA4%&hP!^ukedM^9lCs*~0vM+E;WZz^(%`u74Jm28y&%G5}_P*N+ae z|HO!X-{saAu(u~pG;VWowk)j}Pu6nh-S^Vm(n2s8s(AgI+h@wC427cvf!Aq*wWG{OJly2A`*#k=dHW=iO)VtG7-~br{>_uq_!A_gtal< z+YvU)ySS|CU0>VP&Zj>95Suo1BBkJbZ$IM`K7|@#f*C9SY=pdrr5{$1zL9yp^w>#e z)465uu&baWVG|8VCNloGD!}F~+u6KjJN1n%W$E_o_N!cCHT6UjwX}3^V8ia)-ugnw z(wB29gPy8$@n4skFv^e>oLLia>$~acyw_PQ13fkBV;=P<0JU@5Ch;GC?H9}~o7M_) zS&wbeH$o^yEEZ+Uh8{-8COLEd5}8~<#~lJ{L%N(IZbQ!1)UV+Qu@%{@f`HliEE{{e z*t2~TQcBL8y};MM^9+;I9-VC0LAV>N1OyUb+WL*nkjG>NA<<6Xe2jd-U`t1oc*I!L zfaDx_VJOYF`%^T>0^Hde!Bt=i2gA(~$WGJKR!1Zeziw$B?N}QiCBaaL9d|uUQ|DTp z#CuD!^wnFiB$vzc*b~pwKd7y#t5k`K@2-P&4f^Mt1LuZubLGNi6Fp$(sdtObdanYc zc%P!cCp=%&^w=u#6QBK<{P06}6Eb|Y*#5xEvPdT5Y+m0@-f=m5;S#C&EW=)Zcb(Vc z(HPP!N=pd3W7;Q9J32F&&E-i%qujh>6UmwcXV3NW*pn~&08#_d4LS#67&@D71i_3# zuoi-K5UwXffWy7BjO1JznnNUm2DVYI%*{B8qvJV#_vjd>XLIam2=l(Sh)07u1Z$^}SHpjC12UocqJa&@vy_f0g?pOxLUi@K(bv$ClAXE!> ztTa4ucCNq|UmxR`Ex4;A!bdj7Nkk;Bf>(#re5*Ib%R^~Qu~6yJ41|c*HX(C^Z}}c1 zJzY~#EP{01x5w35;age;wACQqxKv6>DfDRXO~8|@09klR6KDlRshKNN)z{Y(kH-lH zgOFdMBHAfCw_CPtV)yRt{Nq=jAd|_EEfhF2tvd!IUU|Fpn5&wgP96?<%IH@HMmTl8 zPu+2QflR@zQWH{OnGi~XNPq|;LQzyC;bqhXM%2J)3LujfM$F`NHqWWEQ+#J6MJ%M9 z?csEuiF5%ebX#sRAh2XH2WLScL)6M6h2;;!u0JkVGC}gz9|gBS-qk(rSNnvp4Y43% zJXK<$Bj_PXv3JAGdaqlM5WY@ADW%xFv4@VfW~6E9xxy?`gYIj86NZUt+t{{EE~goy znVe1vmgY@)6|Bt>-7Gbl)#T-DD$O^(_q@92u3JSQ7!tKL3C>^Yt8gC(#z52}l1YRq zb+@2pV6_IZ+Jnd%Tl;gU5}VbV&ZE-0x?7@^6}9}SOLyGz(ZLc5SjSVu7bu0aEF|B>m^8P}sIr5_tn~3OQV-Q1YXt zG?0dV6j>j&m}9hvvu#VAX}>5+mD?OSen!3Y+M%(B?z=-J6Y<6hH(lBg?LmZs2-87C zf|&IItd<~VgN>3xF9^5N$JZW>zmXjrSC2+W+Ernpo96}?qj8lxDVB8={J)L{HRXG` zJa@_gnmtBq6(CjXxMF63L?PWg!Y!ck8779VFg-EC?9>>z+L{=qi5&=%XlNx---2lt zAUmgj?|~?2=%F=!^b}>K+QM*LLbd=x2sJW3?f%t2eEZVAJv(GqXKQ_l*l@FmutK(2 z$WRy~Au(Fwm`MX+xD~?KSB(XbxVUVqkFcfIr&#SekekzGEbk%T*I)iG7Hi^CJ`rRo zn4L+J&15MQ3JazU2Taf7^V)oELt7EcfV@-MQ)+;X=&@~uRe&srf{XuA7!+^|IZhrw z!Q{nbq-LimE{z*%`#_QOuhAu-CO>#7s{E$GxVJl==$_0*1+btr6P7 z{#)Yeu1V?1dMfpv0ZGxt6w=>OR+L^8T4X{q0L;#$$z*ak&cednfQdKW&7)N%)I}i@ zgsD`8C$S0LJhuuEe|$$(xw8;jY+!7Bg1O02y&%0RS^uoQ+`5`*^9&> zbx|z>K2?rGEVMS7%{$zR`(6f!i*A@Hd7}l#WpZkk$kuFK5KEK^gn7{c%d_UJEfo7$x^C{pX%_}GRnfv+<1 zagWh@M-@+QgiQKvXk9F!6u6EfwsyBr9}hA=GeS|i$6x7F^pjl$v7pV7lb3ku=p_oC zwp3dLI%8hlPgQnI2na}B)jN}eY~B?pS7bAJv3Xqwk34V((Qt^DkDO#aJ&QeY7SXf@ z)v|8UU{o!E5Jzb^SyJ#58x#EMt~x^gYDOmunK3A2mmdW5P8`xx=QbomBa?jX+t2Xd ze(wwX+24Jgm)|%-@8tn{FAs41%mto(c|UI)K0!PgXYIN!9HrR*#v#7=l_x-n3VjCm!ZRZy$g1w_jssHcjx*GX%EY zgNh`)Ie%BTAg+aR^M< z(rO!nsd9j#l*`8T-E7;ujzla<-{1%%qZ1S+N3lZ@+M4STR*-pDUsfdq%In`%lvUAP zpf)J^_?j^PeoqaZal2g5=cG~TF;r#(q2A>Qm)O&RozsJ?g$re;&@*|CWY-K~s`Pl7NoSh#a5pr8znogd`YqxuF(LCM8h zY=lOUwJi3B^rduBBfxBGp8EP4HgD=-+s3u@w6`!nH%D*(2!-=U*r|qaTh=pR)uWV) zD=b`T;|f!c3&^{)hb8wk+kAd~lwaN)rakP{lNBtfX~<7&nNg{=h@^NI z8zG85sA2HpD+l=P-}?gngF`x2P+*w`Th?`Q_boek;LbfnLO~qIW!fjqkk1#m+&9F? z_#{r1@R(&n(1J4~dRF{OAdAkXUjUA*0;GJ*d*<~nbagLEp@xS?*w)h|Zn|kV6O&Uo zxlDOHv42xD3{G7f;f<4*yz$I>o?uHI19j0JZ7_y&Xwu=73>BPFK&K`cW8X{ zjiPEY>@itei%Vl}KF7dx#(m&@w^P^9B<3c@C=?1>2*7u(^KPD#mxefSwjVs{zU~+_ z1$})yqLR%jCW4D}@bJ7Ah%1(O6>g)k=nkkDuYc{_Yp}(l?)EKAoYV zHV#4%j7IdV0p%4J6y2Yz0i@!48I;tw{g0A5$szzT4Njf8$p84`|IP5&B#wfh9pLtz zoB6e$`xNiLYY*{Qh@fp!n~buytC5C=1Q!QJ7#^Lvs`KpniWjA6(9l>zeO;VHEJC5+ zkk1zu`zDRRMc~jo;d|Z)01@b>{Rp~=KT_OsQkkrBU8Nqnd#{Yg63k3a;1u#DohyOP zGM>IX!hzHM;Oa%+6^F)Plw~tynkLamjHc!$ z)~@TJYwcP(ySu4xY$Om0lz07#-BO^W!J(rk`Hlba$9(PaXE@t?g*T3z;OyCpjE+r^ zozEZ&wliM z1T3q>WGNJZfJH}hJ!dZtaQgfeFF||tBOD5F`%Rnp*-yQnkKK0*x9!@5QqbEstf~3R zC?|x@s6YEoSdkkAAObBx+Tz6`hL8dWC61rIC?{u9;+{M9(a_MO&3Z0FzTkk6xWMVl zqZ~ZlkMaPr)&mHO07Qz)qH@d7R-}JU=SBbuj)Mel-o2H&x|+&0R&485s^onFiJU#l4g*nwo(C3@1eXI?T%Bxb@J%`WnG;xJD27!|L&U{J$1eWkoKl} z{>5h=X6uGdEr8_vey&o4LjeqFaO&)321X{9pEfKdH*a6ZkA3_;e&&@%U4(AcdqMoS-pUPuLDKo0(^P zZi@c#NxpRD8BD{(woF=T>Zp$;=%{TcRud#z8)Qv;GY!qPv~)J2l)^9!>g(%>L_!Dz zwr!VF2)VhEe>pZb#^lrtXU|_IHIw4eM;@TQp_W2+mek}Jr_Y@)cTob}Yns`(zEhvS zQ#pEDN)y*r+`4lMci*y&LnqFG*Gjus7}e9&%>VgQAK|{+c3@jUOw+=$B$jD%*WRtX zeE1B{A2`Ky`SXj=jst7G4c__Z4iNwGI`AjJe*#*IyH_TgBo?m5q%T`jU84e8`WB2yhZ)On3b(xz@GdVrU ztEUfQsvs626pC0ZNJB$Z@9F}Y+Z$+ZtOp^mgAq>zQ9*vbbiT8bQ(U~%&+y0u0qM}y z(aQbryR(${ab34$C2Y%N>$)}6#v=<>d}*k(6of+o)_1j0R}*7=a`7IgL@dJnx9w!# zu8jn23&W6DmWdF0oSSJFY+cuhZJA32>m92ASxECG&|PJH=>CAx`OnX1dH$tC96NEI z*Y|B>+xkv+ZR=s+$`CIfIalEd_~UZL%3Sk?kY>J$0Lka`Jp0lc965fPj@CQ1)XlY2 z+X}&jOIP^LQ!g+wJ_!OsR)`pNFxO8^|57f zE~O+HixLbhUw&^Gl1)7wG}YHIw#bBA(=@nk*Jgg`p*xAk!ulMASU3!Iskv|Z_Jnx{ zRC&xB1Q=noaKlG{eds>*%8TO`Y1mo5^1WC2&a?YzX^4}~x(tlWl$j}2QtvDGHTc}E zVi}0PO&fIodUF^q%;@@{yecp{Il&u;j&t|E-Na+zazW8mfZ{61W*xr%-RF7zz)4gA zY{TM#uDkfumJbm(gStpjIRq_(mShtR@j7<3ZP&v{ojmjTd1f}ul6KOh@+n;9l1tBX z`otj)9C?oM)C4o0z_t>Q&1;uRXWYsSTrom~ltoxs)J}zH%!I(nP;eZwj;KfqDy7Id z`Ngy7zJS&+4WiMA)(xmSLLrl6BEo}r?_%fXbp!)8mL*GL-2hz2C0odI;OIHhsU^o1 z=H3Yw>qh7Y=>R?h{3u#P%vv&1p%_HF%%*e9UK(5UWml89^^;7l@8);MH9-1P`pl-M zXL<6uSNNe1-p95r>sF3%NJWWV1&Ny9=yh z?`J6lmH`3-rQ0k^U#6?2g`Jz%5{(A1t#Xm6^z4`rlA*CF zzW>^B#%E@578Yy@^yI0bcVZE~R{>%J9{_%cvZ_hN9kvZ<4eRN&YoZW}+kE81J=}hK zXDJQwsrx!Oe0h@J`|>%y`t*Qqp;d^iTM{N)mYS`0#grF|#H=!il!6ytJ;)mekMqF2 z`xbdKwZLXijF0ooi*GPCK82Lfk!+>Ab`8QXp|Dbs2DR`Caj6W{(4vlAfyIm~d#S8(it?q6I@KaQ3@@ims zN{IU!;QkiaUZdxn=khL-Gg%y0VHg6#5QGB;x9_Oq{#zPos0oy%fUY%5>HC9-sokug zS$;B;&2goFXj%89{Duau>ypbCa6L<52#F!z!kn#A1S2uF?A%9N=Xy_AL0uJa(WWTz zmU!tI%Q9(hsI7R_==e0R96ZHbD&r*;7yEccJR0Hdy<2IhO_Z)nQ(cn#Zre^Q5?X}E z(AoRB)I5(p`#Kk|ENLKI;1%AX@oe&K21juytD=W4IVd;Mp;gkXhl5Hw(0Ej-W) zyXznx&?QO;FmhSW9PZ=FrLhvrq5$%OU0pS7Srf-JMA5|(d3X6ux#CNBESKx*o{qYJ z&WZSU)x#&wkxJ*h(g?T0ttdzSI-4yGy*n2dQ@-G6ljdGOFtlid3xxtLt!vr1?G_@j zq|e=}`jV#Ml?#}3-@BHE&5b2<5(?Yw+`5*KZ^lX9aX5bd3S-kL3aTu83+MZ$Y$rZ9wOmTg>`uL z^Mgwkik6lxTHDs@TCK(W6~`sk9O0%P-$m>0=JL6F-?_b| zo{rXriffd~=D0F2q9dCn-WLS{%kmUXpspsyy|-+mCLYyd8Vfv6!Sk;jc?K0vW$Km3Y z0X;*cR3s>>S0fZM1S~;QQv-=um}n@-czTAz1IL-0pDdTOza=W}AWcb4Z4-7dTIMgp;*Pw>qG5hR(cXI)oErSz4fc2>n$Ryu==5+=~r^w{;3vPq=xCA_n?h(470aCnLn>?!! z1Rex_1+9>`7;AMe@HzBEDdBA@Y_5TgHJV~oq706rV3zCsTYMCN3+Ki-b!-6TuMzwu$j!v@r{$;w<{s*-CggswiGnVaXYzWgNr^!Uq6&dia`Ef6JlyqM!_z=?N~PL*$? z^@ySc7ed||3j?18{$Jo`4>ZMNgO~GI<4@#u$Mh&?OH`yOuKI-vd50IDyTFNKL%NjP zg_imdw{EK~B@buwkjgI~pfC63ECad|nn5f6Y2Uywm->cuB)^~mN$T;?Mi7)_OnVY!vr*9z`FIo&X@&$({UOK?x6XzEujl6r6%jL=C3xt9;_uRIFjGHI*@)R0rGbCgERu4LU#ccfoOT4+2;Xh(gV-uyw^sG z>pW^9b~iw6pj@M~NJ?SRqC+7dl`fD@=V@t+lc)(14VlcQ@_he|5$3awmbmm}Mun(M z5|&7(C1|-K_3OC09d{Fk?@mPpGTY3oER=I7~ zBkT^H?B%Q9dllDpc;JrR+`fB@N3=PdJlDrRJ^nIZ{N}U#uP;BriL;j~WrQk^Jn)ae zH{MD2>g@(dySMN=sLX;(bmP;_o(r}SJz{N5@%Ld9u`#LV2-?Qto4Y`}UB!vC{_3S- znaK1^7B}zE*bpI+3=p#=`%g`BejrUjLCAp4qWq8Fh4yu!`ZsdJ2b^LV{AdnFGI|p& zQ$4!UvG@mZ4llKAVGZmO?nq;M_UdZf@tcpWVmqAKpXTo=)s=Sbr14(j(+N za3BP;vok#N(i{AT-}*BS9y=?QT4>S=j!VAaaL1mF)Fq>smgTE;P@KIq#ADAMBpwfO z&n-LHy>$cmY>vmDJHWsD?Z4)$-+PJYUO&pf@c44zD3+FFy>%1zHrDwY9U!g1$AM-` zQfLg4mjWZNN(tML$5!3!SsRJB=+`DRYZWrD;$l}$5LlGeni)@%%NB?yf;7~JIdFQ4 z{immq9s$rD^~QII#TTG|SycO@rqp5%S5jKTNdehxmV-x5a`N;Ay4SQ*Usr=+R*Nra z3nGL>N|Quwg!SvzuzS~L)@@r$Q+F+i#xQnVAZ%6cdN7rK7*&tr^Og&?3-xJeOd+~w zFgDK;+M1>7-Zs|Xx0a@D%>-&I|FnwA_5+wxv14I1PKYok<{>NYB_?cb-3gs#c zVJt|7j!n!^UmNGvT^k8U!X+RV9EbX3jLz12>Jl+-+qH>gEW#gu@w@!jKlw7p&t4&w z&MXHEzej6&8|ZDoJ>K0p24rq@fIJGcm;!nd$ldiAQ40!6cl0^BP{+c%MTRHIv%5j7 ztS@kDmdJ2dv-^HG$St@GT$yIDe}-_#WImna*&|c9t|H@=m;5$$PYQw|YxkYPbE8Nvz&Jz!W= z_(DQ}Wigvd^XmR1{C|J&zxao*Kgq=_gGeDtO;f6>WmtIbTsB)^%lZx?p%79^q|nS( zFle)BZ5y|4-vq$_^E-dTAARvVjEqeor9eti$_0f(0XB8FbKCCCY+t{I_Lh2V%Osc2 zlg$;1Q}QfyUEX%jq8#$vueUXs_jVoIfjh9h`)@wMV^6)nnR6GxsW29cu9JWrAY_LK1}xUJ z)w8y%g{|w?aQEIVbgyZmy}1shps#O)FF*Do|M?I8p2?ZHGOr&E6AcI1(9^*Kx9{em zyY{eSQx~RbGBGv7>E1qGIdqyYedl?Ooa!Y#pY`AUlfb_~_om;F7Q_bL2W(X6!I|B0 zom`7p8Vns#{pd-cy5orMgf8JwOIV7fT&V@iN>r@TQtX;`1+xXgL|QSG)mAKS>+>=e z;q%ILWZA>7h>Ja|H<5*bR8^SZlMosn8E5~Y6Fm3YK}N?XF$|Mn&?X!T=^TP#cubI3 zoMcp0@kTM`C?*?9l0Lpzq}ccFmk267bc0f%P~f#g$NBTW`6|EjXaB(CPrk_T@L2I< ziz8QxsVAfOt)!ILwoM=qKp@B$Tt+8nIeoF8!>4=Mf9yP&Y@UvmdTNqUF87V_`+xH- zPMoXMosLC9{L~LW%)j}CALikE_tM$gP!b^zg@SanHL+{UI&R&$iF7v0iL;l5ulzmX z#U`(yo2PC#fVdumb+fmr(;U$YQh5u@mJqceY9S(4d2WZ_of7`Vwn8>`0YFO0E9vFO z1Q(|jd8I8a@g5!v>@hM(AbetDnJ;;$0 zXE}QO4EbE1NF_}Y>LYR zx?H5ArJnCSdw?(e)05;2j^_~w*0eYA3qSrSzx0zIV@+oZmJp~yp)%p96cR`#V(i_% zfx(dpPF?6L#x34dA)ZD{lD;j++g0u^(2}92y+A6ED!`RFom{hg0q{~agNiqoqgcuc z00_rHx{x%WJ_MKNw2L&Jh9=u5X6>QC&xpA!1aC3D(HPYASy`vj@OT@L>n%6|$Xq7R z*>hJocfOBMz~=A1{2jWw+PU?n?QB`UmUUgNG&j~$Uza2t3gNmAu2Pt$g_I^JMWLW5 zm+KePreFvI%dkNRT-U+znz%-WMwyzNWo%-a=UzF$s|Sy$i&qBey);02KI2xg<`cjh z!1saY&<$&)Lep?CKy5NcLv5VSwk86B5W)+}FAj`x_VN(vbe1c9qx{1+USMuE&Hm$O z$z*dSi`!Ti6}tICFDL-fJNQ;ebhJOC3M+=?`%9)CF<$%q2fl+RWQH zyYTJo2NCGe1rtCb?`i?IOu;v>_1!p>cJtaIFgma6vd+vvM+B0#&y5$pBE>4;`rWN! zFZG(3cC~yjz2rUCdN;DdS}UbUXR=(lGRTGgLB9X|>ohjj&{|hRdrKoN&5i8Xwu#32 zTGn*55DAAc4M|NhPHla?Z}60FJCQInG|I%(3`!|3UKyZ&aD-=G+|R(!7(*lD3=EGl zJ3Ci>pR!&e?hVgn`5wC7X*ar5NG}%CpsT%!TXt^bL-*ax_DyREgroW_T$j@ zOBcVL0Fh{^`e!|gPEKTCGKY#pVq}t|CoZ6XaO6qCp&*HPl=h}N zBB1~Q+oq$VjYuS-;|Qlf!E;U0^BMa3hsoq}uYeG%8taV9hPB`h=$+M{nUK>_Gvw89QY3K zFwiiaLyhN!~X!#>Cu$|P0f|RzT?|A=2B@fmr64-Ho>{xD-?qR zO{Eq`T$jI;s}!#5D&h5ldo+~r{Cef$Oal{2sbj!lj{!Ocj4VQz8lLqFnx=_mS?t)f zmWS@TiH6#^?iF_wPTDunTu5TkFpu7UD|>dV=WCC@%pd*rx0${&TareVrcG^a4YqBg z9JSC|pWu_4e^R92$p+ z4K+be{>SmXkE%RR*E1ECSD_xU5T=BJ>uIuiR$&o-#zBh?OnJNc0WWV9^!WW6@9!ht zzftdJ!}s?NdeYL2X9?!LpT{WIE)}lxat3@~Q)ri{lBQcq-`F;6lv0dN%#q7xN~bVC zpW&$&4sy%(4Sf8OyNJglIF4J|1@xCwI6{j!NJDb+LO*}{zrW32|I?GqrsnHLfy?R;w^)YCWqqKVa z$qDT)b;c^$EUJ@*OP_DCz^>KX`Dd=v0>2wGpo`W>lLxL#-_V$@%W{{W-6Mn~o6YmW z{u5m4AEBW(iDemF>>Df%#W{7Mk6-^Ef6ez_Jjnfb?x8jwCF%9Pr*k=;e&Z0_5E~vfll| zaUAC7GrV@}6ukpCvpwGBGsA|iQc&Qyo^YHJ965Q0uYT_(`i95pZg1qn_uazxoj3(ZRf{NY7`PnoTh>KF#RF z3{x{H@_EM-XQ?O|xQHHq@tYn!%?~0#Ko^(wq9>L{y&Dmn$wOZn=JF_8>fqOwi-#0% z1|W-pt3Qo8J&hWlFD3a(t7AH^ElNX3gG4S-4`p}&8Ce`4-ze_HH{g*%Cpg;yk;@gx z<_m1@X=hDaV<{h_92cb^lXJP)H_ShL`vo3*Za>*vfjjnWPkKJb;Zqm*(zl;u|IxEdr{=hI z$9jI|(+_d;)*fwrg-dH=E&FzEbarg+F0?k)($mo-qTwK>lwxW&z0`_N1AhnnrRT1` zW7DzU?HPs2+hq?aIH44lf@Vh%NIRIL8Mruy7|tMau6Birx@JZHU;5f^OQXHLhx9c( zIZBrnkO}|*1*S74>uIm*0c5RjowMp^np;L@ZOfR+Ydx8IsuI+nA5Ahzl z88*6V^e%Ld`t9h1b)9=S8~GZ?L)NXdRn=9O&{8&5B{5N}quW}Sk~q;Qhm?{=5=n|A zl2Hj~t5t24Q&qQ?R?#iio@}dZ-I}H7C1UH^3l&7s)@89MU6h^dbN=Y*^PE3s=J(9| zdB5*F-#_Mg=8yTkM{ZG?P>#Q(xC2EiK2h#?@}ARj#rYw@XsGwzdMX`E%Woe)XYZA> z>-Os8_k;bvU+c7QdUQhH#GNlhNzx+Y+6trbdZB{?hC!I=xqv#CVPKY^+{0B^sjr$U zscJiSZMcx()=9adgnu#{c3&ElSHF+^@jq!>HoXdKD!WL_&!5Y|-1HtMB6lXMU#|-| zczs`DUgAnf;?A)BBI2zo@!jP2A5hBmgl}Z&OWoNaW4{NMoif^2C{%>ZlqQh}J=;fP zyx#aBInQSIO?2;`kek6aAHCXzVdKc7V8w$#hJL6{SEyq?@jI|!Y_zUT z`mc9}tb~=k>o=^-D$s^zukWUK9j|rvHGg8Oti{wx-7!J>8c%Y*Y7fg!Tr7Dq8dvDz z>O|F_h9@w?uf*z_KIzDl1kUQ|$&d=G2Q{6Rk0#MZ%hAnt3;rAO?BD6}N-He;Boi+` z_#5|_JFgiIXvN5?c33Q|X^`p057!)I^}&?pxr>}Ex0;Qnx<7{}nXWtQu4>lT&iNWv zH_%cg@E&?{?JFx@;)#+L$CqFADSFx)210r(8<1^4Z|8s{>+FM7Y0Dq@D|LXgd?~EA zqvwtple^z8m$I3MXk7Aj2%L#EiY#}J*R@w)5V@?7%-JU2PiK&rL*(A&eFO^8seMsVRm;Qp##dWtWiG!_8gkx>no?})4%xYGx)NM#GbL6V zy~&mxk#Nf;{tKtJBoy@y7i~JOOZle(YGY@swG#89!(k?&_^{fScKmzc$aiyUQPsuO z+}8S0Exv_FuDv*Z5xtB0u=M@VL-{akJC0JCQ6<4?tNV19Ja?Y%_KDB$>aV#a8?V-p zs+-$An@(SuD>#SIa~M(L6SG|B|o&t*1X>T&&K5XhLZ#Hn-2bYz^EjIwa?mV>$O`JLFOYrwhj&I zXrxbX$sTMbzGw{p*8Hv62LnCBq5i;@oXtgdS!45=+9Isa_A>7dZrc>_Vr0&91{@hE zS7eaCnUyZzKr2jU^+d_{)S^i8w9=<%t_MsN$Xo^xT>6UdPy2gQ#Ytxk2zE(@1s}&R z+bfHu-YzA}F=?0DCj;*8F*2$UtXH~@5U0R!y~dady|+Ky7$MA>WXZ!xl|h2RW5yr8 zkf29{4a6C`_sK)|PFoTZJ=-MBmv9;vmMb#KtqxV#oH^VK8lYk_O<#}2?Hz7S$@_Ol z#Zlz6j-%;v=USk%m&Jk0)=yc-ulWXAX8j<}(R|#~*d$sm-5v30{x{UerNedP%yJA; zwyv37>N;x@SuoH!)4!p7&{k(7K9UMuO*yWySL znk};^TReLtSfOzefk1>oo)n1Ai;iY;A;dvW6my4=7b~DM!5wS};tN467NQFxFkB{t zRYk$?ylD`D3vr^ER9-{`Q$XdzG@B-5GN^1C6v<&wIdmQu?H(%-z>2mQK9^P2vAqSZ zWUZ>ipg~NKE?~kJ1b_nodz^zk00iLhARY(e@OC)(jH?o)D*h)Sn#bToi2r{=GZr`o zC%CIB2ze1u98JJPM5x-uVzIG80anO{?_e1`I+hbnV=>`_Lyxp~vR6GgkBenP(NS0q zm%$WaRLjjT$DCog?JBtdc5HMw2zv17u_}u|kPPPw5e{YR_pzTb1eG)hdNVmJHiQ87 zIE}Y>7{iRwCp!OSf;bF_twQ1fLk}1+{sbYx-a=wN6Y+PeSlGY=m4UvW>5u|o3gO`7 t2si@(fWy~3U40EBTR+k9=fuMvJ7OGt+}vtTXfWY0g5c@vaog=!(q9V0FLeL_ literal 0 HcmV?d00001 diff --git a/setup-ahitclient.py b/setup-ahitclient.py new file mode 100644 index 0000000000..18fd6a1887 --- /dev/null +++ b/setup-ahitclient.py @@ -0,0 +1,642 @@ +import base64 +import datetime +import os +import platform +import shutil +import sys +import sysconfig +import typing +import warnings +import zipfile +import urllib.request +import io +import json +import threading +import subprocess + +from collections.abc import Iterable +from hashlib import sha3_512 +from pathlib import Path + + +# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +try: + requirement = 'cx-Freeze>=6.15.2' + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze + if '--yes' not in sys.argv and '-y' not in sys.argv: + input(f'Requirement {requirement} is not satisfied, press enter to install it') + subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) + import pkg_resources + +import cx_Freeze + +# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line +import setuptools.command.build + +if __name__ == "__main__": + # need to run this early to import from Utils and Launcher + # TODO: move stuff to not require this + import ModuleUpdate + ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) + ModuleUpdate.update_ran = False # restore for later + +from worlds.LauncherComponents import components, icon_paths +from Utils import version_tuple, is_windows, is_linux +from Cython.Build import cythonize + + +# On Python < 3.10 LogicMixin is not currently supported. +non_apworlds: set = { + "A Link to the Past", + "Adventure", + "ArchipIDLE", + "Archipelago", + "ChecksFinder", + "Clique", + "DLCQuest", + "Final Fantasy", + "Hylics 2", + "Kingdom Hearts 2", + "Lufia II Ancient Cave", + "Meritous", + "Ocarina of Time", + "Overcooked! 2", + "Raft", + "Secret of Evermore", + "Slay the Spire", + "Starcraft 2 Wings of Liberty", + "Sudoku", + "Super Mario 64", + "VVVVVV", + "Wargroove", + "Zillion", +} + +# LogicMixin is broken before 3.10 import revamp +if sys.version_info < (3,10): + non_apworlds.add("Hollow Knight") + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + +signtool: typing.Optional[str] +if os.path.exists("X:/pw.txt"): + print("Using signtool") + with open("X:/pw.txt", encoding="utf-8-sig") as f: + pw = f.read() + signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ + r'" /fd sha256 /tr http://timestamp.digicert.com/ ' +else: + signtool = None + + +build_platform = sysconfig.get_platform() +arch_folder = "exe.{platform}-{version}".format(platform=build_platform, + version=sysconfig.get_python_version()) +buildfolder = Path("build", arch_folder) +build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine() + + +# see Launcher.py on how to add scripts to setup.py +def resolve_icon(icon_name: str): + base_path = icon_paths[icon_name] + if is_windows: + path, extension = os.path.splitext(base_path) + ico_file = path + ".ico" + assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist." + return ico_file + else: + return base_path + + +exes = [ + cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name="ArchipelagoAHITClient.exe", + #target_name=c.frozen_name + (".exe" if is_windows else ""), + icon=resolve_icon(c.icon), + base="Win32GUI" if is_windows and not c.cli else None + ) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name +] + +#if is_windows: +if False: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + +extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] +extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] + + +def remove_sprites_from_folder(folder): + for file in os.listdir(folder): + if file != ".gitignore": + os.remove(folder / file) + + +def _threaded_hash(filepath): + hasher = sha3_512() + hasher.update(open(filepath, "rb").read()) + return base64.b85encode(hasher.digest()).decode() + + +# cx_Freeze's build command runs other commands. Override to accept --yes and store that. +class BuildCommand(setuptools.command.build.build): + user_options = [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ] + yes: bool + last_yes: bool = False # used by sub commands of build + + def initialize_options(self): + super().initialize_options() + type(self).last_yes = self.yes = False + + def finalize_options(self): + super().finalize_options() + type(self).last_yes = self.yes + + +# Override cx_Freeze's build_exe command for pre and post build steps +class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): + user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ('extra-data=', None, 'Additional files to add.'), + ] + yes: bool + extra_data: Iterable # [any] not available in 3.8 + extra_libs: Iterable # work around broken include_files + + buildfolder: Path + libfolder: Path + library: Path + buildtime: datetime.datetime + + def initialize_options(self): + super().initialize_options() + self.yes = BuildCommand.last_yes + self.extra_data = [] + self.extra_libs = [] + + def finalize_options(self): + super().finalize_options() + self.buildfolder = self.build_exe + self.libfolder = Path(self.buildfolder, "lib") + self.library = Path(self.libfolder, "library.zip") + + def installfile(self, path, subpath=None, keep_content: bool = False): + folder = self.buildfolder + if subpath: + folder /= subpath + print('copying', path, '->', folder) + if path.is_dir(): + folder /= path.name + if folder.is_dir() and not keep_content: + shutil.rmtree(folder) + shutil.copytree(path, folder, dirs_exist_ok=True) + elif path.is_file(): + shutil.copy(path, folder) + else: + print('Warning,', path, 'not found') + + def create_manifest(self, create_hashes=False): + # Since the setup is now split into components and the manifest is not, + # it makes most sense to just remove the hashes for now. Not aware of anyone using them. + hashes = {} + manifestpath = os.path.join(self.buildfolder, "manifest.json") + if create_hashes: + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor() + for dirpath, dirnames, filenames in os.walk(self.buildfolder): + for filename in filenames: + path = os.path.join(dirpath, filename) + hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path) + + import json + manifest = { + "buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"), + "hashes": {path: hash.result() for path, hash in hashes.items()}, + "version": version_tuple} + + json.dump(manifest, open(manifestpath, "wt"), indent=4) + print("Created Manifest") + + def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + + # pre-build steps + print(f"Outputting to: {self.buildfolder}") + os.makedirs(self.buildfolder, exist_ok=True) + import ModuleUpdate + ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) + ModuleUpdate.update(yes=self.yes) + + # auto-build cython modules + build_ext = self.distribution.get_command_obj("build_ext") + build_ext.inplace = False + self.run_command("build_ext") + # find remains of previous in-place builds, try to delete and warn otherwise + for path in build_ext.get_outputs(): + parts = os.path.split(path)[-1].split(".") + pattern = parts[0] + ".*." + parts[-1] + for match in Path().glob(pattern): + try: + match.unlink() + print(f"Removed {match}") + except Exception as ex: + warnings.warn(f"Could not delete old build output: {match}\n" + f"{ex}\nPlease close all AP instances and delete manually.") + + # regular cx build + self.buildtime = datetime.datetime.utcnow() + super().run() + + # manually copy built modules to lib folder. cx_Freeze does not know they exist. + for src in build_ext.get_outputs(): + print(f"copying {src} -> {self.libfolder}") + shutil.copy(src, self.libfolder, follow_symlinks=False) + + # need to finish download before copying + sni_thread.join() + + # include_files seems to not be done automatically. implement here + for src, dst in self.include_files: + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # now that include_files is completely broken, run find_libs here + for src, dst in find_libs(*self.extra_libs): + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # post build steps + if is_windows: # kivy_deps is win32 only, linux picks them up automatically + from kivy_deps import sdl2, glew + for folder in sdl2.dep_bins + glew.dep_bins: + shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) + print(f"copying {folder} -> {self.libfolder}") + + for data in self.extra_data: + self.installfile(Path(data)) + + # kivi data files + import kivy + shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), + self.buildfolder / "data", + dirs_exist_ok=True) + + os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + assert not non_apworlds - set(AutoWorldRegister.world_types), \ + f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" + folders_to_remove: typing.List[str] = [] + generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) + for worldname, worldtype in AutoWorldRegister.world_types.items(): + if worldname not in non_apworlds: + file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] + world_directory = self.libfolder / "worlds" / file_name + # this method creates an apworld that cannot be moved to a different OS or minor python version, + # which should be ok + with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + for path in world_directory.rglob("*.*"): + relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) + zf.write(path, relative_path) + folders_to_remove.append(file_name) + shutil.rmtree(world_directory) + shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") + # TODO: fix LttP options one day + shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") + try: + from maseya import z3pr + except ImportError: + print("Maseya Palette Shuffle not found, skipping data files.") + else: + # maseya Palette Shuffle exists and needs its data files + print("Maseya Palette Shuffle found, including data files...") + file = z3pr.__file__ + self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True) + + if signtool: + for exe in self.distribution.executables: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) + print("Signing SNI") + os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) + print("Signing OoT Utils") + for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): + os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) + + remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr") + + self.create_manifest() + + if is_windows: + # Inno setup stuff + with open("setup.ini", "w") as f: + min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000" + f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n") + with open("installdelete.iss", "w") as f: + f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n" + for world_directory in folders_to_remove) + else: + # make sure extra programs are executable + enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' + sni_exe = self.buildfolder / 'SNI/sni' + extra_exes = (enemizer_exe, sni_exe) + for extra_exe in extra_exes: + if extra_exe.is_file(): + extra_exe.chmod(0o755) + + +class AppImageCommand(setuptools.Command): + description = "build an app image from build output" + user_options = [ + ("build-folder=", None, "Folder to convert to AppImage."), + ("dist-file=", None, "AppImage output file."), + ("app-dir=", None, "Folder to use for packaging."), + ("app-icon=", None, "The icon to use for the AppImage."), + ("app-exec=", None, "The application to run inside the image."), + ("yes", "y", 'Answer "yes" to all questions.'), + ] + build_folder: typing.Optional[Path] + dist_file: typing.Optional[Path] + app_dir: typing.Optional[Path] + app_name: str + app_exec: typing.Optional[Path] + app_icon: typing.Optional[Path] # source file + app_id: str # lower case name, used for icon and .desktop + yes: bool + + def write_desktop(self): + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" + with open(desktop_filename, 'w', encoding="utf-8") as f: + f.write("\n".join(( + "[Desktop Entry]", + f'Name={self.app_name}', + f'Exec={self.app_exec}', + "Type=Application", + "Categories=Game", + f'Icon={self.app_id}', + '' + ))) + desktop_filename.chmod(0o755) + + def write_launcher(self, default_exe: Path): + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" + with open(launcher_filename, 'w', encoding="utf-8") as f: + f.write(f"""#!/bin/sh +exe="{default_exe}" +match="${{1#--executable=}}" +if [ "${{#match}}" -lt "${{#1}}" ]; then + exe="$match" + shift +elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then + exe="$2" + shift; shift +fi +tmp="${{exe#*/}}" +if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then + exe="{default_exe.parent}/$exe" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" +$APPDIR/$exe "$@" +""") + launcher_filename.chmod(0o755) + + def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" + try: + from PIL import Image + except ModuleNotFoundError: + if not self.yes: + input("Requirement PIL is not satisfied, press enter to install it") + subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) + from PIL import Image + im = Image.open(src) + res, _ = im.size + + if not name: + name = src.stem + ext = src.suffix + dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps') + dest_dir.mkdir(parents=True, exist_ok=True) + dest_file = dest_dir / f'{name}{ext}' + shutil.copy(src, dest_file) + if symlink: + symlink.symlink_to(dest_file.relative_to(symlink.parent)) + + def initialize_options(self): + self.build_folder = None + self.app_dir = None + self.app_name = self.distribution.metadata.name + self.app_icon = self.distribution.executables[0].icon + self.app_exec = Path('opt/{app_name}/{exe}'.format( + app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name + )) + self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format( + app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version, + platform=sysconfig.get_platform() + )) + self.yes = False + + def finalize_options(self): + if not self.app_dir: + self.app_dir = self.build_folder.parent / "AppDir" + self.app_id = self.app_name.lower() + + def run(self): + self.dist_file.parent.mkdir(parents=True, exist_ok=True) + if self.app_dir.is_dir(): + shutil.rmtree(self.app_dir) + self.app_dir.mkdir(parents=True) + opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + shutil.copytree(self.build_folder, opt_dir) + root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' + self.install_icon(self.app_icon, self.app_id, symlink=root_icon) + shutil.copy(root_icon, self.app_dir / '.DirIcon') + self.write_desktop() + self.write_launcher(self.app_exec) + print(f'{self.app_dir} -> {self.dist_file}') + subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) + + +def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: + """Try to find system libraries to be included.""" + if not args: + return [] + + arch = build_arch.replace('_', '-') + libc = 'libc6' # we currently don't support musl + + def parse(line): + lib, path = line.strip().split(' => ') + lib, typ = lib.split(' ', 1) + for test_arch in ('x86-64', 'i386', 'aarch64'): + if test_arch in typ: + lib_arch = test_arch + break + else: + lib_arch = '' + for test_libc in ('libc6',): + if test_libc in typ: + lib_libc = test_libc + break + else: + lib_libc = '' + return (lib, lib_arch, lib_libc), path + + if not hasattr(find_libs, "cache"): + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } + + def find_lib(lib, arch, libc): + for k, v in find_libs.cache.items(): + if k == (lib, arch, libc): + return v + for k, v, in find_libs.cache.items(): + if k[0].startswith(lib) and k[1] == arch and k[2] == libc: + return v + return None + + res = [] + for arg in args: + # try exact match, empty libc, empty arch, empty arch and libc + file = find_lib(arg, arch, libc) + file = file or find_lib(arg, arch, '') + file = file or find_lib(arg, '', libc) + file = file or find_lib(arg, '', '') + # resolve symlinks + for n in range(0, 5): + res.append((file, os.path.join('lib', os.path.basename(file)))) + if not os.path.islink(file): + break + dirname = os.path.dirname(file) + file = os.readlink(file) + if not os.path.isabs(file): + file = os.path.join(dirname, file) + return res + + +cx_Freeze.setup( + name="Archipelago", + version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}", + description="Archipelago", + executables=exes, + ext_modules=cythonize("_speedups.pyx"), + options={ + "build_exe": { + "packages": ["worlds", "kivy", "cymem", "websockets"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["worlds", "sc2"], + "include_files": [], # broken in cx 6.14.0, we use more special sauce now + "include_msvcr": False, + "replace_paths": ["*."], + "optimize": 1, + "build_exe": buildfolder, + "extra_data": extra_data, + "extra_libs": extra_libs, + "bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else [] + }, + "bdist_appimage": { + "build_folder": buildfolder, + }, + }, + # override commands to get custom stuff in + cmdclass={ + "build": BuildCommand, + "build_exe": BuildExeCommand, + "bdist_appimage": AppImageCommand, + }, +) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py new file mode 100644 index 0000000000..f51d4948ee --- /dev/null +++ b/worlds/ahit/DeathWishLocations.py @@ -0,0 +1,262 @@ +from .Types import HatInTimeLocation, HatInTimeItem +from .Regions import connect_regions, create_region +from BaseClasses import Region, LocationProgressType, ItemClassification +from worlds.generic.Rules import add_rule +from worlds.AutoWorld import World +from typing import List +from .Locations import death_wishes + + +dw_prereqs = { + "So You're Back From Outer Space": ["Beat the Heat"], + "Snatcher's Hit List": ["Beat the Heat"], + "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"], + "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"], + "Collect-a-thon": ["So You're Back From Outer Space"], + "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"], + "Mafia's Jumps": ["She Speedran from Outer Space"], + "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"], + "Encore! Encore!": ["Collect-a-thon"], + + "Security Breach": ["Beat the Heat"], + "Rift Collapse: Dead Bird Studio": ["Security Breach"], + "The Great Big Hootenanny": ["Security Breach"], + "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"], + "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"], + "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"], + "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"], + "Zero Jumps": ["Rift Collapse: Dead Bird Studio"], + "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"], + + "Speedrun Well": ["Beat the Heat"], + "Rift Collapse: Sleepy Subcon": ["Speedrun Well"], + "Boss Rush": ["Speedrun Well"], + "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"], + "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"], + "Community Rift: Twilight Travels": ["Quality Time with Snatcher"], + "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"], + + "Bird Sanctuary": ["Beat the Heat"], + "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"], + "Wound-Up Windmill": ["Bird Sanctuary"], + "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"], + "Camera Tourist": ["Rift Collapse: Alpine Skyline"], + "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"], + "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"], + + "The Mustache Gauntlet": ["Wound-Up Windmill"], + "No More Bad Guys": ["The Mustache Gauntlet"], + "Seal the Deal": ["Encore! Encore!", "Killing Two Birds", + "Breaching the Contract", "No More Bad Guys"], + + "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio", + "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"], + + "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"], +} + +dw_candles = [ + "Snatcher's Hit List", + "Zero Jumps", + "Camera Tourist", + "Snatcher Coins in Mafia Town", + "Snatcher Coins in Battle of the Birds", + "Snatcher Coins in Subcon Forest", + "Snatcher Coins in Alpine Skyline", + "Snatcher Coins in Nyakuza Metro", +] + +annoying_dws = [ + "Vault Codes in the Wind", + "Boss Rush", + "Camera Tourist", + "The Mustache Gauntlet", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + "Seal the Deal", # Non-excluded if goal +] + +# includes the above as well +annoying_bonuses = [ + "So You're Back From Outer Space", + "Encore! Encore!", + "Snatcher's Hit List", + "10 Seconds until Self-Destruct", + "Killing Two Birds", + "Zero Jumps", + "Bird Sanctuary", + "Wound-Up Windmill", + "Seal the Deal", +] + +dw_classes = { + "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder", + "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace", + "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody", + "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy", + "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown", + "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX", + "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien", + "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien", + "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault", + "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown", + + "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards", + "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade", + "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds", + "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime", + "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX", + "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds", + "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses", + + "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell", + "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon", + "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush", + "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest", + "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX", + "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon", + + "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse", + "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps", + "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill", + "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness", + "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps", + "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1", + + "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle", + "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX", + + "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX", + "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise", + "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks", + + "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump", + "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels", + "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift", + + "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro", +} + + +def create_dw_regions(world: World): + if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + for name in annoying_dws: + world.get_excluded_dws().append(name) + + if world.multiworld.DWEnableBonus[world.player].value == 0 \ + or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + for name in death_wishes: + world.get_excluded_bonuses().append(name) + elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + for name in annoying_bonuses: + world.get_excluded_bonuses().append(name) + + if world.multiworld.DWExcludeCandles[world.player].value > 0: + for name in dw_candles: + if name in world.get_excluded_dws(): + continue + world.get_excluded_dws().append(name) + + spaceship = world.multiworld.get_region("Spaceship", world.player) + dw_map: Region = create_region(world, "Death Wish Map") + entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + + add_rule(entrance, lambda state: state.has("Time Piece", world.player, + world.multiworld.DWTimePieceRequirement[world.player].value)) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list: List[str] = [] + for name in death_wishes.keys(): + if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): + continue + + dw_list.append(name) + + world.random.shuffle(dw_list) + count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + dw_shuffle: List[str] = [] + total = min(len(dw_list), count) + for i in range(total): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.multiworld.EndGoal[world.player].value == 3: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.set_dw_shuffle(dw_shuffle) + prev_dw: Region + for i in range(len(dw_shuffle)): + name = dw_shuffle[i] + dw = create_region(world, name) + + if i == 0: + connect_regions(dw_map, dw, f"-> {name}", world.player) + else: + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + + loc_id = death_wishes[name] + main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) + + if name in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) + prev_dw = dw + else: + for key, loc_id in death_wishes.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + world.get_excluded_dws().append(key) + continue + + dw = create_region(world, key) + + if key == "Beat the Heat": + connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + elif key in dw_prereqs.keys(): + for name in dw_prereqs[key]: + parent = world.multiworld.get_region(name, world.player) + connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + + main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", + ItemClassification.progression, None, world.player)) + + if key in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(key): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py new file mode 100644 index 0000000000..c448484036 --- /dev/null +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,539 @@ +from worlds.AutoWorld import World, CollectionState +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData +from .DeathWishLocations import dw_prereqs, dw_candles +from BaseClasses import Entrance, Location, ItemClassification +from worlds.generic.Rules import add_rule, set_rule +from typing import List, Callable +from .Regions import act_chapters +from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "Beat the Heat": LocData(umbrella=True), + "So You're Back From Outer Space": LocData(hookshot=True), + "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), + "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), + + "Security Breach": LocData(hit_requirement=1), + "10 Seconds until Self-Destruct": LocData(hookshot=True), + "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), + + "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Boss Rush": LocData(umbrella=True, hookshot=True), + "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Bird Sanctuary": LocData(hookshot=True), + "Wound-Up Windmill": LocData(hookshot=True), + "The Illness has Speedrun": LocData(hookshot=True), + "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Camera Tourist": LocData(misc_required=["Camera Badge"]), + + "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Rift Collapse - Deep Sea": LocData(hookshot=True), +} + +# Includes main objective requirements +dw_bonus_requirements = { + # Some One-Hit Hero requirements need badge pins as well because of Hookshot + "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]), + + "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), + + "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), + "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]), + + "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), + + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]), +} + +dw_stamp_costs = { + "So You're Back From Outer Space": 2, + "Collect-a-thon": 5, + "She Speedran from Outer Space": 8, + "Encore! Encore!": 10, + + "Security Breach": 4, + "The Great Big Hootenanny": 7, + "10 Seconds until Self-Destruct": 15, + "Killing Two Birds": 25, + "Snatcher Coins in Nyakuza Metro": 30, + + "Speedrun Well": 10, + "Boss Rush": 15, + "Quality Time with Snatcher": 20, + "Breaching the Contract": 40, + + "Bird Sanctuary": 15, + "Wound-Up Windmill": 30, + "The Illness has Speedrun": 35, + + "The Mustache Gauntlet": 35, + "No More Bad Guys": 50, + "Seal the Deal": 70, +} + +required_snatcher_coins = { + "Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower", + "Snatcher Coin - Under Ruined Tower"], + + "Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush", + "Snatcher Coin - Picture Perfect"], + + "Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof", + "Snatcher Coin - Giant Time Piece"], + + "Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake", + "Snatcher Coin - Windmill"], + + "Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train", + "Snatcher Coin - Pink Paw Fence"], +} + + +def set_dw_rules(world: World): + if "Snatcher's Hit List" not in world.get_excluded_dws() \ + or "Camera Tourist" not in world.get_excluded_dws(): + set_enemy_rules(world) + + dw_list: List[str] = [] + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list = world.get_dw_shuffle() + else: + for name in death_wishes.keys(): + dw_list.append(name) + + for name in dw_list: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + dw = world.multiworld.get_region(name, world.player) + temp_list: List[Location] = [] + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) + bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) + temp_list.append(main_objective) + temp_list.append(full_clear) + + if world.multiworld.DWShuffle[world.player].value == 0: + if name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + + if world.multiworld.DWEnableBonus[world.player].value == 0: + # place nothing, but let the locations exist still, so we can use them for bonus stamp rules + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + full_clear.show_in_spoiler = False + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + # Specific Rules + modify_dw_rules(world, name) + + main_rule: Callable[[CollectionState], bool] + for i in range(len(temp_list)): + loc = temp_list[i] + data: LocData + + if loc.name == main_objective.name: + data = dw_requirements.get(name) + else: + data = dw_bonus_requirements.get(name) + + if data is None: + continue + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(loc, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + main_rule = main_objective.access_rule + + if loc.name == main_objective.name: + add_rule(main_stamp, loc.access_rule) + elif loc.name == full_clear.name: + add_rule(loc, main_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + and not world.is_bonus_excluded(loc.name): + add_rule(bonus_stamps, loc.access_rule) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_shuffle = world.get_dw_shuffle() + for i in range(len(dw_shuffle)): + if i == 0: + continue + + name = dw_shuffle[i] + prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) + add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) + else: + for key, reqs in dw_prereqs.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + access_rules: List[Callable[[CollectionState], bool]] = [] + entrances: List[Entrance] = [] + + for parent in reqs: + entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player) + entrances.append(entrance) + + if not world.is_dw_excluded(parent): + access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player)) + + for entrance in entrances: + for rule in access_rules: + add_rule(entrance, rule) + + if world.multiworld.EndGoal[world.player].value == 3: + world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", + world.player) + + +def modify_dw_rules(world: World, name: str): + difficulty: Difficulty = get_difficulty(world) + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "The Illness has Speedrun": + # All stamps with hookshot only in Expert + if difficulty >= Difficulty.EXPERT: + set_rule(full_clear, lambda state: True) + else: + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + + elif name == "The Mustache Gauntlet": + add_rule(main_objective, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING)) + + elif name == "Vault Codes in the Wind": + # Sprint is normally expected here + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Speedrun Well": + # All stamps with nothing :) + if difficulty >= Difficulty.EXPERT: + set_rule(main_objective, lambda state: True) + + elif name == "Mafia's Jumps": + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + set_rule(full_clear, lambda state: True) + + elif name == "So You're Back from Outer Space": + # Without Hookshot + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Wound-Up Windmill": + # No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it. + if difficulty >= Difficulty.MODERATE: + set_rule(full_clear, lambda state: can_use_hookshot(state, world) + and state.has("One-Hit Hero Badge", world.player)) + + if name in dw_candles: + set_candle_dw_rules(name, world) + + +def get_total_dw_stamps(state: CollectionState, world: World) -> int: + if world.multiworld.DWShuffle[world.player].value > 0: + return 999 # no stamp costs in death wish shuffle + + count: int = 0 + + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + if state.has(f"1 Stamp - {name}", world.player): + count += 1 + else: + continue + + if state.has(f"2 Stamps - {name}", world.player): + count += 2 + elif name not in dw_candles: + # most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + count += 1 + + return count + + +def set_candle_dw_rules(name: str, world: World): + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "Zero Jumps": + add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) + add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) + + # No Ice Hat/painting required in Expert for Toilet Zero Jumps + # This painting wall can only be skipped via cherry hover. + if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + else: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world)) + + set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + elif name == "Snatcher's Hit List": + add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) + add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + + elif name == "Camera Tourist": + add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) + and state.has("Triple Enemy Photo", world.player)) + + elif "Snatcher Coins" in name: + for coin in required_snatcher_coins[name]: + add_rule(main_objective, lambda state: state.has(coin, world.player), "or") + add_rule(full_clear, lambda state: state.has(coin, world.player)) + + +def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: + total: int = 0 + + for name in act_chapters.keys(): + n = f"{name} (Zero Jumps)" + if n not in zero_jumps: + continue + + if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert: + continue + + if not state.has(n, world.player): + continue + + total += 1 + + return total + + +def get_reachable_enemy_count(state: CollectionState, world: World) -> int: + count: int = 0 + for enemy in hit_list.keys(): + if enemy in bosses: + continue + + if state.has(enemy, world.player): + count += 1 + + return count + + +def can_reach_all_bosses(state: CollectionState, world: World) -> bool: + for boss in bosses: + if not state.has(boss, world.player): + return False + + return True + + +def create_enemy_events(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \ + and area not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(area, world.player) + event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region) + event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + + for name in triple_enemy_locations: + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \ + and name not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(name, world.player) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + if name == "The Mustache Gauntlet": + add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + +def set_enemy_rules(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + event = world.multiworld.get_location(f"{enemy} - {area}", world.player) + + if enemy == "Toxic Flower": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + if area == "The Illness has Spread": + add_rule(event, lambda state: not zipline_logic(world) or + state.has("Zipline Unlock - The Birdhouse Path", world.player) + or state.has("Zipline Unlock - The Lava Cake Path", world.player) + or state.has("Zipline Unlock - The Windmill Path", world.player)) + + elif enemy == "Director": + if area == "Dead Bird Studio Basement": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + elif enemy == "Snatcher" or enemy == "Mustache Girl": + if area == "Boss Rush": + # need to be able to kill toilet and snatcher + add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world)) + if enemy == "Mustache Girl": + add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world)) + + elif area == "The Finale" and enemy == "Mustache Girl": + add_rule(event, lambda state: can_use_hookshot(state, world) + and can_use_hat(state, world, HatType.DWELLER)) + + elif enemy == "Shock Squid" or enemy == "Ninja Cat": + if area == "Time Rift - Deep Sea": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + +# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them +hit_list = { + "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", + "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks", + "So You're Back From Outer Space"], + + "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell", + "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet", + "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"], + + "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"], + + "Rat": ["Down with the Mafia!", "Bluefin Tunnel"], + + "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea", + "Rift Collapse: Sleepy Subcon"], + + "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"], + + "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well", + "The Lava Cake", "The Windmill"], + + "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary", + "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"], + + "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"], + + "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"], + + "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"], + + "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet", + "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea", + "Rift Collapse: Alpine Skyline"], + + # Bosses + "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], + + "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Toilet": ["Toilet of Doom", "Boss Rush"], + + "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", + "Quality Time with Snatcher"], + + "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"], + + "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], +} + +# Camera Tourist has a bonus that requires getting three different types of enemies in one picture. +triple_enemy_locations = [ + "She Came from Outer Space", + "She Speedran from Outer Space", + "Mafia's Jumps", + "The Mustache Gauntlet", + "The Birdhouse", + "Bird Sanctuary", + "Time Rift - Tour", +] + +bosses = [ + "Mafia Boss", + "Conductor", + "Toilet", + "Snatcher", + "Toxic Flower", + "Mustache Girl", +] diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py new file mode 100644 index 0000000000..869f998a9d --- /dev/null +++ b/worlds/ahit/Items.py @@ -0,0 +1,286 @@ +from BaseClasses import Item, ItemClassification +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem +from .Locations import get_total_locations +from .Rules import get_difficulty +from typing import Optional, List, Dict + + +def create_itempool(world: World) -> List[Item]: + itempool: List[Item] = [] + if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0: + calculate_yarn_costs(world) + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.multiworld.YarnAvailable[world.player].value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(world, name): + continue + + if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values(): + continue + + item_type: ItemClassification = item_table.get(name).classification + + if world.is_dw_only(): + if item_type is ItemClassification.progression \ + or item_type is ItemClassification.progression_skip_balancing: + continue + else: + if name == "Scooter Badge": + if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + elif name == "No Bonk Badge": + if get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + continue + + if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + continue + + if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if world.is_dlc1(): + max_extra += 6 + + if world.is_dlc2(): + max_extra += 10 + + tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) + + for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) + + itempool += create_junk_items(world, get_total_locations(world) - len(itempool)) + return itempool + + +def calculate_yarn_costs(world: World): + mw = world.multiworld + p = world.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + world.get_hat_yarn_costs()[HatType(i)] = cost + max_cost += cost + + available_yarn: int = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + if max_cost + mw.MinExtraYarn[p].value > available_yarn: + mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + + +def item_dlc_enabled(world: World, name: str) -> bool: + data = item_table[name] + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + + return False + + +def create_item(world: World, name: str) -> Item: + data = item_table[name] + return HatInTimeItem(name, data.classification, data.code, world.player) + + +def create_multiple_items(world: World, name: str, count: int = 1, + item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + + data = item_table[name] + itemlist: List[Item] = [] + + for i in range(count): + itemlist += [HatInTimeItem(name, item_type, data.code, world.player)] + + return itemlist + + +def create_junk_items(world: World, count: int) -> List[Item]: + trap_chance = world.multiworld.TrapChance[world.player].value + junk_pool: List[Item] = [] + junk_list: Dict[str, int] = {} + trap_list: Dict[str, int] = {} + ic: ItemClassification + + for name in item_table.keys(): + ic = item_table[name].classification + if ic == ItemClassification.filler: + if world.is_dw_only() and "Pons" in name: + continue + + junk_list[name] = junk_weights.get(name) + + elif trap_chance > 0 and ic == ItemClassification.trap: + if name == "Baby Trap": + trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value + elif name == "Laser Trap": + trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value + elif name == "Parade Trap": + trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value + + for i in range(count): + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: + junk_pool += [world.create_item( + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + else: + junk_pool += [world.create_item( + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + + return junk_pool + + +ahit_items = { + "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing), + "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing), + + # for HatItems option + "Sprint Hat": ItemData(2000300049, ItemClassification.progression), + "Brewing Hat": ItemData(2000300050, ItemClassification.progression), + "Ice Hat": ItemData(2000300051, ItemClassification.progression), + "Dweller Mask": ItemData(2000300052, ItemClassification.progression), + "Time Stop Hat": ItemData(2000300053, ItemClassification.progression), + + # Relics + "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression), + "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression), + "Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression), + "Relic (Train)": ItemData(2000300009, ItemClassification.progression), + "Relic (UFO)": ItemData(2000300010, ItemClassification.progression), + "Relic (Cow)": ItemData(2000300011, ItemClassification.progression), + "Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression), + "Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression), + "Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression), + "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression), + "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression), + "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression), + + # Badges + "Projectile Badge": ItemData(2000300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), + "Hover Badge": ItemData(2000300026, ItemClassification.useful), + "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), + "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), + "Compass Badge": ItemData(2000300030, ItemClassification.useful), + "Scooter Badge": ItemData(2000300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), + + # Other + "Badge Pin": ItemData(2000300043, ItemClassification.useful), + "Umbrella": ItemData(2000300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), + + # Garbage items + "25 Pons": ItemData(2000300034, ItemClassification.filler), + "50 Pons": ItemData(2000300035, ItemClassification.filler), + "100 Pons": ItemData(2000300036, ItemClassification.filler), + "Health Pon": ItemData(2000300037, ItemClassification.filler), + "Random Cosmetic": ItemData(2000300044, ItemClassification.filler), + + # Traps + "Baby Trap": ItemData(2000300039, ItemClassification.trap), + "Laser Trap": ItemData(2000300040, ItemClassification.trap), + "Parade Trap": ItemData(2000300041, ItemClassification.trap), + + # DLC1 items + "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), + + # DLC2 items + "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2), +} + +act_contracts = { + "Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression), + "Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression), + "Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression), + "Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression), +} + +alps_hooks = { + "Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression), + "Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression), + "Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression), + "Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression), +} + +relic_groups = { + "Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"}, + "Train": {"Relic (Mountain Set)", "Relic (Train)"}, + "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"}, + "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"}, + "Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"}, + "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"}, +} + +item_frequencies = { + "Badge Pin": 2, + "Progressive Painting Unlock": 3, +} + +junk_weights = { + "25 Pons": 50, + "50 Pons": 25, + "100 Pons": 10, + "Health Pon": 35, + "Random Cosmetic": 35, +} + +item_table = { + **ahit_items, + **act_contracts, + **alps_hooks, +} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py new file mode 100644 index 0000000000..bf31c8cba8 --- /dev/null +++ b/worlds/ahit/Locations.py @@ -0,0 +1,977 @@ +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, LocData, Difficulty +from typing import Dict +from .Options import TasksanityCheckCount + + +TASKSANITY_START_ID = 2000300204 + + +def get_total_locations(world: World) -> int: + total: int = 0 + + if not world.is_dw_only(): + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 + + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + if world.is_dw(): + if world.multiworld.DWShuffle[world.player].value > 0: + total += len(world.get_dw_shuffle()) + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += len(world.get_dw_shuffle()) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += 37 + if world.is_dlc2(): + total += 1 + + return total + + +def location_dlc_enabled(world: World, location: str) -> bool: + data = location_table.get(location) or event_locs.get(location) + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw(): + return True + + return False + + +def is_location_valid(world: World, location: str) -> bool: + if not location_dlc_enabled(world, location): + return False + + if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + and location in storybook_pages.keys(): + return False + + if world.multiworld.ShuffleActContracts[world.player].value == 0 \ + and location in contract_locations.keys(): + return False + + if location not in world.shop_locs and location in shop_locations: + return False + + data = location_table.get(location) or event_locs.get(location) + if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + return False + + # No need for all those event items if we're not doing candles + if data.dlc_flags & HatDLC.death_wish: + if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): + return False + + if world.multiworld.DWShuffle[world.player].value > 0 \ + and data.region in death_wishes and data.region not in world.get_dw_shuffle(): + return False + + if location in zero_jumps: + if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + return False + + difficulty: int = world.multiworld.LogicDifficulty[world.player].value + if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): + return False + + if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT): + return False + + return True + + +def get_location_names() -> Dict[str, int]: + names = {name: data.id for name, data in location_table.items()} + id_start: int = TASKSANITY_START_ID + for i in range(TasksanityCheckCount.range_end): + names.setdefault(f"Tasksanity Check {i+1}", id_start+i) + + for (key, loc_id) in death_wishes.items(): + names.setdefault(f"{key} - Main Objective", loc_id) + names.setdefault(f"{key} - All Clear", loc_id+1) + + return names + + +ahit_locations = { + "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), + + # 300000 range - Mafia Town/Batle of the Birds + "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), + "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"), + "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"), + "Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"), + "Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"), + "Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"), + "Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]), + "Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"), + "Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"), + "Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"), + "Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"), + "Mafia Town - Port": LocData(2000305219, "Mafia Town Area"), + "Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"), + "Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]), + "Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"), + + # These can be accessed from HUMT, the above locations can't be + "Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"), + "Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"), + "Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"), + "Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"), + "Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"), + "Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"), + "Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"), + "Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"), + "Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"), + "Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"), + "Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"), + "Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), + "Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"), + "Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"), + "Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"), + + "Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), + "Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"), + "Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]), + "Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"), + + "Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), + + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + + "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"), + + "Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express", + required_hats=[HatType.BREWING]), + + "Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"), + "Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"), + + "Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"), + "Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"), + + "Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True), + + # Subcon Forest + "Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"), + "Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"), + "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"), + "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"), + "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"), + "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"), + + "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=1), + + "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2), + + "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=2), + + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), + "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1), + + "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=2), + + "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=3), + + "Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + hookshot=True, + paintings=3), + + "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), + "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), + + "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, + paintings=1), + + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + # Alpine Skyline + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), + + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", + required_hats=[HatType.BREWING]), + + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), + "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"), + "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"), + + "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse", + required_hats=[HatType.DWELLER]), + + "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"), + + "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"), + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"), + "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), + "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), + "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), + "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), + "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), + "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"), + "Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"), + + "The Finale - Frozen Item": LocData(2000304108, "The Finale"), + + "Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.BREWING]), + + "Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.TIME_STOP, HatType.DWELLER]), +} + +act_completions = { + "Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"), + + "Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"), + "Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"), + "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"), + "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"), + "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), + "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"), + "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"), + "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"), + "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"), + + "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1), + "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"), + "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"), + "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True), + "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True), + "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"), + "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True), + "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"), + "Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"), + "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"), + + "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), + + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1, + paintings=1), + + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, + paintings=1), + + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + + "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", + required_hats=[HatType.SPRINT]), + + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), + "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), + "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"), + + "Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"), + "Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"), + "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"), + "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"), + "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True), + + "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell", + required_hats=[HatType.DWELLER]), + + "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"), + + "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), + "Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1), + + "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), + + "Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + hookshot=True), + + "Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), + + "Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE, HatType.DWELLER]), + + "Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.ICE, HatType.BREWING]), + + "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), +} + +storybook_pages = { + "Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"), + + "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"), + + "Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"), + + "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"), + + "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + + "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), +} + +shop_locations = { + "Badge Seller - Item 1": LocData(2000301003, "Badge Seller"), + "Badge Seller - Item 2": LocData(2000301004, "Badge Seller"), + "Badge Seller - Item 3": LocData(2000301005, "Badge Seller"), + "Badge Seller - Item 4": LocData(2000301006, "Badge Seller"), + "Badge Seller - Item 5": LocData(2000301007, "Badge Seller"), + "Badge Seller - Item 6": LocData(2000301008, "Badge Seller"), + "Badge Seller - Item 7": LocData(2000301009, "Badge Seller"), + "Badge Seller - Item 8": LocData(2000301010, "Badge Seller"), + "Badge Seller - Item 9": LocData(2000301011, "Badge Seller"), + "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"), + "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"), + + "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2), + + "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + hookshot=True, required_hats=[HatType.DWELLER]), + + "Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + + "Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + + "Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + + "Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + + "Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + + "Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + + "Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + + # This guy requires either the yellow ticket or the Ice Hat + "Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + + "Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + + "Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + +} + +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"), +} + +# Don't put any of the locations from peaks here, the rules for their entrances are set already +zipline_unlocks = { + "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path", + + "Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path", + + "Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path", + "Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path", + + "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", +} + +# act completion rules should be set automatically as these are all event items +zero_jumps_hard = { + "Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "The Big Parade": LocData(0, "The Big Parade", + umbrella=True, + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread", + required_hats=[HatType.ICE], hookshot=True, + hit_requirement=1, dlc_flags=HatDLC.death_wish), + + "The Finale (Zero Jumps)": LocData(0, "The Finale", + required_hats=[HatType.ICE, HatType.DWELLER], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), +} + +zero_jumps_expert = { + "The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + + "The Windmill (Zero Jumps)": LocData(0, "The Windmill", + required_hats=[HatType.ICE], + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + + "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), +} + +zero_jumps = { + **zero_jumps_hard, + **zero_jumps_expert, + "Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + + "Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race", + required_hats=[HatType.TIME_STOP], + dlc_flags=HatDLC.death_wish), + + "The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio", + required_hats=[HatType.ICE], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Train Rush (Zero Jumps)": LocData(0, "Train Rush", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations", + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired", + umbrella=True, + dlc_flags=HatDLC.death_wish), + + # No ice hat/painting required in Expert + "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom", + hookshot=True, + hit_requirement=1, + required_hats=[HatType.ICE], + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service", + required_hats=[HatType.SPRINT], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), + + "Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.dlc2_dw), +} + +# noinspection PyDictDuplicateKeys +snatcher_coins = { + "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw), +} + +event_locs = { + **zero_jumps, + **snatcher_coins, + "HUMT Access": LocData(0, "Heating Up Mafia Town"), + "TOD Access": LocData(0, "Toilet of Doom"), + "YCHE Access": LocData(0, "Your Contract has Expired"), + + "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), + "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), + "Windmill Cleared": LocData(0, "The Windmill", act_event=True), + "Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True), + "Time Piece Cluster": LocData(0, "The Finale", act_event=True), + + # not really an act + "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True), +} + +# DO NOT ALTER THE ORDER OF THIS LIST +death_wishes = { + "Beat the Heat": 2000350000, + "Snatcher's Hit List": 2000350002, + "So You're Back From Outer Space": 2000350004, + "Collect-a-thon": 2000350006, + "Rift Collapse: Mafia of Cooks": 2000350008, + "She Speedran from Outer Space": 2000350010, + "Mafia's Jumps": 2000350012, + "Vault Codes in the Wind": 2000350014, + "Encore! Encore!": 2000350016, + "Snatcher Coins in Mafia Town": 2000350018, + + "Security Breach": 2000350020, + "The Great Big Hootenanny": 2000350022, + "Rift Collapse: Dead Bird Studio": 2000350024, + "10 Seconds until Self-Destruct": 2000350026, + "Killing Two Birds": 2000350028, + "Snatcher Coins in Battle of the Birds": 2000350030, + "Zero Jumps": 2000350032, + + "Speedrun Well": 2000350034, + "Rift Collapse: Sleepy Subcon": 2000350036, + "Boss Rush": 2000350038, + "Quality Time with Snatcher": 2000350040, + "Breaching the Contract": 2000350042, + "Snatcher Coins in Subcon Forest": 2000350044, + + "Bird Sanctuary": 2000350046, + "Rift Collapse: Alpine Skyline": 2000350048, + "Wound-Up Windmill": 2000350050, + "The Illness has Speedrun": 2000350052, + "Snatcher Coins in Alpine Skyline": 2000350054, + "Camera Tourist": 2000350056, + + "The Mustache Gauntlet": 2000350058, + "No More Bad Guys": 2000350060, + + "Seal the Deal": 2000350062, + "Rift Collapse: Deep Sea": 2000350064, + "Cruisin' for a Bruisin'": 2000350066, + + "Community Rift: Rhythm Jump Studio": 2000350068, + "Community Rift: Twilight Travels": 2000350070, + "Community Rift: The Mountain Rift": 2000350072, + "Snatcher Coins in Nyakuza Metro": 2000350074, +} + +location_table = { + **ahit_locations, + **act_completions, + **storybook_pages, + **contract_locations, + **shop_locations, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py new file mode 100644 index 0000000000..f3dd2a8c66 --- /dev/null +++ b/worlds/ahit/Options.py @@ -0,0 +1,728 @@ +import typing +from worlds.AutoWorld import World +from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict + + +def adjust_options(world: World): + world.multiworld.HighestChapterCost[world.player].value = max( + world.multiworld.HighestChapterCost[world.player].value, + world.multiworld.LowestChapterCost[world.player].value) + + world.multiworld.LowestChapterCost[world.player].value = min( + world.multiworld.LowestChapterCost[world.player].value, + world.multiworld.HighestChapterCost[world.player].value) + + world.multiworld.FinalChapterMinCost[world.player].value = min( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value) + + world.multiworld.FinalChapterMaxCost[world.player].value = max( + world.multiworld.FinalChapterMaxCost[world.player].value, + world.multiworld.FinalChapterMinCost[world.player].value) + + world.multiworld.BadgeSellerMinItems[world.player].value = min( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.BadgeSellerMaxItems[world.player].value = max( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.NyakuzaThugMinShopItems[world.player].value = min( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.DWShuffleCountMin[world.player].value = min( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + world.multiworld.DWShuffleCountMax[world.player].value = max( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + total_tps: int = get_total_time_pieces(world) + if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: + world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.LowestChapterCost[world.player].value > total_tps-5: + world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: + world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + + if world.multiworld.FinalChapterMinCost[world.player].value > total_tps: + world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5) + + # Don't allow Rush Hour goal if DLC2 content is disabled + if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: + world.multiworld.EndGoal[world.player].value = 1 + + # Don't allow Seal the Deal goal if Death Wish content is disabled + if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): + world.multiworld.EndGoal[world.player].value = 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + + if world.is_dw_only(): + world.multiworld.EndGoal[world.player].value = 3 + world.multiworld.ActRandomizer[world.player].value = 0 + world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 + world.multiworld.ShuffleSubconPaintings[world.player].value = 0 + world.multiworld.ShuffleStorybookPages[world.player].value = 0 + world.multiworld.ShuffleActContracts[world.player].value = 0 + world.multiworld.EnableDLC1[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = -1 + world.multiworld.DWTimePieceRequirement[world.player].value = 0 + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + + +class EndGoal(Choice): + """The end goal required to beat the game. + Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. + + Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 + will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. + Requires DLC2 content to be enabled. + + Seal the Deal: Reach and complete the Seal the Deal death wish main objective. + Requires Death Wish content to be enabled.""" + display_name = "End Goal" + option_finale = 1 + option_rush_hour = 2 + option_seal_the_deal = 3 + default = 1 + + +class ActRandomizer(Choice): + """If enabled, shuffle the game's Acts between each other. + Light will cause Time Rifts to only be shuffled amongst each other, + and Blue Time Rifts and Purple Time Rifts to be shuffled separately.""" + display_name = "Shuffle Acts" + option_false = 0 + option_light = 1 + option_insanity = 2 + default = 1 + + +class ActPlando(OptionDict): + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + display_name = "Act Plando" + + +class FinaleShuffle(Toggle): + """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" + display_name = "Finale Shuffle" + default = 0 + + +class LogicDifficulty(Choice): + """Choose the difficulty setting for logic.""" + display_name = "Logic Difficulty" + option_normal = -1 + option_moderate = 0 + option_hard = 1 + option_expert = 2 + default = -1 + + +class CTRLogic(Choice): + """Choose how you want to logically clear Cheating the Race.""" + display_name = "Cheating the Race Logic" + option_time_stop_only = 0 + option_scooter = 1 + option_sprint = 2 + option_nothing = 3 + default = 0 + + +class RandomizeHatOrder(Choice): + """Randomize the order that hats are stitched in. + Time Stop Last will force Time Stop to be the last hat in the sequence.""" + display_name = "Randomize Hat Order" + option_false = 0 + option_true = 1 + option_time_stop_last = 2 + default = 1 + + +class YarnBalancePercent(Range): + """How much (in percentage) of the yarn in the pool that will be progression balanced.""" + display_name = "Yarn Balance Percentage" + default = 20 + range_start = 0 + range_end = 100 + + +class TimePieceBalancePercent(Range): + """How much (in percentage) of time pieces in the pool that will be progression balanced.""" + display_name = "Time Piece Balance Percentage" + default = 35 + range_start = 0 + range_end = 100 + + +class StartWithCompassBadge(Toggle): + """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world + (instead of just Relics). Recommended if you're not familiar with where item locations are.""" + display_name = "Start with Compass Badge" + default = 1 + + +class CompassBadgeMode(Choice): + """closest - Compass Badge points to the closest item regardless of classification + important_only - Compass Badge points to progression/useful items only + important_first - Compass Badge points to progression/useful items first, then it will point to junk items""" + display_name = "Compass Badge Mode" + option_closest = 1 + option_important_only = 2 + option_important_first = 3 + default = 1 + + +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + +class ShuffleStorybookPages(Toggle): + """If enabled, each storybook page in the purple Time Rifts is an item check. + The Compass Badge can track these down for you.""" + display_name = "Shuffle Storybook Pages" + default = 1 + + +class ShuffleActContracts(Toggle): + """If enabled, shuffle Snatcher's act contracts into the pool as items""" + display_name = "Shuffle Contracts" + default = 1 + + +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + +class ShuffleSubconPaintings(Toggle): + """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. + These items are progressive, with the order of Village-Swamp-Courtyard.""" + display_name = "Shuffle Subcon Paintings" + default = 0 + + +class NoPaintingSkips(Toggle): + """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings.""" + display_name = "No Subcon Fire Wall Skips" + default = 0 + + +class StartingChapter(Choice): + """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game.""" + display_name = "Starting Chapter" + option_1 = 1 + option_2 = 2 + option_3 = 3 + option_4 = 4 + default = 1 + + +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 4 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch a hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch a hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 80 + default = 50 + + +class MinExtraYarn(Range): + """The minimum amount of extra yarn in the item pool. + There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, + there must be at least 50 yarn in the pool.""" + display_name = "Max Extra Yarn" + range_start = 5 + range_end = 15 + default = 10 + + +class HatItems(Toggle): + """Removes all yarn from the pool and turns the hats into individual items instead.""" + display_name = "Hat Items" + default = 0 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 300 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + +class EnableDLC1(Toggle): + """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 6" + default = 0 + + +class Tasksanity(Toggle): + """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" + display_name = "Tasksanity" + default = 0 + + +class TasksanityTaskStep(Range): + """How many tasks the player must complete in Tasksanity to send a check.""" + display_name = "Tasksanity Task Step" + range_start = 1 + range_end = 3 + default = 1 + + +class TasksanityCheckCount(Range): + """How many Tasksanity checks there will be in total.""" + display_name = "Tasksanity Check Count" + range_start = 5 + range_end = 30 + default = 18 + + +class ExcludeTour(Toggle): + """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with + important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages + when your goal is Time's End.""" + display_name = "Exclude Tour Time Rift" + default = 0 + + +class ShipShapeCustomTaskGoal(Range): + """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + display_name = "Ship Shape Custom Task Goal" + range_start = 1 + range_end = 30 + default = 18 + + +class EnableDLC2(Toggle): + """Shuffle content from Nyakuza Metro (Chapter 7) into the game. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 7" + default = 0 + + +class MetroMinPonCost(Range): + """The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Minimum Pon Cost" + range_start = 10 + range_end = 800 + default = 50 + + +class MetroMaxPonCost(Range): + """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Maximum Pon Cost" + range_start = 10 + range_end = 800 + default = 200 + + +class NyakuzaThugMinShopItems(Range): + """The smallest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Minimum Shop Items" + range_start = 0 + range_end = 5 + default = 2 + + +class NyakuzaThugMaxShopItems(Range): + """The largest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Maximum Shop Items" + range_start = 0 + range_end = 5 + default = 4 + + +class BaseballBat(Toggle): + """Replace the Umbrella with the baseball bat from Nyakuza Metro. + DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" + display_name = "Baseball Bat" + default = 0 + + +class EnableDeathWish(Toggle): + """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Enable Death Wish" + default = 0 + + +class DeathWishOnly(Toggle): + """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed. + This has the following effects: + - Death Wish is instantly unlocked from the start + - All hats and other progression items are instantly given to you + - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start + - All chapters and their levels are unlocked, act shuffle is forced off + - Any checks other than Death Wish contracts are completely removed + - All Pons in the item pool are replaced with Health Pons or random cosmetics + - The EndGoal option is forced to complete Seal the Deal""" + display_name = "Death Wish Only" + default = 0 + + +class DWShuffle(Toggle): + """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order. + Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence. + If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. + Disabling candles is highly recommended.""" + display_name = "Death Wish Shuffle" + default = 0 + + +class DWShuffleCountMin(Range): + """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Minimum Count" + range_start = 5 + range_end = 38 + default = 18 + + +class DWShuffleCountMax(Range): + """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Maximum Count" + range_start = 5 + range_end = 38 + default = 25 + + +class DWEnableBonus(Toggle): + """In Death Wish, allow the full completion of contracts to reward items. + WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! + ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! + Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" + display_name = "Shuffle Death Wish Full Completions" + default = 0 + + +class DWAutoCompleteBonuses(Toggle): + """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. + This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" + display_name = "Auto Complete Bonus Stamps" + default = 1 + + +class DWExcludeAnnoyingContracts(Toggle): + """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. + Excluded Death Wishes are automatically completed as soon as they are unlocked. + This option currently excludes the following contracts: + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" + display_name = "Exclude Annoying Death Wish Contracts" + default = 1 + + +class DWExcludeAnnoyingBonuses(Toggle): + """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. + Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. + This option currently excludes the following bonuses: + - So You're Back From Outer Space + - Encore! Encore! + - Snatcher's Hit List + - 10 Seconds until Self-Destruct + - Killing Two Birds + - Zero Jumps + - Bird Sanctuary + - Wound-Up Windmill + - Seal the Deal""" + display_name = "Exclude Annoying Death Wish Full Completions" + default = 1 + + +class DWExcludeCandles(Toggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + default = 1 + + +class DWTimePieceRequirement(Range): + """How many Time Pieces that will be required to unlock Death Wish.""" + display_name = "Death Wish Time Piece Requirement" + range_start = 0 + range_end = 35 + default = 15 + + +class TrapChance(Range): + """The chance for any junk item in the pool to be replaced by a trap.""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 0 + + +class BabyTrapWeight(Range): + """The weight of Baby Traps in the trap pool. + Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance.""" + display_name = "Baby Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class LaserTrapWeight(Range): + """The weight of Laser Traps in the trap pool. + Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location.""" + display_name = "Laser Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class ParadeTrapWeight(Range): + """The weight of Parade Traps in the trap pool. + Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement.""" + display_name = "Parade Trap Weight" + range_start = 0 + range_end = 100 + default = 20 + + +ahit_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ActPlando": ActPlando, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "FinaleShuffle": FinaleShuffle, + "LogicDifficulty": LogicDifficulty, + "YarnBalancePercent": YarnBalancePercent, + "TimePieceBalancePercent": TimePieceBalancePercent, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "StartingChapter": StartingChapter, + "CTRLogic": CTRLogic, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ExcludeTour": ExcludeTour, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DWShuffleCountMin": DWShuffleCountMin, + "DWShuffleCountMax": DWShuffleCountMax, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, + "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, + "DWExcludeCandles": DWExcludeCandles, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "BaseballBat": BaseballBat, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, + "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + + "LowestChapterCost": LowestChapterCost, + "HighestChapterCost": HighestChapterCost, + "ChapterCostIncrement": ChapterCostIncrement, + "ChapterCostMinDifference": ChapterCostMinDifference, + "MaxExtraTimePieces": MaxExtraTimePieces, + + "FinalChapterMinCost": FinalChapterMinCost, + "FinalChapterMaxCost": FinalChapterMaxCost, + + "YarnCostMin": YarnCostMin, + "YarnCostMax": YarnCostMax, + "YarnAvailable": YarnAvailable, + "MinExtraYarn": MinExtraYarn, + "HatItems": HatItems, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + "BadgeSellerMinItems": BadgeSellerMinItems, + "BadgeSellerMaxItems": BadgeSellerMaxItems, + + "TrapChance": TrapChance, + "BabyTrapWeight": BabyTrapWeight, + "LaserTrapWeight": LaserTrapWeight, + "ParadeTrapWeight": ParadeTrapWeight, + + "death_link": DeathLink, +} + +slot_data_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "CTRLogic": CTRLogic, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "HatItems": HatItems, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + + "death_link": DeathLink, +} diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py new file mode 100644 index 0000000000..807f1ee77f --- /dev/null +++ b/worlds/ahit/Regions.py @@ -0,0 +1,900 @@ +from worlds.AutoWorld import World +from BaseClasses import Region, Entrance, ItemClassification, Location +from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem +from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard +import typing +from .Rules import set_rift_rules, get_difficulty + + +# ChapterIndex: region +chapter_regions = { + ChapterIndex.SPACESHIP: "Spaceship", + ChapterIndex.MAFIA: "Mafia Town", + ChapterIndex.BIRDS: "Battle of the Birds", + ChapterIndex.SUBCON: "Subcon Forest", + ChapterIndex.ALPINE: "Alpine Skyline", + ChapterIndex.FINALE: "Time's End", + ChapterIndex.CRUISE: "The Arctic Cruise", + ChapterIndex.METRO: "Nyakuza Metro", +} + +# entrance: region +act_entrances = { + "Welcome to Mafia Town": "Mafia Town - Act 1", + "Barrel Battle": "Mafia Town - Act 2", + "She Came from Outer Space": "Mafia Town - Act 3", + "Down with the Mafia!": "Mafia Town - Act 4", + "Cheating the Race": "Mafia Town - Act 5", + "Heating Up Mafia Town": "Mafia Town - Act 6", + "The Golden Vault": "Mafia Town - Act 7", + + "Dead Bird Studio": "Battle of the Birds - Act 1", + "Murder on the Owl Express": "Battle of the Birds - Act 2", + "Picture Perfect": "Battle of the Birds - Act 3", + "Train Rush": "Battle of the Birds - Act 4", + "The Big Parade": "Battle of the Birds - Act 5", + "Award Ceremony": "Battle of the Birds - Finale A", + "Dead Bird Studio Basement": "Battle of the Birds - Finale B", + + "Contractual Obligations": "Subcon Forest - Act 1", + "The Subcon Well": "Subcon Forest - Act 2", + "Toilet of Doom": "Subcon Forest - Act 3", + "Queen Vanessa's Manor": "Subcon Forest - Act 4", + "Mail Delivery Service": "Subcon Forest - Act 5", + "Your Contract has Expired": "Subcon Forest - Finale", + + "Alpine Free Roam": "Alpine Skyline - Free Roam", + "The Illness has Spread": "Alpine Skyline - Finale", + + "The Finale": "Time's End - Act 1", + + "Bon Voyage!": "The Arctic Cruise - Act 1", + "Ship Shape": "The Arctic Cruise - Act 2", + "Rock the Boat": "The Arctic Cruise - Finale", + + "Nyakuza Free Roam": "Nyakuza Metro - Free Roam", + "Rush Hour": "Nyakuza Metro - Finale", +} + +act_chapters = { + "Time Rift - Gallery": "Spaceship", + "Time Rift - The Lab": "Spaceship", + + "Welcome to Mafia Town": "Mafia Town", + "Barrel Battle": "Mafia Town", + "She Came from Outer Space": "Mafia Town", + "Down with the Mafia!": "Mafia Town", + "Cheating the Race": "Mafia Town", + "Heating Up Mafia Town": "Mafia Town", + "The Golden Vault": "Mafia Town", + "Time Rift - Mafia of Cooks": "Mafia Town", + "Time Rift - Sewers": "Mafia Town", + "Time Rift - Bazaar": "Mafia Town", + + "Dead Bird Studio": "Battle of the Birds", + "Murder on the Owl Express": "Battle of the Birds", + "Picture Perfect": "Battle of the Birds", + "Train Rush": "Battle of the Birds", + "The Big Parade": "Battle of the Birds", + "Award Ceremony": "Battle of the Birds", + "Dead Bird Studio Basement": "Battle of the Birds", + "Time Rift - Dead Bird Studio": "Battle of the Birds", + "Time Rift - The Owl Express": "Battle of the Birds", + "Time Rift - The Moon": "Battle of the Birds", + + "Contractual Obligations": "Subcon Forest", + "The Subcon Well": "Subcon Forest", + "Toilet of Doom": "Subcon Forest", + "Queen Vanessa's Manor": "Subcon Forest", + "Mail Delivery Service": "Subcon Forest", + "Your Contract has Expired": "Subcon Forest", + "Time Rift - Sleepy Subcon": "Subcon Forest", + "Time Rift - Pipe": "Subcon Forest", + "Time Rift - Village": "Subcon Forest", + + "Alpine Free Roam": "Alpine Skyline", + "The Illness has Spread": "Alpine Skyline", + "Time Rift - Alpine Skyline": "Alpine Skyline", + "Time Rift - The Twilight Bell": "Alpine Skyline", + "Time Rift - Curly Tail Trail": "Alpine Skyline", + + "The Finale": "Time's End", + "Time Rift - Tour": "Time's End", + + "Bon Voyage!": "The Arctic Cruise", + "Ship Shape": "The Arctic Cruise", + "Rock the Boat": "The Arctic Cruise", + "Time Rift - Balcony": "The Arctic Cruise", + "Time Rift - Deep Sea": "The Arctic Cruise", + + "Nyakuza Free Roam": "Nyakuza Metro", + "Rush Hour": "Nyakuza Metro", + "Time Rift - Rumbi Factory": "Nyakuza Metro", +} + +# region: list[Region] +rift_access_regions = { + "Time Rift - Gallery": ["Spaceship"], + "Time Rift - The Lab": ["Spaceship"], + + "Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "The Golden Vault"], + + "Time Rift - The Owl Express": ["Murder on the Owl Express"], + "Time Rift - The Moon": ["Picture Perfect", "The Big Parade"], + "Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"], + + "Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Village": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - The Twilight Bell": ["Alpine Free Roam"], + "Time Rift - Curly Tail Trail": ["Alpine Free Roam"], + "Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"], + + "Time Rift - Tour": ["Time's End"], + + "Time Rift - Balcony": ["Cruise Ship"], + "Time Rift - Deep Sea": ["Bon Voyage!"], + + "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], +} + +# Time piece identifiers to be used in act shuffle +chapter_act_info = { + "Time Rift - Gallery": "Spaceship_WaterRift_Gallery", + "Time Rift - The Lab": "Spaceship_WaterRift_MailRoom", + + "Welcome to Mafia Town": "chapter1_tutorial", + "Barrel Battle": "chapter1_barrelboss", + "She Came from Outer Space": "chapter1_cannon_repair", + "Down with the Mafia!": "chapter1_boss", + "Cheating the Race": "harbor_impossible_race", + "Heating Up Mafia Town": "mafiatown_lava", + "The Golden Vault": "mafiatown_goldenvault", + "Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia", + "Time Rift - Sewers": "TimeRift_Water_Mafia_Easy", + "Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard", + + "Dead Bird Studio": "DeadBirdStudio", + "Murder on the Owl Express": "chapter3_murder", + "Picture Perfect": "moon_camerasnap", + "Train Rush": "trainwreck_selfdestruct", + "The Big Parade": "moon_parade", + "Award Ceremony": "award_ceremony", + "Dead Bird Studio Basement": "chapter3_secret_finale", + "Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement", + "Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels", + "Time Rift - The Moon": "TimeRift_Water_TWreck_Parade", + + "Contractual Obligations": "subcon_village_icewall", + "The Subcon Well": "subcon_cave", + "Toilet of Doom": "chapter2_toiletboss", + "Queen Vanessa's Manor": "vanessa_manor_attic", + "Mail Delivery Service": "subcon_maildelivery", + "Your Contract has Expired": "snatcher_boss", + "Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon", + "Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot", + "Time Rift - Village": "TimeRift_Water_Subcon_Dwellers", + + "Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece + "The Illness has Spread": "AlpineSkyline_Finale", + "Time Rift - Alpine Skyline": "TimeRift_Cave_Alps", + "Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats", + "Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats", + + "The Finale": "TheFinale_FinalBoss", + "Time Rift - Tour": "TimeRift_Cave_Tour", + + "Bon Voyage!": "Cruise_Boarding", + "Ship Shape": "Cruise_Working", + "Rock the Boat": "Cruise_Sinking", + "Time Rift - Balcony": "Cruise_WaterRift_Slide", + "Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium", + + "Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece + "Rush Hour": "Metro_Escape", + "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" +} + +# Guarantee that the first level a player can access is a location dense area beatable with no items +guaranteed_first_acts = [ + "Welcome to Mafia Town", + "Barrel Battle", + "She Came from Outer Space", + "Down with the Mafia!", + "Heating Up Mafia Town", # Removed in umbrella logic + "The Golden Vault", + + "Contractual Obligations", # Removed in painting logic + "Queen Vanessa's Manor", # Removed in umbrella/painting logic +] + +purple_time_rifts = [ + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline", + "Time Rift - Deep Sea", + "Time Rift - Tour", + "Time Rift - Rumbi Factory", +] + +chapter_finales = [ + "Dead Bird Studio Basement", + "Your Contract has Expired", + "The Illness has Spread", + "Rock the Boat", + "Rush Hour", +] + +# Acts blacklisted in act shuffle +# entrance: region +blacklisted_acts = { + "Battle of the Birds - Finale A": "Award Ceremony", +} + +# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. +blacklisted_combos = { + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", + "Contractual Obligations"], + + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], +} + + +def create_regions(world: World): + w = world + mw = world.multiworld + p = world.player + + # ------------------------------------------- HUB -------------------------------------------------- # + menu = create_region(w, "Menu") + spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + + # we only need the menu and the spaceship regions + if world.is_dw_only(): + return + + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) + create_rift_connections(w, create_region(w, "Time Rift - The Lab")) + + # ------------------------------------------- MAFIA TOWN ------------------------------------------- # + mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship) + mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) + mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town) + mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) + mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) + mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) + mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town) + mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town) + + # ------------------------------------------- BOTB ------------------------------------------------- # + botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) + dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) + create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) + pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) + tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) + create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) + create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) + basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) + create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) + create_rift_connections(w, create_region(w, "Time Rift - The Moon")) + + # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert + ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) + if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + + # ------------------------------------------- SUBCON FOREST --------------------------------------- # + subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) + sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) + sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) + sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) + sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) + sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) + create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + + # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # + alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) + alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) + alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) + + # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness + alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + alpine_area) + + create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) + create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) + create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area) + create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) + + illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p) + create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) + create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) + create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) + + # ------------------------------------------- OTHER -------------------------------------------------- # + mt_area: Region = create_region(w, "Mafia Town Area") + mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") + connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p) + connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p) + connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p) + connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p) + connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p) + connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p) + connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p) + connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p) + + create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) + create_rift_connections(w, create_region(w, "Time Rift - Sewers")) + create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) + + sf_area: Region = create_region(w, "Subcon Forest Area") + connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p) + connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p) + connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p) + connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p) + connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p) + + create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) + create_rift_connections(w, create_region(w, "Time Rift - Pipe")) + create_rift_connections(w, create_region(w, "Time Rift - Village")) + + badge_seller = create_badge_seller(w) + connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) + connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) + connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) + connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p) + connect_regions(pp, badge_seller, "PP -> Badge Seller", p) + connect_regions(tr, badge_seller, "TR -> Badge Seller", p) + connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p) + + times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) + create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) + + # ------------------------------------------- DLC1 ------------------------------------------------- # + if w.is_dlc1(): + arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) + cruise_ship = create_region(w, "Cruise Ship") + + ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) + ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) + ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) + + connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p) + connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p) + connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) + create_rift_connections(w, create_region(w, "Time Rift - Balcony")) + create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) + + if mw.ExcludeTour[world.player].value == 0: + create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.Tasksanity[p].value > 0: + create_tasksanity_locations(w) + + connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) + + if w.is_dlc2(): + nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) + metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) + create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) + + yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) + green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam) + pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) + create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole + + create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) + create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green) + create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) + + create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) + create_thug_shops(w) + + +def create_rift_connections(world: World, region: Region): + i = 1 + for name in rift_access_regions[region.name]: + act_region = world.multiworld.get_region(name, world.player) + entrance_name = f"{region.name} Portal - Entrance {i}" + connect_regions(act_region, region, entrance_name, world.player) + i += 1 + + # fix for some weird keyerror from tests + if region.name == "Time Rift - Rumbi Factory": + for entrance in region.entrances: + world.multiworld.get_entrance(entrance.name, world.player) + + +def create_tasksanity_locations(world: World): + ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) + id_start: int = TASKSANITY_START_ID + for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) + ship_shape.locations.append(location) + + +def is_valid_plando(world: World, region: str) -> bool: + if region in blacklisted_acts.values(): + return False + + if region not in world.multiworld.ActPlando[world.player].keys(): + return False + + act = world.multiworld.ActPlando[world.player].get(region) + if act in blacklisted_acts.values(): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ + and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) + + if is_first_act: + if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + return False + + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): + return False + + if act not in guaranteed_first_acts: + return False + + # Don't allow straight up impossible mappings + if region == "The Illness has Spread" and act == "Alpine Free Roam": + return False + + if region == "Rush Hour" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + world.multiworld.get_regions(world.player)) + + +def randomize_act_entrances(world: World): + region_list: typing.List[Region] = get_act_regions(world) + world.random.shuffle(region_list) + + separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) + + for region in region_list.copy(): + if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ + and "Time Rift" not in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if "Time Rift" in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in chapter_finales: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in world.multiworld.ActPlando[world.player].keys(): + if is_valid_plando(world, region.name): + region_list.remove(region) + region_list.append(region) + else: + print("Disallowing act plando for", + world.multiworld.player_name[world.player], + "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + + # Reverse the list, so we can do what we want to do first + region_list.reverse() + + shuffled_list: typing.List[Region] = [] + mapped_list: typing.List[Region] = [] + rift_dict: typing.Dict[str, Region] = {} + first_chapter: Region = get_first_chapter_region(world) + has_guaranteed: bool = False + + i: int = 0 + while i < len(region_list): + region = region_list[i] + i += 1 + + # Get the first accessible act, so we can map that to something first + if not has_guaranteed: + if act_chapters[region.name] != first_chapter.name: + continue + + if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ + and "Free Roam" not in act_entrances[region.name]: + continue + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + has_guaranteed = True + + i = 0 + + # Already mapped to something else + if region in mapped_list: + continue + + mapped_list.append(region) + + # Look for candidates to map this act to + candidate_list: typing.List[Region] = [] + for candidate in region_list: + # We're mapping something to the first act, make sure it is valid + if not has_guaranteed: + if candidate.name not in guaranteed_first_acts: + continue + + if candidate.name in world.multiworld.ActPlando[world.player].values(): + continue + + # Not completable without Umbrella + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): + continue + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \ + and "Subcon Forest" in act_entrances[candidate.name]: + continue + + candidate_list.append(candidate) + has_guaranteed = True + break + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + candidate_list.clear() + candidate_list.append( + world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + break + + # Already mapped onto something else + if candidate in shuffled_list: + continue + + if separate_rifts: + # Don't map Time Rifts to normal acts + if "Time Rift" in region.name and "Time Rift" not in candidate.name: + continue + + # Don't map normal acts to Time Rifts + if "Time Rift" not in region.name and "Time Rift" in candidate.name: + continue + + # Separate purple rifts + if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ + or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: + continue + + if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: + continue + + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.multiworld.ShuffleActContracts[world.player].value == 0: + if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ + and candidate.name == "Contractual Obligations": + continue + + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if candidate.name not in chapter_finales: + continue + + if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: + continue + + candidate_list.append(candidate) + + candidate: Region + if len(candidate_list) > 0: + candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] + else: + # plando can still break certain rules, so acts may not always end up shuffled. + for c in region_list: + if c not in shuffled_list: + candidate = c + break + + shuffled_list.append(candidate) + # print(region, candidate) + + # Vanilla + if candidate.name == region.name: + if region.name in rift_access_regions.keys(): + rift_dict.setdefault(region.name, candidate) + + update_chapter_act_info(world, region, candidate) + continue + + if region.name in rift_access_regions.keys(): + connect_time_rift(world, region, candidate) + rift_dict.setdefault(region.name, candidate) + else: + if candidate.name in rift_access_regions.keys(): + for e in candidate.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + + entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) + reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) + + update_chapter_act_info(world, region, candidate) + + for name in blacklisted_acts.values(): + if not is_act_blacklisted(world, name): + continue + + region: Region = world.multiworld.get_region(name, world.player) + update_chapter_act_info(world, region, region) + + set_rift_rules(world, rift_dict) + + +def connect_time_rift(world: World, time_rift: Region, exit_region: Region): + count: int = len(rift_access_regions[time_rift.name]) + i: int = 1 + while i <= count: + name = f"{time_rift.name} Portal - Entrance {i}" + entrance: Entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) + i += 1 + + +def get_act_regions(world: World) -> typing.List[Region]: + act_list: typing.List[Region] = [] + for region in world.multiworld.get_regions(world.player): + if region.name in chapter_act_info.keys(): + if not is_act_blacklisted(world, region.name): + act_list.append(region) + + return act_list + + +def is_act_blacklisted(world: World, name: str) -> bool: + plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ + or name in world.multiworld.ActPlando[world.player].values() + + if name == "The Finale": + return not plando and world.multiworld.EndGoal[world.player].value == 1 + + if name == "Rush Hour": + return not plando and world.multiworld.EndGoal[world.player].value == 2 + + if name == "Time Rift - Tour": + return world.multiworld.ExcludeTour[world.player].value > 0 + + return name in blacklisted_acts.values() + + +def create_region(world: World, name: str) -> Region: + reg = Region(name, world.player, world.multiworld) + + for (key, data) in location_table.items(): + if world.is_dw_only(): + break + + if data.nyakuza_thug != "": + continue + + if data.region == name: + if key in storybook_pages.keys() \ + and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + continue + + location = HatInTimeLocation(world.player, key, data.id, reg) + reg.locations.append(location) + if location.name in shop_locations: + world.shop_locs.append(location.name) + + world.multiworld.regions.append(reg) + return reg + + +def create_badge_seller(world: World) -> Region: + badge_seller = Region("Badge Seller", world.player, world.multiworld) + world.multiworld.regions.append(badge_seller) + count: int = 0 + max_items: int = 0 + + if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + if max_items <= 0: + world.set_badge_seller_count(0) + return badge_seller + + for (key, data) in shop_locations.items(): + if "Badge Seller" not in key: + continue + + location = HatInTimeLocation(world.player, key, data.id, badge_seller) + badge_seller.locations.append(location) + world.shop_locs.append(location.name) + + count += 1 + if count >= max_items: + break + + world.set_badge_seller_count(max_items) + return badge_seller + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance + + +# Takes an entrance, removes its old connections, and reconnects it between the two regions specified. +def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region): + if entrance in entrance.connected_region.entrances: + entrance.connected_region.entrances.remove(entrance) + + if entrance in entrance.parent_region.exits: + entrance.parent_region.exits.remove(entrance) + + if entrance in start_region.exits: + start_region.exits.remove(entrance) + + if entrance in exit_region.entrances: + exit_region.entrances.remove(entrance) + + entrance.parent_region = start_region + start_region.exits.append(entrance) + entrance.connect(exit_region) + + +def create_region_and_connect(world: World, + name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: + + reg: Region = create_region(world, name) + entrance_region: Region + exit_region: Region + + if is_exit: + entrance_region = connected_region + exit_region = reg + else: + entrance_region = reg + exit_region = connected_region + + connect_regions(entrance_region, exit_region, entrancename, world.player) + return reg + + +def get_first_chapter_region(world: World) -> Region: + start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player] + return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) + + +def get_act_original_chapter(world: World, act_name: str) -> Region: + return world.multiworld.get_region(act_chapters[act_name], world.player) + + +# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game +def update_chapter_act_info(world: World, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + world.act_connections[original_act_info] = new_act_info + + +def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name + + +def create_thug_shops(world: World): + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value + + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value + count: int = -1 + step: int = 0 + old_name: str = "" + thug_items = world.get_nyakuza_thug_items() + + for key, data in shop_locations.items(): + if data.nyakuza_thug == "": + continue + + if old_name != "" and old_name == data.nyakuza_thug: + continue + + try: + if thug_items[data.nyakuza_thug] <= 0: + continue + except KeyError: + pass + + if count == -1: + count = world.random.randint(min_items, max_items) + thug_items.setdefault(data.nyakuza_thug, count) + if count <= 0: + continue + + if count >= 1: + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + + step += 1 + if step >= count: + old_name = data.nyakuza_thug + step = 0 + count = -1 + + world.set_nyakuza_thug_items(thug_items) + + +def create_events(world: World) -> int: + count: int = 0 + + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + item_name: str = name + if world.is_dw(): + if name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + elif name in zero_jumps: + if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: + continue + + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) + event.show_in_spoiler = False + count += 1 + + return count + + +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: + event = HatInTimeLocation(world.player, name, None, region) + region.locations.append(event) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) + return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py new file mode 100644 index 0000000000..7eb09bedfc --- /dev/null +++ b/worlds/ahit/Rules.py @@ -0,0 +1,944 @@ +from worlds.AutoWorld import World, CollectionState +from worlds.generic.Rules import add_rule, set_rule +from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ + shop_locations, event_locs, snatcher_coins +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC +from BaseClasses import Location, Entrance, Region +import typing + + +act_connections = { + "Mafia Town - Act 2": ["Mafia Town - Act 1"], + "Mafia Town - Act 3": ["Mafia Town - Act 1"], + "Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"], + "Mafia Town - Act 6": ["Mafia Town - Act 4"], + "Mafia Town - Act 7": ["Mafia Town - Act 4"], + "Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"], + + "Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"], + "Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"], + + "Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2", + "Subcon Forest - Act 3", "Subcon Forest - Act 4", + "Subcon Forest - Act 5"], + + "The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"], + "The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"], +} + + +def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: + if world.multiworld.HatItems[world.player].value > 0: + return state.has(hat_type_to_item[hat], world.player) + + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) + + +def get_hat_cost(world: World, hat: HatType) -> int: + cost: int = 0 + costs = world.get_hat_yarn_costs() + for h in world.get_hat_craft_order(): + cost += costs[h] + if h == hat: + break + + return cost + + +def can_sdj(state: CollectionState, world: World): + return can_use_hat(state, world, HatType.SPRINT) + + +def painting_logic(world: World) -> bool: + return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 + + +# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert +def get_difficulty(world: World) -> Difficulty: + return Difficulty(world.multiworld.LogicDifficulty[world.player].value) + + +def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: + if not painting_logic(world): + return True + + if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip: + # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena + if get_difficulty(world) >= Difficulty.MODERATE: + return True + + return state.count("Progressive Painting Unlock", world.player) >= count + + +def zipline_logic(world: World) -> bool: + return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0 + + +def can_use_hookshot(state: CollectionState, world: World): + return state.has("Hookshot Badge", world.player) + + +def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): + if world.multiworld.UmbrellaLogic[world.player].value == 0: + return True + + return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) + + +def can_surf(state: CollectionState, world: World): + return state.has("No Bonk Badge", world.player) + + +def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: + return state.has_group(relic, world.player, len(world.item_name_groups[relic])) + + +def get_relic_count(state: CollectionState, world: World, relic: str) -> int: + return state.count_group(relic, world.player) + + +# Only use for rifts +def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: + entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) + if not state.can_reach(entrance.connected_region, "Region", world.player): + return False + + if "Free Roam" in entrance.connected_region.name: + return True + + name: str = f"Act Completion ({entrance.connected_region.name})" + return world.multiworld.get_location(name, world.player).access_rule(state) + + +def can_clear_alpine(state: CollectionState, world: World) -> bool: + return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ + and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) + + +def can_clear_metro(state: CollectionState, world: World) -> bool: + return state.has("Nyakuza Intro Cleared", world.player) \ + and state.has("Yellow Overpass Station Cleared", world.player) \ + and state.has("Yellow Overpass Manhole Cleared", world.player) \ + and state.has("Green Clean Station Cleared", world.player) \ + and state.has("Green Clean Manhole Cleared", world.player) \ + and state.has("Bluefin Tunnel Cleared", world.player) \ + and state.has("Pink Paw Station Cleared", world.player) \ + and state.has("Pink Paw Manhole Cleared", world.player) + + +def set_rules(world: World): + # First, chapter access + starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) + world.set_chapter_cost(starting_chapter, 0) + + # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale + chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] + + final_chapter = ChapterIndex.FINALE + if world.multiworld.EndGoal[world.player].value == 2: + final_chapter = ChapterIndex.METRO + chapter_list.append(ChapterIndex.FINALE) + elif world.multiworld.EndGoal[world.player].value == 3: + final_chapter = None + chapter_list.append(ChapterIndex.FINALE) + + if world.is_dlc1(): + chapter_list.append(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.append(ChapterIndex.METRO) + + chapter_list.remove(starting_chapter) + world.random.shuffle(chapter_list) + + if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): + index1: int = 69 + index2: int = 69 + pos: int + lowest_index: int + chapter_list.remove(ChapterIndex.ALPINE) + + if world.is_dlc1(): + index1 = chapter_list.index(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + index2 = chapter_list.index(ChapterIndex.METRO) + + lowest_index = min(index1, index2) + if lowest_index == 0: + pos = 0 + else: + pos = world.random.randint(0, lowest_index) + + chapter_list.insert(pos, ChapterIndex.ALPINE) + + if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.remove(ChapterIndex.METRO) + index = chapter_list.index(ChapterIndex.CRUISE) + if index >= len(chapter_list): + chapter_list.append(ChapterIndex.METRO) + else: + chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + + lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value + highest_cost: int = world.multiworld.HighestChapterCost[world.player].value + + cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value + min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value + last_cost: int = 0 + cost: int + loop_count: int = 0 + + for chapter in chapter_list: + min_range: int = lowest_cost + (cost_increment * loop_count) + if min_range >= highest_cost: + min_range = highest_cost-1 + + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) + + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) + if loop_count >= 1: + if last_cost + min_difference > cost: + cost = last_cost + min_difference + + cost = min(cost, highest_cost) + world.set_chapter_cost(chapter, cost) + last_cost = cost + loop_count += 1 + + if final_chapter is not None: + world.set_chapter_cost(final_chapter, world.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) + + add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) + + add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON))) + + add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) + and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) + + if world.is_dlc2(): + add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) + and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) + + if world.multiworld.ActRandomizer[world.player].value == 0: + set_default_rift_rules(world) + + table = location_table | event_locs + location: Location + for (key, data) in table.items(): + if not is_location_valid(world, key): + continue + + if key in contract_locations.keys(): + continue + + if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): + key = f"{key} ({data.region})" + + location = world.multiworld.get_location(key, world.player) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) + + if data.hookshot: + add_rule(location, lambda state: can_use_hookshot(state, world)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(location, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(location, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + for misc in data.misc_required: + add_rule(location, lambda state, item=misc: state.has(item, world.player)) + + set_specific_rules(world) + + # Putting all of this here, so it doesn't get overridden by anything + # Illness starts the player past the intro + alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) + add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) + + if zipline_logic(world): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) + + if zipline_logic(world): + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), + lambda state, z=zipline: state.has(z, world.player)) + + for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: + if "Goat Village" in loc.name: + continue + + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for (key, acts) in act_connections.items(): + if "Arctic Cruise" in key and not world.is_dlc1(): + continue + + i: int = 1 + entrance: Entrance = world.multiworld.get_entrance(key, world.player) + region: Region = entrance.connected_region + access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + entrance.parent_region.exits.remove(entrance) + + # Entrances to this act that we have to set access_rules on + entrances: typing.List[Entrance] = [] + + for act in acts: + act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) + access_rules.append(act_entrance.access_rule) + required_region = act_entrance.connected_region + name: str = f"{key}: Connection {i}" + new_entrance: Entrance = connect_regions(required_region, region, name, world.player) + entrances.append(new_entrance) + + # Copy access rules from act completions + if "Free Roam" not in required_region.name: + rule: typing.Callable[[CollectionState], bool] + name = f"Act Completion ({required_region.name})" + rule = world.multiworld.get_location(name, world.player).access_rule + access_rules.append(rule) + + i += 1 + + for e in entrances: + for rules in access_rules: + add_rule(e, rules) + + set_event_rules(world) + + if world.multiworld.EndGoal[world.player].value == 1: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) + elif world.multiworld.EndGoal[world.player].value == 2: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) + + +def set_specific_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), + lambda state: state.has("Time Piece", world.player, 12) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), + lambda state: state.has("Time Piece", world.player, 4)) + + set_mafia_town_rules(world) + set_botb_rules(world) + set_subcon_rules(world) + set_alps_rules(world) + + if world.is_dlc1(): + set_dlc1_rules(world) + + if world.is_dlc2(): + set_dlc2_rules(world) + + difficulty: Difficulty = get_difficulty(world) + + if difficulty >= Difficulty.MODERATE: + set_moderate_rules(world) + + if difficulty >= Difficulty.HARD: + set_hard_rules(world) + + if difficulty >= 2: + set_expert_rules(world) + + +def set_moderate_rules(world: World): + # Moderate: Gallery without Brewing Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) + + # Moderate: Above Boats via Ice Hat Sliding + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + # Moderate: Clock Tower Chest + Ruined Tower with nothing + add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) + + # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell + for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: + set_rule(loc, lambda state: has_paintings(state, world, 1)) + + # Moderate: Vanessa Manor with nothing + for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) + + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat + set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world)) + set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access + set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: True) + + # Moderate: Twilight Path without Dweller Mask + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) + + # Moderate: Mystifying Time Mesa time trial without hats + set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: Finale without Hookshot + set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + # Moderate: clear Rock the Boat without Ice Hat + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) + + # Moderate: clear Deep Sea without Ice Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. + # Yellow Overpass time piece can also be reached without Hookshot quite easily. + if world.is_dlc2(): + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) + + # The player can quite literally walk past the fan from the side without Time Stop. + set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) + + # Moderate: clear Rush Hour without Hookshot + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and can_use_hat(state, world, HatType.ICE) + and can_use_hat(state, world, HatType.BREWING)) + + # Moderate: Bluefin Tunnel without tickets + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + + +def set_hard_rules(world: World): + # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only + add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + # No Dweller Mask required + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), + lambda state: has_paintings(state, world, 3)) + + # Cherry bridge over boss arena gap (painting still expected) + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # SDJ + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: can_sdj(state, world), "or") + + # Finale Telescope with only Ice Hat + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + if world.is_dlc1(): + # Hard: clear Deep Sea without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world)) + + if world.is_dlc2(): + # Hard: clear Green Clean Manhole without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + # Hard: clear Rush Hour with Brewing Hat only + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + +def set_expert_rules(world: World): + # Finale Telescope with no hats + set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) + + # Expert: Mafia Town - Above Boats with nothing + set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + + # Expert: Clear Dead Bird Studio with nothing + for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True) + + # Expert: get to and clear Twilight Bell without Dweller Mask. + # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER) + or can_use_hat(state, world, HatType.SPRINT) + or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player))) + + # Expert: Time Rift - Curly Tail Trail with nothing + # Time Rift - Twilight Bell and Time Rift - Village with nothing + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: True) + + # Expert: Cherry Hovering + entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), + world.multiworld.get_region("Subcon Forest Area", world.player), + "Subcon Forest Entrance YCHE", world.player) + + if world.multiworld.NoPaintingSkips[world.player].value > 0: + add_rule(entrance, lambda state: has_paintings(state, world, 1)) + + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, True)) + + # Set painting rules only. Skipping paintings is determined in has_paintings + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + + # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him + connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), + world.multiworld.get_region("Your Contract has Expired", world.player), + "Snatcher Hover", world.player) + set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), + lambda state: True) + + if world.is_dlc2(): + # Expert: clear Rush Hour with nothing + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + + +def set_mafia_town_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Old guys don't appear in SCFOS + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + # Only available outside She Came from Outer Space + add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Only available outside Down with the Mafia! (for some reason) + add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("She Came from Outer Space", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # For some reason, the brewing crate is removed in HUMT + add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + # Can bounce across the lava to get this without Hookshot (need to die though) + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + ctr_logic: int = world.multiworld.CTRLogic[world.player].value + if ctr_logic == 3: + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) + elif ctr_logic == 2: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT), "or") + elif ctr_logic == 1: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + +def set_botb_rules(world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: + set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + + +def set_subcon_rules(world: World): + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.DWELLER)) + + # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) + and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # The painting wall can't be skipped without cherry hover, which is Expert + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player), + lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player), + lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player), + lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player), + lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) + + if painting_logic(world): + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + for key in contract_locations: + if key == "Snatcher's Contract - The Subcon Well": + continue + + add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) + + +def set_alps_rules(world: World): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) + + add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), + lambda state: can_clear_alpine(state, world)) + + +def set_dlc1_rules(world: World): + add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), + lambda state: can_use_hookshot(state, world)) + + # This particular item isn't present in Act 3 for some reason, yes in vanilla too + add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), + lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + or state.can_reach("Ship Shape", "Region", world.player)) + + +def set_dlc2_rules(world: World): + add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), + lambda state: state.has("Metro Ticket - Green", world.player) + or state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), + lambda state: can_clear_metro(state, world)) + + add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) + + for key in shop_locations.keys(): + if "Green Clean Station Thug B" in key and is_location_valid(world, key): + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player), "or") + + +def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): + reg: Region + entrance: Entrance + if isinstance(region, str): + reg = world.multiworld.get_region(region, world.player) + else: + reg = region + + if isinstance(unlocked_entrance, str): + entrance = world.multiworld.get_entrance(unlocked_entrance, world.player) + else: + entrance = unlocked_entrance + + world.multiworld.register_indirect_condition(reg, entrance) + + +# See randomize_act_entrances in Regions.py +# Called before set_rules +def set_rift_rules(world: World, regions: typing.Dict[str, Region]): + + # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. + for entrance in regions["Time Rift - Gallery"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in regions["Time Rift - The Lab"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in regions["Time Rift - Sewers"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Bazaar"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Mafia of Cooks"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in regions["Time Rift - The Owl Express"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - The Moon"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Dead Bird Studio"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in regions["Time Rift - Pipe"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", + world.player).connected_region, entrance) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Village"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", + world.player).connected_region, entrance) + + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Sleepy Subcon"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in regions["Time Rift - Curly Tail Trail"].entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in regions["Time Rift - The Twilight Bell"].entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in regions["Time Rift - Alpine Skyline"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1() > 0: + for entrance in regions["Time Rift - Balcony"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in regions["Time Rift - Deep Sea"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2() > 0: + for entrance in regions["Time Rift - Rumbi Factory"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +# Basically the same as above, but without the need of the dict since we are just setting defaults +# Called if Act Rando is disabled +def set_default_rift_rules(world: World): + + for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, "Down with the Mafia!", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, "Heating Up Mafia Town", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, "Murder on the Owl Express", entrance.name) + reg_act_connection(world, "Picture Perfect", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, "Train Rush", entrance.name) + reg_act_connection(world, "The Big Parade", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, "The Subcon Well", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1(): + for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2(): + for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +def set_event_rules(world: World): + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + + event: Location = world.multiworld.get_location(name, world.player) + + if data.act_event: + add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py new file mode 100644 index 0000000000..16255d7ec5 --- /dev/null +++ b/worlds/ahit/Types.py @@ -0,0 +1,80 @@ +from enum import IntEnum, IntFlag +from typing import NamedTuple, Optional, List +from BaseClasses import Location, Item, ItemClassification + + +class HatInTimeLocation(Location): + game: str = "A Hat in Time" + + +class HatInTimeItem(Item): + game: str = "A Hat in Time" + + +class HatType(IntEnum): + NONE = -1 + SPRINT = 0 + BREWING = 1 + ICE = 2 + DWELLER = 3 + TIME_STOP = 4 + + +class HatDLC(IntFlag): + none = 0b000 + dlc1 = 0b001 + dlc2 = 0b010 + death_wish = 0b100 + dlc1_dw = 0b101 + dlc2_dw = 0b110 + + +class ChapterIndex(IntEnum): + SPACESHIP = 0 + MAFIA = 1 + BIRDS = 2 + SUBCON = 3 + ALPINE = 4 + FINALE = 5 + CRUISE = 6 + METRO = 7 + + +class Difficulty(IntEnum): + NORMAL = -1 + MODERATE = 0 + HARD = 1 + EXPERT = 2 + + +class LocData(NamedTuple): + id: Optional[int] = 0 + region: Optional[str] = "" + required_hats: Optional[List[HatType]] = [HatType.NONE] + hookshot: Optional[bool] = False + dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] + + # For UmbrellaLogic setting + umbrella: Optional[bool] = False # Umbrella required for this check + hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) + + # Other + act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion + nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + + +class ItemData(NamedTuple): + code: Optional[int] + classification: ItemClassification + dlc_flags: Optional[HatDLC] = HatDLC.none + + +hat_type_to_item = { + HatType.SPRINT: "Sprint Hat", + HatType.BREWING: "Brewing Hat", + HatType.ICE: "Ice Hat", + HatType.DWELLER: "Dweller Mask", + HatType.TIME_STOP: "Time Stop Hat", +} diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py new file mode 100644 index 0000000000..0ed14c6376 --- /dev/null +++ b/worlds/ahit/__init__.py @@ -0,0 +1,334 @@ +from BaseClasses import Item, ItemClassification, Tutorial +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool +from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID +from .Rules import set_rules +from .Options import ahit_options, slot_data_options, adjust_options +from .Types import HatType, ChapterIndex, HatInTimeItem +from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes +from .DeathWishRules import set_dw_rules, create_enemy_events +from worlds.AutoWorld import World, WebWorld +from typing import List, Dict, TextIO +from worlds.LauncherComponents import Component, components, icon_paths +from Utils import local_path + +hat_craft_order: Dict[int, List[HatType]] = {} +hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} +chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} +excluded_dws: Dict[int, List[str]] = {} +excluded_bonuses: Dict[int, List[str]] = {} +dw_shuffle: Dict[int, List[str]] = {} +nyakuza_thug_items: Dict[int, Dict[str, int]] = {} +badge_seller_count: Dict[int, int] = {} + +components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) +icon_paths['yatta'] = local_path('data', 'yatta.png') + + +class AWebInTime(WebWorld): + theme = "partyTime" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide for setting up A Hat in Time to be played in Archipelago.", + "English", + "ahit_en.md", + "setup/en", + ["CookieCat"] + )] + + +class HatInTimeWorld(World): + """ + A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers! + Freely explore giant worlds and recover Time Pieces to travel to new heights! + """ + + game = "A Hat in Time" + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = get_location_names() + + option_definitions = ahit_options + act_connections: Dict[str, str] = {} + shop_locs: List[str] = [] + item_name_groups = relic_groups + web = AWebInTime() + + def generate_early(self): + adjust_options(self) + + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + if self.is_dw_only(): + return + + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory + # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock + start_chapter: int = self.multiworld.StartingChapter[self.player].value + + if start_chapter == 4 or start_chapter == 3: + if self.multiworld.ActRandomizer[self.player].value == 0: + if start_chapter == 4: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + + if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + + def create_regions(self): + excluded_dws[self.player] = [] + excluded_bonuses[self.player] = [] + dw_shuffle[self.player] = [] + nyakuza_thug_items[self.player] = {} + badge_seller_count[self.player] = 0 + self.shop_locs = [] + self.topology_present = self.multiworld.ActRandomizer[self.player].value + + create_regions(self) + + if self.multiworld.EnableDeathWish[self.player].value > 0: + create_dw_regions(self) + + if self.is_dw_only(): + return + + create_events(self) + if self.is_dw(): + if "Snatcher's Hit List" not in self.get_excluded_dws() \ + or "Camera Tourist" not in self.get_excluded_dws(): + create_enemy_events(self) + + # place vanilla contract locations if contract shuffle is off + if self.multiworld.ShuffleActContracts[self.player].value == 0: + for name in contract_locations.keys(): + self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + + def create_items(self): + hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + + hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] + + if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0: + self.random.shuffle(hat_craft_order[self.player]) + if self.multiworld.RandomizeHatOrder[self.player].value == 2: + hat_craft_order[self.player].remove(HatType.TIME_STOP) + hat_craft_order[self.player].append(HatType.TIME_STOP) + + self.multiworld.itempool += create_itempool(self) + + def set_rules(self): + self.act_connections = {} + chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} + + if self.is_dw_only(): + # we already have all items if this is the case, no need for rules + self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, + None, self.player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", + self.player) + + if self.multiworld.DWEnableBonus[self.player].value == 0: + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): + continue + + if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + continue + + full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player)) + full_clear.show_in_spoiler = False + + return + + if self.multiworld.ActRandomizer[self.player].value > 0: + randomize_act_entrances(self) + + set_rules(self) + + if self.is_dw(): + set_dw_rules(self) + + def create_item(self, name: str) -> Item: + return create_item(self, name) + + def fill_slot_data(self) -> dict: + slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], + "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], + "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], + "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], + "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], + "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], + "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], + "BadgeSellerItemCount": badge_seller_count[self.player], + "SeedNumber": str(self.multiworld.seed), # For shop prices + "SeedName": self.multiworld.seed_name} + + if self.multiworld.HatItems[self.player].value == 0: + slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) + slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) + slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) + slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER]) + slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP]) + slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0])) + slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1])) + slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2])) + slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) + slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) + + if self.multiworld.ActRandomizer[self.player].value > 0: + for name in self.act_connections.keys(): + slot_data[name] = self.act_connections[name] + + if self.is_dlc2() and not self.is_dw_only(): + for name in nyakuza_thug_items[self.player].keys(): + slot_data[name] = nyakuza_thug_items[self.player][name] + + if self.is_dw(): + i: int = 0 + for name in excluded_dws[self.player]: + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + continue + + slot_data[f"excluded_dw{i}"] = dw_classes[name] + i += 1 + + i = 0 + if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + for name in excluded_bonuses[self.player]: + if name in excluded_dws[self.player]: + continue + + slot_data[f"excluded_bonus{i}"] = dw_classes[name] + i += 1 + + if self.multiworld.DWShuffle[self.player].value > 0: + shuffled_dws = self.get_dw_shuffle() + for i in range(len(shuffled_dws)): + slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] + + for option_name in slot_data_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0: + return + + new_hint_data = {} + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] + metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] + + for key, data in location_table.items(): + if not is_location_valid(self, key): + continue + + location = self.multiworld.get_location(key, self.player) + region_name: str + + if data.region in alpine_regions: + region_name = "Alpine Free Roam" + elif data.region in metro_regions: + region_name = "Nyakuza Free Roam" + elif data.region in chapter_act_info.keys(): + region_name = location.parent_region.name + else: + continue + + new_hint_data[location.address] = get_shuffled_region(self, region_name) + + if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: + ship_shape_region = get_shuffled_region(self, "Ship Shape") + id_start: int = TASKSANITY_START_ID + for i in range(self.multiworld.TasksanityCheckCount[self.player].value): + new_hint_data[id_start+i] = ship_shape_region + + hint_data[self.player] = new_hint_data + + def write_spoiler_header(self, spoiler_handle: TextIO): + for i in self.get_chapter_costs(): + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) + + for hat in hat_craft_order[self.player]: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) + + def set_chapter_cost(self, chapter: ChapterIndex, cost: int): + chapter_timepiece_costs[self.player][chapter] = cost + + def get_chapter_cost(self, chapter: ChapterIndex) -> int: + return chapter_timepiece_costs[self.player][chapter] + + def get_hat_craft_order(self): + return hat_craft_order[self.player] + + def get_hat_yarn_costs(self): + return hat_yarn_costs[self.player] + + def get_chapter_costs(self): + return chapter_timepiece_costs[self.player] + + def is_dlc1(self) -> bool: + return self.multiworld.EnableDLC1[self.player].value > 0 + + def is_dlc2(self) -> bool: + return self.multiworld.EnableDLC2[self.player].value > 0 + + def is_dw(self) -> bool: + return self.multiworld.EnableDeathWish[self.player].value > 0 + + def is_dw_only(self) -> bool: + return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + + def get_excluded_dws(self): + return excluded_dws[self.player] + + def get_excluded_bonuses(self): + return excluded_bonuses[self.player] + + def is_dw_excluded(self, name: str) -> bool: + # don't exclude Seal the Deal if it's our goal + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + return False + + if name in excluded_dws[self.player]: + return True + + return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + + def is_bonus_excluded(self, name: str) -> bool: + if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + return True + + return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + + def get_dw_shuffle(self): + return dw_shuffle[self.player] + + def set_dw_shuffle(self, shuffle: List[str]): + dw_shuffle[self.player] = shuffle + + def get_badge_seller_count(self) -> int: + return badge_seller_count[self.player] + + def set_badge_seller_count(self, value: int): + badge_seller_count[self.player] = value + + def get_nyakuza_thug_items(self): + return nyakuza_thug_items[self.player] + + def set_nyakuza_thug_items(self, items: Dict[str, int]): + nyakuza_thug_items[self.player] = items diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md new file mode 100644 index 0000000000..c4a4341763 --- /dev/null +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -0,0 +1,31 @@ +# A Hat in Time + +## 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? + +Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. + +To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. + +## What items and locations get shuffled? + +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations. + +Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. + +## 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. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in A Hat in Time? + +Items belonging to other worlds are represented by a badge with the Archipelago logo on it. + +## When the player receives an item, what happens? + +When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md new file mode 100644 index 0000000000..d2db2fe47f --- /dev/null +++ b/worlds/ahit/docs/setup_en.md @@ -0,0 +1,43 @@ +# Setup Guide for A Hat in Time in Archipelago + +## Required Software +- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/) + +- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601) + + +## Instructions + +1. Have Steam running. Open the Steam console with [this link.](steam://open/console) + +2. In the Steam console, enter the following command: +`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. + +3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + +4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. + +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. + + + +## Connecting to the Archipelago server + +When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command. + + +## Console Commands + +Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. + +`ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. + +`ap_deathlink` - Toggle Death Link. + +`ap_set_connection_info ` - Set the connection info for the save file. The IP address MUST BE IN QUOTES! + +`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py new file mode 100644 index 0000000000..7c2b9783e6 --- /dev/null +++ b/worlds/ahit/test/TestActs.py @@ -0,0 +1,31 @@ +from worlds.ahit.Regions import act_chapters +from worlds.ahit.test.TestBase import HatInTimeTestBase + + +class TestActs(HatInTimeTestBase): + def run_default_tests(self) -> bool: + return False + + def testAllStateCanReachEverything(self): + pass + + options = { + "ActRandomizer": 2, + "EnableDLC1": 1, + "EnableDLC2": 1, + "ShuffleActContracts": 0, + } + + def test_act_shuffle(self): + for i in range(1000): + self.world_setup() + self.collect_all_but([""]) + + for name in act_chapters.keys(): + region = self.multiworld.get_region(name, 1) + for entrance in region.entrances: + self.assertTrue(self.can_reach_entrance(entrance.name), + f"Can't reach {name} from {entrance}\n" + f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} " + f"-> {entrance} -> {name}" + f" (expected method of access)") diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py new file mode 100644 index 0000000000..1eb4dd6555 --- /dev/null +++ b/worlds/ahit/test/TestBase.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 58fcd50721dc773c5b90c5ac40ef9b68552f3fb1 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:42:07 -0500 Subject: [PATCH 057/143] Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. --- BaseClasses.py | 218 +++++++---- Fill.py | 45 ++- Generate.py | 17 +- Main.py | 7 +- SNIClient.py | 4 +- Utils.py | 64 +++- WebHostLib/options.py | 6 + WebHostLib/static/assets/faq/faq_en.md | 109 +++--- WebHostLib/static/assets/weighted-options.js | 291 ++++----------- WebHostLib/static/styles/weighted-options.css | 6 + WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 10 +- WebHostLib/tracker.py | 19 +- test/bases.py | 28 +- test/general/test_fill.py | 4 +- test/general/test_host_yaml.py | 4 +- test/general/test_locations.py | 3 - worlds/AutoWorld.py | 36 +- worlds/__init__.py | 42 ++- worlds/_bizhawk/context.py | 63 +++- worlds/adventure/Rom.py | 6 +- worlds/alttp/Client.py | 3 +- worlds/alttp/Dungeons.py | 3 +- worlds/alttp/ItemPool.py | 1 - worlds/alttp/Rom.py | 6 +- worlds/alttp/Rules.py | 28 +- worlds/alttp/Shops.py | 3 - worlds/alttp/UnderworldGlitchRules.py | 2 +- worlds/alttp/__init__.py | 46 +-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/archipidle/Rules.py | 7 +- worlds/blasphemous/Options.py | 1 + worlds/blasphemous/Rules.py | 8 +- worlds/blasphemous/docs/en_Blasphemous.md | 1 + worlds/checksfinder/__init__.py | 4 +- worlds/checksfinder/docs/en_ChecksFinder.md | 13 +- worlds/dlcquest/Rules.py | 4 +- worlds/dlcquest/__init__.py | 4 +- worlds/ff1/docs/en_Final Fantasy.md | 3 +- worlds/hk/Items.py | 45 ++- worlds/hk/Rules.py | 15 +- worlds/hk/__init__.py | 16 +- worlds/ladx/Locations.py | 6 +- worlds/ladx/__init__.py | 13 +- worlds/lufia2ac/Rom.py | 5 +- worlds/meritous/Regions.py | 4 +- worlds/messenger/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 7 + worlds/musedash/MuseDashData.txt | 11 +- worlds/musedash/__init__.py | 2 +- worlds/noita/Items.py | 84 +++-- worlds/noita/Regions.py | 74 ++-- worlds/noita/Rules.py | 13 +- worlds/oot/Entrance.py | 10 +- worlds/oot/EntranceShuffle.py | 52 ++- worlds/oot/Patches.py | 2 +- worlds/oot/Rules.py | 21 +- worlds/oot/__init__.py | 344 ++++++++++-------- worlds/oot/docs/en_Ocarina of Time.md | 7 + worlds/pokemon_rb/__init__.py | 17 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45570 -> 45893 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45511 -> 45875 bytes .../docs/en_Pokemon Red and Blue.md | 6 + worlds/pokemon_rb/rom.py | 6 +- worlds/pokemon_rb/rom_addresses.py | 10 +- worlds/pokemon_rb/rules.py | 38 +- worlds/ror2/Options.py | 42 +-- worlds/ror2/Rules.py | 3 +- .../docs/en_Starcraft 2 Wings of Liberty.md | 22 +- worlds/sm/__init__.py | 19 +- worlds/sm64ex/Options.py | 7 + worlds/sm64ex/Rules.py | 7 +- worlds/sm64ex/__init__.py | 1 + worlds/smz3/__init__.py | 8 +- worlds/soe/__init__.py | 2 +- worlds/stardew_valley/mods/mod_data.py | 8 + worlds/stardew_valley/stardew_rule.py | 16 +- worlds/stardew_valley/test/TestBackpack.py | 49 +-- worlds/stardew_valley/test/TestGeneration.py | 39 +- worlds/stardew_valley/test/TestItems.py | 6 +- .../test/TestLogicSimplification.py | 91 ++--- worlds/stardew_valley/test/TestOptions.py | 35 +- worlds/stardew_valley/test/TestRegions.py | 4 +- worlds/stardew_valley/test/TestRules.py | 2 +- 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 +- worlds/terraria/docs/setup_en.md | 2 + worlds/tloz/docs/en_The Legend of Zelda.md | 14 +- worlds/tloz/docs/multiworld_en.md | 1 + worlds/undertale/__init__.py | 2 +- worlds/undertale/docs/en_Undertale.md | 19 +- worlds/wargroove/docs/en_Wargroove.md | 9 +- worlds/witness/hints.py | 4 +- worlds/zillion/__init__.py | 33 +- worlds/zillion/docs/en_Zillion.md | 10 +- 101 files changed, 1458 insertions(+), 1186 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d35739c324..a70dd70a92 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]) @@ -303,11 +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) + + @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) @@ -329,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) @@ -424,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] @@ -467,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 @@ -608,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]] @@ -620,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()} @@ -668,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 @@ -712,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 @@ -736,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: @@ -749,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 @@ -816,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) @@ -855,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. @@ -866,6 +931,7 @@ class Region: if rule: exit_.access_rule = rule exit_.connect(connecting_region) + return exit_ def create_exit(self, name: str) -> Entrance: """ 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/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/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/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/Utils.py b/Utils.py index 5fb037a173..bb68602cce 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 @@ -73,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]: @@ -90,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)) @@ -146,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) @@ -257,15 +289,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): @@ -905,3 +935,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/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/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. diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index bdd121eff5..3811bd42ba 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); } }); @@ -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; } 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: diff --git a/test/bases.py b/test/bases.py index 5fe4df2014..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) @@ -284,7 +310,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_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/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/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/AutoWorld.py b/worlds/AutoWorld.py index d4fe0f49a2..d05797cf9e 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: @@ -400,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/__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(): 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) 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/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) 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/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/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 65e36da3bd..d89e65c59d 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) @@ -585,27 +586,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) @@ -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/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/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/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? 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/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/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/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/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'} 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) 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 6c89db3891..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): @@ -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..eaaea5be2f 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 @@ -518,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 @@ -526,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/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 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/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/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'))) 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/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 diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index ca53c96233..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] @@ -84,8 +91,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,43 +102,46 @@ 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, 1), "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), +} +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": 10, - "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, + **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/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())) 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) 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/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] diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index fa198e0ce1..529411f6fc 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 @@ -223,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 6af19683f4..e9c889d6f6 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 @@ -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,8 +198,10 @@ class OOTWorld(World): option_value = result.current_key setattr(self, option_name, option_value) + self.regions = [] # internal caches of regions for this world, used later + self._regions_cache = {} + self.shop_prices = {} - self.regions = [] # internal cache of regions for this world, used later self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -489,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) @@ -526,6 +534,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,8 +567,10 @@ 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 + + # 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'}] @@ -585,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 = {} @@ -610,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', @@ -622,7 +640,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,9 +654,46 @@ 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 + + # 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, @@ -658,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: @@ -671,7 +728,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 +739,13 @@ 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)) + + # Create items, starting item handling, boss prize fill (before entrance randomizer) 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 = [] @@ -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,8 +828,9 @@ 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.get_state_with_complete_itempool() 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] @@ -781,7 +841,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: @@ -792,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(locations=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(locations=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 @@ -836,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) @@ -861,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: @@ -883,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))) @@ -896,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': @@ -954,48 +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': @@ -1032,30 +1059,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 +1138,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 +1146,7 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } + def modify_multidata(self, multidata: dict): # Replace connect name @@ -1156,6 +1161,16 @@ 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): er_hint_data[self.player] = {} @@ -1202,6 +1217,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 +1227,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 @@ -1218,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) @@ -1242,24 +1284,29 @@ 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.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) - 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_entrances(self): + return self.multiworld.get_entrances(self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1294,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) @@ -1336,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', @@ -1355,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 + 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/__init__.py b/worlds/pokemon_rb/__init__.py index 11aa737e0f..b2ee0702c9 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 @@ -467,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)] @@ -559,7 +559,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/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/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/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/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, 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), 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 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/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/sm/__init__.py b/worlds/sm/__init__.py index f208e600b9..3e9015eab7 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 @@ -294,7 +291,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) @@ -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 @@ -390,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) @@ -563,8 +562,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/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): 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/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/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/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/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/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_()) 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/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)) 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] 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 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/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 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/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/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_) 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 f4f405fc764d56ff1ec425542e105d19b259af7c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:44:21 -0500 Subject: [PATCH 058/143] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b9ca4b8d28..361ea34407 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage -/oot/ # Byte-compiled / optimized / DLL files __pycache__/ From 74715c397cbcfac1314df26542cd94aa191fa96a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 14:09:29 -0500 Subject: [PATCH 059/143] 1.3.6 --- worlds/ahit/DeathWishLocations.py | 1 + worlds/ahit/Locations.py | 4 +- worlds/ahit/Options.py | 11 +++++ worlds/ahit/Regions.py | 10 ++--- worlds/ahit/Rules.py | 67 ++++++++++++++++++++++++------- worlds/ahit/__init__.py | 6 ++- worlds/ahit/test/TestActs.py | 7 ++-- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index f51d4948ee..bc529261c1 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -197,6 +197,7 @@ def create_dw_regions(world: World): if i == 0: connect_regions(dw_map, dw, f"-> {name}", world.player) else: + # noinspection PyUnboundLocalVariable connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) loc_id = death_wishes[name] diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index bf31c8cba8..2f47c2ebc0 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -283,7 +283,7 @@ ahit_locations = { # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", @@ -900,6 +900,8 @@ event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town"), "TOD Access": LocData(0, "Toilet of Doom"), "YCHE Access": LocData(0, "Your Contract has Expired"), + "AFR Access": LocData(0, "Alpine Free Roam"), + "TIHS Access": LocData(0, "The Illness has Spread"), "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index f3dd2a8c66..18d93802a1 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -458,6 +458,15 @@ class NyakuzaThugMaxShopItems(Range): default = 4 +class NoTicketSkips(Choice): + """Prevent metro gate skips from being in logic on higher difficulties. + Rush Hour option will only consider the ticket skips for Rush Hour in logic.""" + display_name = "No Ticket Skips" + option_false = 0 + option_true = 1 + option_rush_hour = 2 + + class BaseballBat(Toggle): """Replace the Umbrella with the baseball bat from Nyakuza Metro. DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" @@ -656,6 +665,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "MetroMaxPonCost": MetroMaxPonCost, "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + "NoTicketSkips": NoTicketSkips, "LowestChapterCost": LowestChapterCost, "HighestChapterCost": HighestChapterCost, @@ -720,6 +730,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "MetroMinPonCost": MetroMinPonCost, "MetroMaxPonCost": MetroMaxPonCost, "BaseballBat": BaseballBat, + "NoTicketSkips": NoTicketSkips, "MinPonCost": MinPonCost, "MaxPonCost": MaxPonCost, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 807f1ee77f..0737f880be 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -501,12 +501,12 @@ def randomize_act_entrances(world: World): region_list.append(region) for region in region_list.copy(): - if "Time Rift" in region.name: + if region.name in chapter_finales: region_list.remove(region) region_list.append(region) for region in region_list.copy(): - if region.name in chapter_finales: + if "Time Rift" in region.name: region_list.remove(region) region_list.append(region) @@ -631,8 +631,8 @@ def randomize_act_entrances(world: World): candidate = c break + # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) - # print(region, candidate) # Vanilla if candidate.name == region.name: diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7eb09bedfc..b50a7cdf35 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -317,9 +317,24 @@ def set_rules(world: World): for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: if "Goat Village" in loc.name: continue + # This needs some special handling + if loc.name == "Alpine Skyline - Goat Refinery": + add_rule(loc, lambda state: state.has("AFR Access", world.player) + and can_use_hookshot(state, world) + and can_hit(state, world, True)) + + difficulty: Difficulty = Difficulty(world.multiworld.LogicDifficulty[world.player].value) + if difficulty >= Difficulty.MODERATE: + add_rule(loc, lambda state: state.has("TIHS Access", world.player) + and can_use_hat(state, world, HatType.SPRINT), "or") + elif difficulty >= Difficulty.HARD: + add_rule(loc, lambda state: state.has("TIHS Access", world.player, "or")) + + continue add_rule(loc, lambda state: can_use_hookshot(state, world)) + dummy_entrances: typing.List[Entrance] = [] for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): continue @@ -328,7 +343,7 @@ def set_rules(world: World): entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] - entrance.parent_region.exits.remove(entrance) + dummy_entrances.append(entrance) # Entrances to this act that we have to set access_rules on entrances: typing.List[Entrance] = [] @@ -354,6 +369,9 @@ def set_rules(world: World): for rules in access_rules: add_rule(e, rules) + for e in dummy_entrances: + set_rule(e, lambda state: False) + set_event_rules(world) if world.multiworld.EndGoal[world.player].value == 1: @@ -448,13 +466,12 @@ def set_moderate_rules(world: World): # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. # Yellow Overpass time piece can also be reached without Hookshot quite easily. if world.is_dlc2(): - set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + # No Hookshot set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), lambda state: True) + # No Dweller, Hookshot, or Time Stop for these set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) - - # The player can quite literally walk past the fan from the side without Time Stop. set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) # Moderate: clear Rush Hour without Hookshot @@ -465,8 +482,10 @@ def set_moderate_rules(world: World): and can_use_hat(state, world, HatType.ICE) and can_use_hat(state, world, HatType.BREWING)) - # Moderate: Bluefin Tunnel without tickets - set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + # Moderate: Bluefin Tunnel + Pink Paw Station without tickets + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) def set_hard_rules(world: World): @@ -483,6 +502,13 @@ def set_hard_rules(world: World): set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") @@ -508,8 +534,15 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + if world.multiworld.NoTicketSkips[world.player].value != 1: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_expert_rules(world: World): @@ -517,8 +550,10 @@ def set_expert_rules(world: World): set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) - # Expert: Mafia Town - Above Boats with nothing + # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True) # Expert: Clear Dead Bird Studio with nothing for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: @@ -561,13 +596,9 @@ def set_expert_rules(world: World): # Set painting rules only. Skipping paintings is determined in has_paintings set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: has_paintings(state, world, 1, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), - lambda state: has_paintings(state, world, 2, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: has_paintings(state, world, 2, True)) set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: has_paintings(state, world, 3, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), lambda state: has_paintings(state, world, 3, True)) # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him @@ -579,7 +610,13 @@ def set_expert_rules(world: World): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_mafia_town_rules(world: World): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 0ed14c6376..805dc57898 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,7 +1,8 @@ from BaseClasses import Item, ItemClassification, Tutorial from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region -from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ + get_total_locations from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem @@ -173,7 +174,8 @@ class HatInTimeWorld(World): "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": str(self.multiworld.seed), # For shop prices - "SeedName": self.multiworld.seed_name} + "SeedName": self.multiworld.seed_name, + "TotalLocations": get_total_locations(self)} if self.multiworld.HatItems[self.player].value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 7c2b9783e6..da3d5f5c0c 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -1,4 +1,5 @@ from worlds.ahit.Regions import act_chapters +from worlds.ahit.Rules import act_connections from worlds.ahit.test.TestBase import HatInTimeTestBase @@ -6,9 +7,6 @@ class TestActs(HatInTimeTestBase): def run_default_tests(self) -> bool: return False - def testAllStateCanReachEverything(self): - pass - options = { "ActRandomizer": 2, "EnableDLC1": 1, @@ -24,6 +22,9 @@ class TestActs(HatInTimeTestBase): for name in act_chapters.keys(): region = self.multiworld.get_region(name, 1) for entrance in region.entrances: + if entrance.name in act_connections.keys(): + continue + self.assertTrue(self.can_reach_entrance(entrance.name), f"Can't reach {name} from {entrance}\n" f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} " From 98c29b77f393add0308c0b13e9efad919256e6f6 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Dec 2023 11:07:30 -0500 Subject: [PATCH 060/143] Final touch-ups --- worlds/ahit/DeathWishRules.py | 2 +- worlds/ahit/Options.py | 2 ++ worlds/ahit/Rules.py | 2 +- worlds/ahit/docs/en_A Hat in Time.md | 8 ++++++-- worlds/ahit/docs/setup_en.md | 7 +++---- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index da8d639a92..3eb92b3dfe 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -406,7 +406,7 @@ def create_enemy_events(world: World): and name not in world.get_dw_shuffle(): continue - region = world.options.get_region(name, world.player) + region = world.multiworld.get_region(name, world.player) event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) region.locations.append(event) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 626b4671c3..af21dae4c9 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -666,6 +666,7 @@ class AHITOptions(PerGameCommonOptions): MetroMaxPonCost: MetroMaxPonCost NyakuzaThugMinShopItems: NyakuzaThugMinShopItems NyakuzaThugMaxShopItems: NyakuzaThugMaxShopItems + NoTicketSkips: NoTicketSkips LowestChapterCost: LowestChapterCost HighestChapterCost: HighestChapterCost @@ -729,6 +730,7 @@ slot_data_options: List[str] = [ "MetroMinPonCost", "MetroMaxPonCost", "BaseballBat", + "NoTicketSkips", "MinPonCost", "MaxPonCost", diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 44a815d00d..1001645284 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -483,7 +483,7 @@ def set_moderate_rules(world: World): and can_use_hat(state, world, HatType.BREWING)) # Moderate: Bluefin Tunnel + Pink Paw Station without tickets - if world.multiworld.NoTicketSkips[world.player].value == 0: + if world.options.NoTicketSkips.value == 0: set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index c4a4341763..6bfb2196cf 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -9,11 +9,11 @@ config file. Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. -To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. +To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to access that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. ## What items and locations get shuffled? -Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations. +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Hats can also optionally be shuffled as individual items instead. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are locations. Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. @@ -29,3 +29,7 @@ Items belonging to other worlds are represented by a badge with the Archipelago ## When the player receives an item, what happens? When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. + +## Is the DLC required to play A Hat in Time in Archipelago? + +No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC. \ No newline at end of file diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index d2db2fe47f..cea74380bd 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -11,7 +11,7 @@ 1. Have Steam running. Open the Steam console with [this link.](steam://open/console) 2. In the Steam console, enter the following command: -`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. +`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. @@ -24,10 +24,9 @@ 7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. - ## Connecting to the Archipelago server -When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command. +To connect to the multiworld server, simply run the **ArchipelagoAHITClient** and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. ## Console Commands @@ -38,6 +37,6 @@ Commands will not work on the title screen, you must be in-game to use them. To `ap_deathlink` - Toggle Death Link. -`ap_set_connection_info ` - Set the connection info for the save file. The IP address MUST BE IN QUOTES! +`ap_set_connection_info ` - Usually not necessary. Set the connection info for the save file. **The IP address MUST be in double quotes!** `ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file From b9fce5a33cf33918d483e8f1e5d503e006a6f9ea Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 19 Dec 2023 16:18:53 -0500 Subject: [PATCH 061/143] Fix client and leftover old options api --- AHITClient.py | 240 +--------------------------------- worlds/ahit/Client.py | 235 +++++++++++++++++++++++++++++++++ worlds/ahit/DeathWishRules.py | 1 - worlds/ahit/Items.py | 1 - worlds/ahit/Regions.py | 2 +- worlds/ahit/Rules.py | 6 +- worlds/ahit/__init__.py | 31 +++-- 7 files changed, 267 insertions(+), 249 deletions(-) create mode 100644 worlds/ahit/Client.py diff --git a/AHITClient.py b/AHITClient.py index 884f3ee5c7..6ed7d7b49d 100644 --- a/AHITClient.py +++ b/AHITClient.py @@ -1,236 +1,8 @@ -import asyncio +from worlds.ahit.Client import launch import Utils -import websockets -import functools -from copy import deepcopy -from typing import List, Any, Iterable -from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem -from MultiServer import Endpoint -from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser +import ModuleUpdate +ModuleUpdate.update() -DEBUG = False - - -class AHITJSONToTextParser(JSONtoTextParser): - def _handle_color(self, node: JSONMessagePart): - return self._handle_text(node) # No colors for the in-game text - - -class AHITCommandProcessor(ClientCommandProcessor): - def __init__(self, ctx: CommonContext): - super().__init__(ctx) - - def _cmd_ahit(self): - """Check AHIT Connection State""" - if isinstance(self.ctx, AHITContext): - logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") - - -class AHITContext(CommonContext): - command_processor = AHITCommandProcessor - game = "A Hat in Time" - - def __init__(self, server_address, password): - super().__init__(server_address, password) - self.proxy = None - self.proxy_task = None - self.gamejsontotext = AHITJSONToTextParser(self) - self.autoreconnect_task = None - self.endpoint = None - self.items_handling = 0b111 - self.room_info = None - self.connected_msg = None - self.game_connected = False - self.awaiting_info = False - self.full_inventory: List[Any] = [] - self.server_msgs: List[Any] = [] - - async def server_auth(self, password_requested: bool = False): - if password_requested and not self.password: - await super(AHITContext, self).server_auth(password_requested) - - await self.get_username() - await self.send_connect() - - def get_ahit_status(self) -> str: - if not self.is_proxy_connected(): - return "Not connected to A Hat in Time" - - return "Connected to A Hat in Time" - - async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: - """ `msgs` JSON serializable """ - if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: - return False - - if DEBUG: - logger.info(f"Outgoing message: {msgs}") - - await self.endpoint.socket.send(msgs) - return True - - async def disconnect(self, allow_autoreconnect: bool = False): - await super().disconnect(allow_autoreconnect) - - async def disconnect_proxy(self): - if self.endpoint and not self.endpoint.socket.closed: - await self.endpoint.socket.close() - if self.proxy_task is not None: - await self.proxy_task - - def is_connected(self) -> bool: - return self.server and self.server.socket.open - - def is_proxy_connected(self) -> bool: - return self.endpoint and self.endpoint.socket.open - - def on_print_json(self, args: dict): - text = self.gamejsontotext(deepcopy(args["data"])) - msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} - self.server_msgs.append(encode([msg])) - - if self.ui: - self.ui.print_json(args["data"]) - else: - text = self.jsontotextparser(args["data"]) - logger.info(text) - - def update_items(self): - # just to be safe - we might still have an inventory from a different room - if not self.is_connected(): - return - - self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) - - def on_package(self, cmd: str, args: dict): - if cmd == "Connected": - self.connected_msg = encode([args]) - if self.awaiting_info: - self.server_msgs.append(self.room_info) - self.update_items() - self.awaiting_info = False - - elif cmd == "ReceivedItems": - if args["index"] == 0: - self.full_inventory.clear() - - for item in args["items"]: - self.full_inventory.append(NetworkItem(*item)) - - self.server_msgs.append(encode([args])) - - elif cmd == "RoomInfo": - self.seed_name = args["seed_name"] - self.room_info = encode([args]) - - else: - if cmd != "PrintJSON": - self.server_msgs.append(encode([args])) - - def run_gui(self): - from kvui import GameManager - - class AHITManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago A Hat in Time Client" - - self.ui = AHITManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") - - -async def proxy(websocket, path: str = "/", ctx: AHITContext = None): - ctx.endpoint = Endpoint(websocket) - try: - await on_client_connected(ctx) - - if ctx.is_proxy_connected(): - async for data in websocket: - if DEBUG: - logger.info(f"Incoming message: {data}") - - for msg in decode(data): - if msg["cmd"] == "Connect": - # Proxy is connecting, make sure it is valid - if msg["game"] != "A Hat in Time": - logger.info("Aborting proxy connection: game is not A Hat in Time") - await ctx.disconnect_proxy() - break - - if ctx.seed_name: - seed_name = msg.get("seed_name", "") - if seed_name != "" and seed_name != ctx.seed_name: - logger.info("Aborting proxy connection: seed mismatch from save file") - logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") - text = encode([{"cmd": "PrintJSON", - "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) - await ctx.send_msgs_proxy(text) - await ctx.disconnect_proxy() - break - - if ctx.connected_msg and ctx.is_connected(): - await ctx.send_msgs_proxy(ctx.connected_msg) - ctx.update_items() - continue - - if not ctx.is_proxy_connected(): - break - - await ctx.send_msgs([msg]) - - except Exception as e: - if not isinstance(e, websockets.WebSocketException): - logger.exception(e) - finally: - await ctx.disconnect_proxy() - - -async def on_client_connected(ctx: AHITContext): - if ctx.room_info and ctx.is_connected(): - await ctx.send_msgs_proxy(ctx.room_info) - else: - ctx.awaiting_info = True - - -async def main(): - parser = get_base_parser() - args = parser.parse_args() - - ctx = AHITContext(args.connect, args.password) - logger.info("Starting A Hat in Time proxy server") - ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), - host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) - ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - await ctx.proxy - await ctx.proxy_task - await ctx.exit_event.wait() - - -async def proxy_loop(ctx: AHITContext): - try: - while not ctx.exit_event.is_set(): - if len(ctx.server_msgs) > 0: - for msg in ctx.server_msgs: - await ctx.send_msgs_proxy(msg) - - ctx.server_msgs.clear() - await asyncio.sleep(0.1) - except Exception as e: - logger.exception(e) - logger.info("Aborting AHIT Proxy Client due to errors") - - -if __name__ == '__main__': - Utils.init_logging("AHITClient") - options = Utils.get_options() - - import colorama - colorama.init() - asyncio.run(main()) - colorama.deinit() +if __name__ == "__main__": + Utils.init_logging("AHITClient", exception_logger="Client") + launch() diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py new file mode 100644 index 0000000000..f6f87a35a6 --- /dev/null +++ b/worlds/ahit/Client.py @@ -0,0 +1,235 @@ +import asyncio +import Utils +import websockets +import functools +from copy import deepcopy +from typing import List, Any, Iterable +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from MultiServer import Endpoint +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser + +DEBUG = False + + +class AHITJSONToTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(node) # No colors for the in-game text + + +class AHITCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_ahit(self): + """Check AHIT Connection State""" + if isinstance(self.ctx, AHITContext): + logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") + + +class AHITContext(CommonContext): + command_processor = AHITCommandProcessor + game = "A Hat in Time" + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.proxy = None + self.proxy_task = None + self.gamejsontotext = AHITJSONToTextParser(self) + self.autoreconnect_task = None + self.endpoint = None + self.items_handling = 0b111 + self.room_info = None + self.connected_msg = None + self.game_connected = False + self.awaiting_info = False + self.full_inventory: List[Any] = [] + self.server_msgs: List[Any] = [] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AHITContext, self).server_auth(password_requested) + + await self.get_username() + await self.send_connect() + + def get_ahit_status(self) -> str: + if not self.is_proxy_connected(): + return "Not connected to A Hat in Time" + + return "Connected to A Hat in Time" + + async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: + """ `msgs` JSON serializable """ + if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: + return False + + if DEBUG: + logger.info(f"Outgoing message: {msgs}") + + await self.endpoint.socket.send(msgs) + return True + + async def disconnect(self, allow_autoreconnect: bool = False): + await super().disconnect(allow_autoreconnect) + + async def disconnect_proxy(self): + if self.endpoint and not self.endpoint.socket.closed: + await self.endpoint.socket.close() + if self.proxy_task is not None: + await self.proxy_task + + def is_connected(self) -> bool: + return self.server and self.server.socket.open + + def is_proxy_connected(self) -> bool: + return self.endpoint and self.endpoint.socket.open + + def on_print_json(self, args: dict): + text = self.gamejsontotext(deepcopy(args["data"])) + msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} + self.server_msgs.append(encode([msg])) + + if self.ui: + self.ui.print_json(args["data"]) + else: + text = self.jsontotextparser(args["data"]) + logger.info(text) + + def update_items(self): + # just to be safe - we might still have an inventory from a different room + if not self.is_connected(): + return + + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.connected_msg = encode([args]) + if self.awaiting_info: + self.server_msgs.append(self.room_info) + self.update_items() + self.awaiting_info = False + + elif cmd == "ReceivedItems": + if args["index"] == 0: + self.full_inventory.clear() + + for item in args["items"]: + self.full_inventory.append(NetworkItem(*item)) + + self.server_msgs.append(encode([args])) + + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.room_info = encode([args]) + + else: + if cmd != "PrintJSON": + self.server_msgs.append(encode([args])) + + def run_gui(self): + from kvui import GameManager + + class AHITManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago A Hat in Time Client" + + self.ui = AHITManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def proxy(websocket, path: str = "/", ctx: AHITContext = None): + ctx.endpoint = Endpoint(websocket) + try: + await on_client_connected(ctx) + + if ctx.is_proxy_connected(): + async for data in websocket: + if DEBUG: + logger.info(f"Incoming message: {data}") + + for msg in decode(data): + if msg["cmd"] == "Connect": + # Proxy is connecting, make sure it is valid + if msg["game"] != "A Hat in Time": + logger.info("Aborting proxy connection: game is not A Hat in Time") + await ctx.disconnect_proxy() + break + + if ctx.seed_name: + seed_name = msg.get("seed_name", "") + if seed_name != "" and seed_name != ctx.seed_name: + logger.info("Aborting proxy connection: seed mismatch from save file") + logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + + if ctx.connected_msg and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.connected_msg) + ctx.update_items() + continue + + if not ctx.is_proxy_connected(): + break + + await ctx.send_msgs([msg]) + + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logger.exception(e) + finally: + await ctx.disconnect_proxy() + + +async def on_client_connected(ctx: AHITContext): + if ctx.room_info and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.room_info) + else: + ctx.awaiting_info = True + + +async def proxy_loop(ctx: AHITContext): + try: + while not ctx.exit_event.is_set(): + if len(ctx.server_msgs) > 0: + for msg in ctx.server_msgs: + await ctx.send_msgs_proxy(msg) + + ctx.server_msgs.clear() + await asyncio.sleep(0.1) + except Exception as e: + logger.exception(e) + logger.info("Aborting AHIT Proxy Client due to errors") + + +def launch(): + async def main(): + parser = get_base_parser() + args = parser.parse_args() + + ctx = AHITContext(args.connect, args.password) + logger.info("Starting A Hat in Time proxy server") + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), + host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) + ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.proxy + await ctx.proxy_task + await ctx.exit_event.wait() + + Utils.init_logging("AHITClient") + # options = Utils.get_options() + + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 3eb92b3dfe..90309bdb51 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -286,7 +286,6 @@ def get_total_dw_stamps(state: CollectionState, world: World) -> int: if state.has(f"2 Stamps - {name}", world.player): count += 2 elif name not in dw_candles: - # most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) count += 1 return count diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 3a13b3e3c8..6b0ccba7ea 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -89,7 +89,6 @@ def create_itempool(world: World) -> List[Item]: def calculate_yarn_costs(world: World): mw = world.multiworld - p = world.player min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 6c0266bf44..7178075c5f 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -439,7 +439,7 @@ def create_rift_connections(world: World, region: Region): def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = TASKSANITY_START_ID - for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + for i in range(world.options.TasksanityCheckCount.value): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 1001645284..9fd7381779 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -322,7 +322,7 @@ def set_rules(world: World): and can_use_hookshot(state, world) and can_hit(state, world, True)) - difficulty: Difficulty = Difficulty(world.multiworld.LogicDifficulty[world.player].value) + difficulty: Difficulty = Difficulty(world.options.LogicDifficulty.value) if difficulty >= Difficulty.MODERATE: add_rule(loc, lambda state: state.has("TIHS Access", world.player) and can_use_hat(state, world, HatType.SPRINT), "or") @@ -534,7 +534,7 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - if world.multiworld.NoTicketSkips[world.player].value != 1: + if world.options.NoTicketSkips.value != 1: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING)) else: @@ -610,7 +610,7 @@ def set_expert_rules(world: World): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - if world.multiworld.NoTicketSkips[world.player].value == 0: + if world.options.NoTicketSkips.value == 0: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) else: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 14eba116a6..f8527e95f4 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -10,9 +10,20 @@ from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events from worlds.AutoWorld import World, WebWorld from typing import List, Dict, TextIO -from worlds.LauncherComponents import Component, components, icon_paths +from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type from Utils import local_path + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="AHITClient") + + +components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client, + component_type=Type.CLIENT, icon='yatta')) + +icon_paths['yatta'] = local_path('data', 'yatta.png') + hat_craft_order: Dict[int, List[HatType]] = {} hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} @@ -22,9 +33,6 @@ dw_shuffle: Dict[int, List[str]] = {} nyakuza_thug_items: Dict[int, Dict[str, int]] = {} badge_seller_count: Dict[int, int] = {} -components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) -icon_paths['yatta'] = local_path('data', 'yatta.png') - class AWebInTime(WebWorld): theme = "partyTime" @@ -85,7 +93,8 @@ class HatInTimeWorld(World): nyakuza_thug_items[self.player] = {} badge_seller_count[self.player] = 0 self.shop_locs = [] - self.topology_present = self.options.ActRandomizer.value + # noinspection PyClassVar + self.topology_present = bool(self.options.ActRandomizer.value) create_regions(self) if self.options.EnableDeathWish.value > 0: @@ -231,7 +240,9 @@ class HatInTimeWorld(World): return new_hint_data = {} - alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", + "The Twilight Bell", "Alpine Skyline Area", "Alpine Skyline Area (TIHS)"] + metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] for key, data in location_table.items(): @@ -245,6 +256,8 @@ class HatInTimeWorld(World): region_name = "Alpine Free Roam" elif data.region in metro_regions: region_name = "Nyakuza Free Roam" + elif "Dead Bird Studio - " in data.region: + region_name = "Dead Bird Studio" elif data.region in chapter_act_info.keys(): region_name = location.parent_region.name else: @@ -303,19 +316,19 @@ class HatInTimeWorld(World): def is_dw_excluded(self, name: str) -> bool: # don't exclude Seal the Deal if it's our goal if self.options.EndGoal.value == 3 and name == "Seal the Deal" \ - and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + and f"{name} - Main Objective" not in self.options.exclude_locations: return False if name in excluded_dws[self.player]: return True - return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + return f"{name} - Main Objective" in self.options.exclude_locations def is_bonus_excluded(self, name: str) -> bool: if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: return True - return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + return f"{name} - All Clear" in self.options.exclude_locations def get_dw_shuffle(self): return dw_shuffle[self.player] From 6d629a01786adf9c40f61703922e11f47872805b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 19 Dec 2023 16:35:46 -0500 Subject: [PATCH 062/143] Delete setup-ahitclient.py --- setup-ahitclient.py | 642 -------------------------------------------- 1 file changed, 642 deletions(-) delete mode 100644 setup-ahitclient.py diff --git a/setup-ahitclient.py b/setup-ahitclient.py deleted file mode 100644 index 18fd6a1887..0000000000 --- a/setup-ahitclient.py +++ /dev/null @@ -1,642 +0,0 @@ -import base64 -import datetime -import os -import platform -import shutil -import sys -import sysconfig -import typing -import warnings -import zipfile -import urllib.request -import io -import json -import threading -import subprocess - -from collections.abc import Iterable -from hashlib import sha3_512 -from pathlib import Path - - -# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it -try: - requirement = 'cx-Freeze>=6.15.2' - import pkg_resources - try: - pkg_resources.require(requirement) - install_cx_freeze = False - except pkg_resources.ResolutionError: - install_cx_freeze = True -except ImportError: - install_cx_freeze = True - pkg_resources = None # type: ignore [assignment] - -if install_cx_freeze: - # check if pip is available - try: - import pip # noqa: F401 - except ImportError: - raise RuntimeError("pip not available. Please install pip.") - # install and import cx_freeze - if '--yes' not in sys.argv and '-y' not in sys.argv: - input(f'Requirement {requirement} is not satisfied, press enter to install it') - subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) - import pkg_resources - -import cx_Freeze - -# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line -import setuptools.command.build - -if __name__ == "__main__": - # need to run this early to import from Utils and Launcher - # TODO: move stuff to not require this - import ModuleUpdate - ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) - ModuleUpdate.update_ran = False # restore for later - -from worlds.LauncherComponents import components, icon_paths -from Utils import version_tuple, is_windows, is_linux -from Cython.Build import cythonize - - -# On Python < 3.10 LogicMixin is not currently supported. -non_apworlds: set = { - "A Link to the Past", - "Adventure", - "ArchipIDLE", - "Archipelago", - "ChecksFinder", - "Clique", - "DLCQuest", - "Final Fantasy", - "Hylics 2", - "Kingdom Hearts 2", - "Lufia II Ancient Cave", - "Meritous", - "Ocarina of Time", - "Overcooked! 2", - "Raft", - "Secret of Evermore", - "Slay the Spire", - "Starcraft 2 Wings of Liberty", - "Sudoku", - "Super Mario 64", - "VVVVVV", - "Wargroove", - "Zillion", -} - -# LogicMixin is broken before 3.10 import revamp -if sys.version_info < (3,10): - non_apworlds.add("Hollow Knight") - -def download_SNI(): - print("Updating SNI") - machine_to_go = { - "x86_64": "amd64", - "aarch64": "arm64", - "armv7l": "arm" - } - platform_name = platform.system().lower() - machine_name = platform.machine().lower() - # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH - machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) - with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: - data = json.load(request) - files = data["assets"] - - source_url = None - - for file in files: - download_url: str = file["browser_download_url"] - machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name - if platform_name in download_url and machine_match: - # prefer "many" builds - if "many" in download_url: - source_url = download_url - break - source_url = download_url - - if source_url and source_url.endswith(".zip"): - with urllib.request.urlopen(source_url) as download: - with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: - for member in zf.infolist(): - zf.extract(member, path="SNI") - print(f"Downloaded SNI from {source_url}") - - elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): - import tarfile - mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" - with urllib.request.urlopen(source_url) as download: - sni_dir = None - with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: - for member in tf.getmembers(): - if member.name.startswith("/") or "../" in member.name: - raise ValueError(f"Unexpected file '{member.name}' in {source_url}") - elif member.isdir() and not sni_dir: - sni_dir = member.name - elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): - raise ValueError(f"Expected folder before '{member.name}' in {source_url}") - elif member.isfile() and sni_dir: - tf.extract(member) - # sadly SNI is in its own folder on non-windows, so we need to rename - shutil.rmtree("SNI", True) - os.rename(sni_dir, "SNI") - print(f"Downloaded SNI from {source_url}") - - elif source_url: - print(f"Don't know how to extract SNI from {source_url}") - - else: - print(f"No SNI found for system spec {platform_name} {machine_name}") - - -signtool: typing.Optional[str] -if os.path.exists("X:/pw.txt"): - print("Using signtool") - with open("X:/pw.txt", encoding="utf-8-sig") as f: - pw = f.read() - signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ - r'" /fd sha256 /tr http://timestamp.digicert.com/ ' -else: - signtool = None - - -build_platform = sysconfig.get_platform() -arch_folder = "exe.{platform}-{version}".format(platform=build_platform, - version=sysconfig.get_python_version()) -buildfolder = Path("build", arch_folder) -build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine() - - -# see Launcher.py on how to add scripts to setup.py -def resolve_icon(icon_name: str): - base_path = icon_paths[icon_name] - if is_windows: - path, extension = os.path.splitext(base_path) - ico_file = path + ".ico" - assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist." - return ico_file - else: - return base_path - - -exes = [ - cx_Freeze.Executable( - script=f"{c.script_name}.py", - target_name="ArchipelagoAHITClient.exe", - #target_name=c.frozen_name + (".exe" if is_windows else ""), - icon=resolve_icon(c.icon), - base="Win32GUI" if is_windows and not c.cli else None - ) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name -] - -#if is_windows: -if False: - # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help - c = next(component for component in components if component.script_name == "Launcher") - exes.append(cx_Freeze.Executable( - script=f"{c.script_name}.py", - target_name=f"{c.frozen_name}(DEBUG).exe", - icon=resolve_icon(c.icon), - )) - -extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] -extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] - - -def remove_sprites_from_folder(folder): - for file in os.listdir(folder): - if file != ".gitignore": - os.remove(folder / file) - - -def _threaded_hash(filepath): - hasher = sha3_512() - hasher.update(open(filepath, "rb").read()) - return base64.b85encode(hasher.digest()).decode() - - -# cx_Freeze's build command runs other commands. Override to accept --yes and store that. -class BuildCommand(setuptools.command.build.build): - user_options = [ - ('yes', 'y', 'Answer "yes" to all questions.'), - ] - yes: bool - last_yes: bool = False # used by sub commands of build - - def initialize_options(self): - super().initialize_options() - type(self).last_yes = self.yes = False - - def finalize_options(self): - super().finalize_options() - type(self).last_yes = self.yes - - -# Override cx_Freeze's build_exe command for pre and post build steps -class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): - user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ - ('yes', 'y', 'Answer "yes" to all questions.'), - ('extra-data=', None, 'Additional files to add.'), - ] - yes: bool - extra_data: Iterable # [any] not available in 3.8 - extra_libs: Iterable # work around broken include_files - - buildfolder: Path - libfolder: Path - library: Path - buildtime: datetime.datetime - - def initialize_options(self): - super().initialize_options() - self.yes = BuildCommand.last_yes - self.extra_data = [] - self.extra_libs = [] - - def finalize_options(self): - super().finalize_options() - self.buildfolder = self.build_exe - self.libfolder = Path(self.buildfolder, "lib") - self.library = Path(self.libfolder, "library.zip") - - def installfile(self, path, subpath=None, keep_content: bool = False): - folder = self.buildfolder - if subpath: - folder /= subpath - print('copying', path, '->', folder) - if path.is_dir(): - folder /= path.name - if folder.is_dir() and not keep_content: - shutil.rmtree(folder) - shutil.copytree(path, folder, dirs_exist_ok=True) - elif path.is_file(): - shutil.copy(path, folder) - else: - print('Warning,', path, 'not found') - - def create_manifest(self, create_hashes=False): - # Since the setup is now split into components and the manifest is not, - # it makes most sense to just remove the hashes for now. Not aware of anyone using them. - hashes = {} - manifestpath = os.path.join(self.buildfolder, "manifest.json") - if create_hashes: - from concurrent.futures import ThreadPoolExecutor - pool = ThreadPoolExecutor() - for dirpath, dirnames, filenames in os.walk(self.buildfolder): - for filename in filenames: - path = os.path.join(dirpath, filename) - hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path) - - import json - manifest = { - "buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"), - "hashes": {path: hash.result() for path, hash in hashes.items()}, - "version": version_tuple} - - json.dump(manifest, open(manifestpath, "wt"), indent=4) - print("Created Manifest") - - def run(self): - # start downloading sni asap - sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") - sni_thread.start() - - # pre-build steps - print(f"Outputting to: {self.buildfolder}") - os.makedirs(self.buildfolder, exist_ok=True) - import ModuleUpdate - ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) - ModuleUpdate.update(yes=self.yes) - - # auto-build cython modules - build_ext = self.distribution.get_command_obj("build_ext") - build_ext.inplace = False - self.run_command("build_ext") - # find remains of previous in-place builds, try to delete and warn otherwise - for path in build_ext.get_outputs(): - parts = os.path.split(path)[-1].split(".") - pattern = parts[0] + ".*." + parts[-1] - for match in Path().glob(pattern): - try: - match.unlink() - print(f"Removed {match}") - except Exception as ex: - warnings.warn(f"Could not delete old build output: {match}\n" - f"{ex}\nPlease close all AP instances and delete manually.") - - # regular cx build - self.buildtime = datetime.datetime.utcnow() - super().run() - - # manually copy built modules to lib folder. cx_Freeze does not know they exist. - for src in build_ext.get_outputs(): - print(f"copying {src} -> {self.libfolder}") - shutil.copy(src, self.libfolder, follow_symlinks=False) - - # need to finish download before copying - sni_thread.join() - - # include_files seems to not be done automatically. implement here - for src, dst in self.include_files: - print(f"copying {src} -> {self.buildfolder / dst}") - shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) - - # now that include_files is completely broken, run find_libs here - for src, dst in find_libs(*self.extra_libs): - print(f"copying {src} -> {self.buildfolder / dst}") - shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) - - # post build steps - if is_windows: # kivy_deps is win32 only, linux picks them up automatically - from kivy_deps import sdl2, glew - for folder in sdl2.dep_bins + glew.dep_bins: - shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) - print(f"copying {folder} -> {self.libfolder}") - - for data in self.extra_data: - self.installfile(Path(data)) - - # kivi data files - import kivy - shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), - self.buildfolder / "data", - dirs_exist_ok=True) - - os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) - from Options import generate_yaml_templates - from worlds.AutoWorld import AutoWorldRegister - assert not non_apworlds - set(AutoWorldRegister.world_types), \ - f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" - folders_to_remove: typing.List[str] = [] - generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) - for worldname, worldtype in AutoWorldRegister.world_types.items(): - if worldname not in non_apworlds: - file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] - world_directory = self.libfolder / "worlds" / file_name - # this method creates an apworld that cannot be moved to a different OS or minor python version, - # which should be ok - with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, - compresslevel=9) as zf: - for path in world_directory.rglob("*.*"): - relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) - zf.write(path, relative_path) - folders_to_remove.append(file_name) - shutil.rmtree(world_directory) - shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") - # TODO: fix LttP options one day - shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") - try: - from maseya import z3pr - except ImportError: - print("Maseya Palette Shuffle not found, skipping data files.") - else: - # maseya Palette Shuffle exists and needs its data files - print("Maseya Palette Shuffle found, including data files...") - file = z3pr.__file__ - self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True) - - if signtool: - for exe in self.distribution.executables: - print(f"Signing {exe.target_name}") - os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) - print("Signing SNI") - os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) - print("Signing OoT Utils") - for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): - os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) - - remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr") - - self.create_manifest() - - if is_windows: - # Inno setup stuff - with open("setup.ini", "w") as f: - min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000" - f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n") - with open("installdelete.iss", "w") as f: - f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n" - for world_directory in folders_to_remove) - else: - # make sure extra programs are executable - enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' - sni_exe = self.buildfolder / 'SNI/sni' - extra_exes = (enemizer_exe, sni_exe) - for extra_exe in extra_exes: - if extra_exe.is_file(): - extra_exe.chmod(0o755) - - -class AppImageCommand(setuptools.Command): - description = "build an app image from build output" - user_options = [ - ("build-folder=", None, "Folder to convert to AppImage."), - ("dist-file=", None, "AppImage output file."), - ("app-dir=", None, "Folder to use for packaging."), - ("app-icon=", None, "The icon to use for the AppImage."), - ("app-exec=", None, "The application to run inside the image."), - ("yes", "y", 'Answer "yes" to all questions.'), - ] - build_folder: typing.Optional[Path] - dist_file: typing.Optional[Path] - app_dir: typing.Optional[Path] - app_name: str - app_exec: typing.Optional[Path] - app_icon: typing.Optional[Path] # source file - app_id: str # lower case name, used for icon and .desktop - yes: bool - - def write_desktop(self): - assert self.app_dir, "Invalid app_dir" - desktop_filename = self.app_dir / f"{self.app_id}.desktop" - with open(desktop_filename, 'w', encoding="utf-8") as f: - f.write("\n".join(( - "[Desktop Entry]", - f'Name={self.app_name}', - f'Exec={self.app_exec}', - "Type=Application", - "Categories=Game", - f'Icon={self.app_id}', - '' - ))) - desktop_filename.chmod(0o755) - - def write_launcher(self, default_exe: Path): - assert self.app_dir, "Invalid app_dir" - launcher_filename = self.app_dir / "AppRun" - with open(launcher_filename, 'w', encoding="utf-8") as f: - f.write(f"""#!/bin/sh -exe="{default_exe}" -match="${{1#--executable=}}" -if [ "${{#match}}" -lt "${{#1}}" ]; then - exe="$match" - shift -elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then - exe="$2" - shift; shift -fi -tmp="${{exe#*/}}" -if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then - exe="{default_exe.parent}/$exe" -fi -export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" -$APPDIR/$exe "$@" -""") - launcher_filename.chmod(0o755) - - def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): - assert self.app_dir, "Invalid app_dir" - try: - from PIL import Image - except ModuleNotFoundError: - if not self.yes: - input("Requirement PIL is not satisfied, press enter to install it") - subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) - from PIL import Image - im = Image.open(src) - res, _ = im.size - - if not name: - name = src.stem - ext = src.suffix - dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps') - dest_dir.mkdir(parents=True, exist_ok=True) - dest_file = dest_dir / f'{name}{ext}' - shutil.copy(src, dest_file) - if symlink: - symlink.symlink_to(dest_file.relative_to(symlink.parent)) - - def initialize_options(self): - self.build_folder = None - self.app_dir = None - self.app_name = self.distribution.metadata.name - self.app_icon = self.distribution.executables[0].icon - self.app_exec = Path('opt/{app_name}/{exe}'.format( - app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name - )) - self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format( - app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version, - platform=sysconfig.get_platform() - )) - self.yes = False - - def finalize_options(self): - if not self.app_dir: - self.app_dir = self.build_folder.parent / "AppDir" - self.app_id = self.app_name.lower() - - def run(self): - self.dist_file.parent.mkdir(parents=True, exist_ok=True) - if self.app_dir.is_dir(): - shutil.rmtree(self.app_dir) - self.app_dir.mkdir(parents=True) - opt_dir = self.app_dir / "opt" / self.distribution.metadata.name - shutil.copytree(self.build_folder, opt_dir) - root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' - self.install_icon(self.app_icon, self.app_id, symlink=root_icon) - shutil.copy(root_icon, self.app_dir / '.DirIcon') - self.write_desktop() - self.write_launcher(self.app_exec) - print(f'{self.app_dir} -> {self.dist_file}') - subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) - - -def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: - """Try to find system libraries to be included.""" - if not args: - return [] - - arch = build_arch.replace('_', '-') - libc = 'libc6' # we currently don't support musl - - def parse(line): - lib, path = line.strip().split(' => ') - lib, typ = lib.split(' ', 1) - for test_arch in ('x86-64', 'i386', 'aarch64'): - if test_arch in typ: - lib_arch = test_arch - break - else: - lib_arch = '' - for test_libc in ('libc6',): - if test_libc in typ: - lib_libc = test_libc - break - else: - lib_libc = '' - return (lib, lib_arch, lib_libc), path - - if not hasattr(find_libs, "cache"): - ldconfig = shutil.which("ldconfig") - assert ldconfig, "Make sure ldconfig is in PATH" - data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] - find_libs.cache = { # type: ignore [attr-defined] - k: v for k, v in (parse(line) for line in data if "=>" in line) - } - - def find_lib(lib, arch, libc): - for k, v in find_libs.cache.items(): - if k == (lib, arch, libc): - return v - for k, v, in find_libs.cache.items(): - if k[0].startswith(lib) and k[1] == arch and k[2] == libc: - return v - return None - - res = [] - for arg in args: - # try exact match, empty libc, empty arch, empty arch and libc - file = find_lib(arg, arch, libc) - file = file or find_lib(arg, arch, '') - file = file or find_lib(arg, '', libc) - file = file or find_lib(arg, '', '') - # resolve symlinks - for n in range(0, 5): - res.append((file, os.path.join('lib', os.path.basename(file)))) - if not os.path.islink(file): - break - dirname = os.path.dirname(file) - file = os.readlink(file) - if not os.path.isabs(file): - file = os.path.join(dirname, file) - return res - - -cx_Freeze.setup( - name="Archipelago", - version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}", - description="Archipelago", - executables=exes, - ext_modules=cythonize("_speedups.pyx"), - options={ - "build_exe": { - "packages": ["worlds", "kivy", "cymem", "websockets"], - "includes": [], - "excludes": ["numpy", "Cython", "PySide2", "PIL", - "pandas"], - "zip_include_packages": ["*"], - "zip_exclude_packages": ["worlds", "sc2"], - "include_files": [], # broken in cx 6.14.0, we use more special sauce now - "include_msvcr": False, - "replace_paths": ["*."], - "optimize": 1, - "build_exe": buildfolder, - "extra_data": extra_data, - "extra_libs": extra_libs, - "bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else [] - }, - "bdist_appimage": { - "build_folder": buildfolder, - }, - }, - # override commands to get custom stuff in - cmdclass={ - "build": BuildCommand, - "build_exe": BuildExeCommand, - "bdist_appimage": AppImageCommand, - }, -) From 083f5b7f83046768402850ae801b7e9fb5a5c6b5 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 19 Dec 2023 16:38:17 -0500 Subject: [PATCH 063/143] Update .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 91b6db5f58..022abe38fe 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage -/oot/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -199,5 +198,3 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini -A Hat in Time.yaml -ahit.apworld From f96758842847f0f4ac191f7e8ae09210ab51d0b6 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 4 Jan 2024 18:22:49 -0500 Subject: [PATCH 064/143] old python version fix --- worlds/ahit/Rules.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 9fd7381779..ff6e279167 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -245,7 +245,7 @@ def set_rules(world: World): if world.options.ActRandomizer.value == 0: set_default_rift_rules(world) - table = location_table | event_locs + table = {**location_table, **event_locs} location: Location for (key, data) in table.items(): if not is_location_valid(world, key): @@ -561,6 +561,10 @@ def set_expert_rules(world: World): set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True) + # Expert: Clear Dead Bird Studio Basement without Hookshot + for loc in world.multiworld.get_region("Dead Bird Studio Basement", world.player).locations: + set_rule(loc, lambda state: True) + # Expert: get to and clear Twilight Bell without Dweller Mask. # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), From 4c6974f1842e6f684fea60d094b293e8c5b91a60 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 5 Jan 2024 12:06:28 -0500 Subject: [PATCH 065/143] proper warnings for invalid act plandos --- worlds/ahit/Regions.py | 11 ++++++----- worlds/ahit/docs/en_A Hat in Time.md | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 7178075c5f..0c1c1e2a4d 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -445,14 +445,14 @@ def create_tasksanity_locations(world: World): def is_valid_plando(world: World, region: str) -> bool: - if region in blacklisted_acts.values(): + if region in blacklisted_acts.values() or region not in act_entrances.keys(): return False if region not in world.options.ActPlando.keys(): return False act = world.options.ActPlando.get(region) - if act in blacklisted_acts.values(): + if act in blacklisted_acts.values() or act not in act_entrances.keys(): return False # Don't allow plando-ing things onto the first act that aren't completable with nothing @@ -515,9 +515,10 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) else: - print("Disallowing act plando for", - world.multiworld.player_name[world.player], - "-", region.name, ":", world.options.ActPlando.get(region.name)) + print(f"[WARNING] ActPlando " + f"({world.multiworld.get_player_name(world.player)}) - " + f"{region.name}: {world.options.ActPlando.get(region.name)} " + f"is an invalid or disallowed act plando combination!") # Reverse the list, so we can do what we want to do first region_list.reverse() diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index 6bfb2196cf..8ebfe77893 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -13,7 +13,7 @@ To unlock and access a chapter's Time Rift in act shuffle, the levels in place o ## What items and locations get shuffled? -Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Hats can also optionally be shuffled as individual items instead. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are locations. +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched in a set order once you gather enough yarn for each hat. Hats can also optionally be shuffled as individual items instead. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are locations. Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. From 1548da91c417d1075ba5414b6dfd27493c3f369a Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:22:16 -0500 Subject: [PATCH 066/143] Update worlds/ahit/docs/en_A Hat in Time.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> --- worlds/ahit/docs/en_A Hat in Time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index 8ebfe77893..28d5e8dbdb 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -1,6 +1,6 @@ # A Hat in Time -## Where is the settings page? +## Where is the options page? The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. From 2dd3a453f848ed3e7bbe5381828840e253ae797f Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:22:41 -0500 Subject: [PATCH 067/143] Update worlds/ahit/docs/setup_en.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com> --- worlds/ahit/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index cea74380bd..fd648b80d2 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -33,7 +33,7 @@ To connect to the multiworld server, simply run the **ArchipelagoAHITClient** an Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. -`ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. +`ap_say ` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`. `ap_deathlink` - Toggle Death Link. From a431e1998038aab2dcd61d0ff9b41e4bd5560ccf Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:55:12 -0500 Subject: [PATCH 068/143] 120 char per line --- worlds/ahit/docs/en_A Hat in Time.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index 28d5e8dbdb..a9310120e9 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -7,15 +7,31 @@ config file. ## What does randomization do to this game? -Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. +Items which the player would normally acquire throughout the game have been moved around. +Chapter costs are randomized in a progressive order based on your settings, +so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, +the levels and Time Rifts in these chapters will be randomized as well. -To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to access that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. +To unlock and access a chapter's Time Rift in act shuffle, +the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, +and then you must enter a level that allows you to access that Time Rift. +For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. +To unlock this Time Rift in act shuffle (and therefore the level it contains) +you must complete the level that was shuffled in place of Heating Up Mafia Town +and then enter the Time Rift through a Mafia Town level. ## What items and locations get shuffled? -Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched in a set order once you gather enough yarn for each hat. Hats can also optionally be shuffled as individual items instead. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are locations. +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. +Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched +in a set order once you gather enough yarn for each hat. +Hats can also optionally be shuffled as individual items instead. +Any items in the world, shops, act completions, +and optionally storybook pages or Death Wish contracts are locations. -Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. +Any freestanding items that are considered to be progression or useful +will have a rainbow streak particle attached to them. +Filler items will have a white glow attached to them instead. ## Which items can be in another player's world? @@ -32,4 +48,4 @@ When the player receives an item, it will play the item collect effect and infor ## Is the DLC required to play A Hat in Time in Archipelago? -No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC. \ No newline at end of file +No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC. From 0e590b00ce96ae97f2d12af1de4e90a6608feb15 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:57:09 -0500 Subject: [PATCH 069/143] "settings" to "options" --- worlds/ahit/docs/en_A Hat in Time.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index a9310120e9..7b0ca4d65e 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -2,13 +2,13 @@ ## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options 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? Items which the player would normally acquire throughout the game have been moved around. -Chapter costs are randomized in a progressive order based on your settings, +Chapter costs are randomized in a progressive order based on your options, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. From 59f45a22fb0e3afbf21e794d12151f6481ce3f33 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 17 Jan 2024 09:50:05 -0500 Subject: [PATCH 070/143] Update DeathWishRules.py --- worlds/ahit/DeathWishRules.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 90309bdb51..b49f3cee91 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -12,7 +12,6 @@ from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wis dw_requirements = { "Beat the Heat": LocData(umbrella=True), "So You're Back From Outer Space": LocData(hookshot=True), - "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), @@ -20,7 +19,7 @@ dw_requirements = { "10 Seconds until Self-Destruct": LocData(hookshot=True), "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), - "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Speedrun Well": LocData(hookshot=True, hit_requirement=1), "Boss Rush": LocData(umbrella=True, hookshot=True), "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), From b38d1cd3e8b2c55c0bbfca9610c45294ac702464 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Tue, 13 Feb 2024 18:19:16 -0500 Subject: [PATCH 071/143] Update worlds/ahit/docs/en_A Hat in Time.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/ahit/docs/en_A Hat in Time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index 7b0ca4d65e..390aa13288 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -2,7 +2,7 @@ ## Where is the options page? -The [player options page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? From 9bec7391f109b583e091fa926d6d6bc7dcbca04d Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 18 Feb 2024 21:36:58 -0500 Subject: [PATCH 072/143] No more loading the data package --- worlds/ahit/DeathWishLocations.py | 6 ++++++ worlds/ahit/Options.py | 8 +++++++- worlds/ahit/__init__.py | 9 ++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 470e8087c9..75758ab853 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -81,11 +81,17 @@ annoying_bonuses = [ "So You're Back From Outer Space", "Encore! Encore!", "Snatcher's Hit List", + "Vault Codes in the Wind", "10 Seconds until Self-Destruct", "Killing Two Birds", "Zero Jumps", + "Boss Rush", "Bird Sanctuary", + "The Mustache Gauntlet", "Wound-Up Windmill", + "Camera Tourist", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", "Seal the Deal", ] diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index af21dae4c9..b6b4eaa786 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -566,7 +566,13 @@ class DWExcludeAnnoyingBonuses(Toggle): - Zero Jumps - Bird Sanctuary - Wound-Up Windmill - - Seal the Deal""" + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index f8527e95f4..e7775fae28 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,4 +1,4 @@ -from BaseClasses import Item, ItemClassification, Tutorial +from BaseClasses import Item, ItemClassification, Tutorial, Location from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ @@ -229,6 +229,13 @@ class HatInTimeWorld(World): for i in range(len(shuffled_dws)): slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] + shop_item_names: Dict[str, str] = {} + for name in self.shop_locs: + loc: Location = self.multiworld.get_location(name, self.player) + shop_item_names.setdefault(str(loc.address), loc.item.name) + + slot_data["ShopItemNames"] = shop_item_names + for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items(): if name in slot_data_options: slot_data[name] = value From 64dfb294421b52ab4706c4b9717af8421730b91e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 19 Mar 2024 21:29:51 -0400 Subject: [PATCH 073/143] cleanup + act plando fixes --- worlds/ahit/DeathWishLocations.py | 20 +++-- worlds/ahit/DeathWishRules.py | 26 +++--- worlds/ahit/Items.py | 18 ++-- worlds/ahit/Locations.py | 13 +-- worlds/ahit/Options.py | 15 ++-- worlds/ahit/Regions.py | 135 ++++++++++++++++++++---------- worlds/ahit/Rules.py | 79 ++++++++--------- worlds/ahit/test/TestActs.py | 2 +- worlds/ahit/test/TestBase.py | 5 -- worlds/ahit/test/__init__.py | 5 ++ 10 files changed, 190 insertions(+), 128 deletions(-) delete mode 100644 worlds/ahit/test/TestBase.py diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 75758ab853..00902262ad 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -2,10 +2,12 @@ from .Types import HatInTimeLocation, HatInTimeItem from .Regions import connect_regions, create_region from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule -from worlds.AutoWorld import World -from typing import List +from typing import List, TYPE_CHECKING from .Locations import death_wishes +if TYPE_CHECKING: + from . import HatInTimeWorld + dw_prereqs = { "So You're Back From Outer Space": ["Beat the Heat"], @@ -81,17 +83,17 @@ annoying_bonuses = [ "So You're Back From Outer Space", "Encore! Encore!", "Snatcher's Hit List", - "Vault Codes in the Wind", + "Vault Codes in the Wind", "10 Seconds until Self-Destruct", "Killing Two Birds", "Zero Jumps", - "Boss Rush", + "Boss Rush", "Bird Sanctuary", - "The Mustache Gauntlet", + "The Mustache Gauntlet", "Wound-Up Windmill", - "Camera Tourist", - "Rift Collapse: Deep Sea", - "Cruisin' for a Bruisin'", + "Camera Tourist", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", "Seal the Deal", ] @@ -144,7 +146,7 @@ dw_classes = { } -def create_dw_regions(world: World): +def create_dw_regions(world: "HatInTimeWorld"): if world.options.DWExcludeAnnoyingContracts.value > 0: for name in annoying_dws: world.get_excluded_dws().append(name) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index b49f3cee91..1418af676b 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,13 +1,17 @@ -from worlds.AutoWorld import World, CollectionState +from worlds.AutoWorld import CollectionState from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData from .DeathWishLocations import dw_prereqs, dw_candles from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule -from typing import List, Callable +from typing import List, Callable, TYPE_CHECKING from .Regions import act_chapters from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes +if TYPE_CHECKING: + from . import HatInTimeWorld + + # Any speedruns expect the player to have Sprint Hat dw_requirements = { "Beat the Heat": LocData(umbrella=True), @@ -98,7 +102,7 @@ required_snatcher_coins = { } -def set_dw_rules(world: World): +def set_dw_rules(world: "HatInTimeWorld"): if "Snatcher's Hit List" not in world.get_excluded_dws() \ or "Camera Tourist" not in world.get_excluded_dws(): set_enemy_rules(world) @@ -221,7 +225,7 @@ def set_dw_rules(world: World): world.player) -def modify_dw_rules(world: World, name: str): +def modify_dw_rules(world: "HatInTimeWorld", name: str): difficulty: Difficulty = get_difficulty(world) main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) @@ -267,7 +271,7 @@ def modify_dw_rules(world: World, name: str): set_candle_dw_rules(name, world) -def get_total_dw_stamps(state: CollectionState, world: World) -> int: +def get_total_dw_stamps(state: CollectionState, world: "HatInTimeWorld") -> int: if world.options.DWShuffle.value > 0: return 999 # no stamp costs in death wish shuffle @@ -290,7 +294,7 @@ def get_total_dw_stamps(state: CollectionState, world: World) -> int: return count -def set_candle_dw_rules(name: str, world: World): +def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) @@ -327,7 +331,7 @@ def set_candle_dw_rules(name: str, world: World): add_rule(full_clear, lambda state: state.has(coin, world.player)) -def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: +def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") -> int: total: int = 0 for name in act_chapters.keys(): @@ -349,7 +353,7 @@ def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: return total -def get_reachable_enemy_count(state: CollectionState, world: World) -> int: +def get_reachable_enemy_count(state: CollectionState, world: "HatInTimeWorld") -> int: count: int = 0 for enemy in hit_list.keys(): if enemy in bosses: @@ -361,7 +365,7 @@ def get_reachable_enemy_count(state: CollectionState, world: World) -> int: return count -def can_reach_all_bosses(state: CollectionState, world: World) -> bool: +def can_reach_all_bosses(state: CollectionState, world: "HatInTimeWorld") -> bool: for boss in bosses: if not state.has(boss, world.player): return False @@ -369,7 +373,7 @@ def can_reach_all_bosses(state: CollectionState, world: World) -> bool: return True -def create_enemy_events(world: World): +def create_enemy_events(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() for enemy, regions in hit_list.items(): @@ -413,7 +417,7 @@ def create_enemy_events(world: World): add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) -def set_enemy_rules(world: World): +def set_enemy_rules(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() for enemy, regions in hit_list.items(): diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 6b0ccba7ea..5411aec68f 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,12 +1,14 @@ from BaseClasses import Item, ItemClassification -from worlds.AutoWorld import World from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem from .Locations import get_total_locations from .Rules import get_difficulty -from typing import Optional, List, Dict +from typing import Optional, List, Dict, TYPE_CHECKING + +if TYPE_CHECKING: + from . import HatInTimeWorld -def create_itempool(world: World) -> List[Item]: +def create_itempool(world: "HatInTimeWorld") -> List[Item]: itempool: List[Item] = [] if not world.is_dw_only() and world.options.HatItems.value == 0: calculate_yarn_costs(world) @@ -87,7 +89,7 @@ def create_itempool(world: World) -> List[Item]: return itempool -def calculate_yarn_costs(world: World): +def calculate_yarn_costs(world: "HatInTimeWorld"): mw = world.multiworld min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) @@ -107,7 +109,7 @@ def calculate_yarn_costs(world: World): world.options.YarnAvailable.value += (max_cost + world.options.MinExtraYarn.value) - available_yarn -def item_dlc_enabled(world: World, name: str) -> bool: +def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool: data = item_table[name] if data.dlc_flags == HatDLC.none: @@ -122,12 +124,12 @@ def item_dlc_enabled(world: World, name: str) -> bool: return False -def create_item(world: World, name: str) -> Item: +def create_item(world: "HatInTimeWorld", name: str) -> Item: data = item_table[name] return HatInTimeItem(name, data.classification, data.code, world.player) -def create_multiple_items(world: World, name: str, count: int = 1, +def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1, item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: data = item_table[name] @@ -139,7 +141,7 @@ def create_multiple_items(world: World, name: str, count: int = 1, return itemlist -def create_junk_items(world: World, count: int) -> List[Item]: +def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]: trap_chance = world.options.TrapChance.value junk_pool: List[Item] = [] junk_list: Dict[str, int] = {} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 6e6e041bc2..9d148f64c0 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -1,11 +1,14 @@ -from worlds.AutoWorld import World from .Types import HatDLC, HatType, LocData, Difficulty -from typing import Dict +from typing import Dict, TYPE_CHECKING from .Options import TasksanityCheckCount +if TYPE_CHECKING: + from . import HatInTimeWorld + TASKSANITY_START_ID = 2000300204 -def get_total_locations(world: World) -> int: + +def get_total_locations(world: "HatInTimeWorld") -> int: total: int = 0 if not world.is_dw_only(): @@ -34,7 +37,7 @@ def get_total_locations(world: World) -> int: return total -def location_dlc_enabled(world: World, location: str) -> bool: +def location_dlc_enabled(world: "HatInTimeWorld", location: str) -> bool: data = location_table.get(location) or event_locs.get(location) if data.dlc_flags == HatDLC.none: @@ -53,7 +56,7 @@ def location_dlc_enabled(world: World, location: str) -> bool: return False -def is_location_valid(world: World, location: str) -> bool: +def is_location_valid(world: "HatInTimeWorld", location: str) -> bool: if not location_dlc_enabled(world, location): return False diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index b6b4eaa786..d58a12ded2 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,10 +1,13 @@ -from typing import List +from typing import List, TYPE_CHECKING from dataclasses import dataclass -from worlds.AutoWorld import World, PerGameCommonOptions +from worlds.AutoWorld import PerGameCommonOptions from Options import Range, Toggle, DeathLink, Choice, OptionDict +if TYPE_CHECKING: + from . import HatInTimeWorld + -def adjust_options(world: World): +def adjust_options(world: "HatInTimeWorld"): world.options.HighestChapterCost.value = max( world.options.HighestChapterCost.value, world.options.LowestChapterCost.value) @@ -81,7 +84,7 @@ def adjust_options(world: World): world.options.DWTimePieceRequirement.value = 0 -def get_total_time_pieces(world: World) -> int: +def get_total_time_pieces(world: "HatInTimeWorld") -> int: count: int = 40 if world.is_dlc1(): count += 6 @@ -566,13 +569,13 @@ class DWExcludeAnnoyingBonuses(Toggle): - Zero Jumps - Bird Sanctuary - Wound-Up Windmill - - Vault Codes in the Wind + - Vault Codes in the Wind - Boss Rush - Camera Tourist - The Mustache Gauntlet - Rift Collapse: Deep Sea - Cruisin' for a Bruisin' - - Seal the Deal""" + - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0c1c1e2a4d..038a4ef3ba 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,11 +1,13 @@ -from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard -import typing +from typing import TYPE_CHECKING, List, Dict from .Rules import set_rift_rules, get_difficulty +if TYPE_CHECKING: + from . import HatInTimeWorld + # ChapterIndex: region chapter_regions = { @@ -268,7 +270,7 @@ blacklisted_combos = { } -def create_regions(world: World): +def create_regions(world: "HatInTimeWorld"): w = world p = world.player @@ -422,7 +424,7 @@ def create_regions(world: World): create_thug_shops(w) -def create_rift_connections(world: World, region: Region): +def create_rift_connections(world: "HatInTimeWorld", region: Region): i = 1 for name in rift_access_regions[region.name]: act_region = world.multiworld.get_region(name, world.player) @@ -436,7 +438,7 @@ def create_rift_connections(world: World, region: Region): world.multiworld.get_entrance(entrance.name, world.player) -def create_tasksanity_locations(world: World): +def create_tasksanity_locations(world: "HatInTimeWorld"): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = TASKSANITY_START_ID for i in range(world.options.TasksanityCheckCount.value): @@ -444,15 +446,37 @@ def create_tasksanity_locations(world: World): ship_shape.locations.append(location) -def is_valid_plando(world: World, region: str) -> bool: - if region in blacklisted_acts.values() or region not in act_entrances.keys(): +def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: + # Duplicated keys will throw an exception for us, but we still need to check for duplicated values + if is_candidate: + found_list: List = [] + old_region = region + for name in world.options.ActPlando.keys(): + act = world.options.ActPlando.get(name) + if act == old_region: + region = name + found_list.append(name) + + if len(found_list) == 0: + return False + + if len(found_list) > 1: + raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Duplicated act plando mapping found for act: \"{old_region}\"") + elif region not in world.options.ActPlando.keys(): return False - if region not in world.options.ActPlando.keys(): + if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): return False act = world.options.ActPlando.get(region) - if act in blacklisted_acts.values() or act not in act_entrances.keys(): + try: + world.multiworld.get_region(region, world.player) + world.multiworld.get_region(act, world.player) + except KeyError: + return False + + if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): return False # Don't allow plando-ing things onto the first act that aren't completable with nothing @@ -471,24 +495,27 @@ def is_valid_plando(world: World, region: str) -> bool: return False # Don't allow straight up impossible mappings - if region == "The Illness has Spread" and act == "Alpine Free Roam": + if (region == "Time Rift - Curly Tail Trail" + or region == "Time Rift - The Twilight Bell" + or region == "The Illness has Spread") \ + and act == "Alpine Free Roam": return False - if region == "Rush Hour" and act == "Nyakuza Free Roam": - return False - - if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ + and act == "Nyakuza Free Roam": return False if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": return False - return any(a.name == world.options.ActPlando.get(region) for a in - world.multiworld.get_regions(world.player)) + if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": + return False + + return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) -def randomize_act_entrances(world: World): - region_list: typing.List[Region] = get_act_regions(world) +def randomize_act_entrances(world: "HatInTimeWorld"): + region_list: List[Region] = get_act_regions(world) world.random.shuffle(region_list) separate_rifts: bool = bool(world.options.ActRandomizer.value == 1) @@ -509,23 +536,41 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) + for name in world.options.ActPlando.keys(): + try: + world.multiworld.get_region(name, world.player) + except KeyError: + print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Act \"{name}\" does not exist in the multiworld." + f"Possible reasons are typos, case-sensitivity, or DLC options.") + for region in region_list.copy(): if region.name in world.options.ActPlando.keys(): - if is_valid_plando(world, region.name): + try: + act = world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player) + except KeyError: + print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Act \"{world.options.ActPlando.get(region.name)}\" does not exist in the multiworld." + f"Possible reasons are typos, case-sensitivity, or DLC options.") + continue + + if is_valid_plando(world, region.name) and is_valid_plando(world, act.name, True): region_list.remove(region) region_list.append(region) + region_list.remove(act) + region_list.append(act) else: print(f"[WARNING] ActPlando " - f"({world.multiworld.get_player_name(world.player)}) - " - f"{region.name}: {world.options.ActPlando.get(region.name)} " - f"is an invalid or disallowed act plando combination!") + f"({world.multiworld.get_player_name(world.player)}) - " + f"\"{region.name}: {world.options.ActPlando.get(region.name)}\" " + f"is an invalid or disallowed act plando combination!") # Reverse the list, so we can do what we want to do first region_list.reverse() - shuffled_list: typing.List[Region] = [] - mapped_list: typing.List[Region] = [] - rift_dict: typing.Dict[str, Region] = {} + shuffled_list: List[Region] = [] + mapped_list: List[Region] = [] + rift_dict: Dict[str, Region] = {} first_chapter: Region = get_first_chapter_region(world) has_guaranteed: bool = False @@ -543,7 +588,7 @@ def randomize_act_entrances(world: World): and "Free Roam" not in act_entrances[region.name]: continue - if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): + if is_valid_plando(world, region.name): has_guaranteed = True i = 0 @@ -555,14 +600,14 @@ def randomize_act_entrances(world: World): mapped_list.append(region) # Look for candidates to map this act to - candidate_list: typing.List[Region] = [] + candidate_list: List[Region] = [] for candidate in region_list: # We're mapping something to the first act, make sure it is valid if not has_guaranteed: if candidate.name not in guaranteed_first_acts: continue - if candidate.name in world.options.ActPlando.values(): + if is_valid_plando(world, candidate.name, True): continue # Not completable without Umbrella @@ -579,7 +624,7 @@ def randomize_act_entrances(world: World): has_guaranteed = True break - if region.name in world.options.ActPlando.keys() and is_valid_plando(world, region.name): + if is_valid_plando(world, region.name): candidate_list.clear() candidate_list.append( world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player)) @@ -666,7 +711,7 @@ def randomize_act_entrances(world: World): set_rift_rules(world, rift_dict) -def connect_time_rift(world: World, time_rift: Region, exit_region: Region): +def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 while i <= count: @@ -676,8 +721,8 @@ def connect_time_rift(world: World, time_rift: Region, exit_region: Region): i += 1 -def get_act_regions(world: World) -> typing.List[Region]: - act_list: typing.List[Region] = [] +def get_act_regions(world: "HatInTimeWorld") -> List[Region]: + act_list: List[Region] = [] for region in world.multiworld.get_regions(world.player): if region.name in chapter_act_info.keys(): if not is_act_blacklisted(world, region.name): @@ -686,9 +731,9 @@ def get_act_regions(world: World) -> typing.List[Region]: return act_list -def is_act_blacklisted(world: World, name: str) -> bool: - plando: bool = name in world.options.ActPlando.keys() \ - or name in world.options.ActPlando.values() +def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: + plando: bool = name in world.options.ActPlando.keys() and is_valid_plando(world, name) \ + or name in world.options.ActPlando.values() and is_valid_plando(world, name, True) if name == "The Finale": return not plando and world.options.EndGoal.value == 1 @@ -702,7 +747,7 @@ def is_act_blacklisted(world: World, name: str) -> bool: return name in blacklisted_acts.values() -def create_region(world: World, name: str) -> Region: +def create_region(world: "HatInTimeWorld", name: str) -> Region: reg = Region(name, world.player, world.multiworld) for (key, data) in location_table.items(): @@ -726,7 +771,7 @@ def create_region(world: World, name: str) -> Region: return reg -def create_badge_seller(world: World) -> Region: +def create_badge_seller(world: "HatInTimeWorld") -> Region: badge_seller = Region("Badge Seller", world.player, world.multiworld) world.multiworld.regions.append(badge_seller) count: int = 0 @@ -782,7 +827,7 @@ def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Reg entrance.connect(exit_region) -def create_region_and_connect(world: World, +def create_region_and_connect(world: "HatInTimeWorld", name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: reg: Region = create_region(world, name) @@ -800,17 +845,17 @@ def create_region_and_connect(world: World, return reg -def get_first_chapter_region(world: World) -> Region: - start_chapter: ChapterIndex = world.options.StartingChapter.value +def get_first_chapter_region(world: "HatInTimeWorld") -> Region: + start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter.value) return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) -def get_act_original_chapter(world: World, act_name: str) -> Region: +def get_act_original_chapter(world: "HatInTimeWorld", act_name: str) -> Region: return world.multiworld.get_region(act_chapters[act_name], world.player) # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game -def update_chapter_act_info(world: World, original_region: Region, new_region: Region): +def update_chapter_act_info(world: "HatInTimeWorld", original_region: Region, new_region: Region): original_act_info = chapter_act_info[original_region.name] new_act_info = chapter_act_info[new_region.name] world.act_connections[original_act_info] = new_act_info @@ -825,7 +870,7 @@ def get_shuffled_region(self, region: str) -> str: return name -def create_thug_shops(world: World): +def create_thug_shops(world: "HatInTimeWorld"): min_items: int = min(world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value) max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value) count: int = -1 @@ -867,7 +912,7 @@ def create_thug_shops(world: World): world.set_nyakuza_thug_items(thug_items) -def create_events(world: World) -> int: +def create_events(world: "HatInTimeWorld") -> int: count: int = 0 for (name, data) in event_locs.items(): @@ -892,7 +937,7 @@ def create_events(world: World) -> int: return count -def create_event(name: str, item_name: str, region: Region, world: World) -> Location: +def create_event(name: str, item_name: str, region: Region, world: "HatInTimeWorld") -> Location: event = HatInTimeLocation(world.player, name, None, region) region.locations.append(event) event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index ff6e279167..1f24961d5c 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,11 +1,14 @@ -from worlds.AutoWorld import World, CollectionState +from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ shop_locations, event_locs, snatcher_coins from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC from BaseClasses import Location, Entrance, Region -import typing +from typing import TYPE_CHECKING, List, Callable, Union, Dict +if TYPE_CHECKING: + from . import HatInTimeWorld + act_connections = { "Mafia Town - Act 2": ["Mafia Town - Act 1"], @@ -31,14 +34,14 @@ act_connections = { } -def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: +def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) -> bool: if world.options.HatItems.value > 0: return state.has(hat_type_to_item[hat], world.player) return state.count("Yarn", world.player) >= get_hat_cost(world, hat) -def get_hat_cost(world: World, hat: HatType) -> int: +def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: cost: int = 0 costs = world.get_hat_yarn_costs() for h in world.get_hat_craft_order(): @@ -49,20 +52,20 @@ def get_hat_cost(world: World, hat: HatType) -> int: return cost -def can_sdj(state: CollectionState, world: World): +def can_sdj(state: CollectionState, world: "HatInTimeWorld"): return can_use_hat(state, world, HatType.SPRINT) -def painting_logic(world: World) -> bool: +def painting_logic(world: "HatInTimeWorld") -> bool: return world.options.ShuffleSubconPaintings.value > 0 # -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert -def get_difficulty(world: World) -> Difficulty: +def get_difficulty(world: "HatInTimeWorld") -> Difficulty: return Difficulty(world.options.LogicDifficulty.value) -def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: +def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, allow_skip: bool = True) -> bool: if not painting_logic(world): return True @@ -74,35 +77,35 @@ def has_paintings(state: CollectionState, world: World, count: int, allow_skip: return state.count("Progressive Painting Unlock", world.player) >= count -def zipline_logic(world: World) -> bool: +def zipline_logic(world: "HatInTimeWorld") -> bool: return world.options.ShuffleAlpineZiplines.value > 0 -def can_use_hookshot(state: CollectionState, world: World): +def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"): return state.has("Hookshot Badge", world.player) -def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): +def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False): if world.options.UmbrellaLogic.value == 0: return True return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) -def can_surf(state: CollectionState, world: World): +def can_surf(state: CollectionState, world: "HatInTimeWorld"): return state.has("No Bonk Badge", world.player) -def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: +def has_relic_combo(state: CollectionState, world: "HatInTimeWorld", relic: str) -> bool: return state.has_group(relic, world.player, len(world.item_name_groups[relic])) -def get_relic_count(state: CollectionState, world: World, relic: str) -> int: +def get_relic_count(state: CollectionState, world: "HatInTimeWorld", relic: str) -> int: return state.count_group(relic, world.player) # Only use for rifts -def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: +def can_clear_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool: entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) if not state.can_reach(entrance.connected_region, "Region", world.player): return False @@ -114,12 +117,12 @@ def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bo return world.multiworld.get_location(name, world.player).access_rule(state) -def can_clear_alpine(state: CollectionState, world: World) -> bool: +def can_clear_alpine(state: CollectionState, world: "HatInTimeWorld") -> bool: return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) -def can_clear_metro(state: CollectionState, world: World) -> bool: +def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool: return state.has("Nyakuza Intro Cleared", world.player) \ and state.has("Yellow Overpass Station Cleared", world.player) \ and state.has("Yellow Overpass Manhole Cleared", world.player) \ @@ -130,14 +133,14 @@ def can_clear_metro(state: CollectionState, world: World) -> bool: and state.has("Pink Paw Manhole Cleared", world.player) -def set_rules(world: World): +def set_rules(world: "HatInTimeWorld"): # First, chapter access starting_chapter = ChapterIndex(world.options.StartingChapter.value) world.set_chapter_cost(starting_chapter, 0) # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale - chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, - ChapterIndex.SUBCON, ChapterIndex.ALPINE] + chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] final_chapter = ChapterIndex.FINALE if world.options.EndGoal.value == 2: @@ -333,7 +336,7 @@ def set_rules(world: World): add_rule(loc, lambda state: can_use_hookshot(state, world)) - dummy_entrances: typing.List[Entrance] = [] + dummy_entrances: List[Entrance] = [] for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): @@ -342,11 +345,11 @@ def set_rules(world: World): i: int = 1 entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region - access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + access_rules: List[Callable[[CollectionState], bool]] = [] dummy_entrances.append(entrance) # Entrances to this act that we have to set access_rules on - entrances: typing.List[Entrance] = [] + entrances: List[Entrance] = [] for act in acts: act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) @@ -358,7 +361,7 @@ def set_rules(world: World): # Copy access rules from act completions if "Free Roam" not in required_region.name: - rule: typing.Callable[[CollectionState], bool] + rule: Callable[[CollectionState], bool] name = f"Act Completion ({required_region.name})" rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) @@ -380,7 +383,7 @@ def set_rules(world: World): world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) -def set_specific_rules(world: World): +def set_specific_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), lambda state: state.has("Time Piece", world.player, 12) and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) @@ -411,7 +414,7 @@ def set_specific_rules(world: World): set_expert_rules(world) -def set_moderate_rules(world: World): +def set_moderate_rules(world: "HatInTimeWorld"): # Moderate: Gallery without Brewing Hat set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) @@ -488,7 +491,7 @@ def set_moderate_rules(world: World): set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) -def set_hard_rules(world: World): +def set_hard_rules(world: "HatInTimeWorld"): # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) @@ -545,7 +548,7 @@ def set_hard_rules(world: World): and state.has("Metro Ticket - Pink", world.player)) -def set_expert_rules(world: World): +def set_expert_rules(world: "HatInTimeWorld"): # Finale Telescope with no hats set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) @@ -623,7 +626,7 @@ def set_expert_rules(world: World): and state.has("Metro Ticket - Pink", world.player)) -def set_mafia_town_rules(world: World): +def set_mafia_town_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) or state.can_reach("Down with the Mafia!", "Region", world.player) @@ -683,7 +686,7 @@ def set_mafia_town_rules(world: World): and state.has("Scooter Badge", world.player), "or") -def set_botb_rules(world: World): +def set_botb_rules(world: "HatInTimeWorld"): if world.options.UmbrellaLogic.value == 0 and get_difficulty(world) < Difficulty.MODERATE: set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) @@ -695,7 +698,7 @@ def set_botb_rules(world: World): lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) -def set_subcon_rules(world: World): +def set_subcon_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.DWELLER)) @@ -733,7 +736,7 @@ def set_subcon_rules(world: World): add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) -def set_alps_rules(world: World): +def set_alps_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) @@ -753,7 +756,7 @@ def set_alps_rules(world: World): lambda state: can_clear_alpine(state, world)) -def set_dlc1_rules(world: World): +def set_dlc1_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), lambda state: can_use_hookshot(state, world)) @@ -763,7 +766,7 @@ def set_dlc1_rules(world: World): or state.can_reach("Ship Shape", "Region", world.player)) -def set_dlc2_rules(world: World): +def set_dlc2_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: state.has("Metro Ticket - Green", world.player) or state.has("Metro Ticket - Blue", world.player)) @@ -786,7 +789,7 @@ def set_dlc2_rules(world: World): lambda state: state.has("Metro Ticket - Yellow", world.player), "or") -def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): +def reg_act_connection(world: "HatInTimeWorld", region: Union[str, Region], unlocked_entrance: Union[str, Entrance]): reg: Region entrance: Entrance if isinstance(region, str): @@ -804,7 +807,7 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked # See randomize_act_entrances in Regions.py # Called before set_rules -def set_rift_rules(world: World, regions: typing.Dict[str, Region]): +def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. for entrance in regions["Time Rift - Gallery"].entrances: @@ -890,7 +893,7 @@ def set_rift_rules(world: World, regions: typing.Dict[str, Region]): # Basically the same as above, but without the need of the dict since we are just setting defaults # Called if Act Rando is disabled -def set_default_rift_rules(world: World): +def set_default_rift_rules(world: "HatInTimeWorld"): for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) @@ -964,7 +967,7 @@ def set_default_rift_rules(world: World): add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) -def set_event_rules(world: World): +def set_event_rules(world: "HatInTimeWorld"): for (name, data) in event_locs.items(): if not is_location_valid(world, name): continue diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 28e1a430d4..b49259c947 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -1,6 +1,6 @@ from worlds.ahit.Regions import act_chapters from worlds.ahit.Rules import act_connections -from worlds.ahit.test.TestBase import HatInTimeTestBase +from . import HatInTimeTestBase class TestActs(HatInTimeTestBase): diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py deleted file mode 100644 index 1eb4dd6555..0000000000 --- a/worlds/ahit/test/TestBase.py +++ /dev/null @@ -1,5 +0,0 @@ -from test.TestBase import WorldTestBase - - -class HatInTimeTestBase(WorldTestBase): - game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py index e69de29bb2..67b750a65c 100644 --- a/worlds/ahit/test/__init__.py +++ b/worlds/ahit/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" From 4f6bf80fb960339b1217491c75bb287816ad50d0 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 21 Mar 2024 17:32:39 -0400 Subject: [PATCH 074/143] almost forgot --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 1f24961d5c..5f910599c7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -432,7 +432,7 @@ def set_moderate_rules(world: "HatInTimeWorld"): # Moderate: Vanessa Manor with nothing for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: - set_rule(loc, lambda state: True) + set_rule(loc, lambda state: has_paintings(state, world, 1)) set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) From de02f92166f3346b712b189e95960a9c7e836980 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 21 Mar 2024 17:33:07 -0400 Subject: [PATCH 075/143] Update Rules.py --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 5f910599c7..b6c8d55efe 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -434,7 +434,7 @@ def set_moderate_rules(world: "HatInTimeWorld"): for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: set_rule(loc, lambda state: has_paintings(state, world, 1)) - set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: has_paintings(state, world, 1)) # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), From 0db3dcbddcb7dcf48b3cd574cdc7090e58a0c200 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 22 Mar 2024 21:53:28 -0400 Subject: [PATCH 076/143] a --- worlds/ahit/Regions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 038a4ef3ba..8772202b34 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -716,7 +716,13 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R i: int = 1 while i <= count: name = f"{time_rift.name} Portal - Entrance {i}" - entrance: Entrance = world.multiworld.get_entrance(name, world.player) + entrance: Entrance + try: + entrance = world.multiworld.get_entrance(name, world.player) + except KeyError: + entrance = time_rift.entrances[0] + + # noinspection PyUnboundLocalVariable reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 From 9f4e34cfc29236bf2a12317001d48fdcedc11713 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:34:11 -0400 Subject: [PATCH 077/143] Update worlds/ahit/Options.py Co-authored-by: Ixrec --- worlds/ahit/Options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index d58a12ded2..b99533d437 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -126,6 +126,9 @@ class ActRandomizer(Choice): class ActPlando(OptionDict): """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" display_name = "Act Plando" + schema = Schema({ + str: str + }) class FinaleShuffle(Toggle): From d47d6c2f797e5e375a000a90473378c95a628f78 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 25 Mar 2024 14:44:43 -0400 Subject: [PATCH 078/143] Options stuff --- worlds/ahit/Options.py | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index d58a12ded2..9a88b3ac2e 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,7 +1,7 @@ from typing import List, TYPE_CHECKING from dataclasses import dataclass from worlds.AutoWorld import PerGameCommonOptions -from Options import Range, Toggle, DeathLink, Choice, OptionDict +from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle if TYPE_CHECKING: from . import HatInTimeWorld @@ -131,11 +131,12 @@ class ActPlando(OptionDict): class FinaleShuffle(Toggle): """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" display_name = "Finale Shuffle" - default = 0 class LogicDifficulty(Choice): - """Choose the difficulty setting for logic.""" + """Choose the difficulty setting for logic. + For an exhaustive list of all logic tricks for each difficulty, see this Google Doc: + https://docs.google.com/document/d/1x9VLSQ5davfx1KGamR9T0mD5h69_lDXJ6H7Gq7knJRI/edit?usp=sharing""" display_name = "Logic Difficulty" option_normal = -1 option_moderate = 0 @@ -180,11 +181,10 @@ class TimePieceBalancePercent(Range): range_end = 100 -class StartWithCompassBadge(Toggle): +class StartWithCompassBadge(DefaultOnToggle): """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world (instead of just Relics). Recommended if you're not familiar with where item locations are.""" display_name = "Start with Compass Badge" - default = 1 class CompassBadgeMode(Choice): @@ -201,39 +201,33 @@ class CompassBadgeMode(Choice): class UmbrellaLogic(Toggle): """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" display_name = "Umbrella Logic" - default = 0 -class ShuffleStorybookPages(Toggle): +class ShuffleStorybookPages(DefaultOnToggle): """If enabled, each storybook page in the purple Time Rifts is an item check. The Compass Badge can track these down for you.""" display_name = "Shuffle Storybook Pages" - default = 1 -class ShuffleActContracts(Toggle): +class ShuffleActContracts(DefaultOnToggle): """If enabled, shuffle Snatcher's act contracts into the pool as items""" display_name = "Shuffle Contracts" - default = 1 class ShuffleAlpineZiplines(Toggle): """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" display_name = "Shuffle Alpine Ziplines" - default = 0 class ShuffleSubconPaintings(Toggle): """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. These items are progressive, with the order of Village-Swamp-Courtyard.""" display_name = "Shuffle Subcon Paintings" - default = 0 class NoPaintingSkips(Toggle): """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings.""" display_name = "No Subcon Fire Wall Skips" - default = 0 class StartingChapter(Choice): @@ -343,7 +337,6 @@ class MinExtraYarn(Range): class HatItems(Toggle): """Removes all yarn from the pool and turns the hats into individual items instead.""" display_name = "Hat Items" - default = 0 class MinPonCost(Range): @@ -382,13 +375,11 @@ class EnableDLC1(Toggle): """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" display_name = "Shuffle Chapter 6" - default = 0 class Tasksanity(Toggle): """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" display_name = "Tasksanity" - default = 0 class TasksanityTaskStep(Range): @@ -412,7 +403,6 @@ class ExcludeTour(Toggle): important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages when your goal is Time's End.""" display_name = "Exclude Tour Time Rift" - default = 0 class ShipShapeCustomTaskGoal(Range): @@ -427,7 +417,6 @@ class EnableDLC2(Toggle): """Shuffle content from Nyakuza Metro (Chapter 7) into the game. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" display_name = "Shuffle Chapter 7" - default = 0 class MetroMinPonCost(Range): @@ -475,14 +464,12 @@ class BaseballBat(Toggle): """Replace the Umbrella with the baseball bat from Nyakuza Metro. DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" display_name = "Baseball Bat" - default = 0 class EnableDeathWish(Toggle): """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" display_name = "Enable Death Wish" - default = 0 class DeathWishOnly(Toggle): @@ -496,7 +483,6 @@ class DeathWishOnly(Toggle): - All Pons in the item pool are replaced with Health Pons or random cosmetics - The EndGoal option is forced to complete Seal the Deal""" display_name = "Death Wish Only" - default = 0 class DWShuffle(Toggle): @@ -505,7 +491,6 @@ class DWShuffle(Toggle): If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. Disabling candles is highly recommended.""" display_name = "Death Wish Shuffle" - default = 0 class DWShuffleCountMin(Range): @@ -532,17 +517,15 @@ class DWEnableBonus(Toggle): ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" display_name = "Shuffle Death Wish Full Completions" - default = 0 -class DWAutoCompleteBonuses(Toggle): +class DWAutoCompleteBonuses(DefaultOnToggle): """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" display_name = "Auto Complete Bonus Stamps" - default = 1 -class DWExcludeAnnoyingContracts(Toggle): +class DWExcludeAnnoyingContracts(DefaultOnToggle): """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. Excluded Death Wishes are automatically completed as soon as they are unlocked. This option currently excludes the following contracts: @@ -554,10 +537,9 @@ class DWExcludeAnnoyingContracts(Toggle): - Cruisin' for a Bruisin' - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" display_name = "Exclude Annoying Death Wish Contracts" - default = 1 -class DWExcludeAnnoyingBonuses(Toggle): +class DWExcludeAnnoyingBonuses(DefaultOnToggle): """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. This option currently excludes the following bonuses: @@ -577,13 +559,11 @@ class DWExcludeAnnoyingBonuses(Toggle): - Cruisin' for a Bruisin' - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" - default = 1 -class DWExcludeCandles(Toggle): +class DWExcludeCandles(DefaultOnToggle): """If enabled, exclude all candle Death Wishes.""" display_name = "Exclude Candle Death Wishes" - default = 1 class DWTimePieceRequirement(Range): From bf870299dee8c16a94ad646edf39a334c7a3a979 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 25 Mar 2024 14:47:02 -0400 Subject: [PATCH 079/143] oop --- worlds/ahit/Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index ede1899b0d..b08efd7e85 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,4 +1,5 @@ from typing import List, TYPE_CHECKING +from schema import Schema from dataclasses import dataclass from worlds.AutoWorld import PerGameCommonOptions from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle From dbd2d8fe3f199d36505fa314044926b2da5d8f48 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 25 Mar 2024 15:06:38 -0400 Subject: [PATCH 080/143] no unnecessary type hints --- worlds/ahit/DeathWishRules.py | 6 +++--- worlds/ahit/Items.py | 6 +++--- worlds/ahit/Locations.py | 2 +- worlds/ahit/Regions.py | 14 +++++++------- worlds/ahit/Rules.py | 8 ++++---- worlds/ahit/Types.py | 4 ++-- worlds/ahit/__init__.py | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 1418af676b..2300166b5e 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -275,7 +275,7 @@ def get_total_dw_stamps(state: CollectionState, world: "HatInTimeWorld") -> int: if world.options.DWShuffle.value > 0: return 999 # no stamp costs in death wish shuffle - count: int = 0 + count = 0 for name in death_wishes: if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): @@ -332,7 +332,7 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") -> int: - total: int = 0 + total = 0 for name in act_chapters.keys(): n = f"{name} (Zero Jumps)" @@ -354,7 +354,7 @@ def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") - def get_reachable_enemy_count(state: CollectionState, world: "HatInTimeWorld") -> int: - count: int = 0 + count = 0 for enemy in hit_list.keys(): if enemy in bosses: continue diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 5411aec68f..f05b3ba446 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -66,8 +66,8 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue if name == "Time Piece": - tp_count: int = 40 - max_extra: int = 0 + tp_count = 40 + max_extra = 0 if world.is_dlc1(): max_extra += 6 @@ -94,7 +94,7 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) - max_cost: int = 0 + max_cost = 0 for i in range(5): cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) world.get_hat_yarn_costs()[HatType(i)] = cost diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 9d148f64c0..cddd821698 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -9,7 +9,7 @@ TASKSANITY_START_ID = 2000300204 def get_total_locations(world: "HatInTimeWorld") -> int: - total: int = 0 + total = 0 if not world.is_dw_only(): for (name) in location_table.keys(): diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 8772202b34..2b4ec06bae 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -574,7 +574,7 @@ def randomize_act_entrances(world: "HatInTimeWorld"): first_chapter: Region = get_first_chapter_region(world) has_guaranteed: bool = False - i: int = 0 + i = 0 while i < len(region_list): region = region_list[i] i += 1 @@ -780,8 +780,8 @@ def create_region(world: "HatInTimeWorld", name: str) -> Region: def create_badge_seller(world: "HatInTimeWorld") -> Region: badge_seller = Region("Badge Seller", world.player, world.multiworld) world.multiworld.regions.append(badge_seller) - count: int = 0 - max_items: int = 0 + count = 0 + max_items = 0 if world.options.BadgeSellerMaxItems.value > 0: max_items = world.random.randint(world.options.BadgeSellerMinItems.value, @@ -879,9 +879,9 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: "HatInTimeWorld"): min_items: int = min(world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value) max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value) - count: int = -1 - step: int = 0 - old_name: str = "" + count = -1 + step = 0 + old_name = "" thug_items = world.get_nyakuza_thug_items() for key, data in shop_locations.items(): @@ -919,7 +919,7 @@ def create_thug_shops(world: "HatInTimeWorld"): def create_events(world: "HatInTimeWorld") -> int: - count: int = 0 + count = 0 for (name, data) in event_locs.items(): if not is_location_valid(world, name): diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b6c8d55efe..028b062224 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -42,7 +42,7 @@ def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) - def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: - cost: int = 0 + cost = 0 costs = world.get_hat_yarn_costs() for h in world.get_hat_craft_order(): cost += costs[h] @@ -192,9 +192,9 @@ def set_rules(world: "HatInTimeWorld"): highest_cost: int = world.options.HighestChapterCost.value cost_increment: int = world.options.ChapterCostIncrement.value min_difference: int = world.options.ChapterCostMinDifference.value - last_cost: int = 0 - cost: int - loop_count: int = 0 + last_cost = 0 + cost = 0 + loop_count = 0 for chapter in chapter_list: min_range: int = lowest_cost + (cost_increment * loop_count) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 16255d7ec5..df0b910877 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -4,11 +4,11 @@ from BaseClasses import Location, Item, ItemClassification class HatInTimeLocation(Location): - game: str = "A Hat in Time" + game = "A Hat in Time" class HatInTimeItem(Item): - game: str = "A Hat in Time" + game = "A Hat in Time" class HatType(IntEnum): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index e7775fae28..f4e2837ed7 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -207,7 +207,7 @@ class HatInTimeWorld(World): slot_data[name] = nyakuza_thug_items[self.player][name] if self.is_dw(): - i: int = 0 + i = 0 for name in excluded_dws[self.player]: if self.options.EndGoal.value == 3 and name == "Seal the Deal": continue From 8480d515db89b56129f6ac41726bd174f3b343c2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 25 Mar 2024 15:09:24 -0400 Subject: [PATCH 081/143] warn about depot download length in setup guide --- worlds/ahit/docs/setup_en.md | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index fd648b80d2..65ca612eb6 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -12,6 +12,7 @@ 2. In the Steam console, enter the following command: `download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** +This can take a while to finish (30+ minutes) so please be patient. 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. From 54557e7da0ab6d1fafef7623966c6009216fa715 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:00:50 -0400 Subject: [PATCH 082/143] Update worlds/ahit/Options.py Co-authored-by: Ixrec --- worlds/ahit/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index b08efd7e85..69be748e6c 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -516,7 +516,7 @@ class DWShuffleCountMax(Range): class DWEnableBonus(Toggle): - """In Death Wish, allow the full completion of contracts to reward items. + """In Death Wish, add a location for completing all of a DW contract's bonuses, in addition to the location for completing the DW contract normally. WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" From 5700b74f619e6590c7fe9924fb3383b21187a1b0 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:08:12 -0400 Subject: [PATCH 083/143] typo Co-authored-by: Ixrec --- worlds/ahit/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index cddd821698..1097df5d9b 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -114,7 +114,7 @@ def get_location_names() -> Dict[str, int]: ahit_locations = { "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), - # 300000 range - Mafia Town/Batle of the Birds + # 300000 range - Mafia Town/Battle of the Birds "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"), "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"), From b00eb53ebb20d893924e7c9984e1cbc071df27c5 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:11:27 -0400 Subject: [PATCH 084/143] Update worlds/ahit/Rules.py Co-authored-by: Ixrec --- worlds/ahit/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 028b062224..07061d6ead 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -410,7 +410,7 @@ def set_specific_rules(world: "HatInTimeWorld"): if difficulty >= Difficulty.HARD: set_hard_rules(world) - if difficulty >= 2: + if difficulty >= Difficulty.EXPERT: set_expert_rules(world) From 2e14859c2aabc95ad7ed57352a7fd1ebad783114 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 25 Mar 2024 18:49:34 -0400 Subject: [PATCH 085/143] review stuff --- worlds/ahit/DeathWishRules.py | 30 ++++---- worlds/ahit/Locations.py | 124 ++++++++++++++++++++-------------- worlds/ahit/Rules.py | 44 ++++++------ worlds/ahit/Types.py | 12 +++- 4 files changed, 120 insertions(+), 90 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 2300166b5e..27f8a4db86 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -1,6 +1,6 @@ from worlds.AutoWorld import CollectionState from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings -from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData, HitType from .DeathWishLocations import dw_prereqs, dw_candles from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule @@ -14,17 +14,17 @@ if TYPE_CHECKING: # Any speedruns expect the player to have Sprint Hat dw_requirements = { - "Beat the Heat": LocData(umbrella=True), + "Beat the Heat": LocData(hit_type=HitType.umbrella), "So You're Back From Outer Space": LocData(hookshot=True), "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), - "Security Breach": LocData(hit_requirement=1), + "Security Breach": LocData(hit_type=HitType.umbrella_or_brewing), "10 Seconds until Self-Destruct": LocData(hookshot=True), "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), - "Speedrun Well": LocData(hookshot=True, hit_requirement=1), - "Boss Rush": LocData(umbrella=True, hookshot=True), + "Speedrun Well": LocData(hookshot=True, hit_type=HitType.umbrella_or_brewing), + "Boss Rush": LocData(hit_type=HitType.umbrella, hookshot=True), "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), "Bird Sanctuary": LocData(hookshot=True), @@ -168,17 +168,21 @@ def set_dw_rules(world: "HatInTimeWorld"): for misc in data.misc_required: add_rule(loc, lambda state, item=misc: state.has(item, world.player)) - if data.umbrella and world.options.UmbrellaLogic.value > 0: - add_rule(loc, lambda state: state.has("Umbrella", world.player)) - if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_requirement > 0: - if data.hit_requirement == 1: - add_rule(loc, lambda state: can_hit(state, world)) - elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) - add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: + if data.hit_type == HitType.umbrella: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + elif data.hit_type == HitType.umbrella_or_brewing: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING)) + + elif data.hit_type == HitType.dweller_bell: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER)) main_rule = main_objective.access_rule diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index cddd821698..7ece417e67 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -1,4 +1,4 @@ -from .Types import HatDLC, HatType, LocData, Difficulty +from .Types import HatDLC, HatType, LocData, Difficulty, HitType from typing import Dict, TYPE_CHECKING from .Options import TasksanityCheckCount @@ -112,7 +112,7 @@ def get_location_names() -> Dict[str, int]: ahit_locations = { - "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), + "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_type=HitType.umbrella_or_brewing), # 300000 range - Mafia Town/Batle of the Birds "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), @@ -168,15 +168,16 @@ ahit_locations = { "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", - hit_requirement=1), + hit_type=HitType.umbrella_or_brewing), "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", - hit_requirement=1), + hit_type=HitType.umbrella_or_brewing), - "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", + hit_type=HitType.umbrella_or_brewing), "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", - hit_requirement=1), + hit_type=HitType.umbrella_or_brewing), "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), @@ -215,14 +216,14 @@ ahit_locations = { "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=1), - + "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1), - + "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2), @@ -231,16 +232,18 @@ ahit_locations = { "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2), "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2), "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2), - + "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=2), - + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), - "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1), - + + "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", + hit_type=HitType.dweller_bell, paintings=1), + "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=2), - + "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=3), @@ -267,19 +270,30 @@ ahit_locations = { required_hats=[HatType.DWELLER], hookshot=True, paintings=3), - - "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1), - "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1), - "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), - "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), - - "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, - paintings=1), + "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", + hit_type=HitType.umbrella_or_brewing, paintings=1), - "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", + hit_type=HitType.umbrella_or_brewing, paintings=1), + + "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, + hit_type=HitType.umbrella_or_brewing, paintings=1), + + "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", + hit_type=HitType.umbrella_or_brewing, paintings=1), + + "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", + hit_type=HitType.dweller_bell, paintings=1), + + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", + hit_type=HitType.dweller_bell, paintings=1), + + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", + hit_type=HitType.dweller_bell, paintings=1), + + "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", + hit_type=HitType.dweller_bell, paintings=1), # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), @@ -368,17 +382,19 @@ act_completions = { "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"), "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"), "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), - "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", hit_type=HitType.umbrella), "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"), "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"), "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"), "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"), - "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1), + "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", + hit_type=HitType.umbrella_or_brewing), + "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"), "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"), "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True), - "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True), + "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", hit_type=HitType.umbrella), "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"), "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True), "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"), @@ -387,18 +403,21 @@ act_completions = { "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), - "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1, - paintings=1), + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", + hookshot=True, hit_type=HitType.umbrella_or_brewing, paintings=1), - "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, - paintings=1), + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", + hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1), - "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", + hit_type=HitType.umbrella, paintings=1), "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), - "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", + hit_type=HitType.umbrella), + "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"), @@ -408,13 +427,13 @@ act_completions = { "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"), "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"), "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True), - + "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell", required_hats=[HatType.DWELLER]), - + "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail", required_hats=[HatType.ICE]), - + "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"), "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), @@ -693,9 +712,9 @@ shop_locations = { contract_locations = { "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"), - "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"), - "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"), - "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"), + "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area", paintings=1), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area", paintings=1), + "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area", paintings=1), } # Don't put any of the locations from peaks here, the rules for their entrances are set already @@ -723,10 +742,10 @@ zero_jumps_hard = { "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar", required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), - "The Big Parade": LocData(0, "The Big Parade", - umbrella=True, - required_hats=[HatType.ICE], - dlc_flags=HatDLC.death_wish), + "The Big Parade (Zero Jumps)": LocData(0, "The Big Parade", + hit_type=HitType.umbrella, + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish), @@ -735,12 +754,12 @@ zero_jumps_hard = { "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell", required_hats=[HatType.ICE, HatType.DWELLER], - hit_requirement=1, + hit_type=HitType.umbrella_or_brewing, dlc_flags=HatDLC.death_wish), "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread", required_hats=[HatType.ICE], hookshot=True, - hit_requirement=1, dlc_flags=HatDLC.death_wish), + hit_type=HitType.umbrella_or_brewing, dlc_flags=HatDLC.death_wish), "The Finale (Zero Jumps)": LocData(0, "The Finale", required_hats=[HatType.ICE, HatType.DWELLER], @@ -766,7 +785,7 @@ zero_jumps_expert = { dlc_flags=HatDLC.death_wish), "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell", required_hats=[HatType.ICE, HatType.DWELLER], - hit_requirement=1, + hit_type=HitType.umbrella_or_brewing, misc_required=["No Bonk Badge"], dlc_flags=HatDLC.death_wish), @@ -795,7 +814,7 @@ zero_jumps = { "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio", required_hats=[HatType.ICE], - hit_requirement=1, + hit_type=HitType.umbrella_or_brewing, dlc_flags=HatDLC.death_wish), "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express", @@ -814,13 +833,13 @@ zero_jumps = { dlc_flags=HatDLC.death_wish), "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired", - umbrella=True, + hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), # No ice hat/painting required in Expert "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom", hookshot=True, - hit_requirement=1, + hit_type=HitType.umbrella_or_brewing, required_hats=[HatType.ICE], paintings=1, dlc_flags=HatDLC.death_wish), @@ -852,7 +871,10 @@ zero_jumps = { snatcher_coins = { "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", + hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), @@ -879,7 +901,7 @@ snatcher_coins = { "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_type=HitType.dweller_bell, paintings=1, dlc_flags=HatDLC.death_wish), "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 028b062224..8b32579fa3 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -2,7 +2,7 @@ from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ shop_locations, event_locs, snatcher_coins -from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict @@ -193,7 +193,6 @@ def set_rules(world: "HatInTimeWorld"): cost_increment: int = world.options.ChapterCostIncrement.value min_difference: int = world.options.ChapterCostMinDifference.value last_cost = 0 - cost = 0 loop_count = 0 for chapter in chapter_list: @@ -249,7 +248,7 @@ def set_rules(world: "HatInTimeWorld"): set_default_rift_rules(world) table = {**location_table, **event_locs} - location: Location + loc: Location for (key, data) in table.items(): if not is_location_valid(world, key): continue @@ -260,29 +259,33 @@ def set_rules(world: "HatInTimeWorld"): if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): key = f"{key} ({data.region})" - location = world.multiworld.get_location(key, world.player) + loc = world.multiworld.get_location(key, world.player) for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) if data.hookshot: - add_rule(location, lambda state: can_use_hookshot(state, world)) - - if data.umbrella and world.options.UmbrellaLogic.value > 0: - add_rule(location, lambda state: state.has("Umbrella", world.player)) + add_rule(loc, lambda state: can_use_hookshot(state, world)) if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: - add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_requirement > 0: - if data.hit_requirement == 1: - add_rule(location, lambda state: can_hit(state, world)) - elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) - add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: + if data.hit_type == HitType.umbrella: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + elif data.hit_type == HitType.umbrella_or_brewing: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING)) + + elif data.hit_type == HitType.dweller_bell: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER)) for misc in data.misc_required: - add_rule(location, lambda state, item=misc: state.has(item, world.player)) + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) set_specific_rules(world) @@ -434,7 +437,8 @@ def set_moderate_rules(world: "HatInTimeWorld"): for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: set_rule(loc, lambda state: has_paintings(state, world, 1)) - set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: has_paintings(state, world, 1)) + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), + lambda state: has_paintings(state, world, 1)) # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), @@ -729,12 +733,6 @@ def set_subcon_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), lambda state: has_paintings(state, world, 1, False)) - for key in contract_locations: - if key == "Snatcher's Contract - The Subcon Well": - continue - - add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) - def set_alps_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index df0b910877..418590a1dd 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -20,6 +20,13 @@ class HatType(IntEnum): TIME_STOP = 4 +class HitType(IntEnum): + none = 0 + umbrella = 1 + umbrella_or_brewing = 2 + dweller_bell = 3 + + class HatDLC(IntFlag): none = 0b000 dlc1 = 0b001 @@ -56,9 +63,8 @@ class LocData(NamedTuple): paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle misc_required: Optional[List[str]] = [] - # For UmbrellaLogic setting - umbrella: Optional[bool] = False # Umbrella required for this check - hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) + # For UmbrellaLogic setting only. + hit_type: Optional[HitType] = HitType.none # Other act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion From e53e75de5ff282a2bd97fb9e73aa6d2ac201199b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 26 Mar 2024 19:32:11 -0400 Subject: [PATCH 086/143] More stuff from review --- worlds/ahit/DeathWishLocations.py | 18 ++-- worlds/ahit/DeathWishRules.py | 31 +++--- worlds/ahit/Items.py | 2 +- worlds/ahit/Locations.py | 144 +++++++++++++++++-------- worlds/ahit/Options.py | 7 +- worlds/ahit/Regions.py | 17 ++- worlds/ahit/Rules.py | 114 ++++++++------------ worlds/ahit/Types.py | 1 + worlds/ahit/__init__.py | 169 +++++++++++------------------- 9 files changed, 245 insertions(+), 258 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 00902262ad..a6f19cd3e2 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -149,21 +149,21 @@ dw_classes = { def create_dw_regions(world: "HatInTimeWorld"): if world.options.DWExcludeAnnoyingContracts.value > 0: for name in annoying_dws: - world.get_excluded_dws().append(name) + world.excluded_dws.append(name) if world.options.DWEnableBonus.value == 0 \ or world.options.DWAutoCompleteBonuses.value > 0: for name in death_wishes: - world.get_excluded_bonuses().append(name) + world.excluded_bonuses.append(name) elif world.options.DWExcludeAnnoyingBonuses.value > 0: for name in annoying_bonuses: - world.get_excluded_bonuses().append(name) + world.excluded_bonuses.append(name) if world.options.DWExcludeCandles.value > 0: for name in dw_candles: - if name in world.get_excluded_dws(): + if name in world.excluded_dws: continue - world.get_excluded_dws().append(name) + world.excluded_dws.append(name) spaceship = world.multiworld.get_region("Spaceship", world.player) dw_map: Region = create_region(world, "Death Wish Map") @@ -193,7 +193,7 @@ def create_dw_regions(world: "HatInTimeWorld"): dw_shuffle.append("Seal the Deal") - world.set_dw_shuffle(dw_shuffle) + world.dw_shuffle = dw_shuffle prev_dw: Region for i in range(len(dw_shuffle)): name = dw_shuffle[i] @@ -220,7 +220,7 @@ def create_dw_regions(world: "HatInTimeWorld"): bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", ItemClassification.progression, None, world.player)) - if name in world.get_excluded_dws(): + if name in world.excluded_dws: main_objective.progress_type = LocationProgressType.EXCLUDED full_clear.progress_type = LocationProgressType.EXCLUDED elif world.is_bonus_excluded(name): @@ -232,7 +232,7 @@ def create_dw_regions(world: "HatInTimeWorld"): else: for key, loc_id in death_wishes.items(): if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): - world.get_excluded_dws().append(key) + world.excluded_dws.append(key) continue dw = create_region(world, key) @@ -258,7 +258,7 @@ def create_dw_regions(world: "HatInTimeWorld"): bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", ItemClassification.progression, None, world.player)) - if key in world.get_excluded_dws(): + if key in world.excluded_dws: main_objective.progress_type = LocationProgressType.EXCLUDED full_clear.progress_type = LocationProgressType.EXCLUDED elif world.is_bonus_excluded(key): diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 27f8a4db86..e363813746 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -103,13 +103,13 @@ required_snatcher_coins = { def set_dw_rules(world: "HatInTimeWorld"): - if "Snatcher's Hit List" not in world.get_excluded_dws() \ - or "Camera Tourist" not in world.get_excluded_dws(): + if "Snatcher's Hit List" not in world.excluded_dws \ + or "Camera Tourist" not in world.excluded_dws: set_enemy_rules(world) dw_list: List[str] = [] if world.options.DWShuffle.value > 0: - dw_list = world.get_dw_shuffle() + dw_list = world.dw_shuffle else: for name in death_wishes.keys(): dw_list.append(name) @@ -196,13 +196,12 @@ def set_dw_rules(world: "HatInTimeWorld"): add_rule(bonus_stamps, loc.access_rule) if world.options.DWShuffle.value > 0: - dw_shuffle = world.get_dw_shuffle() - for i in range(len(dw_shuffle)): + for i in range(len(world.dw_shuffle)): if i == 0: continue - name = dw_shuffle[i] - prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + name = world.dw_shuffle[i] + prev_dw = world.multiworld.get_region(world.dw_shuffle[i-1], world.player) entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) else: @@ -330,10 +329,16 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): and state.has("Triple Enemy Picture", world.player)) elif "Snatcher Coins" in name: + coins: List[str] = [] for coin in required_snatcher_coins[name]: - add_rule(main_objective, lambda state: state.has(coin, world.player), "or") + coins.append(coin) add_rule(full_clear, lambda state: state.has(coin, world.player)) + # any coin works for the main objective + add_rule(main_objective, lambda state: state.has(coins[0], world.player) + or state.has(coins[1], world.player) + or state.has(coins[2], world.player)) + def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") -> int: total = 0 @@ -378,7 +383,7 @@ def can_reach_all_bosses(state: CollectionState, world: "HatInTimeWorld") -> boo def create_enemy_events(world: "HatInTimeWorld"): - no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses for enemy, regions in hit_list.items(): if no_tourist and enemy in bosses: @@ -395,7 +400,7 @@ def create_enemy_events(world: "HatInTimeWorld"): if area == "Bluefin Tunnel" and not world.is_dlc2(): continue if world.options.DWShuffle.value > 0 and area in death_wishes.keys() \ - and area not in world.get_dw_shuffle(): + and area not in world.dw_shuffle: continue region = world.multiworld.get_region(area, world.player) @@ -409,7 +414,7 @@ def create_enemy_events(world: "HatInTimeWorld"): continue if world.options.DWShuffle.value > 0 and name in death_wishes.keys() \ - and name not in world.get_dw_shuffle(): + and name not in world.dw_shuffle: continue region = world.multiworld.get_region(name, world.player) @@ -422,7 +427,7 @@ def create_enemy_events(world: "HatInTimeWorld"): def set_enemy_rules(world: "HatInTimeWorld"): - no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses for enemy, regions in hit_list.items(): if no_tourist and enemy in bosses: @@ -440,7 +445,7 @@ def set_enemy_rules(world: "HatInTimeWorld"): continue if world.options.DWShuffle.value > 0 and area in death_wishes \ - and area not in world.get_dw_shuffle(): + and area not in world.dw_shuffle: continue event = world.multiworld.get_location(f"{enemy} - {area}", world.player) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index f05b3ba446..111864e271 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -97,7 +97,7 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): max_cost = 0 for i in range(5): cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - world.get_hat_yarn_costs()[HatType(i)] = cost + world.hat_yarn_costs[HatType(i)] = cost max_cost += cost available_yarn: int = world.options.YarnAvailable.value diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index bb06d8a33a..11ab5fe2b2 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -12,7 +12,7 @@ def get_total_locations(world: "HatInTimeWorld") -> int: total = 0 if not world.is_dw_only(): - for (name) in location_table.keys(): + for name in location_table.keys(): if is_location_valid(world, name): total += 1 @@ -21,9 +21,9 @@ def get_total_locations(world: "HatInTimeWorld") -> int: if world.is_dw(): if world.options.DWShuffle.value > 0: - total += len(world.get_dw_shuffle()) + total += len(world.dw_shuffle) if world.options.DWEnableBonus.value > 0: - total += len(world.get_dw_shuffle()) + total += len(world.dw_shuffle) else: total += 37 if world.is_dlc2(): @@ -81,11 +81,11 @@ def is_location_valid(world: "HatInTimeWorld", location: str) -> bool: return False if world.options.DWShuffle.value > 0 \ - and data.region in death_wishes and data.region not in world.get_dw_shuffle(): + and data.region in death_wishes and data.region not in world.dw_shuffle: return False if location in zero_jumps: - if world.options.DWShuffle.value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + if world.options.DWShuffle.value > 0 and "Zero Jumps" not in world.dw_shuffle: return False difficulty: int = world.options.LogicDifficulty.value @@ -298,10 +298,10 @@ ahit_locations = { # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)", hookshot=True), + "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)", hookshot=True), - "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", hookshot=True, required_hats=[HatType.BREWING]), "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), @@ -318,7 +318,7 @@ ahit_locations = { "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"), "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"), - "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"), "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"), "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"), @@ -327,7 +327,7 @@ ahit_locations = { "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), - "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True), "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"), @@ -867,54 +867,112 @@ zero_jumps = { dlc_flags=HatDLC.dlc2_dw), } -# noinspection PyDictDuplicateKeys snatcher_coins = { - "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ (DWTM)": LocData(0, "Down with the Mafia!", snatcher_coin="Snatcher Coin - Top of HQ", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", - hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ (CTR)": LocData(0, "Cheating the Race", snatcher_coin="Snatcher Coin - Top of HQ", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ (HUMT)": LocData(0, "Heating Up Mafia Town", snatcher_coin="Snatcher Coin - Top of HQ", + hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ (TGV)": LocData(0, "The Golden Vault", snatcher_coin="Snatcher Coin - Top of HQ", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, + "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower (DW: CAT)": LocData(0, "Collect-a-thon", snatcher_coin="Snatcher Coin - Top of Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower (SSFOS)": LocData(0, "She Speedran from Outer Space", + snatcher_coin="Snatcher Coin - Top of Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower (DW: MJ)": LocData(0, "Mafia's Jumps", snatcher_coin="Snatcher Coin - Top of Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", + snatcher_coin="Snatcher Coin - Under Ruined Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Under Ruined Tower (DW: CAT)": LocData(0, "Collect-a-thon", + snatcher_coin="Snatcher Coin - Under Ruined Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Under Ruined Tower (DW: SSFOS)": LocData(0, "She Speedran from Outer Space", + snatcher_coin="Snatcher Coin - Under Ruined Tower", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House (DBS)": LocData(0, "Dead Bird Studio - Elevator Area", + snatcher_coin="Snatcher Coin - Top of Red House", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House (DW: SB)": LocData(0, "Security Breach", + snatcher_coin="Snatcher Coin - Top of Red House", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush": LocData(0, "Train Rush", snatcher_coin="Snatcher Coin - Train Rush", + hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush (10 Seconds)": LocData(0, "10 Seconds until Self-Destruct", + snatcher_coin="Snatcher Coin - Train Rush", + hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", snatcher_coin="Snatcher Coin - Picture Perfect", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", snatcher_coin="Snatcher Coin - Swamp Tree", + hookshot=True, paintings=1, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Swamp Tree (Speedrun Well)": LocData(0, "Speedrun Well", + snatcher_coin="Snatcher Coin - Swamp Tree", + hookshot=True, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", snatcher_coin="Snatcher Coin - Manor Roof", + hit_type=HitType.dweller_bell, paintings=1, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", + snatcher_coin="Snatcher Coin - Giant Time Piece", + paintings=3, dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_type=HitType.dweller_bell, paintings=1, - dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", + snatcher_coin="Snatcher Coin - Goat Village Top", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top (Illness Speedrun)": LocData(0, "The Illness has Speedrun", + snatcher_coin="Snatcher Coin - Goat Village Top", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", snatcher_coin="Snatcher Coin - Lava Cake", + dlc_flags=HatDLC.death_wish), - "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw), - "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw), - "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Windmill": LocData(0, "The Windmill", snatcher_coin="Snatcher Coin - Windmill", + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Windmill (DW: WUW)": LocData(0, "Wound-Up Windmill", snatcher_coin="Snatcher Coin - Windmill", + hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", + snatcher_coin="Snatcher Coin - Green Clean Tower", + dlc_flags=HatDLC.dlc2_dw), + + "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", + snatcher_coin="Snatcher Coin - Bluefin Tunnel", + dlc_flags=HatDLC.dlc2_dw), + + "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", + snatcher_coin="Snatcher Coin - Pink Paw Fence", + dlc_flags=HatDLC.dlc2_dw), } event_locs = { diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 69be748e6c..eb4f833748 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,5 +1,5 @@ from typing import List, TYPE_CHECKING -from schema import Schema +from schema import Schema, Optional from dataclasses import dataclass from worlds.AutoWorld import PerGameCommonOptions from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle @@ -128,7 +128,7 @@ class ActPlando(OptionDict): """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" display_name = "Act Plando" schema = Schema({ - str: str + Optional(str): str }) @@ -516,7 +516,8 @@ class DWShuffleCountMax(Range): class DWEnableBonus(Toggle): - """In Death Wish, add a location for completing all of a DW contract's bonuses, in addition to the location for completing the DW contract normally. + """In Death Wish, add a location for completing all of a DW contract's bonuses, + in addition to the location for completing the DW contract normally. WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 2b4ec06bae..aa26db17d2 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -788,7 +788,7 @@ def create_badge_seller(world: "HatInTimeWorld") -> Region: world.options.BadgeSellerMaxItems.value) if max_items <= 0: - world.set_badge_seller_count(0) + world.badge_seller_count = 0 return badge_seller for (key, data) in shop_locations.items(): @@ -803,7 +803,7 @@ def create_badge_seller(world: "HatInTimeWorld") -> Region: if count >= max_items: break - world.set_badge_seller_count(max_items) + world.badge_seller_count = max_items return badge_seller @@ -867,9 +867,9 @@ def update_chapter_act_info(world: "HatInTimeWorld", original_region: Region, ne world.act_connections[original_act_info] = new_act_info -def get_shuffled_region(self, region: str) -> str: +def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: ci: str = chapter_act_info[region] - for key, val in self.act_connections.items(): + for key, val in world.act_connections.items(): if val == ci: for name in chapter_act_info.keys(): if chapter_act_info[name] == key: @@ -882,7 +882,6 @@ def create_thug_shops(world: "HatInTimeWorld"): count = -1 step = 0 old_name = "" - thug_items = world.get_nyakuza_thug_items() for key, data in shop_locations.items(): if data.nyakuza_thug == "": @@ -892,14 +891,14 @@ def create_thug_shops(world: "HatInTimeWorld"): continue try: - if thug_items[data.nyakuza_thug] <= 0: + if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: continue except KeyError: pass if count == -1: count = world.random.randint(min_items, max_items) - thug_items.setdefault(data.nyakuza_thug, count) + world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) if count <= 0: continue @@ -915,8 +914,6 @@ def create_thug_shops(world: "HatInTimeWorld"): step = 0 count = -1 - world.set_nyakuza_thug_items(thug_items) - def create_events(world: "HatInTimeWorld") -> int: count = 0 @@ -928,7 +925,7 @@ def create_events(world: "HatInTimeWorld") -> int: item_name: str = name if world.is_dw(): if name in snatcher_coins.keys(): - name = f"{name} ({data.region})" + item_name = data.snatcher_coin elif name in zero_jumps: if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard: continue diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 0d43e72a3b..929700eddc 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -1,8 +1,8 @@ from worlds.AutoWorld import CollectionState from worlds.generic.Rules import add_rule, set_rule from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ - shop_locations, event_locs, snatcher_coins -from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC, HitType + shop_locations, event_locs +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict @@ -43,9 +43,8 @@ def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) - def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: cost = 0 - costs = world.get_hat_yarn_costs() - for h in world.get_hat_craft_order(): - cost += costs[h] + for h in world.hat_craft_order: + cost += world.hat_yarn_costs[h] if h == hat: break @@ -136,7 +135,7 @@ def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool: def set_rules(world: "HatInTimeWorld"): # First, chapter access starting_chapter = ChapterIndex(world.options.StartingChapter.value) - world.set_chapter_cost(starting_chapter, 0) + world.chapter_timepiece_costs[starting_chapter] = 0 # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, @@ -160,8 +159,8 @@ def set_rules(world: "HatInTimeWorld"): world.random.shuffle(chapter_list) if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): - index1: int = 69 - index2: int = 69 + index1 = 69 + index2 = 69 pos: int lowest_index: int chapter_list.remove(ChapterIndex.ALPINE) @@ -180,75 +179,63 @@ def set_rules(world: "HatInTimeWorld"): chapter_list.insert(pos, ChapterIndex.ALPINE) - if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO: - chapter_list.remove(ChapterIndex.METRO) - index = chapter_list.index(ChapterIndex.CRUISE) - if index >= len(chapter_list): - chapter_list.append(ChapterIndex.METRO) - else: - chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) - lowest_cost: int = world.options.LowestChapterCost.value highest_cost: int = world.options.HighestChapterCost.value cost_increment: int = world.options.ChapterCostIncrement.value min_difference: int = world.options.ChapterCostMinDifference.value last_cost = 0 - loop_count = 0 - for chapter in chapter_list: - min_range: int = lowest_cost + (cost_increment * loop_count) + for i, chapter in enumerate(chapter_list): + min_range: int = lowest_cost + (cost_increment * i) if min_range >= highest_cost: min_range = highest_cost-1 value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) - cost = world.random.randint(value, min(value + cost_increment, highest_cost)) - if loop_count >= 1: + if i >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference cost = min(cost, highest_cost) - world.set_chapter_cost(chapter, cost) + world.chapter_timepiece_costs[chapter] = cost last_cost = cost - loop_count += 1 if final_chapter is not None: - world.set_chapter_cost(final_chapter, world.random.randint( + world.chapter_timepiece_costs[final_chapter] = world.random.randint( world.options.FinalChapterMinCost.value, - world.options.FinalChapterMaxCost.value)) + world.options.FinalChapterMaxCost.value) add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.MAFIA])) add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS])) add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.SUBCON])) add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])) add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE]) and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) if world.is_dlc1(): add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.CRUISE])) if world.is_dlc2(): add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.METRO]) and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) if world.options.ActRandomizer.value == 0: set_default_rift_rules(world) table = {**location_table, **event_locs} - loc: Location for (key, data) in table.items(): if not is_location_valid(world, key): continue @@ -256,9 +243,6 @@ def set_rules(world: "HatInTimeWorld"): if key in contract_locations.keys(): continue - if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): - key = f"{key} ({data.region})" - loc = world.multiworld.get_location(key, world.player) for hat in data.required_hats: @@ -319,33 +303,12 @@ def set_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location(loc, world.player), lambda state, z=zipline: state.has(z, world.player)) - for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: - if "Goat Village" in loc.name: - continue - # This needs some special handling - if loc.name == "Alpine Skyline - Goat Refinery": - add_rule(loc, lambda state: state.has("AFR Access", world.player) - and can_use_hookshot(state, world) - and can_hit(state, world, True)) - - difficulty: Difficulty = Difficulty(world.options.LogicDifficulty.value) - if difficulty >= Difficulty.MODERATE: - add_rule(loc, lambda state: state.has("TIHS Access", world.player) - and can_use_hat(state, world, HatType.SPRINT), "or") - elif difficulty >= Difficulty.HARD: - add_rule(loc, lambda state: state.has("TIHS Access", world.player, "or")) - - continue - - add_rule(loc, lambda state: can_use_hookshot(state, world)) - dummy_entrances: List[Entrance] = [] for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): continue - i: int = 1 entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region access_rules: List[Callable[[CollectionState], bool]] = [] @@ -354,7 +317,7 @@ def set_rules(world: "HatInTimeWorld"): # Entrances to this act that we have to set access_rules on entrances: List[Entrance] = [] - for act in acts: + for i, act in enumerate(acts, start=1): act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region @@ -369,8 +332,6 @@ def set_rules(world: "HatInTimeWorld"): rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) - i += 1 - for e in entrances: for rules in access_rules: add_rule(e, rules) @@ -389,7 +350,7 @@ def set_rules(world: "HatInTimeWorld"): def set_specific_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), lambda state: state.has("Time Piece", world.player, 12) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS])) add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), lambda state: state.has("Time Piece", world.player, 4)) @@ -457,6 +418,11 @@ def set_moderate_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), lambda state: can_use_hookshot(state, world)) + # Moderate: Goat Refinery from TIHS with Sprint only + add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), + lambda state: state.has("TIHS Access", world.player) + and can_use_hat(state, world, HatType.SPRINT), "or") + # Moderate: Finale without Hookshot set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), lambda state: can_use_hat(state, world, HatType.DWELLER)) @@ -530,6 +496,10 @@ def set_hard_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: can_use_hat(state, world, HatType.ICE), "or") + # Hard: Goat Refinery from TIHS with nothing + add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), + lambda state: state.has("TIHS Access", world.player, "or")) + if world.is_dlc1(): # Hard: clear Deep Sea without Dweller Mask set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), @@ -555,7 +525,7 @@ def set_hard_rules(world: "HatInTimeWorld"): def set_expert_rules(world: "HatInTimeWorld"): # Finale Telescope with no hats set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), - lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE])) # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) @@ -753,6 +723,11 @@ def set_alps_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), lambda state: can_clear_alpine(state, world)) + add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), + lambda state: state.has("AFR Access", world.player) + and can_use_hookshot(state, world) + and can_hit(state, world, True)) + def set_dlc1_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), @@ -810,11 +785,11 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. for entrance in regions["Time Rift - Gallery"].entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS])) for entrance in regions["Time Rift - The Lab"].entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])) for entrance in regions["Time Rift - Sewers"].entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) @@ -895,11 +870,11 @@ def set_default_rift_rules(world: "HatInTimeWorld"): for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS])) for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances: add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) - and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])) for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) @@ -970,9 +945,6 @@ def set_event_rules(world: "HatInTimeWorld"): if not is_location_valid(world, name): continue - if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys(): - name = f"{name} ({data.region})" - event: Location = world.multiworld.get_location(name, world.player) if data.act_event: diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 418590a1dd..fb6185188f 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -69,6 +69,7 @@ class LocData(NamedTuple): # Other act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + snatcher_coin: Optional[str] = "" # Only for Snatcher Coin event locations, name of the Snatcher Coin item class ItemData(NamedTuple): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index f4e2837ed7..676b16b70c 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,4 +1,4 @@ -from BaseClasses import Item, ItemClassification, Tutorial, Location +from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ @@ -24,15 +24,6 @@ components.append(Component("A Hat in Time Client", "AHITClient", func=launch_cl icon_paths['yatta'] = local_path('data', 'yatta.png') -hat_craft_order: Dict[int, List[HatType]] = {} -hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} -chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} -excluded_dws: Dict[int, List[str]] = {} -excluded_bonuses: Dict[int, List[str]] = {} -dw_shuffle: Dict[int, List[str]] = {} -nyakuza_thug_items: Dict[int, Dict[str, int]] = {} -badge_seller_count: Dict[int, int] = {} - class AWebInTime(WebWorld): theme = "partyTime" @@ -60,11 +51,33 @@ class HatInTimeWorld(World): options_dataclass = AHITOptions options: AHITOptions - act_connections: Dict[str, str] = {} - shop_locs: List[str] = [] item_name_groups = relic_groups web = AWebInTime() + def __init__(self, multiworld: "MultiWorld", player: int): + super().__init__(multiworld, player) + self.act_connections: Dict[str, str] = {} + self.shop_locs: List[str] = [] + + self.hat_craft_order: List[HatType] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] + + self.hat_yarn_costs: Dict[HatType, int] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + + self.chapter_timepiece_costs: Dict[ChapterIndex, int] = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} + self.excluded_dws: List[str] = [] + self.excluded_bonuses: List[str] = [] + self.dw_shuffle: List[str] = [] + self.nyakuza_thug_items: Dict[str, int] = {} + self.badge_seller_count: int = 0 + def generate_early(self): adjust_options(self) @@ -87,12 +100,6 @@ class HatInTimeWorld(World): self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) def create_regions(self): - excluded_dws[self.player] = [] - excluded_bonuses[self.player] = [] - dw_shuffle[self.player] = [] - nyakuza_thug_items[self.player] = {} - badge_seller_count[self.player] = 0 - self.shop_locs = [] # noinspection PyClassVar self.topology_present = bool(self.options.ActRandomizer.value) @@ -105,8 +112,8 @@ class HatInTimeWorld(World): create_events(self) if self.is_dw(): - if "Snatcher's Hit List" not in self.get_excluded_dws() \ - or "Camera Tourist" not in self.get_excluded_dws(): + if "Snatcher's Hit List" not in self.excluded_dws \ + or "Camera Tourist" not in self.excluded_dws: create_enemy_events(self) # place vanilla contract locations if contract shuffle is off @@ -115,30 +122,15 @@ class HatInTimeWorld(World): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) def create_items(self): - hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, - HatType.DWELLER: -1, HatType.TIME_STOP: -1} - - hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, - HatType.DWELLER, HatType.TIME_STOP] - if self.options.HatItems.value == 0 and self.options.RandomizeHatOrder.value > 0: - self.random.shuffle(hat_craft_order[self.player]) + self.random.shuffle(self.hat_craft_order) if self.options.RandomizeHatOrder.value == 2: - hat_craft_order[self.player].remove(HatType.TIME_STOP) - hat_craft_order[self.player].append(HatType.TIME_STOP) + self.hat_craft_order.remove(HatType.TIME_STOP) + self.hat_craft_order.append(HatType.TIME_STOP) self.multiworld.itempool += create_itempool(self) def set_rules(self): - self.act_connections = {} - chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1, - ChapterIndex.BIRDS: -1, - ChapterIndex.SUBCON: -1, - ChapterIndex.ALPINE: -1, - ChapterIndex.FINALE: -1, - ChapterIndex.CRUISE: -1, - ChapterIndex.METRO: -1} - if self.is_dw_only(): # we already have all items if this is the case, no need for rules self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, @@ -152,7 +144,7 @@ class HatInTimeWorld(World): if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): continue - if self.options.DWShuffle.value > 0 and name not in self.get_dw_shuffle(): + if self.options.DWShuffle.value > 0 and name not in self.dw_shuffle: continue full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) @@ -174,41 +166,41 @@ class HatInTimeWorld(World): return create_item(self, name) def fill_slot_data(self) -> dict: - slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], - "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], - "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], - "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], - "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], - "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], - "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], - "BadgeSellerItemCount": badge_seller_count[self.player], + slot_data: dict = {"Chapter1Cost": self.chapter_timepiece_costs[ChapterIndex.MAFIA], + "Chapter2Cost": self.chapter_timepiece_costs[ChapterIndex.BIRDS], + "Chapter3Cost": self.chapter_timepiece_costs[ChapterIndex.SUBCON], + "Chapter4Cost": self.chapter_timepiece_costs[ChapterIndex.ALPINE], + "Chapter5Cost": self.chapter_timepiece_costs[ChapterIndex.FINALE], + "Chapter6Cost": self.chapter_timepiece_costs[ChapterIndex.CRUISE], + "Chapter7Cost": self.chapter_timepiece_costs[ChapterIndex.METRO], + "BadgeSellerItemCount": self.badge_seller_count, "SeedNumber": str(self.multiworld.seed), # For shop prices "SeedName": self.multiworld.seed_name, "TotalLocations": get_total_locations(self)} if self.options.HatItems.value == 0: - slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) - slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) - slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) - slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER]) - slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP]) - slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0])) - slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1])) - slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2])) - slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) - slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) + slot_data.setdefault("SprintYarnCost", self.hat_yarn_costs[HatType.SPRINT]) + slot_data.setdefault("BrewingYarnCost", self.hat_yarn_costs[HatType.BREWING]) + slot_data.setdefault("IceYarnCost", self.hat_yarn_costs[HatType.ICE]) + slot_data.setdefault("DwellerYarnCost", self.hat_yarn_costs[HatType.DWELLER]) + slot_data.setdefault("TimeStopYarnCost", self.hat_yarn_costs[HatType.TIME_STOP]) + slot_data.setdefault("Hat1", int(self.hat_craft_order[0])) + slot_data.setdefault("Hat2", int(self.hat_craft_order[1])) + slot_data.setdefault("Hat3", int(self.hat_craft_order[2])) + slot_data.setdefault("Hat4", int(self.hat_craft_order[3])) + slot_data.setdefault("Hat5", int(self.hat_craft_order[4])) if self.options.ActRandomizer.value > 0: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] if self.is_dlc2() and not self.is_dw_only(): - for name in nyakuza_thug_items[self.player].keys(): - slot_data[name] = nyakuza_thug_items[self.player][name] + for name in self.nyakuza_thug_items.keys(): + slot_data[name] = self.nyakuza_thug_items[name] if self.is_dw(): i = 0 - for name in excluded_dws[self.player]: + for name in self.excluded_dws: if self.options.EndGoal.value == 3 and name == "Seal the Deal": continue @@ -217,15 +209,15 @@ class HatInTimeWorld(World): i = 0 if self.options.DWAutoCompleteBonuses.value == 0: - for name in excluded_bonuses[self.player]: - if name in excluded_dws[self.player]: + for name in self.excluded_bonuses: + if name in self.excluded_dws: continue slot_data[f"excluded_bonus{i}"] = dw_classes[name] i += 1 if self.options.DWShuffle.value > 0: - shuffled_dws = self.get_dw_shuffle() + shuffled_dws = self.dw_shuffle for i in range(len(shuffled_dws)): slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] @@ -281,26 +273,11 @@ class HatInTimeWorld(World): hint_data[self.player] = new_hint_data def write_spoiler_header(self, spoiler_handle: TextIO): - for i in self.get_chapter_costs(): - spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) + for i in self.chapter_timepiece_costs: + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.chapter_timepiece_costs[ChapterIndex(i)])) - for hat in hat_craft_order[self.player]: - spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) - - def set_chapter_cost(self, chapter: ChapterIndex, cost: int): - chapter_timepiece_costs[self.player][chapter] = cost - - def get_chapter_cost(self, chapter: ChapterIndex) -> int: - return chapter_timepiece_costs[self.player][chapter] - - def get_hat_craft_order(self): - return hat_craft_order[self.player] - - def get_hat_yarn_costs(self): - return hat_yarn_costs[self.player] - - def get_chapter_costs(self): - return chapter_timepiece_costs[self.player] + for hat in self.hat_craft_order: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) def is_dlc1(self) -> bool: return self.options.EnableDLC1.value > 0 @@ -314,43 +291,19 @@ class HatInTimeWorld(World): def is_dw_only(self) -> bool: return self.is_dw() and self.options.DeathWishOnly.value > 0 - def get_excluded_dws(self): - return excluded_dws[self.player] - - def get_excluded_bonuses(self): - return excluded_bonuses[self.player] - def is_dw_excluded(self, name: str) -> bool: # don't exclude Seal the Deal if it's our goal if self.options.EndGoal.value == 3 and name == "Seal the Deal" \ and f"{name} - Main Objective" not in self.options.exclude_locations: return False - if name in excluded_dws[self.player]: + if name in self.excluded_dws: return True return f"{name} - Main Objective" in self.options.exclude_locations def is_bonus_excluded(self, name: str) -> bool: - if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + if self.is_dw_excluded(name) or name in self.excluded_bonuses: return True return f"{name} - All Clear" in self.options.exclude_locations - - def get_dw_shuffle(self): - return dw_shuffle[self.player] - - def set_dw_shuffle(self, shuffle: List[str]): - dw_shuffle[self.player] = shuffle - - def get_badge_seller_count(self) -> int: - return badge_seller_count[self.player] - - def set_badge_seller_count(self, value: int): - badge_seller_count[self.player] = value - - def get_nyakuza_thug_items(self): - return nyakuza_thug_items[self.player] - - def set_nyakuza_thug_items(self, items: Dict[str, int]): - nyakuza_thug_items[self.player] = items From 0b41527c2278cdd1d29a6ef913ef2a5ae03533c7 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Tue, 26 Mar 2024 19:40:09 -0400 Subject: [PATCH 087/143] comment --- worlds/ahit/Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 929700eddc..319c277dae 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -158,6 +158,7 @@ def set_rules(world: "HatInTimeWorld"): chapter_list.remove(starting_chapter) world.random.shuffle(chapter_list) + # Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1 = 69 index2 = 69 From 6318a1aa89b4f55756e6916c0d3f98178807040c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 16:55:02 -0400 Subject: [PATCH 088/143] 1.5 Update --- worlds/ahit/Options.py | 14 +- worlds/ahit/Regions.py | 499 ++++++++++++++++++----------------- worlds/ahit/Rules.py | 2 +- worlds/ahit/__init__.py | 2 + worlds/ahit/docs/setup_en.md | 69 ++++- 5 files changed, 331 insertions(+), 255 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index eb4f833748..40b737468c 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -125,13 +125,24 @@ class ActRandomizer(Choice): class ActPlando(OptionDict): - """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\" will place Alpine Free Roam + at Train Rush.""" display_name = "Act Plando" schema = Schema({ Optional(str): str }) +class ActBlacklist(OptionDict): + """Blacklist acts from being shuffled onto other acts. Multiple can be listed per act. + For example, \"Barrel Battle\": [\"The Big Parade\", \"Dead Bird Studio\"] + will prevent The Big Parade and Dead Bird Studio from being shuffled onto Barrel Battle.""" + display_name = "Act Blacklist" + schema = Schema({ + Optional(str): list + }) + + class FinaleShuffle(Toggle): """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" display_name = "Finale Shuffle" @@ -619,6 +630,7 @@ class AHITOptions(PerGameCommonOptions): EndGoal: EndGoal ActRandomizer: ActRandomizer ActPlando: ActPlando + ActBlacklist: ActBlacklist ShuffleAlpineZiplines: ShuffleAlpineZiplines FinaleShuffle: FinaleShuffle LogicDifficulty: LogicDifficulty diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index aa26db17d2..9aa93ca65c 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -257,6 +257,9 @@ blacklisted_acts = { blacklisted_combos = { "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + + # Bon Voyage is here to prevent the cycle: Owl Express -> Bon Voyage -> Deep Sea -> MOTOE -> Owl Express + # which would make them all inaccessible since those rifts have no other entrances "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", "Contractual Obligations"], @@ -266,7 +269,10 @@ blacklisted_combos = { "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], - "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + + # See above comment + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", + "Murder on the Owl Express"], } @@ -425,17 +431,10 @@ def create_regions(world: "HatInTimeWorld"): def create_rift_connections(world: "HatInTimeWorld", region: Region): - i = 1 - for name in rift_access_regions[region.name]: + for i, name in enumerate(rift_access_regions[region.name]): act_region = world.multiworld.get_region(name, world.player) - entrance_name = f"{region.name} Portal - Entrance {i}" + entrance_name = f"{region.name} Portal - Entrance {i+1}" connect_regions(act_region, region, entrance_name, world.player) - i += 1 - - # fix for some weird keyerror from tests - if region.name == "Time Rift - Rumbi Factory": - for entrance in region.entrances: - world.multiworld.get_entrance(entrance.name, world.player) def create_tasksanity_locations(world: "HatInTimeWorld"): @@ -446,260 +445,87 @@ def create_tasksanity_locations(world: "HatInTimeWorld"): ship_shape.locations.append(location) -def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: - # Duplicated keys will throw an exception for us, but we still need to check for duplicated values - if is_candidate: - found_list: List = [] - old_region = region - for name in world.options.ActPlando.keys(): - act = world.options.ActPlando.get(name) - if act == old_region: - region = name - found_list.append(name) - - if len(found_list) == 0: - return False - - if len(found_list) > 1: - raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Duplicated act plando mapping found for act: \"{old_region}\"") - elif region not in world.options.ActPlando.keys(): - return False - - if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): - return False - - act = world.options.ActPlando.get(region) - try: - world.multiworld.get_region(region, world.player) - world.multiworld.get_region(act, world.player) - except KeyError: - return False - - if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): - return False - - # Don't allow plando-ing things onto the first act that aren't completable with nothing - is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ - and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) - - if is_first_act: - if act_chapters[act] == "Subcon Forest" and world.options.ShuffleSubconPaintings.value > 0: - return False - - if world.options.UmbrellaLogic.value > 0 \ - and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): - return False - - if act not in guaranteed_first_acts: - return False - - # Don't allow straight up impossible mappings - if (region == "Time Rift - Curly Tail Trail" - or region == "Time Rift - The Twilight Bell" - or region == "The Illness has Spread") \ - and act == "Alpine Free Roam": - return False - - if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ - and act == "Nyakuza Free Roam": - return False - - if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": - return False - - if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": - return False - - return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) - - def randomize_act_entrances(world: "HatInTimeWorld"): region_list: List[Region] = get_act_regions(world) world.random.shuffle(region_list) + region_list.sort(key=sort_acts) + candidate_list: List[Region] = region_list.copy() + rift_dict: Dict[str, Region] = {} - separate_rifts: bool = bool(world.options.ActRandomizer.value == 1) - - for region in region_list.copy(): - if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ - and "Time Rift" not in region.name: - region_list.remove(region) - region_list.append(region) - - for region in region_list.copy(): - if region.name in chapter_finales: - region_list.remove(region) - region_list.append(region) - - for region in region_list.copy(): - if "Time Rift" in region.name: - region_list.remove(region) - region_list.append(region) - - for name in world.options.ActPlando.keys(): - try: - world.multiworld.get_region(name, world.player) - except KeyError: - print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Act \"{name}\" does not exist in the multiworld." - f"Possible reasons are typos, case-sensitivity, or DLC options.") - - for region in region_list.copy(): - if region.name in world.options.ActPlando.keys(): + # Check if Plando's are valid, if so, map them + if len(world.options.ActPlando) > 0: + player_name = world.multiworld.get_player_name(world.player) + for (name1, name2) in world.options.ActPlando.items(): + region: Region + act: Region try: - act = world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player) + region = world.multiworld.get_region(name1, world.player) except KeyError: - print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Act \"{world.options.ActPlando.get(region.name)}\" does not exist in the multiworld." + print(f"ActPlando ({player_name}) - " + f"Act \"{name1}\" does not exist in the multiworld. " + f"Possible reasons are typos, case-sensitivity, or DLC options.") + continue + + try: + act = world.multiworld.get_region(name2, world.player) + except KeyError: + print(f"ActPlando ({player_name}) - " + f"Act \"{name2}\" does not exist in the multiworld. " f"Possible reasons are typos, case-sensitivity, or DLC options.") continue if is_valid_plando(world, region.name) and is_valid_plando(world, act.name, True): region_list.remove(region) - region_list.append(region) - region_list.remove(act) - region_list.append(act) + candidate_list.remove(act) + connect_acts(world, region, act, rift_dict) else: - print(f"[WARNING] ActPlando " - f"({world.multiworld.get_player_name(world.player)}) - " - f"\"{region.name}: {world.options.ActPlando.get(region.name)}\" " + print(f"ActPlando " + f"({player_name}) - " + f"\"{name1}: {name2}\" " f"is an invalid or disallowed act plando combination!") - # Reverse the list, so we can do what we want to do first - region_list.reverse() - - shuffled_list: List[Region] = [] - mapped_list: List[Region] = [] - rift_dict: Dict[str, Region] = {} - first_chapter: Region = get_first_chapter_region(world) - has_guaranteed: bool = False - - i = 0 - while i < len(region_list): - region = region_list[i] - i += 1 - - # Get the first accessible act, so we can map that to something first - if not has_guaranteed: - if act_chapters[region.name] != first_chapter.name: - continue - - if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ - and "Free Roam" not in act_entrances[region.name]: - continue - - if is_valid_plando(world, region.name): - has_guaranteed = True - - i = 0 - - # Already mapped to something else - if region in mapped_list: - continue - - mapped_list.append(region) - - # Look for candidates to map this act to - candidate_list: List[Region] = [] - for candidate in region_list: - # We're mapping something to the first act, make sure it is valid - if not has_guaranteed: - if candidate.name not in guaranteed_first_acts: - continue - - if is_valid_plando(world, candidate.name, True): - continue - - # Not completable without Umbrella - if world.options.UmbrellaLogic.value > 0 \ - and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): - continue - - # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings.value > 0 \ - and "Subcon Forest" in act_entrances[candidate.name]: - continue - - candidate_list.append(candidate) - has_guaranteed = True - break - - if is_valid_plando(world, region.name): - candidate_list.clear() - candidate_list.append( - world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player)) - break - - # Already mapped onto something else - if candidate in shuffled_list: - continue - - if separate_rifts: - # Don't map Time Rifts to normal acts - if "Time Rift" in region.name and "Time Rift" not in candidate.name: - continue - - # Don't map normal acts to Time Rifts - if "Time Rift" not in region.name and "Time Rift" in candidate.name: - continue - - # Separate purple rifts - if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ - or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: - continue - - if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: - continue - - # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled - if world.options.ShuffleActContracts.value == 0: - if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ - and candidate.name == "Contractual Obligations": - continue - - if world.options.FinaleShuffle.value > 0 and region.name in chapter_finales: - if candidate.name not in chapter_finales: - continue - - if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: - continue - - candidate_list.append(candidate) + first_act_mapped: bool = False + ignore_certain_rules: bool = False + while len(region_list) > 0: + region: Region + if not first_act_mapped: + region = get_first_act(world) + else: + region = region_list[0] candidate: Region - if len(candidate_list) > 0: - candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] + valid_candidates: List[Region] = [] + + # Look for candidates to map this act to + for c in candidate_list: + # Map the first act before anything + if not first_act_mapped: + if not is_valid_first_act(world, c): + continue + + valid_candidates.append(c) + first_act_mapped = True + break # we can stop here, as we only need one + + if is_valid_act_combo(world, region, c, bool(world.options.ActRandomizer.value == 1), ignore_certain_rules): + valid_candidates.append(c) + + if len(valid_candidates) > 0: + candidate = valid_candidates[world.random.randint(0, len(valid_candidates)-1)] else: - # plando can still break certain rules, so acts may not always end up shuffled. - for c in region_list: - if c not in shuffled_list: - candidate = c - break + # If we fail here, try again with less shuffle rules. If we still somehow fail, there's an issue for sure + if ignore_certain_rules: + raise Exception(f"Failed to find act shuffle candidate for {region}" + f"\nRemaining acts to map to: {region_list}" + f"\nRemaining candidates: {candidate_list}") - # noinspection PyUnboundLocalVariable - shuffled_list.append(candidate) - - # Vanilla - if candidate.name == region.name: - if region.name in rift_access_regions.keys(): - rift_dict.setdefault(region.name, candidate) - - update_chapter_act_info(world, region, candidate) + ignore_certain_rules = True continue - if region.name in rift_access_regions.keys(): - connect_time_rift(world, region, candidate) - rift_dict.setdefault(region.name, candidate) - else: - if candidate.name in rift_access_regions.keys(): - for e in candidate.entrances.copy(): - e.parent_region.exits.remove(e) - e.connected_region.entrances.remove(e) - - entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) - reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) - - update_chapter_act_info(world, region, candidate) + ignore_certain_rules = False + region_list.remove(region) + candidate_list.remove(candidate) + connect_acts(world, region, candidate, rift_dict) for name in blacklisted_acts.values(): if not is_act_blacklisted(world, name): @@ -711,6 +537,130 @@ def randomize_act_entrances(world: "HatInTimeWorld"): set_rift_rules(world, rift_dict) +# Try to do levels that may have specific mapping rules first +def sort_acts(act: Region) -> int: + if "Time Rift" in act.name: + return -5 + + if act.name in chapter_finales: + return -4 + + # Free Roam + if (act_chapters[act.name] == "Alpine Skyline" or act_chapters[act.name] == "Nyakuza Metro") \ + and "Time Rift" not in act.name: + return -3 + + if act.name == "Contractual Obligations": + return -2 + + world = act.multiworld.worlds[act.player] + blacklist = world.options.ActBlacklist + if len(blacklist) > 0: + for name, act_list in blacklist.items(): + if act.name == name or act.name in act_list: + return -1 + + return 0 + + +def get_first_act(world: "HatInTimeWorld") -> Region: + first_chapter = get_first_chapter_region(world) + act: Region + for e in first_chapter.exits: + if "Act 1" in e.name or "Free Roam" in e.name: + act = e.connected_region + break + + # noinspection PyUnboundLocalVariable + return act + + +def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region, rift_dict: Dict[str, Region]): + # Vanilla + if exit_act.name == entrance_act.name: + if entrance_act.name in rift_access_regions.keys(): + rift_dict.setdefault(entrance_act.name, exit_act) + + update_chapter_act_info(world, entrance_act, exit_act) + return + + if entrance_act.name in rift_access_regions.keys(): + connect_time_rift(world, entrance_act, exit_act) + rift_dict.setdefault(entrance_act.name, exit_act) + else: + if exit_act.name in rift_access_regions.keys(): + for e in exit_act.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + + entrance = world.multiworld.get_entrance(act_entrances[entrance_act.name], world.player) + chapter = world.multiworld.get_region(act_chapters[entrance_act.name], world.player) + reconnect_regions(entrance, chapter, exit_act) + + update_chapter_act_info(world, entrance_act, exit_act) + + +def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, + exit_act: Region, separate_rifts: bool, ignore_certain_rules=False) -> bool: + + # Ignore certain rules that aren't to prevent impossible combos. This is needed for ActPlando. + if not ignore_certain_rules: + if separate_rifts and not ignore_certain_rules: + # Don't map Time Rifts to normal acts + if "Time Rift" in entrance_act.name and "Time Rift" not in exit_act.name: + return False + + # Don't map normal acts to Time Rifts + if "Time Rift" not in entrance_act.name and "Time Rift" in exit_act.name: + return False + + # Separate purple rifts + if entrance_act.name in purple_time_rifts and exit_act.name not in purple_time_rifts \ + or entrance_act.name not in purple_time_rifts and exit_act.name in purple_time_rifts: + return False + + if world.options.FinaleShuffle.value > 0 and entrance_act.name in chapter_finales: + if exit_act.name not in chapter_finales: + return False + + if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]: + return False + + # Blacklisted? + if entrance_act.name in blacklisted_combos.keys() and exit_act.name in blacklisted_combos[entrance_act.name]: + return False + + if len(world.options.ActBlacklist) > 0: + act_blacklist = world.options.ActBlacklist.get(entrance_act.name) + if act_blacklist is not None and exit_act.name in act_blacklist: + return False + + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.options.ShuffleActContracts.value == 0: + if (entrance_act.name == "Your Contract has Expired" or entrance_act.name == "The Subcon Well") \ + and exit_act.name == "Contractual Obligations": + return False + + return True + + +def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: + if act.name not in guaranteed_first_acts: + return False + + # Not completable without Umbrella + if world.options.UmbrellaLogic.value > 0 \ + and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + return False + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.options.ShuffleSubconPaintings.value > 0 \ + and "Subcon Forest" in act_entrances[act.name]: + return False + + return True + + def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 @@ -720,7 +670,10 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R try: entrance = world.multiworld.get_entrance(name, world.player) except KeyError: - entrance = time_rift.entrances[0] + if len(time_rift.entrances) > 0: + entrance = time_rift.entrances[i-1] + else: + entrance = connect_regions(time_rift, exit_region, name, world.player) # noinspection PyUnboundLocalVariable reconnect_regions(entrance, entrance.parent_region, exit_region) @@ -753,6 +706,62 @@ def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: return name in blacklisted_acts.values() +def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: + # Duplicated keys will throw an exception for us, but we still need to check for duplicated values + if is_candidate: + found_list: List = [] + old_region = region + for name, act in world.options.ActPlando.items(): + if act == old_region: + region = name + found_list.append(name) + + if len(found_list) == 0: + return False + + if len(found_list) > 1: + raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Duplicated act plando mapping found for act: \"{old_region}\"") + elif region not in world.options.ActPlando.keys(): + return False + + if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): + return False + + act = world.options.ActPlando.get(region) + try: + world.multiworld.get_region(region, world.player) + world.multiworld.get_region(act, world.player) + except KeyError: + return False + + if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + if act == get_first_act(world).name and not is_valid_first_act(world, act): + return False + + # Don't allow straight up impossible mappings + if (region == "Time Rift - Curly Tail Trail" + or region == "Time Rift - The Twilight Bell" + or region == "The Illness has Spread") \ + and act == "Alpine Free Roam": + return False + + if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ + and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": + return False + + return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) + + def create_region(world: "HatInTimeWorld", name: str) -> Region: reg = Region(name, world.player, world.multiworld) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 319c277dae..14263c5d4e 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -499,7 +499,7 @@ def set_hard_rules(world: "HatInTimeWorld"): # Hard: Goat Refinery from TIHS with nothing add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), - lambda state: state.has("TIHS Access", world.player, "or")) + lambda state: state.has("TIHS Access", world.player), "or") if world.is_dlc1(): # Hard: clear Deep Sea without Dweller Mask diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 676b16b70c..3c050730b2 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -95,6 +95,8 @@ class HatInTimeWorld(World): if self.options.ActRandomizer.value == 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + if self.options.UmbrellaLogic.value > 0: + self.multiworld.push_precollected(self.create_item("Umbrella")) if start_chapter == 3 and self.options.ShuffleSubconPaintings.value > 0: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 65ca612eb6..c07857ee37 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -10,34 +10,87 @@ 1. Have Steam running. Open the Steam console with [this link.](steam://open/console) + 2. In the Steam console, enter the following command: `download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) so please be patient. +This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, +**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** +or else the download may potentially become corrupted (see first FAQ question below). + 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + 4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. +In this new text file, input the number **253230** on the first line. -7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. +You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, +go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running +the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. +To connect to the multiworld server, simply run the **ArchipelagoAHITClient** +(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +The game will connect to the client automatically when you create a new save file. ## Console Commands -Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. +Commands will not work on the title screen, you must be in-game to use them. To use console commands, +make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. `ap_say ` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`. `ap_deathlink` - Toggle Death Link. -`ap_set_connection_info ` - Usually not necessary. Set the connection info for the save file. **The IP address MUST be in double quotes!** -`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file +## FAQ/Common Issues +### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! +If you receive an error message such as +**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or +**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot +download was likely corrupted. The only way to fix this is to start the entire download all over again. +Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this +from happening is to ensure that your connection is not interrupted or slowed while downloading. + +### The game keeps crashing on startup after the splash screen! +This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, +try the following: + +- Close Steam **entirely**. +- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. +- Close the game, and then open Steam again. +- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. + +### I followed the setup, but "Live Game Events" still shows up in the options menu! +The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by +default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file +extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. +To show file extensions in Windows 10, open any folder, click the View tab at the top, +and make sure "File name extensions" is checked, and correct the name of the file. If the name of the file is correct, +and you're still running into the issue, re-read the setup guide again in case you missed a step. +If you still can't get it to work, ask for help in the Discord thread. + +### The game is running on the older version, but it's not connecting when starting a new save! +For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu +(rocket icon) in-game, and re-enable the mod. + +### Why do relics disappear from the stands in the Spaceship after they're completed? +This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that +a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed +after being completed to allow for the placement of more relics without being potentially locked out. +The level that the relic set unlocked will stay unlocked. + +### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! +There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly +if you have too many save files. Delete them and it should fix the problem. \ No newline at end of file From d572f22d83f0ab65c1e1b79db84c17ed6a8902da Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 18:12:24 -0400 Subject: [PATCH 089/143] link fix? --- worlds/ahit/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index c07857ee37..9b5f96885f 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -8,7 +8,7 @@ ## Instructions -1. Have Steam running. Open the Steam console with [this link.](steam://open/console) +1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) 2. In the Steam console, enter the following command: From 634ee6acdcbf66c8e2ca7d6007e0d2a8d48eb67a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 18:19:32 -0400 Subject: [PATCH 090/143] link fix 2 --- worlds/ahit/docs/setup_en.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 9b5f96885f..1e221ccedb 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -9,6 +9,8 @@ ## Instructions 1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) +This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, +paste the link into the box, and hit Enter. 2. In the Steam console, enter the following command: From edead9aad1bf88b8b9a102537565d6978f8305ea Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 18:19:50 -0400 Subject: [PATCH 091/143] Update setup_en.md --- worlds/ahit/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 1e221ccedb..5cdbb480cf 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -8,7 +8,7 @@ ## Instructions -1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) +1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, paste the link into the box, and hit Enter. From 6416a376c3bd050b650a8b5d6f837ef6cacf9c78 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 18:22:06 -0400 Subject: [PATCH 092/143] Update setup_en.md --- worlds/ahit/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 5cdbb480cf..d411cbb1c8 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -17,7 +17,7 @@ paste the link into the box, and hit Enter. `download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, **try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** -or else the download may potentially become corrupted (see first FAQ question below). +or else the download may potentially become corrupted (see first FAQ issue below). 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. From 5580f705e6fdeb5b256069685785143e6593c0d8 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 18:24:10 -0400 Subject: [PATCH 093/143] Update setup_en.md --- worlds/ahit/docs/setup_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index d411cbb1c8..fe4157a496 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -78,8 +78,8 @@ try the following: The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. -To show file extensions in Windows 10, open any folder, click the View tab at the top, -and make sure "File name extensions" is checked, and correct the name of the file. If the name of the file is correct, +To show file extensions in Windows 10, open any folder, click the View tab at the top, and check +"File name extensions". Then you can correct the name of the file. If the name of the file is correct, and you're still running into the issue, re-read the setup guide again in case you missed a step. If you still can't get it to work, ask for help in the Discord thread. From ff9f33ec587bbbb51e5315f201e03cf643b6ce72 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 10 May 2024 01:35:50 -0400 Subject: [PATCH 094/143] Evil --- worlds/ahit/Items.py | 75 +++++++++++++++++++++++++++-------------- worlds/ahit/__init__.py | 10 ++++-- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 111864e271..9c0df4a062 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -175,6 +175,31 @@ def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]: return junk_pool +def get_shop_trap_name(world: "HatInTimeWorld") -> str: + rand = world.random.randint(1, 9) + name = "" + if rand == 1: + name = "Time Plece" + elif rand == 2: + name = "Time Piece (Trust me bro)" + elif rand == 3: + name = "TimePiece" + elif rand == 4: + name = "Time Piece?" + elif rand == 5: + name = "Time Pizza" + elif rand == 6: + name = "Time piece" + elif rand == 7: + name = "TIme Piece" + elif rand == 8: + name = "Time Piece (maybe)" + elif rand == 9: + name = "Time Piece ;)" + + return name + + ahit_items = { "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing), "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing), @@ -186,6 +211,18 @@ ahit_items = { "Dweller Mask": ItemData(2000300052, ItemClassification.progression), "Time Stop Hat": ItemData(2000300053, ItemClassification.progression), + # Badges + "Projectile Badge": ItemData(2000300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), + "Hover Badge": ItemData(2000300026, ItemClassification.useful), + "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), + "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), + "Compass Badge": ItemData(2000300030, ItemClassification.useful), + "Scooter Badge": ItemData(2000300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), + # Relics "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression), "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression), @@ -199,23 +236,13 @@ ahit_items = { "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression), "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression), "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression), - - # Badges - "Projectile Badge": ItemData(2000300024, ItemClassification.useful), - "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), - "Hover Badge": ItemData(2000300026, ItemClassification.useful), - "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), - "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), - "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), - "Compass Badge": ItemData(2000300030, ItemClassification.useful), - "Scooter Badge": ItemData(2000300031, ItemClassification.useful), - "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), - "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), - - # Other - "Badge Pin": ItemData(2000300043, ItemClassification.useful), - "Umbrella": ItemData(2000300033, ItemClassification.progression), - "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), + # DLC + "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), + "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), # Garbage items "25 Pons": ItemData(2000300034, ItemClassification.filler), @@ -229,15 +256,11 @@ ahit_items = { "Laser Trap": ItemData(2000300040, ItemClassification.trap), "Parade Trap": ItemData(2000300041, ItemClassification.trap), - # DLC1 items - "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), - "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), - "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), - "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), - - # DLC2 items - "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), - "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), + # Other + "Badge Pin": ItemData(2000300043, ItemClassification.useful), + "Umbrella": ItemData(2000300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), + # DLC "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2), diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 3c050730b2..dd16a87489 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,5 +1,5 @@ from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld -from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations @@ -226,7 +226,13 @@ class HatInTimeWorld(World): shop_item_names: Dict[str, str] = {} for name in self.shop_locs: loc: Location = self.multiworld.get_location(name, self.player) - shop_item_names.setdefault(str(loc.address), loc.item.name) + item_name: str + if loc.item.classification is ItemClassification.trap and loc.item.game == "A Hat in Time": + item_name = get_shop_trap_name(self) + else: + item_name = loc.item.name + + shop_item_names.setdefault(str(loc.address), item_name) slot_data["ShopItemNames"] = shop_item_names From 455b60ffa6008e46039025ded4ef68917760bdec Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 10 May 2024 11:15:34 -0400 Subject: [PATCH 095/143] Good fucking lord --- worlds/ahit/Rules.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 14263c5d4e..0965146fcc 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -424,6 +424,11 @@ def set_moderate_rules(world: "HatInTimeWorld"): lambda state: state.has("TIHS Access", world.player) and can_use_hat(state, world, HatType.SPRINT), "or") + # Moderate: Finale Telescope with only Ice Hat + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.FINALE]) + and can_use_hat(state, world, HatType.ICE), "or") + # Moderate: Finale without Hookshot set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), lambda state: can_use_hat(state, world, HatType.DWELLER)) @@ -493,10 +498,6 @@ def set_hard_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_sdj(state, world), "or") - # Finale Telescope with only Ice Hat - add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), - lambda state: can_use_hat(state, world, HatType.ICE), "or") - # Hard: Goat Refinery from TIHS with nothing add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), lambda state: state.has("TIHS Access", world.player), "or") From 3675e012bbe601949f38e0e5672635fef6088354 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 11 May 2024 20:03:09 -0400 Subject: [PATCH 096/143] Review stuff again + Logic fixes --- worlds/ahit/Items.py | 6 ++---- worlds/ahit/Options.py | 20 ++++++++++---------- worlds/ahit/Rules.py | 5 +++++ worlds/ahit/__init__.py | 21 ++++++++++++++------- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 9c0df4a062..cdc1ce9a44 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -10,8 +10,7 @@ if TYPE_CHECKING: def create_itempool(world: "HatInTimeWorld") -> List[Item]: itempool: List[Item] = [] - if not world.is_dw_only() and world.options.HatItems.value == 0: - calculate_yarn_costs(world) + if world.has_yarn(): yarn_pool: List[Item] = create_multiple_items(world, "Yarn", world.options.YarnAvailable.value, ItemClassification.progression_skip_balancing) @@ -90,13 +89,12 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: def calculate_yarn_costs(world: "HatInTimeWorld"): - mw = world.multiworld min_yarn_cost = int(min(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_yarn_cost = int(max(world.options.YarnCostMin.value, world.options.YarnCostMax.value)) max_cost = 0 for i in range(5): - cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + cost: int = world.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) world.hat_yarn_costs[HatType(i)] = cost max_cost += cost diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 40b737468c..0cc80bf902 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -306,7 +306,7 @@ class FinalChapterMaxCost(Range): class MaxExtraTimePieces(Range): - """Maximum amount of extra Time Pieces from the DLCs. + """Maximum number of extra Time Pieces from the DLCs. Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" display_name = "Max Extra Time Pieces" range_start = 0 @@ -339,8 +339,8 @@ class YarnAvailable(Range): class MinExtraYarn(Range): - """The minimum amount of extra yarn in the item pool. - There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + """The minimum number of extra yarn in the item pool. + There must be at least this much more yarn over the total number of yarn needed to craft all hats. For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, there must be at least 50 yarn in the pool.""" display_name = "Max Extra Yarn" @@ -355,7 +355,7 @@ class HatItems(Toggle): class MinPonCost(Range): - """The minimum amount of Pons that any shop item can cost.""" + """The minimum number of Pons that any shop item can cost.""" display_name = "Minimum Shop Pon Cost" range_start = 10 range_end = 800 @@ -363,7 +363,7 @@ class MinPonCost(Range): class MaxPonCost(Range): - """The maximum amount of Pons that any shop item can cost.""" + """The maximum number of Pons that any shop item can cost.""" display_name = "Maximum Shop Pon Cost" range_start = 10 range_end = 800 @@ -371,7 +371,7 @@ class MaxPonCost(Range): class BadgeSellerMinItems(Range): - """The smallest amount of items that the Badge Seller can have for sale.""" + """The smallest number of items that the Badge Seller can have for sale.""" display_name = "Badge Seller Minimum Items" range_start = 0 range_end = 10 @@ -379,7 +379,7 @@ class BadgeSellerMinItems(Range): class BadgeSellerMaxItems(Range): - """The largest amount of items that the Badge Seller can have for sale.""" + """The largest number of items that the Badge Seller can have for sale.""" display_name = "Badge Seller Maximum Items" range_start = 0 range_end = 10 @@ -421,7 +421,7 @@ class ExcludeTour(Toggle): class ShipShapeCustomTaskGoal(Range): - """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + """Change the number of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" display_name = "Ship Shape Custom Task Goal" range_start = 1 range_end = 30 @@ -451,7 +451,7 @@ class MetroMaxPonCost(Range): class NyakuzaThugMinShopItems(Range): - """The smallest amount of items that the thugs in Nyakuza Metro can have for sale.""" + """The smallest number of items that the thugs in Nyakuza Metro can have for sale.""" display_name = "Nyakuza Thug Minimum Shop Items" range_start = 0 range_end = 5 @@ -459,7 +459,7 @@ class NyakuzaThugMinShopItems(Range): class NyakuzaThugMaxShopItems(Range): - """The largest amount of items that the thugs in Nyakuza Metro can have for sale.""" + """The largest number of items that the thugs in Nyakuza Metro can have for sale.""" display_name = "Nyakuza Thug Maximum Shop Items" range_start = 0 range_end = 5 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 0965146fcc..0f8f2c6aaf 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -452,6 +452,11 @@ def set_moderate_rules(world: "HatInTimeWorld"): # No Dweller, Hookshot, or Time Stop for these set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Pink Paw Station - Pink Ticket Booth", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Pink Paw Station)", world.player), lambda state: True) + for key in shop_locations.keys(): + if "Pink Paw Station Thug" in key and is_location_valid(world, key): + set_rule(world.multiworld.get_location(key, world.player), lambda state: True) # Moderate: clear Rush Hour without Hookshot set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index dd16a87489..c0caf019cf 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,5 +1,6 @@ from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld -from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \ + calculate_yarn_costs from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations @@ -124,11 +125,14 @@ class HatInTimeWorld(World): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) def create_items(self): - if self.options.HatItems.value == 0 and self.options.RandomizeHatOrder.value > 0: - self.random.shuffle(self.hat_craft_order) - if self.options.RandomizeHatOrder.value == 2: - self.hat_craft_order.remove(HatType.TIME_STOP) - self.hat_craft_order.append(HatType.TIME_STOP) + if self.has_yarn(): + calculate_yarn_costs(self) + + if self.options.RandomizeHatOrder.value > 0: + self.random.shuffle(self.hat_craft_order) + if self.options.RandomizeHatOrder.value == 2: + self.hat_craft_order.remove(HatType.TIME_STOP) + self.hat_craft_order.append(HatType.TIME_STOP) self.multiworld.itempool += create_itempool(self) @@ -180,7 +184,7 @@ class HatInTimeWorld(World): "SeedName": self.multiworld.seed_name, "TotalLocations": get_total_locations(self)} - if self.options.HatItems.value == 0: + if self.has_yarn(): slot_data.setdefault("SprintYarnCost", self.hat_yarn_costs[HatType.SPRINT]) slot_data.setdefault("BrewingYarnCost", self.hat_yarn_costs[HatType.BREWING]) slot_data.setdefault("IceYarnCost", self.hat_yarn_costs[HatType.ICE]) @@ -287,6 +291,9 @@ class HatInTimeWorld(World): for hat in self.hat_craft_order: spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) + def has_yarn(self) -> bool: + return not self.is_dw_only() and self.options.HatItems.value == 0 + def is_dlc1(self) -> bool: return self.options.EnableDLC1.value > 0 From 380f576ac3850f2ef997f225343978023126574a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 11 May 2024 20:16:44 -0400 Subject: [PATCH 097/143] More review stuff --- worlds/ahit/Items.py | 2 +- worlds/ahit/Rules.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index cdc1ce9a44..c4d3c776e5 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -94,7 +94,7 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): max_cost = 0 for i in range(5): - cost: int = world.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + cost: int = world.random.randint(min_yarn_cost, max_yarn_cost) world.hat_yarn_costs[HatType(i)] = cost max_cost += cost diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 0f8f2c6aaf..9e458f7f7a 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -103,8 +103,8 @@ def get_relic_count(state: CollectionState, world: "HatInTimeWorld", relic: str) return state.count_group(relic, world.player) -# Only use for rifts -def can_clear_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool: +# This is used to determine if the player can clear an act that's required to unlock a Time Rift +def can_clear_required_act(state: CollectionState, world: "HatInTimeWorld", act_entrance: str) -> bool: entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) if not state.can_reach(entrance.connected_region, "Region", world.player): return False @@ -799,12 +799,12 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])) for entrance in regions["Time Rift - Sewers"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4")) reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4", world.player).connected_region, entrance) for entrance in regions["Time Rift - Bazaar"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6")) reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6", world.player).connected_region, entrance) @@ -812,16 +812,16 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) for entrance in regions["Time Rift - The Owl Express"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3")) reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2", world.player).connected_region, entrance) reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3", world.player).connected_region, entrance) for entrance in regions["Time Rift - The Moon"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5")) reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4", world.player).connected_region, entrance) reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5", @@ -831,14 +831,14 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) for entrance in regions["Time Rift - Pipe"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2")) reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", world.player).connected_region, entrance) if painting_logic(world): add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in regions["Time Rift - Village"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4")) reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", world.player).connected_region, entrance) @@ -861,7 +861,7 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): if world.is_dlc1() > 0: for entrance in regions["Time Rift - Balcony"].entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) @@ -884,25 +884,25 @@ def set_default_rift_rules(world: "HatInTimeWorld"): and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE])) for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 4")) reg_act_connection(world, "Down with the Mafia!", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Mafia Town - Act 6")) reg_act_connection(world, "Heating Up Mafia Town", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 3")) reg_act_connection(world, "Murder on the Owl Express", entrance.name) reg_act_connection(world, "Picture Perfect", entrance.name) for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) - add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Battle of the Birds - Act 5")) reg_act_connection(world, "Train Rush", entrance.name) reg_act_connection(world, "The Big Parade", entrance.name) @@ -910,13 +910,13 @@ def set_default_rift_rules(world: "HatInTimeWorld"): add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2")) reg_act_connection(world, "The Subcon Well", entrance.name) if painting_logic(world): add_rule(entrance, lambda state: has_paintings(state, world, 2)) for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4")) reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) if painting_logic(world): add_rule(entrance, lambda state: has_paintings(state, world, 2)) @@ -937,7 +937,7 @@ def set_default_rift_rules(world: "HatInTimeWorld"): if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: - add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) From 2a7564df8ff9f4b8e036cad3b4b4da5eb2c26eaf Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 11 May 2024 20:30:14 -0400 Subject: [PATCH 098/143] Even more review stuff - we're almost done --- worlds/ahit/Items.py | 13 ++----------- worlds/ahit/Regions.py | 4 ++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index c4d3c776e5..4197065d26 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -2,6 +2,7 @@ from BaseClasses import Item, ItemClassification from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem from .Locations import get_total_locations from .Rules import get_difficulty +from .Options import get_total_time_pieces from typing import Optional, List, Dict, TYPE_CHECKING if TYPE_CHECKING: @@ -65,17 +66,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue if name == "Time Piece": - tp_count = 40 - max_extra = 0 - if world.is_dlc1(): - max_extra += 6 - - if world.is_dlc2(): - max_extra += 10 - - tp_count += min(max_extra, world.options.MaxExtraTimePieces.value) - tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) - + tp_list: List[Item] = create_multiple_items(world, name, get_total_time_pieces(world), item_type) for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent.value))): tp_list[i].classification = ItemClassification.progression diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 9aa93ca65c..2fb5cb16cd 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -886,8 +886,8 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: def create_thug_shops(world: "HatInTimeWorld"): - min_items: int = min(world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value) - max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value) + min_items: int = world.options.NyakuzaThugMinShopItems.value + max_items: int = world.options.NyakuzaThugMaxShopItems.value count = -1 step = 0 old_name = "" From b429021d55cb0945b06569069290db072109777f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 11 May 2024 21:00:56 -0400 Subject: [PATCH 099/143] DW review stuff --- worlds/ahit/Client.py | 3 --- worlds/ahit/DeathWishLocations.py | 10 ++-------- worlds/ahit/DeathWishRules.py | 17 +++++++---------- worlds/ahit/Regions.py | 4 ++-- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py index f6f87a35a6..2cd67e4682 100644 --- a/worlds/ahit/Client.py +++ b/worlds/ahit/Client.py @@ -17,9 +17,6 @@ class AHITJSONToTextParser(JSONtoTextParser): class AHITCommandProcessor(ClientCommandProcessor): - def __init__(self, ctx: CommonContext): - super().__init__(ctx) - def _cmd_ahit(self): """Check AHIT Connection State""" if isinstance(self.ctx, AHITContext): diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index a6f19cd3e2..d614035f27 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -194,16 +194,11 @@ def create_dw_regions(world: "HatInTimeWorld"): dw_shuffle.append("Seal the Deal") world.dw_shuffle = dw_shuffle - prev_dw: Region + prev_dw = dw_map for i in range(len(dw_shuffle)): name = dw_shuffle[i] dw = create_region(world, name) - - if i == 0: - connect_regions(dw_map, dw, f"-> {name}", world.player) - else: - # noinspection PyUnboundLocalVariable - connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) loc_id = death_wishes[name] main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) @@ -252,7 +247,6 @@ def create_dw_regions(world: "HatInTimeWorld"): bonus_stamps.show_in_spoiler = False dw.locations.append(main_stamp) dw.locations.append(bonus_stamps) - main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", ItemClassification.progression, None, world.player)) bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index e363813746..cd22f711a2 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -196,12 +196,9 @@ def set_dw_rules(world: "HatInTimeWorld"): add_rule(bonus_stamps, loc.access_rule) if world.options.DWShuffle.value > 0: - for i in range(len(world.dw_shuffle)): - if i == 0: - continue - - name = world.dw_shuffle[i] - prev_dw = world.multiworld.get_region(world.dw_shuffle[i-1], world.player) + for i in range(len(world.dw_shuffle)-1): + name = world.dw_shuffle[i+1] + prev_dw = world.multiworld.get_region(world.dw_shuffle[i], world.player) entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) else: @@ -326,7 +323,7 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): elif name == "Camera Tourist": add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) - and state.has("Triple Enemy Picture", world.player)) + and state.has("Triple Enemy Photo", world.player)) elif "Snatcher Coins" in name: coins: List[str] = [] @@ -418,8 +415,8 @@ def create_enemy_events(world: "HatInTimeWorld"): continue region = world.multiworld.get_region(name, world.player) - event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) - event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) region.locations.append(event) event.show_in_spoiler = False if name == "The Mustache Gauntlet": @@ -528,7 +525,7 @@ hit_list = { "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], } -# Camera Tourist has a bonus that requires getting three different types of enemies in one picture. +# Camera Tourist has a bonus that requires getting three different types of enemies in one photo. triple_enemy_locations = [ "She Came from Outer Space", "She Speedran from Outer Space", diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 2fb5cb16cd..b8dd678ae5 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -316,10 +316,10 @@ def create_regions(world: "HatInTimeWorld"): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.options.LogicDifficulty.value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) From f47b026bda2bb5a7de39b6cd0a0d1cb00c5ba243 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 11 May 2024 22:05:11 -0400 Subject: [PATCH 100/143] Finish up review stuff --- worlds/ahit/DeathWishLocations.py | 85 ++++++++------------- worlds/ahit/DeathWishRules.py | 123 +++++++++++++----------------- worlds/ahit/__init__.py | 3 +- 3 files changed, 89 insertions(+), 122 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index d614035f27..2866e2eb8c 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -151,8 +151,7 @@ def create_dw_regions(world: "HatInTimeWorld"): for name in annoying_dws: world.excluded_dws.append(name) - if world.options.DWEnableBonus.value == 0 \ - or world.options.DWAutoCompleteBonuses.value > 0: + if world.options.DWEnableBonus.value == 0 or world.options.DWAutoCompleteBonuses.value > 0: for name in death_wishes: world.excluded_bonuses.append(name) elif world.options.DWExcludeAnnoyingBonuses.value > 0: @@ -161,9 +160,8 @@ def create_dw_regions(world: "HatInTimeWorld"): if world.options.DWExcludeCandles.value > 0: for name in dw_candles: - if name in world.excluded_dws: - continue - world.excluded_dws.append(name) + if name not in world.excluded_dws: + world.excluded_dws.append(name) spaceship = world.multiworld.get_region("Spaceship", world.player) dw_map: Region = create_region(world, "Death Wish Map") @@ -171,8 +169,10 @@ def create_dw_regions(world: "HatInTimeWorld"): add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement.value)) if world.options.DWShuffle.value > 0: + # Connect Death Wishes randomly to one another in a linear sequence dw_list: List[str] = [] for name in death_wishes.keys(): + # Don't shuffle excluded or invalid Death Wishes if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): continue @@ -180,7 +180,6 @@ def create_dw_regions(world: "HatInTimeWorld"): world.random.shuffle(dw_list) count = world.random.randint(world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value) - dw_shuffle: List[str] = [] total = min(len(dw_list), count) for i in range(total): @@ -199,64 +198,46 @@ def create_dw_regions(world: "HatInTimeWorld"): name = dw_shuffle[i] dw = create_region(world, name) connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) - - loc_id = death_wishes[name] - main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) - full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) - main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) - bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) - main_stamp.show_in_spoiler = False - bonus_stamps.show_in_spoiler = False - dw.locations.append(main_stamp) - dw.locations.append(bonus_stamps) - - main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", - ItemClassification.progression, None, world.player)) - bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", - ItemClassification.progression, None, world.player)) - - if name in world.excluded_dws: - main_objective.progress_type = LocationProgressType.EXCLUDED - full_clear.progress_type = LocationProgressType.EXCLUDED - elif world.is_bonus_excluded(name): - full_clear.progress_type = LocationProgressType.EXCLUDED - - dw.locations.append(main_objective) - dw.locations.append(full_clear) + create_dw_locations(world, dw) prev_dw = dw else: - for key, loc_id in death_wishes.items(): + # DWShuffle is disabled, use vanilla connections + for key in death_wishes.keys(): if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): world.excluded_dws.append(key) continue dw = create_region(world, key) - if key == "Beat the Heat": - connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + connect_regions(dw_map, dw, f"{dw_map.name} -> Beat the Heat", world.player) elif key in dw_prereqs.keys(): for name in dw_prereqs[key]: parent = world.multiworld.get_region(name, world.player) connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) - main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) - full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) - main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw) - bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw) - main_stamp.show_in_spoiler = False - bonus_stamps.show_in_spoiler = False - dw.locations.append(main_stamp) - dw.locations.append(bonus_stamps) - main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", - ItemClassification.progression, None, world.player)) - bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", - ItemClassification.progression, None, world.player)) + create_dw_locations(world, dw) - if key in world.excluded_dws: - main_objective.progress_type = LocationProgressType.EXCLUDED - full_clear.progress_type = LocationProgressType.EXCLUDED - elif world.is_bonus_excluded(key): - full_clear.progress_type = LocationProgressType.EXCLUDED - dw.locations.append(main_objective) - dw.locations.append(full_clear) +def create_dw_locations(world: "HatInTimeWorld", dw: Region): + loc_id = death_wishes[dw.name] + main_objective = HatInTimeLocation(world.player, f"{dw.name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{dw.name} - All Clear", loc_id + 1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {dw.name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {dw.name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {dw.name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {dw.name}", + ItemClassification.progression, None, world.player)) + + if dw.name in world.excluded_dws: + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(dw.name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) \ No newline at end of file diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index cd22f711a2..5b94e0911f 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -103,8 +103,7 @@ required_snatcher_coins = { def set_dw_rules(world: "HatInTimeWorld"): - if "Snatcher's Hit List" not in world.excluded_dws \ - or "Camera Tourist" not in world.excluded_dws: + if "Snatcher's Hit List" not in world.excluded_dws or "Camera Tourist" not in world.excluded_dws: set_enemy_rules(world) dw_list: List[str] = [] @@ -119,81 +118,33 @@ def set_dw_rules(world: "HatInTimeWorld"): continue dw = world.multiworld.get_region(name, world.player) - temp_list: List[Location] = [] - main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) - full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) - main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) - bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) - temp_list.append(main_objective) - temp_list.append(full_clear) - if world.options.DWShuffle.value == 0: if name in dw_stamp_costs.keys(): for entrance in dw.entrances: add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + all_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) + bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) if world.options.DWEnableBonus.value == 0: # place nothing, but let the locations exist still, so we can use them for bonus stamp rules - full_clear.address = None - full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) - full_clear.show_in_spoiler = False + all_clear.address = None + all_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + all_clear.show_in_spoiler = False # No need for rules if excluded - stamps will be auto-granted if world.is_dw_excluded(name): continue - # Specific Rules modify_dw_rules(world, name) - - main_rule: Callable[[CollectionState], bool] - for i in range(len(temp_list)): - loc = temp_list[i] - data: LocData - - if loc.name == main_objective.name: - data = dw_requirements.get(name) - else: - data = dw_bonus_requirements.get(name) - - if data is None: - continue - - if data.hookshot: - add_rule(loc, lambda state: can_use_hookshot(state, world)) - - for hat in data.required_hats: - if hat is not HatType.NONE: - add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) - - for misc in data.misc_required: - add_rule(loc, lambda state, item=misc: state.has(item, world.player)) - - if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: - add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - - if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: - if data.hit_type == HitType.umbrella: - add_rule(loc, lambda state: state.has("Umbrella", world.player)) - - elif data.hit_type == HitType.umbrella_or_brewing: - add_rule(loc, lambda state: state.has("Umbrella", world.player) - or can_use_hat(state, world, HatType.BREWING)) - - elif data.hit_type == HitType.dweller_bell: - add_rule(loc, lambda state: state.has("Umbrella", world.player) - or can_use_hat(state, world, HatType.BREWING) - or can_use_hat(state, world, HatType.DWELLER)) - - main_rule = main_objective.access_rule - - if loc.name == main_objective.name: - add_rule(main_stamp, loc.access_rule) - elif loc.name == full_clear.name: - add_rule(loc, main_rule) - # Only set bonus stamp rules if we don't auto complete bonuses - if world.options.DWAutoCompleteBonuses.value == 0 \ - and not world.is_bonus_excluded(loc.name): - add_rule(bonus_stamps, loc.access_rule) + add_dw_rules(world, main_objective) + add_dw_rules(world, all_clear) + add_rule(main_stamp, main_objective.access_rule) + add_rule(all_clear, main_objective.access_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.options.DWAutoCompleteBonuses.value == 0 and not world.is_bonus_excluded(all_clear.name): + add_rule(bonus_stamps, all_clear.access_rule) if world.options.DWShuffle.value > 0: for i in range(len(world.dw_shuffle)-1): @@ -221,8 +172,45 @@ def set_dw_rules(world: "HatInTimeWorld"): add_rule(entrance, rule) if world.options.EndGoal.value == 3: - world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", - world.player) + world.multiworld.completion_condition[world.player] = lambda state: \ + state.has("1 Stamp - Seal the Deal", world.player) + + +def add_dw_rules(world: "HatInTimeWorld", loc: Location): + bonus: bool = "All Clear" in loc.name + if not bonus: + data = dw_requirements.get(loc.name) + else: + data = dw_bonus_requirements.get(loc.name) + + if data is None: + return + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: + if data.hit_type == HitType.umbrella: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + elif data.hit_type == HitType.umbrella_or_brewing: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING)) + + elif data.hit_type == HitType.dweller_bell: + add_rule(loc, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER)) def modify_dw_rules(world: "HatInTimeWorld", name: str): @@ -380,8 +368,7 @@ def can_reach_all_bosses(state: CollectionState, world: "HatInTimeWorld") -> boo def create_enemy_events(world: "HatInTimeWorld"): - no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses - + no_tourist = "Camera Tourist" in world.excluded_dws for enemy, regions in hit_list.items(): if no_tourist and enemy in bosses: continue diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index c0caf019cf..1ecfaa7552 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -115,8 +115,7 @@ class HatInTimeWorld(World): create_events(self) if self.is_dw(): - if "Snatcher's Hit List" not in self.excluded_dws \ - or "Camera Tourist" not in self.excluded_dws: + if "Snatcher's Hit List" not in self.excluded_dws or "Camera Tourist" not in self.excluded_dws: create_enemy_events(self) # place vanilla contract locations if contract shuffle is off From bc7e1084fb6eb6c9288b8baac272f51488b37eb9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 12 May 2024 16:32:52 -0400 Subject: [PATCH 101/143] remove leftover stuff --- worlds/ahit/Regions.py | 64 ++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index b8dd678ae5..87b1d21f1e 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -474,7 +474,7 @@ def randomize_act_entrances(world: "HatInTimeWorld"): f"Possible reasons are typos, case-sensitivity, or DLC options.") continue - if is_valid_plando(world, region.name) and is_valid_plando(world, act.name, True): + if is_valid_plando(world, region.name, act.name): region_list.remove(region) candidate_list.remove(act) connect_acts(world, region, act, rift_dict) @@ -528,9 +528,6 @@ def randomize_act_entrances(world: "HatInTimeWorld"): connect_acts(world, region, candidate, rift_dict) for name in blacklisted_acts.values(): - if not is_act_blacklisted(world, name): - continue - region: Region = world.multiworld.get_region(name, world.player) update_chapter_act_info(world, region, region) @@ -650,12 +647,11 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: # Not completable without Umbrella if world.options.UmbrellaLogic.value > 0 \ - and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): return False # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings.value > 0 \ - and "Subcon Forest" in act_entrances[act.name]: + if world.options.ShuffleSubconPaintings.value > 0 and "Subcon Forest" in act_entrances[act.name]: return False return True @@ -691,8 +687,13 @@ def get_act_regions(world: "HatInTimeWorld") -> List[Region]: def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: - plando: bool = name in world.options.ActPlando.keys() and is_valid_plando(world, name) \ - or name in world.options.ActPlando.values() and is_valid_plando(world, name, True) + act_plando = world.options.ActPlando + plando: bool = name in act_plando.keys() and is_valid_plando(world, name, act_plando[name]) + if not plando and name in act_plando.values(): + for key in act_plando.keys(): + if act_plando[key] == name and is_valid_plando(world, key, name): + plando = True + break if name == "The Finale": return not plando and world.options.EndGoal.value == 1 @@ -706,41 +707,31 @@ def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: return name in blacklisted_acts.values() -def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: +def is_valid_plando(world: "HatInTimeWorld", region: str, act: str) -> bool: # Duplicated keys will throw an exception for us, but we still need to check for duplicated values - if is_candidate: - found_list: List = [] - old_region = region - for name, act in world.options.ActPlando.items(): - if act == old_region: - region = name - found_list.append(name) + found_count = 0 + for val in world.options.ActPlando.values(): + if val == act: + found_count += 1 - if len(found_list) == 0: - return False - - if len(found_list) > 1: - raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Duplicated act plando mapping found for act: \"{old_region}\"") - elif region not in world.options.ActPlando.keys(): - return False + if found_count > 1: + raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Duplicated act plando mapping found for act: \"{act}\"") if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): return False - act = world.options.ActPlando.get(region) - try: - world.multiworld.get_region(region, world.player) - world.multiworld.get_region(act, world.player) - except KeyError: - return False - if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): return False - # Don't allow plando-ing things onto the first act that aren't completable with nothing - if act == get_first_act(world).name and not is_valid_first_act(world, act): - return False + # Don't allow plando-ing things onto the first act that aren't permitted + entrance_name = act_entrances.get(region) + if entrance_name != "": + is_first_act: bool = act_chapters.get(region) == get_first_chapter_region(world).name \ + and ("Act 1" in entrance_name or "Free Roam" in entrance_name) + + if is_first_act and not is_valid_first_act(world, world.multiworld.get_region(act, world.player)): + return False # Don't allow straight up impossible mappings if (region == "Time Rift - Curly Tail Trail" @@ -749,8 +740,7 @@ def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = F and act == "Alpine Free Roam": return False - if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ - and act == "Nyakuza Free Roam": + if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") and act == "Nyakuza Free Roam": return False if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": From 989dcab511903f88449884941b6af092b600e810 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 12 May 2024 16:36:02 -0400 Subject: [PATCH 102/143] a --- worlds/ahit/Regions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 87b1d21f1e..d7898703bf 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -446,7 +446,7 @@ def create_tasksanity_locations(world: "HatInTimeWorld"): def randomize_act_entrances(world: "HatInTimeWorld"): - region_list: List[Region] = get_act_regions(world) + region_list: List[Region] = get_shuffleable_act_regions(world) world.random.shuffle(region_list) region_list.sort(key=sort_acts) candidate_list: List[Region] = region_list.copy() @@ -676,7 +676,7 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R i += 1 -def get_act_regions(world: "HatInTimeWorld") -> List[Region]: +def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]: act_list: List[Region] = [] for region in world.multiworld.get_regions(world.player): if region.name in chapter_act_info.keys(): From 3799f49014864c794819d1614409538c46812b43 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 12 May 2024 18:15:41 -0400 Subject: [PATCH 103/143] assert item --- worlds/ahit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 1ecfaa7552..fc37307c1a 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -229,6 +229,7 @@ class HatInTimeWorld(World): shop_item_names: Dict[str, str] = {} for name in self.shop_locs: loc: Location = self.multiworld.get_location(name, self.player) + assert loc.item item_name: str if loc.item.classification is ItemClassification.trap and loc.item.game == "A Hat in Time": item_name = get_shop_trap_name(self) From 092cb1100c4097b64362087014016dd2626db806 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 12 May 2024 22:34:50 -0400 Subject: [PATCH 104/143] add A Hat in Time to readme/codeowners files --- README.md | 1 + docs/CODEOWNERS | 3 +++ worlds/ahit/Rules.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbfdf75f05..2310964899 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Currently, the following games are supported: * Castlevania 64 * A Short Hike * Yoshi's Island +* A Hat in Time 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 ffe6387455..f8861ce0d5 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -13,6 +13,9 @@ # Adventure /worlds/adventure/ @JusticePS +# A Hat in Time +/worlds/ahit/ @CookieCat45 + # A Link to the Past /worlds/alttp/ @Berserker66 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 9e458f7f7a..7b6278dc91 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -859,14 +859,14 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): for entrance in regions["Time Rift - Alpine Skyline"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) - if world.is_dlc1() > 0: + if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) - if world.is_dlc2() > 0: + if world.is_dlc2(): for entrance in regions["Time Rift - Rumbi Factory"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) From 8e8206ef0b8ad01919cdf5f47028c050bb9dddd4 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 12 May 2024 23:09:50 -0400 Subject: [PATCH 105/143] Fix range options not being corrected properly --- worlds/ahit/Options.py | 52 +++++++++++++----------------------------- worlds/ahit/Rules.py | 11 ++++++--- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 0cc80bf902..2184a9ae80 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -9,45 +9,25 @@ if TYPE_CHECKING: def adjust_options(world: "HatInTimeWorld"): - world.options.HighestChapterCost.value = max( - world.options.HighestChapterCost.value, - world.options.LowestChapterCost.value) + if world.options.HighestChapterCost < world.options.LowestChapterCost: + world.options.HighestChapterCost.value, world.options.LowestChapterCost.value = \ + world.options.LowestChapterCost.value, world.options.HighestChapterCost.value - world.options.LowestChapterCost.value = min( - world.options.LowestChapterCost.value, - world.options.HighestChapterCost.value) + if world.options.FinalChapterMaxCost < world.options.FinalChapterMinCost: + world.options.FinalChapterMaxCost.value, world.options.FinalChapterMinCost.value = \ + world.options.FinalChapterMinCost.value, world.options.FinalChapterMaxCost.value - world.options.FinalChapterMinCost.value = min( - world.options.FinalChapterMinCost.value, - world.options.FinalChapterMaxCost.value) + if world.options.BadgeSellerMaxItems < world.options.BadgeSellerMinItems: + world.options.BadgeSellerMaxItems.value, world.options.BadgeSellerMinItems.value = \ + world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value - world.options.FinalChapterMaxCost.value = max( - world.options.FinalChapterMaxCost.value, - world.options.FinalChapterMinCost.value) + if world.options.NyakuzaThugMaxShopItems < world.options.NyakuzaThugMinShopItems: + world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value = \ + world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value - world.options.BadgeSellerMinItems.value = min( - world.options.BadgeSellerMinItems.value, - world.options.BadgeSellerMaxItems.value) - - world.options.BadgeSellerMaxItems.value = max( - world.options.BadgeSellerMinItems.value, - world.options.BadgeSellerMaxItems.value) - - world.options.NyakuzaThugMinShopItems.value = min( - world.options.NyakuzaThugMinShopItems.value, - world.options.NyakuzaThugMaxShopItems.value) - - world.options.NyakuzaThugMaxShopItems.value = max( - world.options.NyakuzaThugMinShopItems.value, - world.options.NyakuzaThugMaxShopItems.value) - - world.options.DWShuffleCountMin.value = min( - world.options.DWShuffleCountMin.value, - world.options.DWShuffleCountMax.value) - - world.options.DWShuffleCountMax.value = max( - world.options.DWShuffleCountMin.value, - world.options.DWShuffleCountMax.value) + if world.options.DWShuffleCountMax < world.options.DWShuffleCountMin: + world.options.DWShuffleCountMax.value, world.options.DWShuffleCountMin.value = \ + world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value total_tps: int = get_total_time_pieces(world) if world.options.HighestChapterCost.value > total_tps-5: @@ -60,7 +40,7 @@ def adjust_options(world: "HatInTimeWorld"): world.options.FinalChapterMaxCost.value = min(50, total_tps) if world.options.FinalChapterMinCost.value > total_tps: - world.options.FinalChapterMinCost.value = min(50, total_tps-5) + world.options.FinalChapterMinCost.value = min(50, total_tps) # Don't allow Rush Hour goal if DLC2 content is disabled if world.options.EndGoal.value == 2 and world.options.EnableDLC2.value == 0: diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7b6278dc91..720f500be3 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -202,9 +202,14 @@ def set_rules(world: "HatInTimeWorld"): last_cost = cost if final_chapter is not None: - world.chapter_timepiece_costs[final_chapter] = world.random.randint( - world.options.FinalChapterMinCost.value, - world.options.FinalChapterMaxCost.value) + final_chapter_cost: int + if world.options.FinalChapterMinCost.value == world.options.FinalChapterMaxCost.value: + final_chapter_cost = world.options.FinalChapterMaxCost.value + else: + final_chapter_cost = world.random.randint(world.options.FinalChapterMinCost.value, + world.options.FinalChapterMaxCost.value) + + world.chapter_timepiece_costs[final_chapter] = final_chapter_cost add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.MAFIA])) From f66c37d746d367cdd8df355a91a0522aa5c2cc74 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 13:50:46 -0400 Subject: [PATCH 106/143] 120 chars per line in docs --- worlds/ahit/docs/en_A Hat in Time.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md index 390aa13288..9f1a593bbd 100644 --- a/worlds/ahit/docs/en_A Hat in Time.md +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -9,8 +9,8 @@ config file. Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your options, -so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, -the levels and Time Rifts in these chapters will be randomized as well. +so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. +If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, @@ -44,8 +44,10 @@ Items belonging to other worlds are represented by a badge with the Archipelago ## When the player receives an item, what happens? -When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. +When the player receives an item, it will play the item collect effect and information about the item +will be printed on the screen and in the in-game developer console. ## Is the DLC required to play A Hat in Time in Archipelago? -No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC. +No, the DLC expansions are not required to play. Their content can be enabled through certain options +that are disabled by default, but please don't turn them on if you don't own the respective DLC. From 4257552b5162be15c86cbf6deff7afa2393381cf Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 13 May 2024 13:52:11 -0400 Subject: [PATCH 107/143] Update worlds/ahit/Regions.py Co-authored-by: Aaron Wagener --- worlds/ahit/Regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index d7898703bf..e1c9159f94 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -627,7 +627,7 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, if entrance_act.name in blacklisted_combos.keys() and exit_act.name in blacklisted_combos[entrance_act.name]: return False - if len(world.options.ActBlacklist) > 0: + if world.options.ActBlacklist: act_blacklist = world.options.ActBlacklist.get(entrance_act.name) if act_blacklist is not None and exit_act.name in act_blacklist: return False From 2e56a0444e6b236bb0f30f2780ce91ce607c6895 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 13 May 2024 13:52:54 -0400 Subject: [PATCH 108/143] Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener --- worlds/ahit/DeathWishLocations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 2866e2eb8c..366e045a6d 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -186,7 +186,7 @@ def create_dw_regions(world: "HatInTimeWorld"): dw_shuffle.append(dw_list[i]) # Seal the Deal is always last if it's the goal - if world.options.EndGoal.value == 3: + if world.options.EndGoal == EndGoal.option_seal_the_deal: if "Seal the Deal" in dw_shuffle: dw_shuffle.remove("Seal the Deal") From 1e6bec6c0d59cbf2c7099f2a502108c6fbe91e31 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 14:03:56 -0400 Subject: [PATCH 109/143] Remove some unnecessary option.class.value --- worlds/ahit/DeathWishLocations.py | 11 ++++++----- worlds/ahit/Items.py | 16 ++++++++-------- worlds/ahit/Options.py | 16 ++++++++-------- worlds/ahit/__init__.py | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 366e045a6d..5c18485995 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -4,6 +4,7 @@ from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule from typing import List, TYPE_CHECKING from .Locations import death_wishes +from .Options import EndGoal if TYPE_CHECKING: from . import HatInTimeWorld @@ -147,18 +148,18 @@ dw_classes = { def create_dw_regions(world: "HatInTimeWorld"): - if world.options.DWExcludeAnnoyingContracts.value > 0: + if world.options.DWExcludeAnnoyingContracts: for name in annoying_dws: world.excluded_dws.append(name) - if world.options.DWEnableBonus.value == 0 or world.options.DWAutoCompleteBonuses.value > 0: + if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: for name in death_wishes: world.excluded_bonuses.append(name) - elif world.options.DWExcludeAnnoyingBonuses.value > 0: + elif world.options.DWExcludeAnnoyingBonuses: for name in annoying_bonuses: world.excluded_bonuses.append(name) - if world.options.DWExcludeCandles.value > 0: + if world.options.DWExcludeCandles: for name in dw_candles: if name not in world.excluded_dws: world.excluded_dws.append(name) @@ -168,7 +169,7 @@ def create_dw_regions(world: "HatInTimeWorld"): entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement.value)) - if world.options.DWShuffle.value > 0: + if world.options.DWShuffle: # Connect Death Wishes randomly to one another in a linear sequence dw_list: List[str] = [] for name in death_wishes.keys(): diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 4197065d26..fd47d4fec2 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -16,7 +16,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: world.options.YarnAvailable.value, ItemClassification.progression_skip_balancing) - for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent.value))): + for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent))): yarn_pool[i].classification = ItemClassification.progression itempool += yarn_pool @@ -28,7 +28,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: if not item_dlc_enabled(world, name): continue - if world.options.HatItems.value == 0 and name in hat_type_to_item.values(): + if not world.options.HatItems and name in hat_type_to_item.values(): continue item_type: ItemClassification = item_table.get(name).classification @@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue else: if name == "Scooter Badge": - if world.options.CTRLogic.value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + if world.options.CTRLogic or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression elif name == "No Bonk Badge": if get_difficulty(world) >= Difficulty.MODERATE: @@ -52,22 +52,22 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: if item_type is ItemClassification.filler or item_type is ItemClassification.trap: continue - if name in act_contracts.keys() and world.options.ShuffleActContracts.value == 0: + if name in act_contracts.keys() and not world.options.ShuffleActContracts: continue - if name in alps_hooks.keys() and world.options.ShuffleAlpineZiplines.value == 0: + if name in alps_hooks.keys() and not world.options.ShuffleAlpineZiplines: continue if name == "Progressive Painting Unlock" \ - and world.options.ShuffleSubconPaintings.value == 0: + and not world.options.ShuffleSubconPaintings: continue - if world.options.StartWithCompassBadge.value > 0 and name == "Compass Badge": + if world.options.StartWithCompassBadge and name == "Compass Badge": continue if name == "Time Piece": tp_list: List[Item] = create_multiple_items(world, name, get_total_time_pieces(world), item_type) - for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent.value))): + for i in range(int(len(tp_list) * (0.01 * world.options.TimePieceBalancePercent))): tp_list[i].classification = ItemClassification.progression itempool += tp_list diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 2184a9ae80..d08fe99081 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -30,27 +30,27 @@ def adjust_options(world: "HatInTimeWorld"): world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value total_tps: int = get_total_time_pieces(world) - if world.options.HighestChapterCost.value > total_tps-5: + if world.options.HighestChapterCost > total_tps-5: world.options.HighestChapterCost.value = min(45, total_tps-5) - if world.options.LowestChapterCost.value > total_tps-5: + if world.options.LowestChapterCost > total_tps-5: world.options.LowestChapterCost.value = min(45, total_tps-5) - if world.options.FinalChapterMaxCost.value > total_tps: + if world.options.FinalChapterMaxCost > total_tps: world.options.FinalChapterMaxCost.value = min(50, total_tps) - if world.options.FinalChapterMinCost.value > total_tps: + if world.options.FinalChapterMinCost > total_tps: world.options.FinalChapterMinCost.value = min(50, total_tps) # Don't allow Rush Hour goal if DLC2 content is disabled - if world.options.EndGoal.value == 2 and world.options.EnableDLC2.value == 0: + if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2: world.options.EndGoal.value = 1 # Don't allow Seal the Deal goal if Death Wish content is disabled - if world.options.EndGoal.value == 3 and not world.is_dw(): + if world.options.EndGoal == EndGoal.option_seal_the_deal and not world.is_dw(): world.options.EndGoal.value = 1 - if world.options.DWEnableBonus.value > 0: + if world.options.DWEnableBonus: world.options.DWAutoCompleteBonuses.value = 0 if world.is_dw_only(): @@ -73,7 +73,7 @@ def get_total_time_pieces(world: "HatInTimeWorld") -> int: if world.is_dlc2(): count += 10 - return min(40+world.options.MaxExtraTimePieces.value, count) + return min(40+world.options.MaxExtraTimePieces, count) class EndGoal(Choice): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index fc37307c1a..33e3053a1b 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -82,7 +82,7 @@ class HatInTimeWorld(World): def generate_early(self): adjust_options(self) - if self.options.StartWithCompassBadge.value > 0: + if self.options.StartWithCompassBadge: self.multiworld.push_precollected(self.create_item("Compass Badge")) if self.is_dw_only(): From 8e80b9c94eabe2074baea472a734e9f3d115a0ba Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 14:07:33 -0400 Subject: [PATCH 110/143] Remove data_version and more option.class.value --- worlds/ahit/DeathWishLocations.py | 2 +- worlds/ahit/Regions.py | 8 ++++---- worlds/ahit/__init__.py | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 5c18485995..8f8a41c29a 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -167,7 +167,7 @@ def create_dw_regions(world: "HatInTimeWorld"): spaceship = world.multiworld.get_region("Spaceship", world.player) dw_map: Region = create_region(world, "Death Wish Map") entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) - add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement.value)) + add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement)) if world.options.DWShuffle: # Connect Death Wishes randomly to one another in a linear sequence diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index e1c9159f94..0f03dd1ae6 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -318,7 +318,7 @@ def create_regions(world: "HatInTimeWorld"): ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) - if world.options.LogicDifficulty.value >= int(Difficulty.EXPERT): + if world.options.LogicDifficulty >= int(Difficulty.EXPERT): connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # @@ -404,10 +404,10 @@ def create_regions(world: "HatInTimeWorld"): create_rift_connections(w, create_region(w, "Time Rift - Balcony")) create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) - if w.options.ExcludeTour.value == 0: + if not w.options.ExcludeTour: create_rift_connections(w, create_region(w, "Time Rift - Tour")) - if w.options.Tasksanity.value > 0: + if w.options.Tasksanity: create_tasksanity_locations(w) connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) @@ -440,7 +440,7 @@ def create_rift_connections(world: "HatInTimeWorld", region: Region): def create_tasksanity_locations(world: "HatInTimeWorld"): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = TASKSANITY_START_ID - for i in range(world.options.TasksanityCheckCount.value): + for i in range(world.options.TasksanityCheckCount): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 33e3053a1b..a45d56973e 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -45,11 +45,8 @@ class HatInTimeWorld(World): """ game = "A Hat in Time" - data_version = 1 - item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = get_location_names() - options_dataclass = AHITOptions options: AHITOptions item_name_groups = relic_groups From ecf69170194bbdcd213fce341d5e0dfa059d68ca Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 13 May 2024 14:11:22 -0400 Subject: [PATCH 111/143] Update worlds/ahit/Items.py Co-authored-by: Aaron Wagener --- worlds/ahit/Items.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index fd47d4fec2..0feecd0f92 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -94,8 +94,9 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): world.options.YarnAvailable.value = max_cost available_yarn = max_cost - if max_cost + world.options.MinExtraYarn.value > available_yarn: - world.options.YarnAvailable.value += (max_cost + world.options.MinExtraYarn.value) - available_yarn + extra_yarn = max_cost + world.options.MinExtraYarn - available_yarn + if extra_yarn > 0: + world.options.YarnAvailable.value += extra_yarn def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool: From c20076fd0c56839a558ef358acef7046b8bdb2df Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 14:38:34 -0400 Subject: [PATCH 112/143] Remove the rest of option.class.value --- worlds/ahit/DeathWishRules.py | 43 +++++++++++++-------------- worlds/ahit/Items.py | 2 +- worlds/ahit/Locations.py | 31 +++++++++---------- worlds/ahit/Regions.py | 31 ++++++++++--------- worlds/ahit/Rules.py | 50 +++++++++++++++---------------- worlds/ahit/__init__.py | 56 +++++++++++++++++------------------ 6 files changed, 103 insertions(+), 110 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 5b94e0911f..1825795f32 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -7,6 +7,7 @@ from worlds.generic.Rules import add_rule, set_rule from typing import List, Callable, TYPE_CHECKING from .Regions import act_chapters from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes +from .Options import EndGoal if TYPE_CHECKING: from . import HatInTimeWorld @@ -107,7 +108,7 @@ def set_dw_rules(world: "HatInTimeWorld"): set_enemy_rules(world) dw_list: List[str] = [] - if world.options.DWShuffle.value > 0: + if world.options.DWShuffle: dw_list = world.dw_shuffle else: for name in death_wishes.keys(): @@ -118,16 +119,15 @@ def set_dw_rules(world: "HatInTimeWorld"): continue dw = world.multiworld.get_region(name, world.player) - if world.options.DWShuffle.value == 0: - if name in dw_stamp_costs.keys(): - for entrance in dw.entrances: - add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + if not world.options.DWShuffle and name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) all_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) - if world.options.DWEnableBonus.value == 0: + if not world.options.DWEnableBonus: # place nothing, but let the locations exist still, so we can use them for bonus stamp rules all_clear.address = None all_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) @@ -143,10 +143,10 @@ def set_dw_rules(world: "HatInTimeWorld"): add_rule(main_stamp, main_objective.access_rule) add_rule(all_clear, main_objective.access_rule) # Only set bonus stamp rules if we don't auto complete bonuses - if world.options.DWAutoCompleteBonuses.value == 0 and not world.is_bonus_excluded(all_clear.name): + if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name): add_rule(bonus_stamps, all_clear.access_rule) - if world.options.DWShuffle.value > 0: + if world.options.DWShuffle: for i in range(len(world.dw_shuffle)-1): name = world.dw_shuffle[i+1] prev_dw = world.multiworld.get_region(world.dw_shuffle[i], world.player) @@ -171,7 +171,7 @@ def set_dw_rules(world: "HatInTimeWorld"): for rule in access_rules: add_rule(entrance, rule) - if world.options.EndGoal.value == 3: + if world.options.EndGoal == EndGoal.option_seal_the_deal: world.multiworld.completion_condition[world.player] = lambda state: \ state.has("1 Stamp - Seal the Deal", world.player) @@ -196,10 +196,10 @@ def add_dw_rules(world: "HatInTimeWorld", loc: Location): for misc in data.misc_required: add_rule(loc, lambda state, item=misc: state.has(item, world.player)) - if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: + if data.paintings > 0 and world.options.ShuffleSubconPaintings: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: + if data.hit_type is not HitType.none and world.options.UmbrellaLogic: if data.hit_type == HitType.umbrella: add_rule(loc, lambda state: state.has("Umbrella", world.player)) @@ -260,7 +260,7 @@ def modify_dw_rules(world: "HatInTimeWorld", name: str): def get_total_dw_stamps(state: CollectionState, world: "HatInTimeWorld") -> int: - if world.options.DWShuffle.value > 0: + if world.options.DWShuffle: return 999 # no stamp costs in death wish shuffle count = 0 @@ -293,7 +293,7 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): # No Ice Hat/painting required in Expert for Toilet Zero Jumps # This painting wall can only be skipped via cherry hover. - if get_difficulty(world) < Difficulty.EXPERT or world.options.NoPaintingSkips.value == 1: + if get_difficulty(world) < Difficulty.EXPERT or world.options.NoPaintingSkips: set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), lambda state: can_use_hookshot(state, world) and can_hit(state, world) and has_paintings(state, world, 1, False)) @@ -377,14 +377,13 @@ def create_enemy_events(world: "HatInTimeWorld"): if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): continue - if area == "Time Rift - Tour" and (not world.is_dlc1() - or world.options.ExcludeTour.value > 0): + if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.options.DWShuffle.value > 0 and area in death_wishes.keys() \ - and area not in world.dw_shuffle: + + if world.options.DWShuffle and area in death_wishes.keys() and area not in world.dw_shuffle: continue region = world.multiworld.get_region(area, world.player) @@ -394,10 +393,10 @@ def create_enemy_events(world: "HatInTimeWorld"): event.show_in_spoiler = False for name in triple_enemy_locations: - if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour.value > 0): + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour): continue - if world.options.DWShuffle.value > 0 and name in death_wishes.keys() \ + if world.options.DWShuffle and name in death_wishes.keys() \ and name not in world.dw_shuffle: continue @@ -421,15 +420,13 @@ def set_enemy_rules(world: "HatInTimeWorld"): if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): continue - if area == "Time Rift - Tour" and (not world.is_dlc1() - or world.options.ExcludeTour.value > 0): + if area == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour): continue if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.options.DWShuffle.value > 0 and area in death_wishes \ - and area not in world.dw_shuffle: + if world.options.DWShuffle and area in death_wishes and area not in world.dw_shuffle: continue event = world.multiworld.get_location(f"{enemy} - {area}", world.player) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 0feecd0f92..2b5ca69fdd 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -96,7 +96,7 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): extra_yarn = max_cost + world.options.MinExtraYarn - available_yarn if extra_yarn > 0: - world.options.YarnAvailable.value += extra_yarn + world.options.YarnAvailable.value += extra_yarn def item_dlc_enabled(world: "HatInTimeWorld", name: str) -> bool: diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 11ab5fe2b2..2d8eed7be4 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -16,20 +16,20 @@ def get_total_locations(world: "HatInTimeWorld") -> int: if is_location_valid(world, name): total += 1 - if world.is_dlc1() and world.options.Tasksanity.value > 0: - total += world.options.TasksanityCheckCount.value + if world.is_dlc1() and world.options.Tasksanity: + total += world.options.TasksanityCheckCount if world.is_dw(): - if world.options.DWShuffle.value > 0: + if world.options.DWShuffle: total += len(world.dw_shuffle) - if world.options.DWEnableBonus.value > 0: + if world.options.DWEnableBonus: total += len(world.dw_shuffle) else: total += 37 if world.is_dlc2(): total += 1 - if world.options.DWEnableBonus.value > 0: + if world.options.DWEnableBonus: total += 37 if world.is_dlc2(): total += 1 @@ -60,39 +60,36 @@ def is_location_valid(world: "HatInTimeWorld", location: str) -> bool: if not location_dlc_enabled(world, location): return False - if world.options.ShuffleStorybookPages.value == 0 \ - and location in storybook_pages.keys(): + if not world.options.ShuffleStorybookPages and location in storybook_pages.keys(): return False - if world.options.ShuffleActContracts.value == 0 \ - and location in contract_locations.keys(): + if not world.options.ShuffleActContracts and location in contract_locations.keys(): return False if location not in world.shop_locs and location in shop_locations: return False data = location_table.get(location) or event_locs.get(location) - if world.options.ExcludeTour.value > 0 and data.region == "Time Rift - Tour": + if world.options.ExcludeTour and data.region == "Time Rift - Tour": return False # No need for all those event items if we're not doing candles if data.dlc_flags & HatDLC.death_wish: - if world.options.DWExcludeCandles.value > 0 and location in event_locs.keys(): + if world.options.DWExcludeCandles and location in event_locs.keys(): return False - if world.options.DWShuffle.value > 0 \ - and data.region in death_wishes and data.region not in world.dw_shuffle: + if world.options.DWShuffle and data.region in death_wishes and data.region not in world.dw_shuffle: return False if location in zero_jumps: - if world.options.DWShuffle.value > 0 and "Zero Jumps" not in world.dw_shuffle: + if world.options.DWShuffle and "Zero Jumps" not in world.dw_shuffle: return False - difficulty: int = world.options.LogicDifficulty.value - if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): + difficulty: Difficulty = Difficulty(world.options.LogicDifficulty) + if location in zero_jumps_hard and difficulty < Difficulty.HARD: return False - if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT): + if location in zero_jumps_expert and difficulty < Difficulty.EXPERT: return False return True diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0f03dd1ae6..992c4884cb 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -4,6 +4,7 @@ from .Locations import location_table, storybook_pages, event_locs, is_location_ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard from typing import TYPE_CHECKING, List, Dict from .Rules import set_rift_rules, get_difficulty +from .Options import ActRandomizer, EndGoal if TYPE_CHECKING: from . import HatInTimeWorld @@ -453,7 +454,7 @@ def randomize_act_entrances(world: "HatInTimeWorld"): rift_dict: Dict[str, Region] = {} # Check if Plando's are valid, if so, map them - if len(world.options.ActPlando) > 0: + if world.options.ActPlando: player_name = world.multiworld.get_player_name(world.player) for (name1, name2) in world.options.ActPlando.items(): region: Region @@ -507,7 +508,7 @@ def randomize_act_entrances(world: "HatInTimeWorld"): first_act_mapped = True break # we can stop here, as we only need one - if is_valid_act_combo(world, region, c, bool(world.options.ActRandomizer.value == 1), ignore_certain_rules): + if is_valid_act_combo(world, region, c, ignore_certain_rules): valid_candidates.append(c) if len(valid_candidates) > 0: @@ -598,11 +599,11 @@ def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, - exit_act: Region, separate_rifts: bool, ignore_certain_rules=False) -> bool: + exit_act: Region, ignore_certain_rules: bool = False) -> bool: # Ignore certain rules that aren't to prevent impossible combos. This is needed for ActPlando. if not ignore_certain_rules: - if separate_rifts and not ignore_certain_rules: + if world.options.ActRandomizer == ActRandomizer.option_light and not ignore_certain_rules: # Don't map Time Rifts to normal acts if "Time Rift" in entrance_act.name and "Time Rift" not in exit_act.name: return False @@ -616,7 +617,7 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, or entrance_act.name not in purple_time_rifts and exit_act.name in purple_time_rifts: return False - if world.options.FinaleShuffle.value > 0 and entrance_act.name in chapter_finales: + if world.options.FinaleShuffle and entrance_act.name in chapter_finales: if exit_act.name not in chapter_finales: return False @@ -633,7 +634,7 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, return False # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled - if world.options.ShuffleActContracts.value == 0: + if not world.options.ShuffleActContracts: if (entrance_act.name == "Your Contract has Expired" or entrance_act.name == "The Subcon Well") \ and exit_act.name == "Contractual Obligations": return False @@ -646,12 +647,11 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: return False # Not completable without Umbrella - if world.options.UmbrellaLogic.value > 0 \ - and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + if world.options.UmbrellaLogic and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): return False # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings.value > 0 and "Subcon Forest" in act_entrances[act.name]: + if world.options.ShuffleSubconPaintings and "Subcon Forest" in act_entrances[act.name]: return False return True @@ -696,13 +696,13 @@ def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: break if name == "The Finale": - return not plando and world.options.EndGoal.value == 1 + return not plando and world.options.EndGoal == EndGoal.option_finale if name == "Rush Hour": - return not plando and world.options.EndGoal.value == 2 + return not plando and world.options.EndGoal == EndGoal.option_rush_hour if name == "Time Rift - Tour": - return world.options.ExcludeTour.value > 0 + return bool(world.options.ExcludeTour) return name in blacklisted_acts.values() @@ -763,8 +763,7 @@ def create_region(world: "HatInTimeWorld", name: str) -> Region: continue if data.region == name: - if key in storybook_pages.keys() \ - and world.options.ShuffleStorybookPages.value == 0: + if key in storybook_pages.keys() and not world.options.ShuffleStorybookPages: continue location = HatInTimeLocation(world.player, key, data.id, reg) @@ -782,7 +781,7 @@ def create_badge_seller(world: "HatInTimeWorld") -> Region: count = 0 max_items = 0 - if world.options.BadgeSellerMaxItems.value > 0: + if world.options.BadgeSellerMaxItems > 0: max_items = world.random.randint(world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value) @@ -851,7 +850,7 @@ def create_region_and_connect(world: "HatInTimeWorld", def get_first_chapter_region(world: "HatInTimeWorld") -> Region: - start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter.value) + start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter) return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 720f500be3..c535f11936 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -5,6 +5,7 @@ from .Locations import location_table, zipline_unlocks, is_location_valid, contr from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict +from .Options import EndGoal, CTRLogic if TYPE_CHECKING: from . import HatInTimeWorld @@ -35,7 +36,7 @@ act_connections = { def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) -> bool: - if world.options.HatItems.value > 0: + if world.options.HatItems: return state.has(hat_type_to_item[hat], world.player) return state.count("Yarn", world.player) >= get_hat_cost(world, hat) @@ -56,19 +57,19 @@ def can_sdj(state: CollectionState, world: "HatInTimeWorld"): def painting_logic(world: "HatInTimeWorld") -> bool: - return world.options.ShuffleSubconPaintings.value > 0 + return bool(world.options.ShuffleSubconPaintings) # -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert def get_difficulty(world: "HatInTimeWorld") -> Difficulty: - return Difficulty(world.options.LogicDifficulty.value) + return Difficulty(world.options.LogicDifficulty) def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, allow_skip: bool = True) -> bool: if not painting_logic(world): return True - if world.options.NoPaintingSkips.value == 0 and allow_skip: + if not world.options.NoPaintingSkips and allow_skip: # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena if get_difficulty(world) >= Difficulty.MODERATE: return True @@ -77,7 +78,7 @@ def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, a def zipline_logic(world: "HatInTimeWorld") -> bool: - return world.options.ShuffleAlpineZiplines.value > 0 + return bool(world.options.ShuffleAlpineZiplines) def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"): @@ -85,7 +86,7 @@ def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"): def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False): - if world.options.UmbrellaLogic.value == 0: + if not world.options.UmbrellaLogic: return True return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) @@ -134,7 +135,7 @@ def can_clear_metro(state: CollectionState, world: "HatInTimeWorld") -> bool: def set_rules(world: "HatInTimeWorld"): # First, chapter access - starting_chapter = ChapterIndex(world.options.StartingChapter.value) + starting_chapter = ChapterIndex(world.options.StartingChapter) world.chapter_timepiece_costs[starting_chapter] = 0 # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale @@ -142,10 +143,10 @@ def set_rules(world: "HatInTimeWorld"): ChapterIndex.SUBCON, ChapterIndex.ALPINE] final_chapter = ChapterIndex.FINALE - if world.options.EndGoal.value == 2: + if world.options.EndGoal == EndGoal.option_rush_hour: final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) - elif world.options.EndGoal.value == 3: + elif world.options.EndGoal == EndGoal.option_seal_the_deal: final_chapter = None chapter_list.append(ChapterIndex.FINALE) @@ -203,7 +204,7 @@ def set_rules(world: "HatInTimeWorld"): if final_chapter is not None: final_chapter_cost: int - if world.options.FinalChapterMinCost.value == world.options.FinalChapterMaxCost.value: + if world.options.FinalChapterMinCost == world.options.FinalChapterMaxCost: final_chapter_cost = world.options.FinalChapterMaxCost.value else: final_chapter_cost = world.random.randint(world.options.FinalChapterMinCost.value, @@ -238,7 +239,7 @@ def set_rules(world: "HatInTimeWorld"): and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.METRO]) and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) - if world.options.ActRandomizer.value == 0: + if not world.options.ActRandomizer: set_default_rift_rules(world) table = {**location_table, **event_locs} @@ -258,10 +259,10 @@ def set_rules(world: "HatInTimeWorld"): if data.hookshot: add_rule(loc, lambda state: can_use_hookshot(state, world)) - if data.paintings > 0 and world.options.ShuffleSubconPaintings.value > 0: + if data.paintings > 0 and world.options.ShuffleSubconPaintings: add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) - if data.hit_type is not HitType.none and world.options.UmbrellaLogic.value > 0: + if data.hit_type is not HitType.none and world.options.UmbrellaLogic: if data.hit_type == HitType.umbrella: add_rule(loc, lambda state: state.has("Umbrella", world.player)) @@ -283,7 +284,7 @@ def set_rules(world: "HatInTimeWorld"): # Illness starts the player past the intro alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) - if world.options.UmbrellaLogic.value > 0: + if world.options.UmbrellaLogic: add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) if zipline_logic(world): @@ -347,9 +348,9 @@ def set_rules(world: "HatInTimeWorld"): set_event_rules(world) - if world.options.EndGoal.value == 1: + if world.options.EndGoal == EndGoal.option_finale: world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) - elif world.options.EndGoal.value == 2: + elif world.options.EndGoal == EndGoal.option_rush_hour: world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) @@ -472,7 +473,7 @@ def set_moderate_rules(world: "HatInTimeWorld"): and can_use_hat(state, world, HatType.BREWING)) # Moderate: Bluefin Tunnel + Pink Paw Station without tickets - if world.options.NoTicketSkips.value == 0: + if not world.options.NoTicketSkips: set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) @@ -523,7 +524,7 @@ def set_hard_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - if world.options.NoTicketSkips.value != 1: + if not world.options.NoTicketSkips: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING)) else: @@ -579,7 +580,7 @@ def set_expert_rules(world: "HatInTimeWorld"): world.multiworld.get_region("Subcon Forest Area", world.player), "Subcon Forest Entrance YCHE", world.player) - if world.options.NoPaintingSkips.value > 0: + if world.options.NoPaintingSkips: add_rule(entrance, lambda state: has_paintings(state, world, 1)) set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), @@ -603,7 +604,7 @@ def set_expert_rules(world: "HatInTimeWorld"): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - if world.options.NoTicketSkips.value == 0: + if not world.options.NoTicketSkips: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) else: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), @@ -660,20 +661,19 @@ def set_mafia_town_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: state.has("HUMT Access", world.player), "or") - ctr_logic: int = world.options.CTRLogic.value - if ctr_logic == 3: + if world.options.CTRLogic == CTRLogic.option_nothing: set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) - elif ctr_logic == 2: + elif world.options.CTRLogic == CTRLogic.option_sprint: add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT), "or") - elif ctr_logic == 1: + elif world.options.CTRLogic == CTRLogic.option_scooter: add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) and state.has("Scooter Badge", world.player), "or") def set_botb_rules(world: "HatInTimeWorld"): - if world.options.UmbrellaLogic.value == 0 and get_difficulty(world) < Difficulty.MODERATE: + if not world.options.UmbrellaLogic and get_difficulty(world) < Difficulty.MODERATE: set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index a45d56973e..62cfc3f423 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -5,7 +5,7 @@ from .Regions import create_regions, randomize_act_entrances, chapter_act_info, from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations from .Rules import set_rules -from .Options import AHITOptions, slot_data_options, adjust_options +from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal from .Types import HatType, ChapterIndex, HatInTimeItem from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events @@ -87,24 +87,24 @@ class HatInTimeWorld(World): # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock - start_chapter: int = self.options.StartingChapter.value + start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter) - if start_chapter == 4 or start_chapter == 3: - if self.options.ActRandomizer.value == 0: - if start_chapter == 4: + if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON: + if not self.options.ActRandomizer: + if start_chapter == ChapterIndex.ALPINE: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) - if self.options.UmbrellaLogic.value > 0: + if self.options.UmbrellaLogic: self.multiworld.push_precollected(self.create_item("Umbrella")) - if start_chapter == 3 and self.options.ShuffleSubconPaintings.value > 0: + if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) def create_regions(self): # noinspection PyClassVar - self.topology_present = bool(self.options.ActRandomizer.value) + self.topology_present = bool(self.options.ActRandomizer) create_regions(self) - if self.options.EnableDeathWish.value > 0: + if self.options.EnableDeathWish: create_dw_regions(self) if self.is_dw_only(): @@ -116,7 +116,7 @@ class HatInTimeWorld(World): create_enemy_events(self) # place vanilla contract locations if contract shuffle is off - if self.options.ShuffleActContracts.value == 0: + if not self.options.ShuffleActContracts: for name in contract_locations.keys(): self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) @@ -124,9 +124,9 @@ class HatInTimeWorld(World): if self.has_yarn(): calculate_yarn_costs(self) - if self.options.RandomizeHatOrder.value > 0: + if self.options.RandomizeHatOrder: self.random.shuffle(self.hat_craft_order) - if self.options.RandomizeHatOrder.value == 2: + if self.options.RandomizeHatOrder == RandomizeHatOrder.option_time_stop_last: self.hat_craft_order.remove(HatType.TIME_STOP) self.hat_craft_order.append(HatType.TIME_STOP) @@ -141,12 +141,12 @@ class HatInTimeWorld(World): self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", self.player) - if self.options.DWEnableBonus.value == 0: + if not self.options.DWEnableBonus: for name in death_wishes: if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): continue - if self.options.DWShuffle.value > 0 and name not in self.dw_shuffle: + if self.options.DWShuffle and name not in self.dw_shuffle: continue full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) @@ -156,7 +156,7 @@ class HatInTimeWorld(World): return - if self.options.ActRandomizer.value > 0: + if self.options.ActRandomizer: randomize_act_entrances(self) set_rules(self) @@ -192,7 +192,7 @@ class HatInTimeWorld(World): slot_data.setdefault("Hat4", int(self.hat_craft_order[3])) slot_data.setdefault("Hat5", int(self.hat_craft_order[4])) - if self.options.ActRandomizer.value > 0: + if self.options.ActRandomizer: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] @@ -203,14 +203,14 @@ class HatInTimeWorld(World): if self.is_dw(): i = 0 for name in self.excluded_dws: - if self.options.EndGoal.value == 3 and name == "Seal the Deal": + if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal": continue slot_data[f"excluded_dw{i}"] = dw_classes[name] i += 1 i = 0 - if self.options.DWAutoCompleteBonuses.value == 0: + if not self.options.DWAutoCompleteBonuses: for name in self.excluded_bonuses: if name in self.excluded_dws: continue @@ -218,7 +218,7 @@ class HatInTimeWorld(World): slot_data[f"excluded_bonus{i}"] = dw_classes[name] i += 1 - if self.options.DWShuffle.value > 0: + if self.options.DWShuffle: shuffled_dws = self.dw_shuffle for i in range(len(shuffled_dws)): slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] @@ -244,7 +244,7 @@ class HatInTimeWorld(World): return slot_data def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): - if self.is_dw_only() or self.options.ActRandomizer.value == 0: + if self.is_dw_only() or not self.options.ActRandomizer: return new_hint_data = {} @@ -273,10 +273,10 @@ class HatInTimeWorld(World): new_hint_data[location.address] = get_shuffled_region(self, region_name) - if self.is_dlc1() and self.options.Tasksanity.value > 0: + if self.is_dlc1() and self.options.Tasksanity: ship_shape_region = get_shuffled_region(self, "Ship Shape") id_start: int = TASKSANITY_START_ID - for i in range(self.options.TasksanityCheckCount.value): + for i in range(self.options.TasksanityCheckCount): new_hint_data[id_start+i] = ship_shape_region hint_data[self.player] = new_hint_data @@ -289,23 +289,23 @@ class HatInTimeWorld(World): spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) def has_yarn(self) -> bool: - return not self.is_dw_only() and self.options.HatItems.value == 0 + return not self.is_dw_only() and not self.options.HatItems def is_dlc1(self) -> bool: - return self.options.EnableDLC1.value > 0 + return bool(self.options.EnableDLC1) def is_dlc2(self) -> bool: - return self.options.EnableDLC2.value > 0 + return bool(self.options.EnableDLC2) def is_dw(self) -> bool: - return self.options.EnableDeathWish.value > 0 + return bool(self.options.EnableDeathWish) def is_dw_only(self) -> bool: - return self.is_dw() and self.options.DeathWishOnly.value > 0 + return self.is_dw() and bool(self.options.DeathWishOnly) def is_dw_excluded(self, name: str) -> bool: # don't exclude Seal the Deal if it's our goal - if self.options.EndGoal.value == 3 and name == "Seal the Deal" \ + if self.options.EndGoal.value == EndGoal.option_seal_the_deal and name == "Seal the Deal" \ and f"{name} - Main Objective" not in self.options.exclude_locations: return False From 61257b12b4a4cf1bfd2e0a6a694b9987ba53a9cc Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 13 May 2024 14:39:34 -0400 Subject: [PATCH 113/143] Update worlds/ahit/DeathWishLocations.py Co-authored-by: Aaron Wagener --- worlds/ahit/DeathWishLocations.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 8f8a41c29a..631edd3598 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -195,10 +195,9 @@ def create_dw_regions(world: "HatInTimeWorld"): world.dw_shuffle = dw_shuffle prev_dw = dw_map - for i in range(len(dw_shuffle)): - name = dw_shuffle[i] - dw = create_region(world, name) - connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + for death_wish_name in dw_shuffle: + dw = create_region(world, death_wish_name) + prev_dw.connect(dw) create_dw_locations(world, dw) prev_dw = dw else: From 9a0964f1566b1992a2d8139fb1fc5a6560e55ab9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 15:05:25 -0400 Subject: [PATCH 114/143] review stuff --- worlds/ahit/Items.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 2b5ca69fdd..c2082c2e33 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -3,7 +3,7 @@ from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatI from .Locations import get_total_locations from .Rules import get_difficulty from .Options import get_total_time_pieces -from typing import Optional, List, Dict, TYPE_CHECKING +from typing import List, Dict, TYPE_CHECKING if TYPE_CHECKING: from . import HatInTimeWorld @@ -120,7 +120,7 @@ def create_item(world: "HatInTimeWorld", name: str) -> Item: def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1, - item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + item_type: ItemClassification = ItemClassification.progression) -> List[Item]: data = item_table[name] itemlist: List[Item] = [] @@ -156,11 +156,11 @@ def create_junk_items(world: "HatInTimeWorld", count: int) -> List[Item]: for i in range(count): if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: - junk_pool += [world.create_item( - world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + junk_pool.append(world.create_item( + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])) else: - junk_pool += [world.create_item( - world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + junk_pool.append(world.create_item( + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])) return junk_pool From 1450d331afbfe27bb1c03e297bbf8d194fa33d75 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 15:23:37 -0400 Subject: [PATCH 115/143] Replace connect_regions with Region.connect --- worlds/ahit/DeathWishLocations.py | 8 ++-- worlds/ahit/Regions.py | 67 ++++++++++++++----------------- worlds/ahit/Rules.py | 19 +++------ 3 files changed, 39 insertions(+), 55 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 631edd3598..704f7f5fc3 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -1,5 +1,5 @@ from .Types import HatInTimeLocation, HatInTimeItem -from .Regions import connect_regions, create_region +from .Regions import create_region from BaseClasses import Region, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule from typing import List, TYPE_CHECKING @@ -166,7 +166,7 @@ def create_dw_regions(world: "HatInTimeWorld"): spaceship = world.multiworld.get_region("Spaceship", world.player) dw_map: Region = create_region(world, "Death Wish Map") - entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + entrance = spaceship.connect(dw_map, "-> Death Wish Map") add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement)) if world.options.DWShuffle: @@ -209,11 +209,11 @@ def create_dw_regions(world: "HatInTimeWorld"): dw = create_region(world, key) if key == "Beat the Heat": - connect_regions(dw_map, dw, f"{dw_map.name} -> Beat the Heat", world.player) + dw_map.connect(dw, f"{dw_map.name} -> Beat the Heat") elif key in dw_prereqs.keys(): for name in dw_prereqs[key]: parent = world.multiworld.get_region(name, world.player) - connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + parent.connect(dw, f"{parent.name} -> {key}") create_dw_locations(world, dw) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 992c4884cb..094f258cee 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -318,9 +318,9 @@ def create_regions(world: "HatInTimeWorld"): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) - connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) + basement.connect(ev_area, "DBS Basement -> Elevator Area") if world.options.LogicDifficulty >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) + basement.connect(post_ev, "DBS Basement -> Post Elevator Area") # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -346,7 +346,7 @@ def create_regions(world: "HatInTimeWorld"): create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) - connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p) + illness.connect(alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)") create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) @@ -354,38 +354,38 @@ def create_regions(world: "HatInTimeWorld"): # ------------------------------------------- OTHER -------------------------------------------------- # mt_area: Region = create_region(w, "Mafia Town Area") mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") - connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p) - connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p) - connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p) - connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p) - connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p) - connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p) - connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p) - connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p) + mt_area.connect(mt_area_humt, "MT Area -> MT Area (HUMT)") + mt_act1.connect(mt_area, "Mafia Town Entrance WTMT") + mt_act2.connect(mt_area, "Mafia Town Entrance BB") + mt_act3.connect(mt_area, "Mafia Town Entrance SCFOS") + mt_act4.connect(mt_area, "Mafia Town Entrance DWTM") + mt_act5.connect(mt_area, "Mafia Town Entrance CTR") + mt_act6.connect(mt_area_humt, "Mafia Town Entrance HUMT") + mt_act7.connect(mt_area, "Mafia Town Entrance TGV") create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) create_rift_connections(w, create_region(w, "Time Rift - Sewers")) create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) sf_area: Region = create_region(w, "Subcon Forest Area") - connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p) - connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p) - connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p) - connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p) - connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p) + sf_act1.connect(sf_area, "Subcon Forest Entrance CO") + sf_act2.connect(sf_area, "Subcon Forest Entrance SW") + sf_act3.connect(sf_area, "Subcon Forest Entrance TOD") + sf_act4.connect(sf_area, "Subcon Forest Entrance QVM") + sf_act5.connect(sf_area, "Subcon Forest Entrance MDS") create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) create_rift_connections(w, create_region(w, "Time Rift - Pipe")) create_rift_connections(w, create_region(w, "Time Rift - Village")) badge_seller = create_badge_seller(w) - connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) - connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) - connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) - connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p) - connect_regions(pp, badge_seller, "PP -> Badge Seller", p) - connect_regions(tr, badge_seller, "TR -> Badge Seller", p) - connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p) + mt_area.connect(badge_seller, "MT Area -> Badge Seller") + mt_area_humt.connect(badge_seller, "MT Area (HUMT) -> Badge Seller") + sf_area.connect(badge_seller, "SF Area -> Badge Seller") + dbs.connect(badge_seller, "DBS -> Badge Seller") + pp.connect(badge_seller, "PP -> Badge Seller") + tr.connect(badge_seller, "TR -> Badge Seller") + alpine_area_tihs.connect(badge_seller, "ASA -> Badge Seller") times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) @@ -399,9 +399,9 @@ def create_regions(world: "HatInTimeWorld"): ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) - connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p) - connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p) - connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) + ac_act1.connect(cruise_ship, "Cruise Ship Entrance BV") + ac_act2.connect(cruise_ship, "Cruise Ship Entrance SS") + ac_act3.connect(cruise_ship, "Cruise Ship Entrance RTB") create_rift_connections(w, create_region(w, "Time Rift - Balcony")) create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) @@ -411,7 +411,7 @@ def create_regions(world: "HatInTimeWorld"): if w.options.Tasksanity: create_tasksanity_locations(w) - connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) + cruise_ship.connect(badge_seller, "CS -> Badge Seller") if w.is_dlc2(): nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) @@ -435,7 +435,7 @@ def create_rift_connections(world: "HatInTimeWorld", region: Region): for i, name in enumerate(rift_access_regions[region.name]): act_region = world.multiworld.get_region(name, world.player) entrance_name = f"{region.name} Portal - Entrance {i+1}" - connect_regions(act_region, region, entrance_name, world.player) + act_region.connect(region, entrance_name) def create_tasksanity_locations(world: "HatInTimeWorld"): @@ -669,7 +669,7 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R if len(time_rift.entrances) > 0: entrance = time_rift.entrances[i-1] else: - entrance = connect_regions(time_rift, exit_region, name, world.player) + entrance = time_rift.connect(exit_region, name) # noinspection PyUnboundLocalVariable reconnect_regions(entrance, entrance.parent_region, exit_region) @@ -805,13 +805,6 @@ def create_badge_seller(world: "HatInTimeWorld") -> Region: return badge_seller -def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: - entrance = Entrance(player, entrancename, start_region) - start_region.exits.append(entrance) - entrance.connect(exit_region) - return entrance - - # Takes an entrance, removes its old connections, and reconnects it between the two regions specified. def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region): if entrance in entrance.connected_region.entrances: @@ -845,7 +838,7 @@ def create_region_and_connect(world: "HatInTimeWorld", entrance_region = reg exit_region = connected_region - connect_regions(entrance_region, exit_region, entrancename, world.player) + entrance_region.connect(exit_region, entrancename) return reg diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index c535f11936..b2e2ca7989 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -329,7 +329,7 @@ def set_rules(world: "HatInTimeWorld"): access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region name: str = f"{key}: Connection {i}" - new_entrance: Entrance = connect_regions(required_region, region, name, world.player) + new_entrance: Entrance = required_region.connect(region, name) entrances.append(new_entrance) # Copy access rules from act completions @@ -576,9 +576,9 @@ def set_expert_rules(world: "HatInTimeWorld"): lambda state: True) # Expert: Cherry Hovering - entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), - world.multiworld.get_region("Subcon Forest Area", world.player), - "Subcon Forest Entrance YCHE", world.player) + subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player) + yche = world.multiworld.get_region("Your Contract has Expired", world.player) + entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE") if world.options.NoPaintingSkips: add_rule(entrance, lambda state: has_paintings(state, world, 1)) @@ -596,9 +596,7 @@ def set_expert_rules(world: "HatInTimeWorld"): lambda state: has_paintings(state, world, 3, True)) # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him - connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), - world.multiworld.get_region("Your Contract has Expired", world.player), - "Snatcher Hover", world.player) + subcon_area.connect(yche, "Snatcher Hover") set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), lambda state: True) @@ -961,10 +959,3 @@ def set_event_rules(world: "HatInTimeWorld"): if data.act_event: add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) - - -def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: - entrance = Entrance(player, entrancename, start_region) - start_region.exits.append(entrance) - entrance.connect(exit_region) - return entrance From e33c5cc352dcf2c3e05474eaf699c964b64b5e39 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 15:32:42 -0400 Subject: [PATCH 116/143] review stuff --- worlds/ahit/DeathWishLocations.py | 2 +- worlds/ahit/Regions.py | 165 +++++++++++++++--------------- 2 files changed, 82 insertions(+), 85 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 704f7f5fc3..9a072dc213 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -240,4 +240,4 @@ def create_dw_locations(world: "HatInTimeWorld", dw: Region): full_clear.progress_type = LocationProgressType.EXCLUDED dw.locations.append(main_objective) - dw.locations.append(full_clear) \ No newline at end of file + dw.locations.append(full_clear) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 094f258cee..9bf6e10159 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -278,82 +278,79 @@ blacklisted_combos = { def create_regions(world: "HatInTimeWorld"): - w = world - p = world.player - # ------------------------------------------- HUB -------------------------------------------------- # - menu = create_region(w, "Menu") - spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + menu = create_region(world, "Menu") + spaceship = create_region_and_connect(world, "Spaceship", "Save File -> Spaceship", menu) # we only need the menu and the spaceship regions if world.is_dw_only(): return - create_rift_connections(w, create_region(w, "Time Rift - Gallery")) - create_rift_connections(w, create_region(w, "Time Rift - The Lab")) + create_rift_connections(world, create_region(world, "Time Rift - Gallery")) + create_rift_connections(world, create_region(world, "Time Rift - The Lab")) # ------------------------------------------- MAFIA TOWN ------------------------------------------- # - mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship) - mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) - mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town) - mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) - mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) - mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) - mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town) - mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town) + mafia_town = create_region_and_connect(world, "Mafia Town", "Telescope -> Mafia Town", spaceship) + mt_act1 = create_region_and_connect(world, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) + mt_act2 = create_region_and_connect(world, "Barrel Battle", "Mafia Town - Act 2", mafia_town) + mt_act3 = create_region_and_connect(world, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) + mt_act4 = create_region_and_connect(world, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) + mt_act6 = create_region_and_connect(world, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) + mt_act5 = create_region_and_connect(world, "Cheating the Race", "Mafia Town - Act 5", mafia_town) + mt_act7 = create_region_and_connect(world, "The Golden Vault", "Mafia Town - Act 7", mafia_town) # ------------------------------------------- BOTB ------------------------------------------------- # - botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) - dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) - create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) - pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) - tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) - create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) - create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) - basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) - create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) - create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) - create_rift_connections(w, create_region(w, "Time Rift - The Moon")) + botb = create_region_and_connect(world, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) + dbs = create_region_and_connect(world, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) + create_region_and_connect(world, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) + pp = create_region_and_connect(world, "Picture Perfect", "Battle of the Birds - Act 3", botb) + tr = create_region_and_connect(world, "Train Rush", "Battle of the Birds - Act 4", botb) + create_region_and_connect(world, "The Big Parade", "Battle of the Birds - Act 5", botb) + create_region_and_connect(world, "Award Ceremony", "Battle of the Birds - Finale A", botb) + basement = create_region_and_connect(world, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + create_rift_connections(world, create_region(world, "Time Rift - Dead Bird Studio")) + create_rift_connections(world, create_region(world, "Time Rift - The Owl Express")) + create_rift_connections(world, create_region(world, "Time Rift - The Moon")) # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert - ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + ev_area = create_region_and_connect(world, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) + post_ev = create_region_and_connect(world, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) basement.connect(ev_area, "DBS Basement -> Elevator Area") if world.options.LogicDifficulty >= int(Difficulty.EXPERT): basement.connect(post_ev, "DBS Basement -> Post Elevator Area") # ------------------------------------------- SUBCON FOREST --------------------------------------- # - subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) - sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) - sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) - sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) - sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) - sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) - create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + subcon_forest = create_region_and_connect(world, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) + sf_act1 = create_region_and_connect(world, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) + sf_act2 = create_region_and_connect(world, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) + sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) + sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) + sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) + create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # - alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) - alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) - alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) + alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) + alpine_freeroam = create_region_and_connect(world, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) + alpine_area = create_region_and_connect(world, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness - alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + alpine_area_tihs = create_region_and_connect(world, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", alpine_area) - create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) - create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) - create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area) - create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) + create_region_and_connect(world, "The Birdhouse", "-> The Birdhouse", alpine_area) + create_region_and_connect(world, "The Lava Cake", "-> The Lava Cake", alpine_area) + create_region_and_connect(world, "The Windmill", "-> The Windmill", alpine_area) + create_region_and_connect(world, "The Twilight Bell", "-> The Twilight Bell", alpine_area) - illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + illness = create_region_and_connect(world, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) illness.connect(alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)") - create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) - create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) - create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) + create_rift_connections(world, create_region(world, "Time Rift - Alpine Skyline")) + create_rift_connections(world, create_region(world, "Time Rift - The Twilight Bell")) + create_rift_connections(world, create_region(world, "Time Rift - Curly Tail Trail")) # ------------------------------------------- OTHER -------------------------------------------------- # - mt_area: Region = create_region(w, "Mafia Town Area") - mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") + mt_area: Region = create_region(world, "Mafia Town Area") + mt_area_humt: Region = create_region(world, "Mafia Town Area (HUMT)") mt_area.connect(mt_area_humt, "MT Area -> MT Area (HUMT)") mt_act1.connect(mt_area, "Mafia Town Entrance WTMT") mt_act2.connect(mt_area, "Mafia Town Entrance BB") @@ -363,22 +360,22 @@ def create_regions(world: "HatInTimeWorld"): mt_act6.connect(mt_area_humt, "Mafia Town Entrance HUMT") mt_act7.connect(mt_area, "Mafia Town Entrance TGV") - create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) - create_rift_connections(w, create_region(w, "Time Rift - Sewers")) - create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) + create_rift_connections(world, create_region(world, "Time Rift - Mafia of Cooks")) + create_rift_connections(world, create_region(world, "Time Rift - Sewers")) + create_rift_connections(world, create_region(world, "Time Rift - Bazaar")) - sf_area: Region = create_region(w, "Subcon Forest Area") + sf_area: Region = create_region(world, "Subcon Forest Area") sf_act1.connect(sf_area, "Subcon Forest Entrance CO") sf_act2.connect(sf_area, "Subcon Forest Entrance SW") sf_act3.connect(sf_area, "Subcon Forest Entrance TOD") sf_act4.connect(sf_area, "Subcon Forest Entrance QVM") sf_act5.connect(sf_area, "Subcon Forest Entrance MDS") - create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) - create_rift_connections(w, create_region(w, "Time Rift - Pipe")) - create_rift_connections(w, create_region(w, "Time Rift - Village")) + create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon")) + create_rift_connections(world, create_region(world, "Time Rift - Pipe")) + create_rift_connections(world, create_region(world, "Time Rift - Village")) - badge_seller = create_badge_seller(w) + badge_seller = create_badge_seller(world) mt_area.connect(badge_seller, "MT Area -> Badge Seller") mt_area_humt.connect(badge_seller, "MT Area (HUMT) -> Badge Seller") sf_area.connect(badge_seller, "SF Area -> Badge Seller") @@ -387,48 +384,48 @@ def create_regions(world: "HatInTimeWorld"): tr.connect(badge_seller, "TR -> Badge Seller") alpine_area_tihs.connect(badge_seller, "ASA -> Badge Seller") - times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) - create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) + times_end = create_region_and_connect(world, "Time's End", "Telescope -> Time's End", spaceship) + create_region_and_connect(world, "The Finale", "Time's End - Act 1", times_end) # ------------------------------------------- DLC1 ------------------------------------------------- # - if w.is_dlc1(): - arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) - cruise_ship = create_region(w, "Cruise Ship") + if world.is_dlc1(): + arctic_cruise = create_region_and_connect(world, "The Arctic Cruise", "Telescope -> Arctic Cruise", spaceship) + cruise_ship = create_region(world, "Cruise Ship") - ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) - ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) - ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) + ac_act1 = create_region_and_connect(world, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) + ac_act2 = create_region_and_connect(world, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) + ac_act3 = create_region_and_connect(world, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) ac_act1.connect(cruise_ship, "Cruise Ship Entrance BV") ac_act2.connect(cruise_ship, "Cruise Ship Entrance SS") ac_act3.connect(cruise_ship, "Cruise Ship Entrance RTB") - create_rift_connections(w, create_region(w, "Time Rift - Balcony")) - create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) + create_rift_connections(world, create_region(world, "Time Rift - Balcony")) + create_rift_connections(world, create_region(world, "Time Rift - Deep Sea")) - if not w.options.ExcludeTour: - create_rift_connections(w, create_region(w, "Time Rift - Tour")) + if not world.options.ExcludeTour: + create_rift_connections(world, create_region(world, "Time Rift - Tour")) - if w.options.Tasksanity: - create_tasksanity_locations(w) + if world.options.Tasksanity: + create_tasksanity_locations(world) cruise_ship.connect(badge_seller, "CS -> Badge Seller") - if w.is_dlc2(): - nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) - metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) - create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) + if world.is_dlc2(): + nyakuza = create_region_and_connect(world, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) + metro_freeroam = create_region_and_connect(world, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza) + create_region_and_connect(world, "Rush Hour", "Nyakuza Metro - Finale", nyakuza) - yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) - green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam) - pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) - create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole + yellow = create_region_and_connect(world, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) + green = create_region_and_connect(world, "Green Clean Station", "-> Green Clean Station", metro_freeroam) + pink = create_region_and_connect(world, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) + create_region_and_connect(world, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole - create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) - create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green) - create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) + create_region_and_connect(world, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) + create_region_and_connect(world, "Green Clean Manhole", "-> Green Clean Manhole", green) + create_region_and_connect(world, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) - create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) - create_thug_shops(w) + create_rift_connections(world, create_region(world, "Time Rift - Rumbi Factory")) + create_thug_shops(world) def create_rift_connections(world: "HatInTimeWorld", region: Region): From e7c2bea61344e6ea758cb2909da6f7d402c9b361 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 15:49:03 -0400 Subject: [PATCH 117/143] Remove unnecessary Optional from LocData --- worlds/ahit/Rules.py | 5 ++--- worlds/ahit/Types.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b2e2ca7989..42d1f6a547 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -229,7 +229,7 @@ def set_rules(world: "HatInTimeWorld"): and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) if world.is_dlc1(): - add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), + add_rule(world.multiworld.get_entrance("Telescope -> Arctic Cruise", world.player), lambda state: state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.ALPINE]) and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.CRUISE])) @@ -253,8 +253,7 @@ def set_rules(world: "HatInTimeWorld"): loc = world.multiworld.get_location(key, world.player) for hat in data.required_hats: - if hat is not HatType.NONE: - add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) if data.hookshot: add_rule(loc, lambda state: can_use_hookshot(state, world)) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index fb6185188f..093413a0cb 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -55,21 +55,21 @@ class Difficulty(IntEnum): class LocData(NamedTuple): - id: Optional[int] = 0 - region: Optional[str] = "" - required_hats: Optional[List[HatType]] = [HatType.NONE] - hookshot: Optional[bool] = False - dlc_flags: Optional[HatDLC] = HatDLC.none - paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle - misc_required: Optional[List[str]] = [] + id: int = 0 + region: str = "" + required_hats: List[HatType] = [] + hookshot: bool = False + dlc_flags: HatDLC = HatDLC.none + paintings: int = 0 # Paintings required for Subcon painting shuffle + misc_required: List[str] = [] # For UmbrellaLogic setting only. - hit_type: Optional[HitType] = HitType.none + hit_type: HitType = HitType.none # Other - act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion - nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) - snatcher_coin: Optional[str] = "" # Only for Snatcher Coin event locations, name of the Snatcher Coin item + act_event: bool = False # Only used for event locations. Copy access rule from act completion + nyakuza_thug: str = "" # Name of Nyakuza thug NPC (for metro shops) + snatcher_coin: str = "" # Only for Snatcher Coin event locations, name of the Snatcher Coin item class ItemData(NamedTuple): From ba0c1842f6dc8e83c9a64a2f72d81fae05eea241 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 15:54:58 -0400 Subject: [PATCH 118/143] Remove HatType.NONE --- worlds/ahit/Types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py index 093413a0cb..468cfcb78a 100644 --- a/worlds/ahit/Types.py +++ b/worlds/ahit/Types.py @@ -12,7 +12,6 @@ class HatInTimeItem(Item): class HatType(IntEnum): - NONE = -1 SPRINT = 0 BREWING = 1 ICE = 2 From 1244477473085a4bbb59f42a076ba2e66fba1592 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 13 May 2024 16:02:54 -0400 Subject: [PATCH 119/143] Update worlds/ahit/test/TestActs.py Co-authored-by: Aaron Wagener --- worlds/ahit/test/TestActs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index b49259c947..c8b9b145e8 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -4,8 +4,7 @@ from . import HatInTimeTestBase class TestActs(HatInTimeTestBase): - def run_default_tests(self) -> bool: - return False + run_default_tests = False def testAllStateCanReachEverything(self): pass From 1f148cb3d88c57076aeaf06e89b4b078697df820 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 16:04:25 -0400 Subject: [PATCH 120/143] fix so default tests actually don't run --- worlds/ahit/test/TestActs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index c8b9b145e8..14f49f2ded 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -5,9 +5,6 @@ from . import HatInTimeTestBase class TestActs(HatInTimeTestBase): run_default_tests = False - - def testAllStateCanReachEverything(self): - pass options = { "ActRandomizer": 2, From f61ab0442fde6c26654471e6f5f30fb85e54c49a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 17:04:35 -0400 Subject: [PATCH 121/143] Improve performance for death wish rules --- worlds/ahit/DeathWishLocations.py | 2 +- worlds/ahit/DeathWishRules.py | 80 +++---------------------------- worlds/ahit/__init__.py | 42 +++++++++++++++- 3 files changed, 48 insertions(+), 76 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 9a072dc213..ef74cadcaa 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -230,7 +230,7 @@ def create_dw_locations(world: "HatInTimeWorld", dw: Region): dw.locations.append(bonus_stamps) main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {dw.name}", ItemClassification.progression, None, world.player)) - bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {dw.name}", + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamp - {dw.name}", ItemClassification.progression, None, world.player)) if dw.name in world.excluded_dws: diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 1825795f32..96638cf8db 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -5,8 +5,7 @@ from .DeathWishLocations import dw_prereqs, dw_candles from BaseClasses import Entrance, Location, ItemClassification from worlds.generic.Rules import add_rule, set_rule from typing import List, Callable, TYPE_CHECKING -from .Regions import act_chapters -from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes +from .Locations import death_wishes from .Options import EndGoal if TYPE_CHECKING: @@ -121,7 +120,7 @@ def set_dw_rules(world: "HatInTimeWorld"): dw = world.multiworld.get_region(name, world.player) if not world.options.DWShuffle and name in dw_stamp_costs.keys(): for entrance in dw.entrances: - add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + add_rule(entrance, lambda state, n=name: state.has("Stamps", world.player, dw_stamp_costs[n])) main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) all_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) @@ -259,36 +258,13 @@ def modify_dw_rules(world: "HatInTimeWorld", name: str): set_candle_dw_rules(name, world) -def get_total_dw_stamps(state: CollectionState, world: "HatInTimeWorld") -> int: - if world.options.DWShuffle: - return 999 # no stamp costs in death wish shuffle - - count = 0 - - for name in death_wishes: - if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): - continue - - if state.has(f"1 Stamp - {name}", world.player): - count += 1 - else: - continue - - if state.has(f"2 Stamps - {name}", world.player): - count += 2 - elif name not in dw_candles: - count += 1 - - return count - - def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) if name == "Zero Jumps": - add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) - add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + add_rule(main_objective, lambda state: state.has("Zero Jumps", world.player)) + add_rule(full_clear, lambda state: state.has("Zero Jumps", world.player, 4) and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) # No Ice Hat/painting required in Expert for Toilet Zero Jumps @@ -306,11 +282,11 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): elif name == "Snatcher's Hit List": add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) - add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + add_rule(full_clear, lambda state: state.has("Enemy", world.player, 12)) elif name == "Camera Tourist": - add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) - add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) + add_rule(main_objective, lambda state: state.has("Enemy", world.player, 8)) + add_rule(full_clear, lambda state: state.has("Boss", world.player, 6) and state.has("Triple Enemy Photo", world.player)) elif "Snatcher Coins" in name: @@ -325,48 +301,6 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): or state.has(coins[2], world.player)) -def get_zero_jump_clear_count(state: CollectionState, world: "HatInTimeWorld") -> int: - total = 0 - - for name in act_chapters.keys(): - n = f"{name} (Zero Jumps)" - if n not in zero_jumps: - continue - - if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard: - continue - - if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert: - continue - - if not state.has(n, world.player): - continue - - total += 1 - - return total - - -def get_reachable_enemy_count(state: CollectionState, world: "HatInTimeWorld") -> int: - count = 0 - for enemy in hit_list.keys(): - if enemy in bosses: - continue - - if state.has(enemy, world.player): - count += 1 - - return count - - -def can_reach_all_bosses(state: CollectionState, world: "HatInTimeWorld") -> bool: - for boss in bosses: - if not state.has(boss, world.player): - return False - - return True - - def create_enemy_events(world: "HatInTimeWorld"): no_tourist = "Camera Tourist" in world.excluded_dws for enemy, regions in hit_list.items(): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 62cfc3f423..759e9e52e2 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -8,8 +8,8 @@ from .Rules import set_rules from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal from .Types import HatType, ChapterIndex, HatInTimeItem from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes -from .DeathWishRules import set_dw_rules, create_enemy_events -from worlds.AutoWorld import World, WebWorld +from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses +from worlds.AutoWorld import World, WebWorld, CollectionState from typing import List, Dict, TextIO from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type from Utils import local_path @@ -288,6 +288,44 @@ class HatInTimeWorld(World): for hat in self.hat_craft_order: spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, self.hat_yarn_costs[hat])) + def collect(self, state: "CollectionState", item: "Item") -> bool: + old_count: int = state.count(item.name, self.player) + change = super().collect(state, item) + if change and old_count == 0: + if "Stamp" in item.name: + if "2 Stamp" in item.name: + state.prog_items[self.player]["Stamps"] += 2 + else: + state.prog_items[self.player]["Stamps"] += 1 + elif "(Zero Jumps)" in item.name: + state.prog_items[self.player]["Zero Jumps"] += 1 + elif item.name in hit_list.keys(): + if item.name not in bosses: + state.prog_items[self.player]["Enemy"] += 1 + else: + state.prog_items[self.player]["Boss"] += 1 + + return change + + def remove(self, state: "CollectionState", item: "Item") -> bool: + old_count: int = state.count(item.name, self.player) + change = super().collect(state, item) + if change and old_count == 1: + if "Stamp" in item.name: + if "2 Stamp" in item.name: + state.prog_items[self.player]["Stamps"] -= 2 + else: + state.prog_items[self.player]["Stamps"] -= 1 + elif "(Zero Jumps)" in item.name: + state.prog_items[self.player]["Zero Jumps"] -= 1 + elif item.name in hit_list.keys(): + if item.name not in bosses: + state.prog_items[self.player]["Enemy"] -= 1 + else: + state.prog_items[self.player]["Boss"] -= 1 + + return change + def has_yarn(self) -> bool: return not self.is_dw_only() and not self.options.HatItems From 0e54140d02c314937bbe2466b2ad14e383bdd1e2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 17:23:54 -0400 Subject: [PATCH 122/143] rename test file --- worlds/ahit/DeathWishRules.py | 3 +-- worlds/ahit/test/{TestActs.py => test_acts.py} | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename worlds/ahit/test/{TestActs.py => test_acts.py} (100%) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 96638cf8db..4070017eab 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -189,8 +189,7 @@ def add_dw_rules(world: "HatInTimeWorld", loc: Location): add_rule(loc, lambda state: can_use_hookshot(state, world)) for hat in data.required_hats: - if hat is not HatType.NONE: - add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) for misc in data.misc_required: add_rule(loc, lambda state, item=misc: state.has(item, world.player)) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/test_acts.py similarity index 100% rename from worlds/ahit/test/TestActs.py rename to worlds/ahit/test/test_acts.py From 850dfba66841276c9cb69222d042cc7bcdce28ed Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 17:47:50 -0400 Subject: [PATCH 123/143] change test imports --- worlds/ahit/test/test_acts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/test/test_acts.py b/worlds/ahit/test/test_acts.py index 14f49f2ded..008553d247 100644 --- a/worlds/ahit/test/test_acts.py +++ b/worlds/ahit/test/test_acts.py @@ -1,5 +1,5 @@ -from worlds.ahit.Regions import act_chapters -from worlds.ahit.Rules import act_connections +from ..Regions import act_chapters +from ..Rules import act_connections from . import HatInTimeTestBase From e061adf77f0e9abff21d798fcb83559237876c1d Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 18:18:12 -0400 Subject: [PATCH 124/143] 1000 is probably unnecessary --- worlds/ahit/test/test_acts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/test/test_acts.py b/worlds/ahit/test/test_acts.py index 008553d247..6502db1d9e 100644 --- a/worlds/ahit/test/test_acts.py +++ b/worlds/ahit/test/test_acts.py @@ -14,7 +14,7 @@ class TestActs(HatInTimeTestBase): } def test_act_shuffle(self): - for i in range(1000): + for i in range(300): self.world_setup() self.collect_all_but([""]) From 6331ab23b21599e4f923ccf608779a780c73c239 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 19:21:12 -0400 Subject: [PATCH 125/143] a --- worlds/ahit/DeathWishRules.py | 3 +-- worlds/ahit/Regions.py | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 4070017eab..13a93058a1 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -329,8 +329,7 @@ def create_enemy_events(world: "HatInTimeWorld"): if name == "Time Rift - Tour" and (not world.is_dlc1() or world.options.ExcludeTour): continue - if world.options.DWShuffle and name in death_wishes.keys() \ - and name not in world.dw_shuffle: + if world.options.DWShuffle and name in death_wishes.keys() and name not in world.dw_shuffle: continue region = world.multiworld.get_region(name, world.player) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 9bf6e10159..25c9e86031 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -2,7 +2,7 @@ from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard -from typing import TYPE_CHECKING, List, Dict +from typing import TYPE_CHECKING, List, Dict, Optional from .Rules import set_rift_rules, get_difficulty from .Options import ActRandomizer, EndGoal @@ -560,13 +560,12 @@ def sort_acts(act: Region) -> int: def get_first_act(world: "HatInTimeWorld") -> Region: first_chapter = get_first_chapter_region(world) - act: Region + act: Optional[Region] = None for e in first_chapter.exits: if "Act 1" in e.name or "Free Roam" in e.name: act = e.connected_region break - # noinspection PyUnboundLocalVariable return act @@ -633,7 +632,7 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled if not world.options.ShuffleActContracts: if (entrance_act.name == "Your Contract has Expired" or entrance_act.name == "The Subcon Well") \ - and exit_act.name == "Contractual Obligations": + and exit_act.name == "Contractual Obligations": return False return True @@ -668,7 +667,6 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R else: entrance = time_rift.connect(exit_region, name) - # noinspection PyUnboundLocalVariable reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 From 64f742fb427364f727cd18c4c7d19efbd0d02974 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 13 May 2024 20:08:16 -0400 Subject: [PATCH 126/143] change state.count to state.has --- worlds/ahit/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 42d1f6a547..94306b5d27 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -39,7 +39,7 @@ def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) - if world.options.HatItems: return state.has(hat_type_to_item[hat], world.player) - return state.count("Yarn", world.player) >= get_hat_cost(world, hat) + return state.has("Yarn", world.player, get_hat_cost(world, hat)) def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: @@ -74,7 +74,7 @@ def has_paintings(state: CollectionState, world: "HatInTimeWorld", count: int, a if get_difficulty(world) >= Difficulty.MODERATE: return True - return state.count("Progressive Painting Unlock", world.player) >= count + return state.has("Progressive Painting Unlock", world.player, count) def zipline_logic(world: "HatInTimeWorld") -> bool: From 39b102e58adac7f46b60a05d8ddee81d0f9a9ce7 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 15 May 2024 00:20:54 -0400 Subject: [PATCH 127/143] stuff --- worlds/ahit/Items.py | 16 +++++++++++++--- worlds/ahit/Rules.py | 16 ++++++---------- worlds/ahit/docs/setup_en.md | 4 ++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index c2082c2e33..93b3d2bb9d 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -85,9 +85,19 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): max_cost = 0 for i in range(5): - cost: int = world.random.randint(min_yarn_cost, max_yarn_cost) - world.hat_yarn_costs[HatType(i)] = cost - max_cost += cost + precollected: bool = False + hat: HatType = HatType(i) + for item in world.multiworld.precollected_items[world.player]: + if item.name == hat_type_to_item[hat]: + precollected = True + break + + if not precollected: + cost: int = world.random.randint(min_yarn_cost, max_yarn_cost) + world.hat_yarn_costs[hat] = cost + max_cost += cost + else: + world.hat_yarn_costs[hat] = 0 available_yarn: int = world.options.YarnAvailable.value if max_cost > available_yarn: diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 94306b5d27..797a5a71d0 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -39,6 +39,9 @@ def can_use_hat(state: CollectionState, world: "HatInTimeWorld", hat: HatType) - if world.options.HatItems: return state.has(hat_type_to_item[hat], world.player) + if world.hat_yarn_costs[hat] <= 0: # this means the hat was put into starting inventory + return True + return state.has("Yarn", world.player, get_hat_cost(world, hat)) @@ -52,10 +55,6 @@ def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: return cost -def can_sdj(state: CollectionState, world: "HatInTimeWorld"): - return can_use_hat(state, world, HatType.SPRINT) - - def painting_logic(world: "HatInTimeWorld") -> bool: return bool(world.options.ShuffleSubconPaintings) @@ -358,9 +357,6 @@ def set_specific_rules(world: "HatInTimeWorld"): lambda state: state.has("Time Piece", world.player, 12) and state.has("Time Piece", world.player, world.chapter_timepiece_costs[ChapterIndex.BIRDS])) - add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), - lambda state: state.has("Time Piece", world.player, 4)) - set_mafia_town_rules(world) set_botb_rules(world) set_subcon_rules(world) @@ -500,13 +496,13 @@ def set_hard_rules(world: "HatInTimeWorld"): # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") + lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or") add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") + lambda state: has_paintings(state, world, 3) and can_use_hat(state, world, HatType.SPRINT), "or") add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), - lambda state: can_sdj(state, world), "or") + lambda state: can_use_hat(state, world, HatType.SPRINT), "or") # Hard: Goat Refinery from TIHS with nothing add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index fe4157a496..509869fc25 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -6,6 +6,10 @@ - [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601) +## Optional Software +- [A Hat in Time Archipelago Map Tracker](https://github.com/Mysteryem/ahit-poptracker/releases), for use with [PopTracker](https://github.com/black-sliver/PopTracker/releases) + + ## Instructions 1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) From 4027d2f96d0267bcf8af7edfef103b93f0498e89 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 15 May 2024 16:44:02 -0400 Subject: [PATCH 128/143] starting inventory hats fix --- worlds/ahit/Items.py | 18 ++++-------------- worlds/ahit/Rules.py | 4 ---- worlds/ahit/__init__.py | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 93b3d2bb9d..6513d7800c 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -2,7 +2,7 @@ from BaseClasses import Item, ItemClassification from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem from .Locations import get_total_locations from .Rules import get_difficulty -from .Options import get_total_time_pieces +from .Options import get_total_time_pieces, CTRLogic from typing import List, Dict, TYPE_CHECKING if TYPE_CHECKING: @@ -39,10 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: continue else: if name == "Scooter Badge": - if world.options.CTRLogic or get_difficulty(world) >= Difficulty.MODERATE: - item_type = ItemClassification.progression - elif name == "No Bonk Badge": - if get_difficulty(world) >= Difficulty.MODERATE: + if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression # some death wish bonuses require one hit hero + hookshot @@ -58,8 +55,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: if name in alps_hooks.keys() and not world.options.ShuffleAlpineZiplines: continue - if name == "Progressive Painting Unlock" \ - and not world.options.ShuffleSubconPaintings: + if name == "Progressive Painting Unlock" and not world.options.ShuffleSubconPaintings: continue if world.options.StartWithCompassBadge and name == "Compass Badge": @@ -85,14 +81,8 @@ def calculate_yarn_costs(world: "HatInTimeWorld"): max_cost = 0 for i in range(5): - precollected: bool = False hat: HatType = HatType(i) - for item in world.multiworld.precollected_items[world.player]: - if item.name == hat_type_to_item[hat]: - precollected = True - break - - if not precollected: + if not world.is_hat_precollected(hat): cost: int = world.random.randint(min_yarn_cost, max_yarn_cost) world.hat_yarn_costs[hat] = cost max_cost += cost diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 797a5a71d0..f88fe3f950 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -91,10 +91,6 @@ def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) -def can_surf(state: CollectionState, world: "HatInTimeWorld"): - return state.has("No Bonk Badge", world.player) - - def has_relic_combo(state: CollectionState, world: "HatInTimeWorld", relic: str) -> bool: return state.has_group(relic, world.player, len(world.item_name_groups[relic])) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 759e9e52e2..738ad955e7 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -6,7 +6,7 @@ from .Locations import location_table, contract_locations, is_location_valid, ge get_total_locations from .Rules import set_rules from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal -from .Types import HatType, ChapterIndex, HatInTimeItem +from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses from worlds.AutoWorld import World, WebWorld, CollectionState @@ -130,6 +130,13 @@ class HatInTimeWorld(World): self.hat_craft_order.remove(HatType.TIME_STOP) self.hat_craft_order.append(HatType.TIME_STOP) + # move precollected hats to the start of the list + for i in range(5): + hat = HatType(i) + if self.is_hat_precollected(hat): + self.hat_craft_order.remove(hat) + self.hat_craft_order.insert(0, hat) + self.multiworld.itempool += create_itempool(self) def set_rules(self): @@ -329,6 +336,13 @@ class HatInTimeWorld(World): def has_yarn(self) -> bool: return not self.is_dw_only() and not self.options.HatItems + def is_hat_precollected(self, hat: HatType) -> bool: + for item in self.multiworld.precollected_items[self.player]: + if item.name == hat_type_to_item[hat]: + return True + + return False + def is_dlc1(self) -> bool: return bool(self.options.EnableDLC1) From 1768246846804fb568fee0c6ba3d897f71dae277 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 15 May 2024 18:47:07 -0400 Subject: [PATCH 129/143] shouldn't have done this lol --- worlds/ahit/Items.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index 6513d7800c..3ef83fe81e 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -41,6 +41,8 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]: if name == "Scooter Badge": if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE: item_type = ItemClassification.progression + elif name == "No Bonk Badge" and world.is_dw(): + item_type = ItemClassification.progression # some death wish bonuses require one hit hero + hookshot if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): From 530b65cd647bad1346e8d15b391278198d710bf3 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 15 May 2024 20:40:05 -0400 Subject: [PATCH 130/143] make ship shape task goal equal to number of tasksanity checks if set to 0 --- worlds/ahit/Options.py | 20 +++++++++++++++----- worlds/ahit/Rules.py | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index d08fe99081..00ed716bdc 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -42,6 +42,13 @@ def adjust_options(world: "HatInTimeWorld"): if world.options.FinalChapterMinCost > total_tps: world.options.FinalChapterMinCost.value = min(50, total_tps) + if world.is_dlc1() and world.options.ShipShapeCustomTaskGoal <= 0: + # automatically determine task count based on Tasksanity settings + if world.options.Tasksanity: + world.options.ShipShapeCustomTaskGoal = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep + else: + world.options.ShipShapeCustomTaskGoal = 18 + # Don't allow Rush Hour goal if DLC2 content is disabled if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2: world.options.EndGoal.value = 1 @@ -388,7 +395,7 @@ class TasksanityTaskStep(Range): class TasksanityCheckCount(Range): """How many Tasksanity checks there will be in total.""" display_name = "Tasksanity Check Count" - range_start = 5 + range_start = 1 range_end = 30 default = 18 @@ -401,11 +408,14 @@ class ExcludeTour(Toggle): class ShipShapeCustomTaskGoal(Range): - """Change the number of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + """Change the number of tasks required to complete Ship Shape. If this option's value is 0, the number of tasks + required will be TasksanityTaskStep x TasksanityCheckCount, if Tasksanity is enabled. If Tasksanity is disabled, + it will use the game's default of 18. + This option will not affect Cruisin' for a Bruisin'.""" display_name = "Ship Shape Custom Task Goal" - range_start = 1 - range_end = 30 - default = 18 + range_start = 0 + range_end = 90 + default = 0 class EnableDLC2(Toggle): diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index f88fe3f950..d97e6d73b3 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -5,7 +5,7 @@ from .Locations import location_table, zipline_unlocks, is_location_valid, contr from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType from BaseClasses import Location, Entrance, Region from typing import TYPE_CHECKING, List, Callable, Union, Dict -from .Options import EndGoal, CTRLogic +from .Options import EndGoal, CTRLogic, NoTicketSkips if TYPE_CHECKING: from . import HatInTimeWorld @@ -515,7 +515,7 @@ def set_hard_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - if not world.options.NoTicketSkips: + if world.options.NoTicketSkips is not NoTicketSkips.option_true: set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: can_use_hat(state, world, HatType.BREWING)) else: From 6951766755867a38abb9093529b2dc9a3b63fc5c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 15 May 2024 21:07:28 -0400 Subject: [PATCH 131/143] a --- worlds/ahit/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 00ed716bdc..87b3768c00 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -47,7 +47,7 @@ def adjust_options(world: "HatInTimeWorld"): if world.options.Tasksanity: world.options.ShipShapeCustomTaskGoal = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep else: - world.options.ShipShapeCustomTaskGoal = 18 + world.options.ShipShapeCustomTaskGoal.value = 18 # Don't allow Rush Hour goal if DLC2 content is disabled if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2: From dead6ebb0d2b017d7e7d1a0cd4d80b6c8b314764 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 16 May 2024 22:29:59 -0400 Subject: [PATCH 132/143] change act shuffle starting acts + logic updates --- worlds/ahit/Options.py | 10 +-- worlds/ahit/Regions.py | 184 +++++++++++++++++++++++++++++++---------- worlds/ahit/Rules.py | 17 ++-- 3 files changed, 156 insertions(+), 55 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 87b3768c00..f7c6ba953d 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -45,30 +45,30 @@ def adjust_options(world: "HatInTimeWorld"): if world.is_dlc1() and world.options.ShipShapeCustomTaskGoal <= 0: # automatically determine task count based on Tasksanity settings if world.options.Tasksanity: - world.options.ShipShapeCustomTaskGoal = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep + world.options.ShipShapeCustomTaskGoal.value = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep else: world.options.ShipShapeCustomTaskGoal.value = 18 # Don't allow Rush Hour goal if DLC2 content is disabled if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2: - world.options.EndGoal.value = 1 + world.options.EndGoal.value = EndGoal.option_finale # Don't allow Seal the Deal goal if Death Wish content is disabled if world.options.EndGoal == EndGoal.option_seal_the_deal and not world.is_dw(): - world.options.EndGoal.value = 1 + world.options.EndGoal.value = EndGoal.option_finale if world.options.DWEnableBonus: world.options.DWAutoCompleteBonuses.value = 0 if world.is_dw_only(): - world.options.EndGoal.value = 3 + world.options.EndGoal.value = EndGoal.option_seal_the_deal world.options.ActRandomizer.value = 0 world.options.ShuffleAlpineZiplines.value = 0 world.options.ShuffleSubconPaintings.value = 0 world.options.ShuffleStorybookPages.value = 0 world.options.ShuffleActContracts.value = 0 world.options.EnableDLC1.value = 0 - world.options.LogicDifficulty.value = -1 + world.options.LogicDifficulty.value = LogicDifficulty.option_normal world.options.DWTimePieceRequirement.value = 0 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 25c9e86031..2cdeab2e81 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,4 +1,4 @@ -from BaseClasses import Region, Entrance, ItemClassification, Location +from BaseClasses import Region, Entrance, ItemClassification, Location, LocationProgressType from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard @@ -10,6 +10,9 @@ if TYPE_CHECKING: from . import HatInTimeWorld +MIN_FIRST_SPHERE_LOCATIONS = 30 + + # ChapterIndex: region chapter_regions = { ChapterIndex.SPACESHIP: "Spaceship", @@ -217,17 +220,32 @@ chapter_act_info = { "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" } -# Guarantee that the first level a player can access is a location dense area beatable with no items +# Some of these may vary depending on options. See is_valid_first_act() guaranteed_first_acts = [ "Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", "Down with the Mafia!", - "Heating Up Mafia Town", # Removed in umbrella logic + "Heating Up Mafia Town", "The Golden Vault", - "Contractual Obligations", # Removed in painting logic - "Queen Vanessa's Manor", # Removed in umbrella/painting logic + "Dead Bird Studio", + "Murder on the Owl Express", + "Dead Bird Studio Basement", + + "Contractual Obligations", + "The Subcon Well", + "Queen Vanessa's Manor", + "Your Contract has Expired", + + "Rock the Boat", + + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline" + "Time Rift - Tour", + "Time Rift - Rumbi Factory", ] purple_time_rifts = [ @@ -482,29 +500,61 @@ def randomize_act_entrances(world: "HatInTimeWorld"): f"\"{name1}: {name2}\" " f"is an invalid or disallowed act plando combination!") - first_act_mapped: bool = False + # Decide what should be on the first few levels before randomizing the rest + first_acts: List[Region] = [] + first_chapter_name = chapter_regions[ChapterIndex(world.options.StartingChapter)] + first_acts.append(get_act_by_number(world, first_chapter_name, 1)) + # Chapter 3 and 4 only have one level accessible at the start + if first_chapter_name == "Mafia Town" or first_chapter_name == "Battle of the Birds": + first_acts.append(get_act_by_number(world, first_chapter_name, 2)) + first_acts.append(get_act_by_number(world, first_chapter_name, 3)) + + valid_first_acts: List[Region] = [] + for candidate in candidate_list: + if is_valid_first_act(world, candidate): + valid_first_acts.append(candidate) + + total_locations = 0 + for level in first_acts: + if level not in region_list: # make sure it hasn't been plando'd + continue + + candidate = valid_first_acts[world.random.randint(0, len(valid_first_acts)-1)] + region_list.remove(level) + candidate_list.remove(candidate) + valid_first_acts.remove(candidate) + connect_acts(world, level, candidate, rift_dict) + + # Only allow one purple rift + if candidate.name in purple_time_rifts: + for act in reversed(valid_first_acts): + if act.name in purple_time_rifts: + valid_first_acts.remove(act) + + total_locations += get_region_location_count(world, candidate.name) + if "Time Rift" not in candidate.name: + chapter = act_chapters.get(candidate.name) + if chapter == "Mafia Town": + total_locations += get_region_location_count(world, "Mafia Town Area (HUMT)") + if candidate.name != "Heating Up Mafia Town": + total_locations += get_region_location_count(world, "Mafia Town Area") + elif chapter == "Subcon Forest": + total_locations += get_region_location_count(world, "Subcon Forest Area") + elif chapter == "The Arctic Cruise": + total_locations += get_region_location_count(world, "Cruise Ship") + + # If we have enough Sphere 1 locations, we can allow the rest to be randomized + if total_locations >= MIN_FIRST_SPHERE_LOCATIONS: + break + ignore_certain_rules: bool = False while len(region_list) > 0: - region: Region - if not first_act_mapped: - region = get_first_act(world) - else: - region = region_list[0] - + region = region_list[0] candidate: Region valid_candidates: List[Region] = [] # Look for candidates to map this act to for c in candidate_list: - # Map the first act before anything - if not first_act_mapped: - if not is_valid_first_act(world, c): - continue - - valid_candidates.append(c) - first_act_mapped = True - break # we can stop here, as we only need one - if is_valid_act_combo(world, region, c, ignore_certain_rules): valid_candidates.append(c) @@ -545,7 +595,7 @@ def sort_acts(act: Region) -> int: and "Time Rift" not in act.name: return -3 - if act.name == "Contractual Obligations": + if act.name == "Contractual Obligations" or act.name == "The Subcon Well": return -2 world = act.multiworld.worlds[act.player] @@ -558,17 +608,6 @@ def sort_acts(act: Region) -> int: return 0 -def get_first_act(world: "HatInTimeWorld") -> Region: - first_chapter = get_first_chapter_region(world) - act: Optional[Region] = None - for e in first_chapter.exits: - if "Act 1" in e.name or "Free Roam" in e.name: - act = e.connected_region - break - - return act - - def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region, rift_dict: Dict[str, Region]): # Vanilla if exit_act.name == entrance_act.name: @@ -642,32 +681,66 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: if act.name not in guaranteed_first_acts: return False - # Not completable without Umbrella - if world.options.UmbrellaLogic and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + # If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels + start_chapter = world.options.StartingChapter + if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON: + if "Time Rift" in act.name: + return False + + if act_chapters[act.name] != "Mafia Town" and act_chapters[act.name] != "Subcon Forest": + return False + + if act.name in purple_time_rifts and not world.options.ShuffleStorybookPages: return False - # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings and "Subcon Forest" in act_entrances[act.name]: + diff = get_difficulty(world) + # Not completable without Umbrella? + if world.options.UmbrellaLogic: + # Needs to be at least moderate to cross the big dweller wall + if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE: + return False + elif act.name == "Your Contract has Expired" and diff < Difficulty.EXPERT: # Snatcher Hover + return False + elif act.name == "Heating Up Mafia Town": # Straight up impossible + return False + + if act.name == "Dead Bird Studio": + # No umbrella logic = moderate, umbrella logic = expert. + if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT: + return False + elif act.name == "Dead Bird Studio Basement" and (diff < Difficulty.EXPERT or world.options.FinaleShuffle): return False + elif act.name == "Rock the Boat" and (diff < Difficulty.MODERATE or world.options.FinaleShuffle): + return False + elif act.name == "The Subcon Well" and diff < Difficulty.MODERATE: + return False + elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings: + return False + + if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name and act_chapters[act.name] == "Subcon Forest": + # This requires a cherry hover to enter Subcon + if act.name == "Your Contract has Expired": + if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: + return False + else: + # Only allow Subcon levels if paintings can be skipped + if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: + return False return True def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): - count: int = len(rift_access_regions[time_rift.name]) - i: int = 1 - while i <= count: + i = 1 + while i <= len(rift_access_regions[time_rift.name]): name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance try: entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) except KeyError: - if len(time_rift.entrances) > 0: - entrance = time_rift.entrances[i-1] - else: - entrance = time_rift.connect(exit_region, name) + time_rift.connect(exit_region, name) - reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 @@ -862,6 +935,27 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: return name +def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int: + count = 0 + region = world.multiworld.get_region(region_name, world.player) + for loc in region.locations: + if loc.address is not None and (not included_only or loc.progress_type is not LocationProgressType.EXCLUDED): + count += 1 + + return count + + +def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> Region: + chapter = world.multiworld.get_region(chapter_name, world.player) + act: Optional[Region] = None + for e in chapter.exits: + if f"Act {num}" in e.name or num == 1 and "Free Roam" in e.name: + act = e.connected_region + break + + return act + + def create_thug_shops(world: "HatInTimeWorld"): min_items: int = world.options.NyakuzaThugMinShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index d97e6d73b3..71f74b17d7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -399,6 +399,10 @@ def set_moderate_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: has_paintings(state, world, 1)) + # Moderate: Village Time Rift with nothing IF umbrella logic is off + if not world.options.UmbrellaLogic: + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), lambda state: can_use_hookshot(state, world)) @@ -478,6 +482,8 @@ def set_hard_rules(world: "HatInTimeWorld"): # No Dweller Mask required set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), lambda state: has_paintings(state, world, 3)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3)) # Cherry bridge over boss arena gap (painting still expected) set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), @@ -494,9 +500,6 @@ def set_hard_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or") - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: has_paintings(state, world, 3) and can_use_hat(state, world, HatType.SPRINT), "or") - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT), "or") @@ -581,8 +584,6 @@ def set_expert_rules(world: "HatInTimeWorld"): # Set painting rules only. Skipping paintings is determined in has_paintings set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: has_paintings(state, world, 1, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: has_paintings(state, world, 3, True)) set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), lambda state: has_paintings(state, world, 3, True)) @@ -601,6 +602,12 @@ def set_expert_rules(world: "HatInTimeWorld"): and state.has("Metro Ticket - Blue", world.player) and state.has("Metro Ticket - Pink", world.player)) + # Expert: Yellow/Green Manhole with nothing using a Boop Clip + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Manhole)", world.player), + lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: True) + def set_mafia_town_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), From 0fcb1a61e2172647d988b724f7a7632ee41e6aa2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 17 May 2024 12:13:35 -0400 Subject: [PATCH 133/143] dumb --- worlds/ahit/Regions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 2cdeab2e81..6a388a98e8 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -717,7 +717,7 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings: return False - if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name and act_chapters[act.name] == "Subcon Forest": + if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": # This requires a cherry hover to enter Subcon if act.name == "Your Contract has Expired": if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: @@ -793,7 +793,7 @@ def is_valid_plando(world: "HatInTimeWorld", region: str, act: str) -> bool: return False # Don't allow plando-ing things onto the first act that aren't permitted - entrance_name = act_entrances.get(region) + entrance_name = act_entrances.get(region, "") if entrance_name != "": is_first_act: bool = act_chapters.get(region) == get_first_chapter_region(world).name \ and ("Act 1" in entrance_name or "Free Roam" in entrance_name) From 3b75bb0eba9b685f9d15a62ddbe94edbf968ab84 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 18 May 2024 02:48:27 -0400 Subject: [PATCH 134/143] option groups + lambda capture cringe + typo --- worlds/ahit/DeathWishRules.py | 2 +- worlds/ahit/Locations.py | 2 +- worlds/ahit/Options.py | 44 +++++++++++++++++++++++++++++++---- worlds/ahit/__init__.py | 3 ++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 13a93058a1..50fafd0a4d 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -292,7 +292,7 @@ def set_candle_dw_rules(name: str, world: "HatInTimeWorld"): coins: List[str] = [] for coin in required_snatcher_coins[name]: coins.append(coin) - add_rule(full_clear, lambda state: state.has(coin, world.player)) + add_rule(full_clear, lambda state, c=coin: state.has(c, world.player)) # any coin works for the main objective add_rule(main_objective, lambda state: state.has(coins[0], world.player) diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 2d8eed7be4..9954514e8f 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -964,7 +964,7 @@ snatcher_coins = { dlc_flags=HatDLC.dlc2_dw), "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", - snatcher_coin="Snatcher Coin - Bluefin Tunnel", + snatcher_coin="Snatcher Coin - Bluefin Cat Train", dlc_flags=HatDLC.dlc2_dw), "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index f7c6ba953d..12c7bde164 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,12 +1,21 @@ -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Dict, Any from schema import Schema, Optional from dataclasses import dataclass from worlds.AutoWorld import PerGameCommonOptions from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle +from BaseClasses import OptionGroup if TYPE_CHECKING: from . import HatInTimeWorld - + + +def create_option_groups() -> List[OptionGroup]: + option_group_list: List[OptionGroup] = [] + for name, options in ahit_option_groups.items(): + option_group_list.append(OptionGroup(name=name, options=options)) + + return option_group_list + def adjust_options(world: "HatInTimeWorld"): if world.options.HighestChapterCost < world.options.LowestChapterCost: @@ -342,7 +351,7 @@ class HatItems(Toggle): class MinPonCost(Range): - """The minimum number of Pons that any shop item can cost.""" + """The minimum number of Pons that any item in the Badge Seller's shop can cost.""" display_name = "Minimum Shop Pon Cost" range_start = 10 range_end = 800 @@ -350,7 +359,7 @@ class MinPonCost(Range): class MaxPonCost(Range): - """The maximum number of Pons that any shop item can cost.""" + """The maximum number of Pons that any item in the Badge Seller's shop can cost.""" display_name = "Maximum Shop Pon Cost" range_start = 10 range_end = 800 @@ -692,6 +701,33 @@ class AHITOptions(PerGameCommonOptions): death_link: DeathLink +ahit_option_groups: Dict[str, List[Any]] = { + "General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings, + MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems, LogicDifficulty, + NoPaintingSkips, CTRLogic], + + "Act Shuffle Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost, + ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost, + FinaleShuffle, ActPlando, ActBlacklist], + + "Item Options": [StartWithCompassBadge, CompassBadgeMode, RandomizeHatOrder, YarnAvailable, YarnCostMin, + YarnCostMax, MinExtraYarn, HatItems, UmbrellaLogic, MaxExtraTimePieces, YarnBalancePercent, + TimePieceBalancePercent], + + "Arctic Cruise Options": [EnableDLC1, Tasksanity, TasksanityTaskStep, TasksanityCheckCount, + ShipShapeCustomTaskGoal, ExcludeTour], + + "Nyakuza Metro Options": [EnableDLC2, MetroMinPonCost, MetroMaxPonCost, NyakuzaThugMinShopItems, + NyakuzaThugMaxShopItems, BaseballBat, NoTicketSkips], + + "Death Wish Options": [EnableDeathWish, DWTimePieceRequirement, DWShuffle, DWShuffleCountMin, DWShuffleCountMax, + DWEnableBonus, DWAutoCompleteBonuses, DWExcludeAnnoyingContracts, DWExcludeAnnoyingBonuses, + DWExcludeCandles, DeathWishOnly], + + "Trap Options": [TrapChance, BabyTrapWeight, LaserTrapWeight, ParadeTrapWeight] +} + + slot_data_options: List[str] = [ "EndGoal", "ActRandomizer", diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 738ad955e7..15140379b9 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -5,7 +5,7 @@ from .Regions import create_regions, randomize_act_entrances, chapter_act_info, from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ get_total_locations from .Rules import set_rules -from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal +from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses @@ -28,6 +28,7 @@ icon_paths['yatta'] = local_path('data', 'yatta.png') class AWebInTime(WebWorld): theme = "partyTime" + option_groups = create_option_groups() tutorials = [Tutorial( "Multiworld Setup Guide", "A guide for setting up A Hat in Time to be played in Archipelago.", From f4c75315e0298b03f646dd0c82c6dd471b8f06a9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 18 May 2024 02:54:35 -0400 Subject: [PATCH 135/143] a --- worlds/ahit/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 12c7bde164..63e1b2f09b 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -706,7 +706,7 @@ ahit_option_groups: Dict[str, List[Any]] = { MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems, LogicDifficulty, NoPaintingSkips, CTRLogic], - "Act Shuffle Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost, + "Act Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost, ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost, FinaleShuffle, ActPlando, ActBlacklist], From a543a3aa6d32b009a31f6962bdf037c3b79455c5 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 18 May 2024 03:01:17 -0400 Subject: [PATCH 136/143] b --- worlds/ahit/Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 63e1b2f09b..77ca1b62b2 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -707,8 +707,8 @@ ahit_option_groups: Dict[str, List[Any]] = { NoPaintingSkips, CTRLogic], "Act Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost, - ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost, - FinaleShuffle, ActPlando, ActBlacklist], + ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost, + FinaleShuffle, ActPlando, ActBlacklist], "Item Options": [StartWithCompassBadge, CompassBadgeMode, RandomizeHatOrder, YarnAvailable, YarnCostMin, YarnCostMax, MinExtraYarn, HatItems, UmbrellaLogic, MaxExtraTimePieces, YarnBalancePercent, From 6f6bf413ea38b8e3cccf184317aef27a23fdc3f9 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 18 May 2024 19:18:36 -0400 Subject: [PATCH 137/143] missing option in groups --- worlds/ahit/Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 77ca1b62b2..715de997c6 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -703,8 +703,8 @@ class AHITOptions(PerGameCommonOptions): ahit_option_groups: Dict[str, List[Any]] = { "General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings, - MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems, LogicDifficulty, - NoPaintingSkips, CTRLogic], + ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems, + LogicDifficulty, NoPaintingSkips, CTRLogic], "Act Options": [ActRandomizer, StartingChapter, LowestChapterCost, HighestChapterCost, ChapterCostIncrement, ChapterCostMinDifference, FinalChapterMinCost, FinalChapterMaxCost, From 2c7ff4bfe698c6c0f50703ec35ea3947a06f718b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 18 May 2024 23:04:14 -0400 Subject: [PATCH 138/143] c --- worlds/ahit/Options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 715de997c6..17c4b95efc 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -2,8 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any from schema import Schema, Optional from dataclasses import dataclass from worlds.AutoWorld import PerGameCommonOptions -from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle -from BaseClasses import OptionGroup +from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup if TYPE_CHECKING: from . import HatInTimeWorld From 445afb971e69001d6a77ff8c9939bd78594da5be Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 22 May 2024 11:33:39 -0400 Subject: [PATCH 139/143] Fix Your Contract Has Expired being placed on first level when it shouldn't --- worlds/ahit/Regions.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 6a388a98e8..72affd2aca 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -699,11 +699,14 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: # Needs to be at least moderate to cross the big dweller wall if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE: return False - elif act.name == "Your Contract has Expired" and diff < Difficulty.EXPERT: # Snatcher Hover - return False elif act.name == "Heating Up Mafia Town": # Straight up impossible return False + # Need to be able to hover + if (diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips) \ + and act.name == "Your Contract has Expired": + return False + if act.name == "Dead Bird Studio": # No umbrella logic = moderate, umbrella logic = expert. if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT: @@ -718,14 +721,9 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: return False if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest": - # This requires a cherry hover to enter Subcon - if act.name == "Your Contract has Expired": - if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: - return False - else: - # Only allow Subcon levels if paintings can be skipped - if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: - return False + # Only allow Subcon levels if painting skips are allowed + if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: + return False return True From 63411f64b85a5fccfd8fb036f97ae782f464652f Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 22 May 2024 13:08:56 -0400 Subject: [PATCH 140/143] formatting --- worlds/ahit/Regions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 72affd2aca..0ba0f5b9a5 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -703,9 +703,9 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: return False # Need to be able to hover - if (diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips) \ - and act.name == "Your Contract has Expired": - return False + if act.name == "Your Contract has Expired": + if diff < Difficulty.EXPERT or world.options.ShuffleSubconPaintings and world.options.NoPaintingSkips: + return False if act.name == "Dead Bird Studio": # No umbrella logic = moderate, umbrella logic = expert. From 814eee2e98ac67164790bf6c0b9e2c36019579ab Mon Sep 17 00:00:00 2001 From: CookieCat Date: Mon, 3 Jun 2024 10:51:19 -0400 Subject: [PATCH 141/143] major logic bug fix for death wish --- worlds/ahit/DeathWishRules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 50fafd0a4d..1432ef5c0d 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -35,7 +35,7 @@ dw_requirements = { "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), - "Rift Collapse - Deep Sea": LocData(hookshot=True), + "Rift Collapse: Deep Sea": LocData(hookshot=True), } # Includes main objective requirements @@ -55,7 +55,7 @@ dw_bonus_requirements = { "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), - "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]), + "Rift Collapse: Deep Sea": LocData(required_hats=[HatType.DWELLER]), } dw_stamp_costs = { @@ -178,9 +178,9 @@ def set_dw_rules(world: "HatInTimeWorld"): def add_dw_rules(world: "HatInTimeWorld", loc: Location): bonus: bool = "All Clear" in loc.name if not bonus: - data = dw_requirements.get(loc.name) + data = dw_requirements.get(loc.parent_region.name) else: - data = dw_bonus_requirements.get(loc.name) + data = dw_bonus_requirements.get(loc.parent_region.name) if data is None: return From af5d6ae5bc2dd3e9f474435408e02349aaa1553b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 12 Jun 2024 18:19:26 -0400 Subject: [PATCH 142/143] Update Regions.py --- worlds/ahit/Regions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0ba0f5b9a5..c6aeaa3577 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -292,6 +292,9 @@ blacklisted_combos = { # See above comment "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", "Murder on the Owl Express"], + + # was causing test failures + "Time Rift - Balcony": ["Alpine Free Roam"], } From 7689d56373011262a552d7346217a1add9c041e2 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 16 Jun 2024 10:17:40 -0400 Subject: [PATCH 143/143] Add missing indirect connections --- worlds/ahit/Rules.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 71f74b17d7..b0513c4332 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -863,6 +863,8 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) @@ -939,6 +941,7 @@ def set_default_rift_rules(world: "HatInTimeWorld"): if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, "Rock the Boat", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake"))