mirror of
				https://github.com/MarioSpore/Grinch-AP.git
				synced 2025-10-21 20:21:32 -06:00 
			
		
		
		
	A Hat in Time: Implement New Game (#2640)
Adds A Hat in Time as a supported game in Archipelago.
This commit is contained in:
		
							
								
								
									
										232
									
								
								worlds/ahit/Client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								worlds/ahit/Client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
							
								
								
									
										243
									
								
								worlds/ahit/DeathWishLocations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								worlds/ahit/DeathWishLocations.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										462
									
								
								worlds/ahit/DeathWishRules.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										462
									
								
								worlds/ahit/DeathWishRules.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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", | ||||
| ] | ||||
							
								
								
									
										302
									
								
								worlds/ahit/Items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								worlds/ahit/Items.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| } | ||||
							
								
								
									
										1057
									
								
								worlds/ahit/Locations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1057
									
								
								worlds/ahit/Locations.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										770
									
								
								worlds/ahit/Options.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										770
									
								
								worlds/ahit/Options.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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", | ||||
| ] | ||||
							
								
								
									
										1027
									
								
								worlds/ahit/Regions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1027
									
								
								worlds/ahit/Regions.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										959
									
								
								worlds/ahit/Rules.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										959
									
								
								worlds/ahit/Rules.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										86
									
								
								worlds/ahit/Types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								worlds/ahit/Types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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", | ||||
| } | ||||
							
								
								
									
										374
									
								
								worlds/ahit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								worlds/ahit/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										53
									
								
								worlds/ahit/docs/en_A Hat in Time.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								worlds/ahit/docs/en_A Hat in Time.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
							
								
								
									
										102
									
								
								worlds/ahit/docs/setup_en.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								worlds/ahit/docs/setup_en.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <message>` - 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. | ||||
							
								
								
									
										5
									
								
								worlds/ahit/test/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								worlds/ahit/test/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| from test.bases import WorldTestBase | ||||
|  | ||||
|  | ||||
| class HatInTimeTestBase(WorldTestBase): | ||||
|     game = "A Hat in Time" | ||||
							
								
								
									
										31
									
								
								worlds/ahit/test/test_acts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								worlds/ahit/test/test_acts.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)") | ||||
		Reference in New Issue
	
	Block a user
	 CookieCat
					CookieCat