A Hat in Time: Implement New Game (#2640)

Adds A Hat in Time as a supported game in Archipelago.
This commit is contained in:
CookieCat 2024-05-20 03:04:06 -04:00 committed by GitHub
parent c792ae76ca
commit fe7bc8784d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 5715 additions and 0 deletions

8
AHITClient.py Normal file
View File

@ -0,0 +1,8 @@
from worlds.ahit.Client import launch
import Utils
import ModuleUpdate
ModuleUpdate.update()
if __name__ == "__main__":
Utils.init_logging("AHITClient", exception_logger="Client")
launch()

View File

@ -69,6 +69,7 @@ Currently, the following games are supported:
* Bomb Rush Cyberfunk
* Aquaria
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
* A Hat in Time
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

BIN
data/yatta.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
data/yatta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -13,6 +13,9 @@
# Adventure
/worlds/adventure/ @JusticePS
# A Hat in Time
/worlds/ahit/ @CookieCat45
# A Link to the Past
/worlds/alttp/ @Berserker66

232
worlds/ahit/Client.py Normal file
View 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()

View 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)

View 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
View 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

File diff suppressed because it is too large Load Diff

770
worlds/ahit/Options.py Normal file
View 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

File diff suppressed because it is too large Load Diff

959
worlds/ahit/Rules.py Normal file
View 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
View 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
View 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

View 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.

View 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.

View File

@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class HatInTimeTestBase(WorldTestBase):
game = "A Hat in Time"

View 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)")