From 8939cf6f8731074487a903b912d74e0bae8243e5 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Mon, 20 May 2024 03:04:06 -0400 Subject: [PATCH] A Hat in Time: Implement New Game (#2640) Adds A Hat in Time as a supported game in Archipelago. --- AHITClient.py | 8 + README.md | 1 + data/yatta.ico | Bin 0 -> 152484 bytes data/yatta.png | Bin 0 -> 34873 bytes docs/CODEOWNERS | 3 + worlds/ahit/Client.py | 232 ++++++ worlds/ahit/DeathWishLocations.py | 243 ++++++ worlds/ahit/DeathWishRules.py | 462 +++++++++++ worlds/ahit/Items.py | 302 ++++++++ worlds/ahit/Locations.py | 1057 ++++++++++++++++++++++++++ worlds/ahit/Options.py | 770 +++++++++++++++++++ worlds/ahit/Regions.py | 1027 +++++++++++++++++++++++++ worlds/ahit/Rules.py | 959 +++++++++++++++++++++++ worlds/ahit/Types.py | 86 +++ worlds/ahit/__init__.py | 374 +++++++++ worlds/ahit/docs/en_A Hat in Time.md | 53 ++ worlds/ahit/docs/setup_en.md | 102 +++ worlds/ahit/test/__init__.py | 5 + worlds/ahit/test/test_acts.py | 31 + 19 files changed, 5715 insertions(+) create mode 100644 AHITClient.py create mode 100644 data/yatta.ico create mode 100644 data/yatta.png create mode 100644 worlds/ahit/Client.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/__init__.py create mode 100644 worlds/ahit/test/test_acts.py diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 000000000000..6ed7d7b49d48 --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,8 @@ +from worlds.ahit.Client import launch +import Utils +import ModuleUpdate +ModuleUpdate.update() + +if __name__ == "__main__": + Utils.init_logging("AHITClient", exception_logger="Client") + launch() diff --git a/README.md b/README.md index efbd5ca3316c..4633c99c664d 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Currently, the following games are supported: * Bomb Rush Cyberfunk * Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 +* 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/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/docs/CODEOWNERS b/docs/CODEOWNERS index 77432bfcd407..c34046d5dc30 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/Client.py b/worlds/ahit/Client.py new file mode 100644 index 000000000000..2cd67e468294 --- /dev/null +++ b/worlds/ahit/Client.py @@ -0,0 +1,232 @@ +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 _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/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py new file mode 100644 index 000000000000..ef74cadcaa53 --- /dev/null +++ b/worlds/ahit/DeathWishLocations.py @@ -0,0 +1,243 @@ +from .Types import HatInTimeLocation, HatInTimeItem +from .Regions import create_region +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 + + +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", + "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", +] + +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: "HatInTimeWorld"): + if world.options.DWExcludeAnnoyingContracts: + for name in annoying_dws: + world.excluded_dws.append(name) + + if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: + for name in death_wishes: + world.excluded_bonuses.append(name) + elif world.options.DWExcludeAnnoyingBonuses: + for name in annoying_bonuses: + world.excluded_bonuses.append(name) + + if world.options.DWExcludeCandles: + for name in dw_candles: + 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") + 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: + # 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 + + dw_list.append(name) + + 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): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.options.EndGoal == EndGoal.option_seal_the_deal: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.dw_shuffle = dw_shuffle + prev_dw = dw_map + 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: + # 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": + 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) + parent.connect(dw, f"{parent.name} -> {key}") + + create_dw_locations(world, dw) + + +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 Stamp - {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) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py new file mode 100644 index 000000000000..50fafd0a4d08 --- /dev/null +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,462 @@ +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, HitType +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 .Locations import death_wishes +from .Options import EndGoal + +if TYPE_CHECKING: + from . import HatInTimeWorld + + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "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_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_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), + "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: "HatInTimeWorld"): + 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: + dw_list = world.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) + if not world.options.DWShuffle and name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + 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) + 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 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)) + all_clear.show_in_spoiler = False + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + modify_dw_rules(world, name) + 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 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: + 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: + 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.options.EndGoal == EndGoal.option_seal_the_deal: + 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: + 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: + 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: + 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): + 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 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: 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 + # This painting wall can only be skipped via cherry hover. + 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)) + 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: state.has("Enemy", world.player, 12)) + + elif name == "Camera Tourist": + 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: + coins: List[str] = [] + for coin in required_snatcher_coins[name]: + coins.append(coin) + 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) + or state.has(coins[1], world.player) + or state.has(coins[2], world.player)) + + +def create_enemy_events(world: "HatInTimeWorld"): + no_tourist = "Camera Tourist" in world.excluded_dws + 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.options.ExcludeTour): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + 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) + 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.options.ExcludeTour): + continue + + 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) + 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: "HatInTimeWorld"): + 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: + 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.options.ExcludeTour): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + 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) + + 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 photo. +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 000000000000..3ef83fe81e6d --- /dev/null +++ b/worlds/ahit/Items.py @@ -0,0 +1,302 @@ +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, CTRLogic +from typing import List, Dict, TYPE_CHECKING + +if TYPE_CHECKING: + from . import HatInTimeWorld + + +def create_itempool(world: "HatInTimeWorld") -> List[Item]: + itempool: List[Item] = [] + if world.has_yarn(): + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.options.YarnAvailable.value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.options.YarnBalancePercent))): + 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 not world.options.HatItems 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.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(): + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and not world.options.ShuffleActContracts: + continue + + if name in alps_hooks.keys() and not world.options.ShuffleAlpineZiplines: + continue + + if name == "Progressive Painting Unlock" and not world.options.ShuffleSubconPaintings: + continue + + 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))): + 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: "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 = 0 + for i in range(5): + hat: HatType = HatType(i) + 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 + else: + world.hat_yarn_costs[hat] = 0 + + available_yarn: int = world.options.YarnAvailable.value + if max_cost > available_yarn: + world.options.YarnAvailable.value = max_cost + available_yarn = max_cost + + 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: + 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: "HatInTimeWorld", name: str) -> Item: + data = item_table[name] + return HatInTimeItem(name, data.classification, data.code, world.player) + + +def create_multiple_items(world: "HatInTimeWorld", name: str, count: int = 1, + item_type: 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: "HatInTimeWorld", count: int) -> List[Item]: + trap_chance = world.options.TrapChance.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.options.BabyTrapWeight.value + elif name == "Laser Trap": + trap_list[name] = world.options.LaserTrapWeight.value + elif name == "Parade Trap": + trap_list[name] = world.options.ParadeTrapWeight.value + + for i in range(count): + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: + junk_pool.append(world.create_item( + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])) + else: + junk_pool.append(world.create_item( + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])) + + 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), + + # 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), + + # 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), + "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), + # 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), + "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), + + # 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), + "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 000000000000..9954514e8f3b --- /dev/null +++ b/worlds/ahit/Locations.py @@ -0,0 +1,1057 @@ +from .Types import HatDLC, HatType, LocData, Difficulty, HitType +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: "HatInTimeWorld") -> int: + total = 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.options.Tasksanity: + total += world.options.TasksanityCheckCount + + if world.is_dw(): + if world.options.DWShuffle: + total += len(world.dw_shuffle) + if world.options.DWEnableBonus: + total += len(world.dw_shuffle) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.options.DWEnableBonus: + total += 37 + if world.is_dlc2(): + total += 1 + + return total + + +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: + 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: "HatInTimeWorld", location: str) -> bool: + if not location_dlc_enabled(world, location): + return False + + if not world.options.ShuffleStorybookPages and location in storybook_pages.keys(): + return False + + 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 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 and location in event_locs.keys(): + return False + + 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 and "Zero Jumps" not in world.dw_shuffle: + return False + + 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 < 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_type=HitType.umbrella_or_brewing), + + # 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"), + "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_type=HitType.umbrella_or_brewing), + + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", + hit_type=HitType.umbrella_or_brewing), + + "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_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"), + "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_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), + + "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_type=HitType.umbrella_or_brewing, 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)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "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)", hookshot=True, + 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)", 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"), + "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)", 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"), + "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", 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_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", 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"), + "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_type=HitType.umbrella_or_brewing, 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", + 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", + 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"), + + "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", 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 +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 (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), + + "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_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_type=HitType.umbrella_or_brewing, 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_type=HitType.umbrella_or_brewing, + 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_type=HitType.umbrella_or_brewing, + 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", + 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_type=HitType.umbrella_or_brewing, + 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), +} + +snatcher_coins = { + "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 (CTR)": LocData(0, "Cheating the Race", snatcher_coin="Snatcher Coin - Top of HQ", + 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 HQ (TGV)": LocData(0, "The Golden Vault", snatcher_coin="Snatcher Coin - Top of HQ", + 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 - 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 - Swamp Tree (Speedrun Well)": LocData(0, "Speedrun Well", + snatcher_coin="Snatcher Coin - Swamp Tree", + hookshot=True, dlc_flags=HatDLC.death_wish), + + "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 - Giant Time Piece": LocData(0, "Subcon Forest Area", + snatcher_coin="Snatcher Coin - Giant Time Piece", + paintings=3, 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 - 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 - Lava Cake": LocData(0, "The Lava Cake", snatcher_coin="Snatcher Coin - Lava Cake", + dlc_flags=HatDLC.death_wish), + + "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 Cat Train", + 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 = { + **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"), + "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), + "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 000000000000..17c4b95efc7a --- /dev/null +++ b/worlds/ahit/Options.py @@ -0,0 +1,770 @@ +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, 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: + world.options.HighestChapterCost.value, world.options.LowestChapterCost.value = \ + 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 + + if world.options.BadgeSellerMaxItems < world.options.BadgeSellerMinItems: + world.options.BadgeSellerMaxItems.value, world.options.BadgeSellerMinItems.value = \ + world.options.BadgeSellerMinItems.value, world.options.BadgeSellerMaxItems.value + + if world.options.NyakuzaThugMaxShopItems < world.options.NyakuzaThugMinShopItems: + world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value = \ + world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.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 > total_tps-5: + world.options.HighestChapterCost.value = min(45, total_tps-5) + + if world.options.LowestChapterCost > total_tps-5: + world.options.LowestChapterCost.value = min(45, total_tps-5) + + if world.options.FinalChapterMaxCost > total_tps: + world.options.FinalChapterMaxCost.value = min(50, total_tps) + + 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.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 = 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 = EndGoal.option_finale + + if world.options.DWEnableBonus: + world.options.DWAutoCompleteBonuses.value = 0 + + if world.is_dw_only(): + 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 = LogicDifficulty.option_normal + world.options.DWTimePieceRequirement.value = 0 + + +def get_total_time_pieces(world: "HatInTimeWorld") -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.options.MaxExtraTimePieces, 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\" 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" + + +class LogicDifficulty(Choice): + """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 + 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(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" + + +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" + + +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" + + +class ShuffleActContracts(DefaultOnToggle): + """If enabled, shuffle Snatcher's act contracts into the pool as items""" + display_name = "Shuffle Contracts" + + +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + + +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" + + +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" + + +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 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 + 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 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" + 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" + + +class MinPonCost(Range): + """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 + default = 75 + + +class MaxPonCost(Range): + """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 + default = 300 + + +class BadgeSellerMinItems(Range): + """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 + default = 4 + + +class BadgeSellerMaxItems(Range): + """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 + 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" + + +class Tasksanity(Toggle): + """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" + display_name = "Tasksanity" + + +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 = 1 + 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" + + +class ShipShapeCustomTaskGoal(Range): + """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 = 0 + range_end = 90 + default = 0 + + +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" + + +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 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 + default = 2 + + +class NyakuzaThugMaxShopItems(Range): + """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 + 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.""" + display_name = "Baseball Bat" + + +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" + + +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" + + +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" + + +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, 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!""" + display_name = "Shuffle Death Wish Full Completions" + + +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" + + +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: + - 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" + + +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: + - 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 + - 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" + + +class DWExcludeCandles(DefaultOnToggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + + +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 + + +@dataclass +class AHITOptions(PerGameCommonOptions): + EndGoal: EndGoal + ActRandomizer: ActRandomizer + ActPlando: ActPlando + ActBlacklist: ActBlacklist + 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 + NoTicketSkips: NoTicketSkips + + 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 + + +ahit_option_groups: Dict[str, List[Any]] = { + "General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings, + ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems, + LogicDifficulty, NoPaintingSkips, CTRLogic], + + "Act 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", + "ShuffleAlpineZiplines", + "LogicDifficulty", + "CTRLogic", + "RandomizeHatOrder", + "UmbrellaLogic", + "StartWithCompassBadge", + "CompassBadgeMode", + "ShuffleStorybookPages", + "ShuffleActContracts", + "ShuffleSubconPaintings", + "NoPaintingSkips", + "HatItems", + + "EnableDLC1", + "Tasksanity", + "TasksanityTaskStep", + "TasksanityCheckCount", + "ShipShapeCustomTaskGoal", + "ExcludeTour", + + "EnableDeathWish", + "DWShuffle", + "DeathWishOnly", + "DWEnableBonus", + "DWAutoCompleteBonuses", + "DWTimePieceRequirement", + + "EnableDLC2", + "MetroMinPonCost", + "MetroMaxPonCost", + "BaseballBat", + "NoTicketSkips", + + "MinPonCost", + "MaxPonCost", + + "death_link", +] diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py new file mode 100644 index 000000000000..6a388a98e87e --- /dev/null +++ b/worlds/ahit/Regions.py @@ -0,0 +1,1027 @@ +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 +from typing import TYPE_CHECKING, List, Dict, Optional +from .Rules import set_rift_rules, get_difficulty +from .Options import ActRandomizer, EndGoal + +if TYPE_CHECKING: + from . import HatInTimeWorld + + +MIN_FIRST_SPHERE_LOCATIONS = 30 + + +# 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" +} + +# 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", + "The Golden Vault", + + "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 = [ + "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"], + + # 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"], + + "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"], + + # See above comment + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", + "Murder on the Owl Express"], +} + + +def create_regions(world: "HatInTimeWorld"): + # ------------------------------------------- HUB -------------------------------------------------- # + 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(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(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(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(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(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(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(world, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + 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(world, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + illness.connect(alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)") + 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(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") + 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(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(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(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(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") + 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(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 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(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(world, create_region(world, "Time Rift - Balcony")) + create_rift_connections(world, create_region(world, "Time Rift - Deep Sea")) + + if not world.options.ExcludeTour: + create_rift_connections(world, create_region(world, "Time Rift - Tour")) + + if world.options.Tasksanity: + create_tasksanity_locations(world) + + cruise_ship.connect(badge_seller, "CS -> Badge Seller") + + 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(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(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(world, create_region(world, "Time Rift - Rumbi Factory")) + create_thug_shops(world) + + +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}" + act_region.connect(region, entrance_name) + + +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): + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) + ship_shape.locations.append(location) + + +def randomize_act_entrances(world: "HatInTimeWorld"): + 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() + rift_dict: Dict[str, Region] = {} + + # Check if Plando's are valid, if so, map them + if world.options.ActPlando: + player_name = world.multiworld.get_player_name(world.player) + for (name1, name2) in world.options.ActPlando.items(): + region: Region + act: Region + try: + region = world.multiworld.get_region(name1, world.player) + except KeyError: + 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, act.name): + region_list.remove(region) + candidate_list.remove(act) + connect_acts(world, region, act, rift_dict) + else: + print(f"ActPlando " + f"({player_name}) - " + f"\"{name1}: {name2}\" " + f"is an invalid or disallowed act plando combination!") + + # 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_list[0] + candidate: Region + valid_candidates: List[Region] = [] + + # Look for candidates to map this act to + for c in candidate_list: + if is_valid_act_combo(world, region, c, ignore_certain_rules): + valid_candidates.append(c) + + if len(valid_candidates) > 0: + candidate = valid_candidates[world.random.randint(0, len(valid_candidates)-1)] + else: + # 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}") + + ignore_certain_rules = True + continue + + 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(): + region: Region = world.multiworld.get_region(name, world.player) + update_chapter_act_info(world, region, region) + + 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" or act.name == "The Subcon Well": + 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 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, 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 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 + + # 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 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 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 + + # 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": + return False + + return True + + +def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: + if act.name not in guaranteed_first_acts: + return False + + # 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 + + 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 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 + + return True + + +def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): + 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: + time_rift.connect(exit_region, name) + + i += 1 + + +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(): + if not is_act_blacklisted(world, region.name): + act_list.append(region) + + return act_list + + +def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: + 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 == EndGoal.option_finale + + if name == "Rush Hour": + return not plando and world.options.EndGoal == EndGoal.option_rush_hour + + if name == "Time Rift - Tour": + return bool(world.options.ExcludeTour) + + return name in blacklisted_acts.values() + + +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 + found_count = 0 + for val in world.options.ActPlando.values(): + if val == act: + found_count += 1 + + 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 + + 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 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" + 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) + + 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 not world.options.ShuffleStorybookPages: + 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: "HatInTimeWorld") -> Region: + badge_seller = Region("Badge Seller", world.player, world.multiworld) + world.multiworld.regions.append(badge_seller) + count = 0 + max_items = 0 + + if world.options.BadgeSellerMaxItems > 0: + max_items = world.random.randint(world.options.BadgeSellerMinItems.value, + world.options.BadgeSellerMaxItems.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 + + +# 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: "HatInTimeWorld", + 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 + + entrance_region.connect(exit_region, entrancename) + return reg + + +def get_first_chapter_region(world: "HatInTimeWorld") -> Region: + start_chapter: ChapterIndex = ChapterIndex(world.options.StartingChapter) + return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) + + +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: "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 + + +def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in world.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + 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 + count = -1 + step = 0 + old_name = "" + + 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.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: "HatInTimeWorld") -> int: + count = 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(): + item_name = data.snatcher_coin + 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: "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)) + return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py new file mode 100644 index 000000000000..71f74b17d7ed --- /dev/null +++ b/worlds/ahit/Rules.py @@ -0,0 +1,959 @@ +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 +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, NoTicketSkips + +if TYPE_CHECKING: + from . import HatInTimeWorld + + +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: "HatInTimeWorld", hat: HatType) -> bool: + 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)) + + +def get_hat_cost(world: "HatInTimeWorld", hat: HatType) -> int: + cost = 0 + for h in world.hat_craft_order: + cost += world.hat_yarn_costs[h] + if h == hat: + break + + return cost + + +def painting_logic(world: "HatInTimeWorld") -> bool: + return bool(world.options.ShuffleSubconPaintings) + + +# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert +def get_difficulty(world: "HatInTimeWorld") -> Difficulty: + 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 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 + + return state.has("Progressive Painting Unlock", world.player, count) + + +def zipline_logic(world: "HatInTimeWorld") -> bool: + return bool(world.options.ShuffleAlpineZiplines) + + +def can_use_hookshot(state: CollectionState, world: "HatInTimeWorld"): + return state.has("Hookshot Badge", world.player) + + +def can_hit(state: CollectionState, world: "HatInTimeWorld", umbrella_only: bool = False): + 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) + + +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: "HatInTimeWorld", relic: str) -> int: + return state.count_group(relic, world.player) + + +# 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 + + 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: "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: "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) \ + 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: "HatInTimeWorld"): + # First, chapter access + 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 + chapter_list: List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] + + final_chapter = ChapterIndex.FINALE + if world.options.EndGoal == EndGoal.option_rush_hour: + final_chapter = ChapterIndex.METRO + chapter_list.append(ChapterIndex.FINALE) + elif world.options.EndGoal == EndGoal.option_seal_the_deal: + 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) + + # 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 + 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) + + 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 + + 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 i >= 1: + if last_cost + min_difference > cost: + cost = last_cost + min_difference + + cost = min(cost, highest_cost) + world.chapter_timepiece_costs[chapter] = cost + last_cost = cost + + if final_chapter is not None: + final_chapter_cost: int + 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, + 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])) + + add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), + 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.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.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.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 -> 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])) + + if world.is_dlc2(): + add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", 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.METRO]) + and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) + + if not world.options.ActRandomizer: + set_default_rift_rules(world) + + table = {**location_table, **event_locs} + for (key, data) in table.items(): + if not is_location_valid(world, key): + continue + + if key in contract_locations.keys(): + continue + + loc = world.multiworld.get_location(key, world.player) + + for hat in data.required_hats: + 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)) + + 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: + 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(loc, 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.options.UmbrellaLogic: + 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)) + + dummy_entrances: List[Entrance] = [] + + for (key, acts) in act_connections.items(): + if "Arctic Cruise" in key and not world.is_dlc1(): + continue + + entrance: Entrance = world.multiworld.get_entrance(key, world.player) + region: Region = entrance.connected_region + access_rules: List[Callable[[CollectionState], bool]] = [] + dummy_entrances.append(entrance) + + # Entrances to this act that we have to set access_rules on + entrances: List[Entrance] = [] + + 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 + name: str = f"{key}: Connection {i}" + new_entrance: Entrance = required_region.connect(region, name) + entrances.append(new_entrance) + + # Copy access rules from act completions + if "Free Roam" not in required_region.name: + 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) + + for e in entrances: + 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.options.EndGoal == EndGoal.option_finale: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) + elif world.options.EndGoal == EndGoal.option_rush_hour: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) + + +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.chapter_timepiece_costs[ChapterIndex.BIRDS])) + + 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 >= Difficulty.EXPERT: + set_expert_rules(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) + + # 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: 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: 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)) + 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: 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 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)) + + 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(): + # 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) + 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), + 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 + Pink Paw Station without tickets + 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) + + +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) + 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)) + 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), + 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_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "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") + + # 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), + 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 + 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: + 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: "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.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) + 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: + set_rule(loc, lambda state: True) + + 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), + 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 + 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)) + + 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 - 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 + subcon_area.connect(yche, "Snatcher Hover") + 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 + 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), + 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)) + + # 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), + 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") + + if world.options.CTRLogic == CTRLogic.option_nothing: + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) + 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 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 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), + 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: "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)) + + # 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)) + + +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)) + + 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)) + + 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), + 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: "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)) + + 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: "HatInTimeWorld", region: Union[str, Region], unlocked_entrance: 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: "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.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.chapter_timepiece_costs[ChapterIndex.ALPINE])) + + for entrance in regions["Time Rift - Sewers"].entrances: + 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_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) + + 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_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_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", + 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_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_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) + + 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(): + 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(): + 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: "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.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.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_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_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_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_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) + + 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_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_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)) + + 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_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")) + + 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: "HatInTimeWorld"): + 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_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 new file mode 100644 index 000000000000..468cfcb78ad3 --- /dev/null +++ b/worlds/ahit/Types.py @@ -0,0 +1,86 @@ +from enum import IntEnum, IntFlag +from typing import NamedTuple, Optional, List +from BaseClasses import Location, Item, ItemClassification + + +class HatInTimeLocation(Location): + game = "A Hat in Time" + + +class HatInTimeItem(Item): + game = "A Hat in Time" + + +class HatType(IntEnum): + SPRINT = 0 + BREWING = 1 + ICE = 2 + DWELLER = 3 + TIME_STOP = 4 + + +class HitType(IntEnum): + none = 0 + umbrella = 1 + umbrella_or_brewing = 2 + dweller_bell = 3 + + +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: 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: HitType = HitType.none + + # Other + 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): + 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 000000000000..15140379b96f --- /dev/null +++ b/worlds/ahit/__init__.py @@ -0,0 +1,374 @@ +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, \ + 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 +from .Rules import set_rules +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 +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 + + +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') + + +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.", + "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" + 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 + 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) + + if self.options.StartWithCompassBadge: + 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: ChapterIndex = ChapterIndex(self.options.StartingChapter) + + 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: + self.multiworld.push_precollected(self.create_item("Umbrella")) + + 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) + + create_regions(self) + if self.options.EnableDeathWish: + 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.excluded_dws or "Camera Tourist" not in self.excluded_dws: + create_enemy_events(self) + + # place vanilla contract locations if contract shuffle is off + 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)) + + def create_items(self): + if self.has_yarn(): + calculate_yarn_costs(self) + + if self.options.RandomizeHatOrder: + self.random.shuffle(self.hat_craft_order) + 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) + + # 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): + 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 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 and name not in self.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.options.ActRandomizer: + 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": 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.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]) + 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: + 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 self.nyakuza_thug_items.keys(): + slot_data[name] = self.nyakuza_thug_items[name] + + if self.is_dw(): + i = 0 + for name in self.excluded_dws: + 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 not self.options.DWAutoCompleteBonuses: + 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: + shuffled_dws = self.dw_shuffle + 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) + 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) + else: + item_name = loc.item.name + + shop_item_names.setdefault(str(loc.address), 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 + + return slot_data + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only() or not self.options.ActRandomizer: + return + + new_hint_data = {} + 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(): + 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 "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: + continue + + new_hint_data[location.address] = get_shuffled_region(self, region_name) + + 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): + 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.chapter_timepiece_costs: + 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 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 + + 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) + + def is_dlc2(self) -> bool: + return bool(self.options.EnableDLC2) + + def is_dw(self) -> bool: + return bool(self.options.EnableDeathWish) + + def is_dw_only(self) -> bool: + 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 == EndGoal.option_seal_the_deal and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.options.exclude_locations: + return False + + 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 self.excluded_bonuses: + return True + + return f"{name} - All Clear" in self.options.exclude_locations 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 000000000000..9f1a593bbdd9 --- /dev/null +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -0,0 +1,53 @@ +# A Hat in Time + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items 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. + +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. + +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. + +## 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. diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md new file mode 100644 index 000000000000..509869fc256a --- /dev/null +++ b/worlds/ahit/docs/setup_en.md @@ -0,0 +1,102 @@ +# 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) + + +## 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) +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: +`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 issue 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. + + +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** +(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. + +`ap_say ` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`. + +`ap_deathlink` - Toggle Death Link. + + +## 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 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. + +### 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 diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py new file mode 100644 index 000000000000..67b750a65c7d --- /dev/null +++ b/worlds/ahit/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" diff --git a/worlds/ahit/test/test_acts.py b/worlds/ahit/test/test_acts.py new file mode 100644 index 000000000000..6502db1d9e6b --- /dev/null +++ b/worlds/ahit/test/test_acts.py @@ -0,0 +1,31 @@ +from ..Regions import act_chapters +from ..Rules import act_connections +from . import HatInTimeTestBase + + +class TestActs(HatInTimeTestBase): + run_default_tests = False + + options = { + "ActRandomizer": 2, + "EnableDLC1": 1, + "EnableDLC2": 1, + "ShuffleActContracts": 0, + } + + def test_act_shuffle(self): + for i in range(300): + 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: + 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} " + f"-> {entrance} -> {name}" + f" (expected method of access)")