1.0 Preparations

This commit is contained in:
CookieCat
2023-09-08 14:27:28 -04:00
parent 215594d094
commit 1daafff04b
8 changed files with 1568 additions and 467 deletions

View File

@@ -1,52 +1,240 @@
from .Locations import HatInTimeLocation, death_wishes
from .Regions import connect_regions, create_region
from BaseClasses import Region, LocationProgressType
from worlds.generic.Rules import add_rule
from worlds.AutoWorld import World
from typing import List
death_wishes = [
"Beat the Heat",
"So You're Back From Outer Space",
"Mafia's Jumps",
"Collect-a-thon",
"She Speedran from Outerspace",
"Vault Codes in the Wind",
"Encore! Encore!",
"Rift Collapse: Mafia of Cooks",
dw_prereqs = {
"So You're Back From Outer Space": ["Beat the Heat"],
"Snatcher's Hit List": ["Beat the Heat"],
"Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"],
"Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"],
"Collect-a-thon": ["So You're Back From Outer Space"],
"She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"],
"Mafia's Jumps": ["She Speedran from Outer Space"],
"Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"],
"Encore! Encore!": ["Collect-a-thon"],
"Security Breach": ["Beat the Heat"],
"Rift Collapse: Dead Bird Studio": ["Security Breach"],
"The Great Big Hootenanny": ["Security Breach"],
"10 Seconds until Self-Destruct": ["The Great Big Hootenanny"],
"Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"],
"Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"],
"Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"],
"Zero Jumps": ["Rift Collapse: Dead Bird Studio"],
"Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"],
"Speedrun Well": ["Beat the Heat"],
"Rift Collapse: Sleepy Subcon": ["Speedrun Well"],
"Boss Rush": ["Speedrun Well"],
"Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"],
"Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"],
"Community Rift: Twilight Travels": ["Quality Time with Snatcher"],
"Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"],
"Bird Sanctuary": ["Beat the Heat"],
"Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"],
"Wound-Up Windmill": ["Bird Sanctuary"],
"Rift Collapse: Alpine Skyline": ["Bird Sanctuary"],
"Camera Tourist": ["Rift Collapse: Alpine Skyline"],
"Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"],
"The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"],
"The Mustache Gauntlet": ["Wound-Up Windmill"],
"No More Bad Guys": ["The Mustache Gauntlet"],
"Seal the Deal": ["Encore! Encore!", "Killing Two Birds",
"Breaching the Contract", "No More Bad Guys"],
"Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio",
"Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"],
"Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"],
}
dw_candles = [
"Snatcher's Hit List",
"Snatcher Coins in Mafia Town"
"Security Breach",
"10 Seconds until Self-Destruct",
"The Great Big Hootenanny",
"Killing Two Birds",
"Rift Collapse: Dead Bird Studio",
"Snatcher Coins in Battle of the Birds",
"Zero Jumps",
"Speedrun Well",
"Boss Rush",
"Quality Time with Snatcher",
"Breaching the Contract",
"Rift Collapse: Sleepy Subcon",
"Snatcher Coins in Subcon Forest",
"Bird Sanctuary",
"Wound-Up Windmill",
"The Illness has Speedrun",
"Rift Collapse: Alpine Skyline",
"Snatcher Coins in Alpine Skyline",
"Camera Tourist",
"The Mustache Gauntlet",
"No More Bad Guys",
"Seal the Deal",
"Rift Collapse: Deep Sea",
"Cruisin' for a Bruisin'",
"Community Map: Rhythm Jump Studio",
"Community Map: Twilight Travels",
"Community Map: The Mountain Rift",
"Snatcher Coins in Mafia Town",
"Snatcher Coins in Battle of the Birds",
"Snatcher Coins in Subcon Forest",
"Snatcher Coins in Alpine Skyline",
"Snatcher Coins in Nyakuza Metro",
]
dw_prereqs = {
"Snatcher's Hit List": ["Beat the Heat"],
annoying_dws = [
"Vault Codes in the Wind",
"Boss Rush",
"Camera Tourist",
"The Mustache Gauntlet",
"Rift Collapse: Deep Sea",
"Cruisin' for a Bruisin'",
"Seal the Deal", # Non-excluded if goal
]
}
# includes the above as well
annoying_bonuses = [
"So You're Back From Outer Space",
"Encore! Encore!",
"Snatcher's Hit List",
"10 Seconds until Self-Destruct",
"Killing Two Birds",
"Snatcher Coins in Battle of the Birds",
"Zero Jumps",
"Bird Sanctuary",
"Wound-Up Windmill",
"Snatcher Coins in Alpine Skyline",
"Seal the Deal",
]
dw_classes = {
"Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder",
"So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace",
"Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody",
"Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy",
"Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown",
"Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX",
"She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien",
"Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien",
"Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault",
"Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown",
"Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards",
"The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade",
"Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds",
"10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime",
"Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX",
"Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds",
"Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses",
"Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell",
"Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon",
"Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush",
"Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest",
"Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX",
"Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon",
"Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse",
"Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps",
"Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill",
"The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness",
"Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps",
"Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1",
"The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle",
"No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX",
"Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX",
"Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise",
"Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks",
"Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump",
"Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels",
"Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift",
"Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro",
}
def create_dw_regions(world: World):
if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0:
for name in annoying_dws:
world.get_excluded_dws().append(name)
if world.multiworld.DWEnableBonus[world.player].value == 0 \
or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0:
for name in death_wishes:
world.get_excluded_bonuses().append(name)
elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0:
for name in annoying_bonuses:
world.get_excluded_bonuses().append(name)
if world.multiworld.DWExcludeCandles[world.player].value > 0:
for name in dw_candles:
if name in world.get_excluded_dws():
continue
world.get_excluded_dws().append(name)
spaceship = world.multiworld.get_region("Spaceship", world.player)
dw_map: Region = create_region(world, "Death Wish Map")
entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player)
add_rule(entrance, lambda state: state.has("Time Piece", world.player,
world.multiworld.DWTimePieceRequirement[world.player].value))
if world.multiworld.DWShuffle[world.player].value > 0:
dw_list: List[str] = []
for name in death_wishes.keys():
if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name):
continue
dw_list.append(name)
world.random.shuffle(dw_list)
count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value,
world.multiworld.DWShuffleCountMax[world.player].value)
dw_shuffle: List[str] = []
total = min(len(dw_list), count)
for i in range(total):
dw_shuffle.append(dw_list[i])
# Seal the Deal is always last if it's the goal
if world.multiworld.EndGoal[world.player].value == 3:
if "Seal the Deal" in dw_shuffle:
dw_shuffle.remove("Seal the Deal")
dw_shuffle.append("Seal the Deal")
world.set_dw_shuffle(dw_shuffle)
prev_dw: Region
for i in range(len(dw_shuffle)):
name = dw_shuffle[i]
dw = create_region(world, name)
if i == 0:
connect_regions(dw_map, dw, f"-> {name}", world.player)
else:
connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player)
loc_id = death_wishes[name]
main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw)
full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw)
if name in world.get_excluded_bonuses():
main_objective.progress_type = LocationProgressType.EXCLUDED
full_clear.progress_type = LocationProgressType.EXCLUDED
elif world.is_bonus_excluded(name):
full_clear.progress_type = LocationProgressType.EXCLUDED
dw.locations.append(main_objective)
dw.locations.append(full_clear)
prev_dw = dw
else:
for key, loc_id in death_wishes.items():
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
world.get_excluded_dws().append(key)
continue
dw = create_region(world, key)
if key == "Beat the Heat":
connect_regions(dw_map, dw, "-> Beat the Heat", world.player)
elif key in dw_prereqs.keys():
for name in dw_prereqs[key]:
parent = world.multiworld.get_region(name, world.player)
connect_regions(parent, dw, f"{parent.name} -> {key}", world.player)
main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw)
full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw)
if key in world.get_excluded_bonuses():
main_objective.progress_type = LocationProgressType.EXCLUDED
full_clear.progress_type = LocationProgressType.EXCLUDED
elif world.is_bonus_excluded(key):
full_clear.progress_type = LocationProgressType.EXCLUDED
dw.locations.append(main_objective)
dw.locations.append(full_clear)

View File

@@ -0,0 +1,491 @@
from worlds.AutoWorld import World, CollectionState
from .Locations import LocData, death_wishes, HatInTimeLocation
from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings
from .Types import HatType
from .DeathWishLocations import dw_prereqs, dw_candles
from .Items import HatInTimeItem
from BaseClasses import Entrance, Location, ItemClassification
from worlds.generic.Rules import add_rule
from typing import List, Callable
# Any speedruns expect the player to have Sprint Hat
dw_requirements = {
"Beat the Heat": LocData(umbrella=True),
"So You're Back From Outer Space": LocData(hookshot=True),
"She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]),
"Mafia's Jumps": LocData(required_hats=[HatType.ICE]),
"Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]),
"Security Breach": LocData(hit_requirement=1),
"Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]),
"Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]),
"Boss Rush": LocData(hit_requirement=1, umbrella=True),
"Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Bird Sanctuary": LocData(hookshot=True),
"Wound-Up Windmill": LocData(hookshot=True),
"The Illness has Speedrun": LocData(hookshot=True, required_hats=[HatType.SPRINT]),
"Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Camera Tourist": LocData(misc_required=["Camera Badge"]),
"The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
"Rift Collapse - Deep Sea": LocData(hookshot=True, required_hats=[HatType.DWELLER]),
}
# Includes main objective requirements
dw_bonus_requirements = {
# Some One-Hit Hero requirements need badge pins as well because of Hookshot
"So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]),
"Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]),
"10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
"Boss Rush": LocData(misc_required=["One-Hit Hero Badge"]),
"Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]),
"Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]),
"Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]),
"The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]),
"Rift Collapse - Deep Sea": LocData(required_hats=[HatType.SPRINT]),
}
dw_stamp_costs = {
"So You're Back From Outer Space": 2,
"Collect-a-thon": 5,
"She Speedran from Outer Space": 8,
"Encore! Encore!": 10,
"Security Breach": 4,
"The Great Big Hootenanny": 7,
"10 Seconds until Self-Destruct": 15,
"Killing Two Birds": 25,
"Snatcher Coins in Nyakuza Metro": 30,
"Speedrun Well": 10,
"Boss Rush": 15,
"Quality Time with Snatcher": 20,
"Breaching the Contract": 40,
"Bird Sanctuary": 15,
"Wound-Up Windmill": 30,
"The Illness has Speedrun": 35,
"The Mustache Gauntlet": 35,
"No More Bad Guys": 50,
"Seal the Deal": 70,
}
def set_dw_rules(world: World):
if "Snatcher's Hit List" not in world.get_excluded_dws() \
or "Camera Tourist" not in world.get_excluded_dws():
create_enemy_events(world)
dw_list: List[str] = []
if world.multiworld.DWShuffle[world.player].value > 0:
dw_list = world.get_dw_shuffle()
else:
for name in death_wishes.keys():
dw_list.append(name)
for name in dw_list:
if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
dw = world.multiworld.get_region(name, world.player)
temp_list: List[Location] = []
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
temp_list.append(main_objective)
temp_list.append(full_clear)
if world.multiworld.DWShuffle[world.player].value == 0:
if name in dw_stamp_costs.keys():
for entrance in dw.entrances:
add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n])
if world.multiworld.DWEnableBonus[world.player].value == 0:
# place nothing, but let the locations exist still, so we can use them for bonus stamp rules
full_clear.address = None
full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player))
full_clear.show_in_spoiler = False
# Stamps are event locations
main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw)
bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw)
main_stamp.show_in_spoiler = False
bonus_stamps.show_in_spoiler = False
dw.locations.append(main_stamp)
dw.locations.append(bonus_stamps)
main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}",
ItemClassification.progression, None, world.player))
bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}",
ItemClassification.progression, None, world.player))
# No need for rules if excluded - stamps will be auto-granted
if world.is_dw_excluded(name):
continue
# Specific Rules
if name == "The Illness has Speedrun":
# killing the flowers without the umbrella is way too slow
add_rule(main_objective, lambda state: state.has("Umbrella", world.player))
elif name == "The Mustache Gauntlet":
# don't get burned bonus requires a way to kill fire crows without being burned
add_rule(full_clear, lambda state: state.has("Umbrella", world.player)
or can_use_hat(state, world, HatType.ICE))
elif name == "Vault Codes in the Wind":
add_rule(main_objective, lambda state: can_use_hat(state, world, HatType.TIME_STOP), "or")
if name in dw_candles:
set_candle_dw_rules(name, world)
main_rule: Callable[[CollectionState], bool]
for i in range(len(temp_list)):
loc = temp_list[i]
data: LocData
if loc.name == main_objective.name:
data = dw_requirements.get(name)
else:
data = dw_bonus_requirements.get(name)
if data is None:
continue
if data.hookshot:
add_rule(loc, lambda state: can_use_hookshot(state, world))
for hat in data.required_hats:
if hat is not HatType.NONE:
add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h))
for misc in data.misc_required:
add_rule(loc, lambda state, item=misc: state.has(item, world.player))
if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0:
add_rule(loc, lambda state: state.has("Umbrella", world.player))
if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0:
add_rule(loc, lambda state, paintings=data.paintings: state.has("Progressive Painting Unlock",
world.player, paintings))
if data.hit_requirement > 0:
if data.hit_requirement == 1:
add_rule(loc, lambda state: can_hit(state, world))
elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells)
add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))
main_rule = main_objective.access_rule
if loc.name == main_objective.name:
add_rule(main_stamp, loc.access_rule)
elif loc.name == full_clear.name:
add_rule(loc, main_rule)
# Only set bonus stamp rules if we don't auto complete bonuses
if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \
and not world.is_bonus_excluded(loc.name):
add_rule(bonus_stamps, loc.access_rule)
if world.multiworld.DWShuffle[world.player].value > 0:
dw_shuffle = world.get_dw_shuffle()
for i in range(len(dw_shuffle)):
if i == 0:
continue
name = dw_shuffle[i]
prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player)
entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player)
add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player))
else:
for key, reqs in dw_prereqs.items():
if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
access_rules: List[Callable[[CollectionState], bool]] = []
entrances: List[Entrance] = []
for parent in reqs:
entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player)
entrances.append(entrance)
if not world.is_dw_excluded(parent):
access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player))
for entrance in entrances:
for rule in access_rules:
add_rule(entrance, rule)
if world.multiworld.EndGoal[world.player].value == 3:
world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal",
world.player)
def get_total_dw_stamps(state: CollectionState, world: World) -> int:
if world.multiworld.DWShuffle[world.player].value > 0:
return 999 # no stamp costs in death wish shuffle
count: int = 0
peace_and_tranquility: bool = world.multiworld.DWEnableBonus[world.player].value == 0 \
and world.multiworld.DWAutoCompleteBonuses[world.player].value == 0
for name in death_wishes:
if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2():
continue
if state.has(f"1 Stamp - {name}", world.player):
count += 1
else:
continue
# If bonus rewards and auto bonus completion is off, obtaining stamps via P&T is in logic
# Candles don't have P&T
if peace_and_tranquility and name not in dw_candles:
count += 2
continue
if state.has(f"2 Stamps - {name}", world.player):
count += 2
elif name not in dw_candles:
# all non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero)
count += 1
return count
def set_candle_dw_rules(name: str, world: World):
main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player)
full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player)
if name == "Zero Jumps":
add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1)
add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4
and state.has("Train Rush Cleared", world.player) and can_use_hat(state, world, HatType.ICE))
elif name == "Snatcher's Hit List":
add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player))
add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12)
elif name == "Camera Tourist":
add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8)
add_rule(full_clear, lambda state: can_reach_all_bosses(state, world))
elif name == "Snatcher Coins in Mafia Town":
add_rule(main_objective, lambda state: state.has("MT Access", world.player)
or state.has("HUMT Access", world.player))
add_rule(full_clear, lambda state: state.has("CTR Access", world.player)
or state.has("HUMT Access", world.player)
and (world.multiworld.UmbrellaLogic[world.player].value == 0 or state.has("Umbrella", world.player))
or state.has("DWTM Access", world.player)
or state.has("TGV Access", world.player))
elif name == "Snatcher Coins in Battle of the Birds":
add_rule(main_objective, lambda state: state.has("PP Access", world.player)
or state.has("DBS Access", world.player)
or state.has("Train Rush Cleared", world.player))
add_rule(full_clear, lambda state: state.has("PP Access", world.player)
and state.has("DBS Access", world.player)
and state.has("Train Rush Cleared", world.player))
elif name == "Snatcher Coins in Subcon Forest":
add_rule(main_objective, lambda state: state.has("SF Access", world.player))
add_rule(main_objective, lambda state: has_paintings(state, world, 1) and (can_use_hookshot(state, world)
or can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))
or has_paintings(state, world, 3))
add_rule(full_clear, lambda state: has_paintings(state, world, 3) and can_use_hookshot(state, world)
and (can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)))
elif name == "Snatcher Coins in Alpine Skyline":
add_rule(main_objective, lambda state: state.has("LC Access", world.player)
or state.has("WM Access", world.player))
add_rule(full_clear, lambda state: state.has("LC Access", world.player)
and state.has("WM Access", world.player))
elif name == "Snatcher Coins in Nyakuza Metro":
add_rule(main_objective, lambda state: state.has("Bluefin Tunnel Cleared", world.player)
or (state.has("Nyakuza Intro Cleared", world.player)
and (state.has("Metro Ticket - Pink", world.player)
or state.has("Metro Ticket - Yellow", world.player)
and state.has("Metro Ticket - Blue", world.player))))
add_rule(full_clear, lambda state: state.has("Bluefin Tunnel Cleared", world.player)
and (state.has("Nyakuza Intro Cleared", world.player)
and (state.has("Metro Ticket - Pink", world.player)
or state.has("Metro Ticket - Yellow", world.player)
and state.has("Metro Ticket - Blue", world.player))))
def get_zero_jump_clear_count(state: CollectionState, world: World) -> int:
total: int = 0
for name, hats in zero_jumps.items():
if not state.has(f"{name} Cleared", world.player):
continue
valid: bool = True
for hat in hats:
if not can_use_hat(state, world, hat):
valid = False
break
if valid:
total += 1
return total
def get_reachable_enemy_count(state: CollectionState, world: World) -> int:
count: int = 0
for enemy in hit_list.keys():
if enemy in bosses:
continue
if state.has(enemy, world.player):
count += 1
return count
def can_reach_all_bosses(state: CollectionState, world: World) -> bool:
for boss in bosses:
if not state.has(boss, world.player):
return False
return True
def create_enemy_events(world: World):
no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses()
for enemy, regions in hit_list.items():
if no_tourist and enemy in bosses:
continue
for area in regions:
if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1():
continue
if area == "Time Rift - Tour" and not world.is_dlc1() \
or world.multiworld.ExcludeTour[world.player].value > 0:
continue
if area == "Bluefin Tunnel" and not world.is_dlc2():
continue
if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \
and area not in world.get_dw_shuffle():
continue
region = world.multiworld.get_region(area, world.player)
event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region)
event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player))
region.locations.append(event)
event.show_in_spoiler = False
if enemy == "Toxic Flower":
add_rule(event, lambda state: can_use_hookshot(state, world))
if area == "The Illness has Spread":
add_rule(event, lambda state: not zipline_logic(world) or
state.has("Zipline Unlock - The Birdhouse Path", world.player)
or state.has("Zipline Unlock - The Lava Cake Path", world.player)
or state.has("Zipline Unlock - The Windmill Path", world.player))
elif enemy == "Director":
if area == "Dead Bird Studio Basement":
add_rule(event, lambda state: can_use_hookshot(state, world))
elif enemy == "Snatcher" or enemy == "Mustache Girl":
if area == "Boss Rush":
# need to be able to kill toilet
add_rule(event, lambda state: can_hit(state, world))
elif area == "The Finale" and enemy == "Mustache Girl":
add_rule(event, lambda state: can_use_hookshot(state, world)
and can_use_hat(state, world, HatType.DWELLER))
elif enemy == "Shock Squid" or enemy == "Ninja Cat":
if area == "Time Rift - Deep Sea":
add_rule(event, lambda state: can_use_hookshot(state, world))
# Zero Jumps completable levels, with required hats if any
zero_jumps = {
"Welcome to Mafia Town": [],
"Cheating the Race": [HatType.TIME_STOP],
"Picture Perfect": [],
"Train Rush": [HatType.ICE],
"Contractual Obligations": [],
"Your Contract has Expired": [],
"Mail Delivery Service": [], # rule for needing sprint is already on act completion
}
# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them
hit_list = {
"Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour",
"Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks",
"So You're Back From Outer Space"],
"Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell",
"She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet",
"Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"],
"UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"],
"Rat": ["Down with the Mafia!", "Bluefin Tunnel"],
"Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea",
"Rift Collapse: Sleepy Subcon"],
"Shromb Egg": ["The Birdhouse", "Bird Sanctuary"],
"Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well",
"The Lava Cake", "The Windmill"],
"Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary",
"Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"],
"Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"],
"Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"],
"Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"],
"Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet",
"Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea",
"Rift Collapse: Alpine Skyline"],
# Bosses
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
"Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
"Toilet": ["Toilet of Doom", "Boss Rush"],
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
"Quality Time with Snatcher"],
"Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"],
"Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"],
}
bosses = [
"Mafia Boss",
"Conductor",
"Toilet",
"Snatcher",
"Toxic Flower",
"Mustache Girl",
]

View File

@@ -1,19 +1,121 @@
from BaseClasses import Item, ItemClassification
from worlds.AutoWorld import World
from .Types import HatDLC
import typing
from .Types import HatDLC, HatType
from .Locations import get_total_locations
from .Rules import get_difficulty, is_player_knowledgeable
from typing import Optional, NamedTuple, List, Dict
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
class ItemData(NamedTuple):
code: Optional[int]
classification: ItemClassification
dlc_flags: typing.Optional[HatDLC] = HatDLC.none
dlc_flags: Optional[HatDLC] = HatDLC.none
class HatInTimeItem(Item):
game: str = "A Hat in Time"
def create_itempool(world: World) -> List[Item]:
itempool: List[Item] = []
if not world.is_dw_only():
calculate_yarn_costs(world)
yarn_pool: List[Item] = create_multiple_items(world, "Yarn",
world.multiworld.YarnAvailable[world.player].value,
ItemClassification.progression_skip_balancing)
for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))):
yarn_pool[i].classification = ItemClassification.progression
itempool += yarn_pool
for name in item_table.keys():
if name == "Yarn":
continue
if not item_dlc_enabled(world, name):
continue
item_type: ItemClassification = item_table.get(name).classification
if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \
and (name == "Scooter Badge" or name == "No Bonk Badge"):
item_type = ItemClassification.progression
# some death wish bonuses require one hit hero + hookshot
if world.is_dw() and name == "Badge Pin":
item_type = ItemClassification.progression
if world.is_dw_only():
if item_type is ItemClassification.progression \
or item_type is ItemClassification.progression_skip_balancing:
continue
# progression balance anything useful, since we have basically no progression in this mode
if item_type is ItemClassification.useful:
item_type = ItemClassification.progression
if item_type is ItemClassification.filler or item_type is ItemClassification.trap:
continue
if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0:
continue
if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0:
continue
if name == "Progressive Painting Unlock" \
and world.multiworld.ShuffleSubconPaintings[world.player].value == 0:
continue
if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge":
continue
if name == "Time Piece":
tp_count: int = 40
max_extra: int = 0
if world.is_dlc1():
max_extra += 6
if world.is_dlc2():
max_extra += 10
tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value)
tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type)
for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))):
tp_list[i].classification = ItemClassification.progression
itempool += tp_list
continue
itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type)
total_locations: int = get_total_locations(world)
itempool += create_junk_items(world, total_locations - len(itempool))
return itempool
def calculate_yarn_costs(world: World):
mw = world.multiworld
p = world.player
min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_cost: int = 0
for i in range(5):
cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost))
world.get_hat_yarn_costs()[HatType(i)] = cost
max_cost += cost
available_yarn: int = mw.YarnAvailable[p].value
if max_cost > available_yarn:
mw.YarnAvailable[p].value = max_cost
available_yarn = max_cost
if max_cost + mw.MinExtraYarn[p].value > available_yarn:
mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn
def item_dlc_enabled(world: World, name: str) -> bool:
data = item_table[name]
@@ -29,43 +131,38 @@ def item_dlc_enabled(world: World, name: str) -> bool:
return False
def get_total_time_pieces(world: World) -> int:
count: int = 40
if world.is_dlc1():
count += 6
if world.is_dlc2():
count += 10
return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count)
def create_item(world: World, name: str) -> Item:
data = item_table[name]
return HatInTimeItem(name, data.classification, data.code, world.player)
def create_multiple_items(world: World, name: str, count: int = 1) -> typing.List[Item]:
def create_multiple_items(world: World, name: str, count: int = 1,
item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]:
data = item_table[name]
itemlist: typing.List[Item] = []
itemlist: List[Item] = []
for i in range(count):
itemlist += [HatInTimeItem(name, data.classification, data.code, world.player)]
itemlist += [HatInTimeItem(name, item_type, data.code, world.player)]
return itemlist
def create_junk_items(world: World, count: int) -> typing.List[Item]:
def create_junk_items(world: World, count: int) -> List[Item]:
trap_chance = world.multiworld.TrapChance[world.player].value
junk_pool: typing.List[Item] = []
junk_list: typing.Dict[str, int] = {}
trap_list: typing.Dict[str, int] = {}
junk_pool: List[Item] = []
junk_list: Dict[str, int] = {}
trap_list: Dict[str, int] = {}
ic: ItemClassification
for name in item_table.keys():
ic = item_table[name].classification
if ic == ItemClassification.filler:
if world.is_dw_only() and "Pons" in name:
continue
junk_list[name] = junk_weights.get(name)
elif trap_chance > 0 and ic == ItemClassification.trap:
if name == "Baby Trap":
trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value
@@ -75,12 +172,12 @@ def create_junk_items(world: World, count: int) -> typing.List[Item]:
trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value
for i in range(count):
if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance:
if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance:
junk_pool += [world.create_item(
world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])]
world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])]
else:
junk_pool += [world.create_item(
world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])]
world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])]
return junk_pool
@@ -110,21 +207,22 @@ ahit_items = {
"Hover Badge": ItemData(300026, ItemClassification.useful),
"Hookshot Badge": ItemData(300027, ItemClassification.progression),
"Item Magnet Badge": ItemData(300028, ItemClassification.useful),
"No Bonk Badge": ItemData(300029, ItemClassification.progression),
"No Bonk Badge": ItemData(300029, ItemClassification.useful),
"Compass Badge": ItemData(300030, ItemClassification.useful),
"Scooter Badge": ItemData(300031, ItemClassification.progression),
"Badge Pin": ItemData(300043, ItemClassification.useful),
"Scooter Badge": ItemData(300031, ItemClassification.useful),
"One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish),
"Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish),
# Other
# "Rift Token": ItemData(300032, ItemClassification.filler),
"Random Cosmetic": ItemData(300044, ItemClassification.filler),
"Umbrella": ItemData(300033, ItemClassification.progression),
"Badge Pin": ItemData(300043, ItemClassification.useful),
# Garbage items
"25 Pons": ItemData(300034, ItemClassification.filler),
"50 Pons": ItemData(300035, ItemClassification.filler),
"100 Pons": ItemData(300036, ItemClassification.filler),
"Health Pon": ItemData(300037, ItemClassification.filler),
"Random Cosmetic": ItemData(300044, ItemClassification.filler),
# Traps
"Baby Trap": ItemData(300039, ItemClassification.trap),
@@ -144,10 +242,6 @@ ahit_items = {
"Metro Ticket - Green": ItemData(300046, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Blue": ItemData(300047, ItemClassification.progression, HatDLC.dlc2),
"Metro Ticket - Pink": ItemData(300048, ItemClassification.progression, HatDLC.dlc2),
# Death Wish items
"One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish),
"Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish),
}
act_contracts = {
@@ -180,10 +274,10 @@ item_frequencies = {
junk_weights = {
"25 Pons": 50,
"50 Pons": 10,
"50 Pons": 25,
"100 Pons": 10,
"Health Pon": 35,
"100 Pons": 5,
"Random Cosmetic": 25,
"Random Cosmetic": 35,
}
item_table = {

View File

@@ -6,12 +6,13 @@ from .Options import TasksanityCheckCount
class LocData(NamedTuple):
id: int
region: str
id: Optional[int] = 0
region: Optional[str] = ""
required_hats: Optional[List[HatType]] = [HatType.NONE]
hookshot: Optional[bool] = False
dlc_flags: Optional[HatDLC] = HatDLC.none
paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle
misc_required: Optional[List[str]] = []
# For UmbrellaLogic setting
umbrella: Optional[bool] = False # Umbrella required for this check
@@ -29,12 +30,28 @@ class HatInTimeLocation(Location):
def get_total_locations(world: World) -> int:
total: int = 0
for (name) in location_table.keys():
if is_location_valid(world, name):
total += 1
if not world.is_dw_only():
for (name) in location_table.keys():
if is_location_valid(world, name):
total += 1
if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0:
total += world.multiworld.TasksanityCheckCount[world.player].value
if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0:
total += world.multiworld.TasksanityCheckCount[world.player].value
if world.is_dw():
if world.multiworld.DWShuffle[world.player].value > 0:
total += len(world.get_dw_shuffle())
if world.multiworld.DWEnableBonus[world.player].value > 0:
total += len(world.get_dw_shuffle())
else:
total += 37
if world.is_dlc2():
total += 1
if world.multiworld.DWEnableBonus[world.player].value > 0:
total += 37
if world.is_dlc2():
total += 1
return total
@@ -58,15 +75,20 @@ def is_location_valid(world: World, location: str) -> bool:
if not location_dlc_enabled(world, location):
return False
if location in storybook_pages.keys() \
and world.multiworld.ShuffleStorybookPages[world.player].value == 0:
if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \
and location in storybook_pages.keys():
return False
if location in shop_locations and location not in world.shop_locs:
if location not in world.shop_locs and location in shop_locations:
return False
data = location_table.get(location) or event_locs.get(location)
if data.region == "Time Rift - Tour" and world.multiworld.ExcludeTour[world.player].value > 0:
if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour":
return False
# No need for all those event items if we're not doing candles
if data.dlc_flags is HatDLC.death_wish and world.multiworld.DWExcludeCandles[world.player].value > 0 \
and location in event_locs.keys():
return False
return True
@@ -76,7 +98,11 @@ def get_location_names() -> Dict[str, int]:
names = {name: data.id for name, data in location_table.items()}
id_start: int = get_tasksanity_start_id()
for i in range(TasksanityCheckCount.range_end):
names.setdefault(format("Tasksanity Check %i") % (i+1), id_start+i)
names.setdefault(f"Tasksanity Check {i+1}", id_start+i)
for (key, loc_id) in death_wishes.items():
names.setdefault(f"{key} - Main Objective", loc_id)
names.setdefault(f"{key} - All Clear", loc_id+1)
return names
@@ -169,7 +195,7 @@ ahit_locations = {
"Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True),
"Dead Bird Studio Basement - Locked Room": LocData(305819, "Dead Bird Studio Basement", hookshot=True),
# 320000 range - Subcon Forest
# Subcon Forest
"Contractual Obligations - Cherry Bomb Bone Cage": LocData(324761, "Contractual Obligations"),
"Subcon Village - Tree Top Ice Cube": LocData(325078, "Subcon Forest Area"),
"Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"),
@@ -178,7 +204,8 @@ ahit_locations = {
"Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1),
"Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"),
"Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"),
"Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area",
required_hats=[HatType.BREWING], paintings=1),
@@ -189,30 +216,6 @@ ahit_locations = {
"Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1),
"Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1),
"Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3),
"Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3),
"Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area",
required_hats=[HatType.DWELLER],
hookshot=True,
paintings=3),
"Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2),
"Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2),
"Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2),
@@ -221,7 +224,6 @@ ahit_locations = {
"Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2),
"Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2),
"Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2),
"Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1),
"Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=2),
@@ -234,6 +236,30 @@ ahit_locations = {
"Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area",
required_hats=[HatType.BREWING], paintings=3),
"Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3),
"Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3),
"Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3),
"Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area",
required_hats=[HatType.DWELLER],
hookshot=True,
paintings=3),
"Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", hit_requirement=1, paintings=1),
"Subcon Well - Above Chest": LocData(324612, "The Subcon Well", hit_requirement=1, paintings=1),
@@ -245,7 +271,7 @@ ahit_locations = {
"Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1),
"Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1),
# 330000 range - Alpine Skyline
# Alpine Skyline
"Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"),
"Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Goat Village"),
"Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"),
@@ -320,7 +346,6 @@ ahit_locations = {
}
act_completions = {
# 310000 range - Act Completions
"Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery"),
"Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"),
@@ -479,13 +504,6 @@ storybook_pages = {
"Rumbi Factory - Page: Last Area": LocData(345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
}
contract_locations = {
"Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"),
"Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"),
"Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"),
"Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"),
}
shop_locations = {
"Badge Seller - Item 1": LocData(301003, "Badge Seller"),
"Badge Seller - Item 2": LocData(301004, "Badge Seller"),
@@ -623,6 +641,13 @@ shop_locations = {
}
contract_locations = {
"Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"),
"Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"),
"Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"),
"Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"),
}
# Don't put any of the locations from peaks here, the rules for their entrances are set already
zipline_unlocks = {
"Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path",
@@ -652,7 +677,9 @@ tihs_locations = [
event_locs = {
"HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False),
"Subcon Forest Access": LocData(0, "Subcon Forest Area", act_complete_event=False),
"TOD Access": LocData(0, "Toilet of Doom", act_complete_event=False),
"YCHE Access": LocData(0, "Your Contract has Expired", act_complete_event=False),
"Birdhouse Cleared": LocData(0, "The Birdhouse"),
"Lava Cake Cleared": LocData(0, "The Lava Cake"),
"Windmill Cleared": LocData(0, "The Windmill"),
@@ -670,6 +697,39 @@ event_locs = {
"Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2),
"Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2),
"Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2),
# -------------- Death Wish Candle Related --------------- #
# Snatcher Coins
"MT Access": LocData(0, "Mafia Town Area", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"DWTM Access": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"CTR Access": LocData(0, "Cheating the Race", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"TGV Access": LocData(0, "The Golden Vault", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"DBS Access": LocData(0, "Dead Bird Studio - Elevator Area", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"PP Access": LocData(0, "Picture Perfect", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"SF Access": LocData(0, "Subcon Forest Area", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"LC Access": LocData(0, "The Lava Cake", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"WM Access": LocData(0, "The Windmill", act_complete_event=False, dlc_flags=HatDLC.death_wish),
# Camera Tourist
"Mafia Boss": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"Conductor": LocData(0, "Dead Bird Studio Basement", dlc_flags=HatDLC.death_wish),
"Snatcher": LocData(0, "Your Contract has Expired", act_complete_event=False, dlc_flags=HatDLC.death_wish),
"Evil Flower": LocData(0, "The Illness has Spread", act_complete_event=False, dlc_flags=HatDLC.death_wish),
# Zero Jumps
"Welcome to Mafia Town Cleared": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish),
"Picture Perfect Cleared": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish),
"Contractual Obligations Cleared": LocData(0, "Contractual Obligations", dlc_flags=HatDLC.death_wish),
"Your Contract has Expired Cleared": LocData(0, "Your Contract has Expired", dlc_flags=HatDLC.death_wish),
"Mail Delivery Service Cleared": LocData(0, "Mail Delivery Service", dlc_flags=HatDLC.death_wish),
"Cheating the Race Cleared": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish),
"Train Rush Cleared": LocData(0, "Train Rush", dlc_flags=HatDLC.death_wish),
}
location_table = {
@@ -679,3 +739,52 @@ location_table = {
**contract_locations,
**shop_locations,
}
# DO NOT ALTER THE ORDER OF THIS LIST
# This file is in here instead of DeathWishLocations.py to prevent circular import problems
death_wishes = {
"Beat the Heat": 350000,
"Snatcher's Hit List": 350002,
"So You're Back From Outer Space": 350004,
"Collect-a-thon": 350006,
"Rift Collapse: Mafia of Cooks": 350008,
"She Speedran from Outer Space": 350010,
"Mafia's Jumps": 350012,
"Vault Codes in the Wind": 350014,
"Encore! Encore!": 350016,
"Snatcher Coins in Mafia Town": 350018,
"Security Breach": 350020,
"The Great Big Hootenanny": 350022,
"Rift Collapse: Dead Bird Studio": 350024,
"10 Seconds until Self-Destruct": 350026,
"Killing Two Birds": 350028,
"Snatcher Coins in Battle of the Birds": 350030,
"Zero Jumps": 350032,
"Speedrun Well": 350034,
"Rift Collapse: Sleepy Subcon": 350036,
"Boss Rush": 350038,
"Quality Time with Snatcher": 350040,
"Breaching the Contract": 350042,
"Snatcher Coins in Subcon Forest": 350044,
"Bird Sanctuary": 350046,
"Rift Collapse: Alpine Skyline": 350048,
"Wound-Up Windmill": 350050,
"The Illness has Speedrun": 350052,
"Snatcher Coins in Alpine Skyline": 350054,
"Camera Tourist": 350056,
"The Mustache Gauntlet": 350058,
"No More Bad Guys": 350060,
"Seal the Deal": 350062,
"Rift Collapse: Deep Sea": 350064,
"Cruisin' for a Bruisin'": 350066,
"Community Rift: Rhythm Jump Studio": 350068,
"Community Rift: Twilight Travels": 350070,
"Community Rift: The Mountain Rift": 350072,
"Snatcher Coins in Nyakuza Metro": 350074,
}

View File

@@ -1,7 +1,6 @@
import typing
from worlds.AutoWorld import World
from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict
from .Items import get_total_time_pieces
def adjust_options(world: World):
@@ -46,25 +45,59 @@ def adjust_options(world: World):
if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0:
world.multiworld.EndGoal[world.player].value = 1
# Don't allow Seal the Deal goal if Death Wish content is disabled
if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw():
world.multiworld.EndGoal[world.player].value = 1
if world.multiworld.DWEnableBonus[world.player].value > 0:
world.multiworld.DWAutoCompleteBonuses[world.player].value = 0
if world.is_dw_only():
world.multiworld.EndGoal[world.player].value = 3
world.multiworld.ActRandomizer[world.player].value = 0
world.multiworld.ShuffleAlpineZiplines[world.player].value = 0
world.multiworld.ShuffleSubconPaintings[world.player].value = 0
world.multiworld.ShuffleStorybookPages[world.player].value = 0
world.multiworld.ShuffleActContracts[world.player].value = 0
world.multiworld.EnableDLC1[world.player].value = 0
world.multiworld.LogicDifficulty[world.player].value = 0
world.multiworld.KnowledgeChecks[world.player].value = 0
world.multiworld.DWTimePieceRequirement[world.player].value = 0
world.multiworld.progression_balancing[world.player].value = 0
def get_total_time_pieces(world: World) -> int:
count: int = 40
if world.is_dlc1():
count += 6
if world.is_dlc2():
count += 10
return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count)
# General
class EndGoal(Choice):
"""The end goal required to beat the game.
Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location.
Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7
will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels.
Requires DLC2 content to be enabled."""
Requires DLC2 content to be enabled.
Seal the Deal: Reach and complete the Seal the Deal death wish main objective.
Requires Death Wish content to be enabled."""
display_name = "End Goal"
option_finale = 1
option_rush_hour = 2
option_seal_the_deal = 3
default = 1
class ActRandomizer(Choice):
"""If enabled, shuffle the game's Acts between each other.
Light will cause Time Rifts to only be shuffled amongst each other,
and Blue Time Rifts and Purple Time Rifts are shuffled separately."""
and Blue Time Rifts and Purple Time Rifts to be shuffled separately."""
display_name = "Shuffle Acts"
option_false = 0
option_light = 1
@@ -77,14 +110,9 @@ class ActPlando(OptionDict):
display_name = "Act Plando"
class ShuffleAlpineZiplines(Toggle):
"""If enabled, Alpine's zipline paths leading to the peaks will be locked behind items."""
display_name = "Shuffle Alpine Ziplines"
default = 0
class FinaleShuffle(Toggle):
"""If enabled, chapter finales will only be shuffled amongst each other in act shuffle."""
display_name = "Finale Shuffle"
default = 0
@@ -105,9 +133,13 @@ class KnowledgeChecks(Toggle):
default = 0
class RandomizeHatOrder(Toggle):
"""Randomize the order that hats are stitched in."""
class RandomizeHatOrder(Choice):
"""Randomize the order that hats are stitched in.
Time Stop Last will force Time Stop to be the last hat in the sequence."""
display_name = "Randomize Hat Order"
option_false = 0
option_true = 1
option_time_stop_last = 2
default = 1
@@ -127,12 +159,6 @@ class TimePieceBalancePercent(Range):
range_end = 100
class UmbrellaLogic(Toggle):
"""Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful"""
display_name = "Umbrella Logic"
default = 0
class StartWithCompassBadge(Toggle):
"""If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world
(instead of just Relics). Recommended if you're not familiar with where item locations are."""
@@ -151,6 +177,12 @@ class CompassBadgeMode(Choice):
default = 1
class UmbrellaLogic(Toggle):
"""Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful"""
display_name = "Umbrella Logic"
default = 0
class ShuffleStorybookPages(Toggle):
"""If enabled, each storybook page in the purple Time Rifts is an item check.
The Compass Badge can track these down for you."""
@@ -164,6 +196,12 @@ class ShuffleActContracts(Toggle):
default = 1
class ShuffleAlpineZiplines(Toggle):
"""If enabled, Alpine's zipline paths leading to the peaks will be locked behind items."""
display_name = "Shuffle Alpine Ziplines"
default = 0
class ShuffleSubconPaintings(Toggle):
"""If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings.
These items are progressive, with the order of Village-Swamp-Courtyard."""
@@ -181,13 +219,138 @@ class StartingChapter(Choice):
default = 1
class ChapterCostIncrement(Range):
"""Lower values mean chapter costs increase slower. Higher values make the cost differences more steep."""
display_name = "Chapter Cost Increment"
range_start = 1
range_end = 8
default = 4
class ChapterCostMinDifference(Range):
"""The minimum difference between chapter costs."""
display_name = "Minimum Chapter Cost Difference"
range_start = 1
range_end = 8
default = 4
class LowestChapterCost(Range):
"""Value determining the lowest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Lowest Possible Chapter Cost"
range_start = 0
range_end = 10
default = 5
class HighestChapterCost(Range):
"""Value determining the highest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Highest Possible Chapter Cost"
range_start = 15
range_end = 45
default = 25
class FinalChapterMinCost(Range):
"""Minimum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Minimum Time Piece Cost"
range_start = 0
range_end = 50
default = 30
class FinalChapterMaxCost(Range):
"""Maximum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Maximum Time Piece Cost"
range_start = 0
range_end = 50
default = 35
class MaxExtraTimePieces(Range):
"""Maximum amount of extra Time Pieces from the DLCs.
Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56."""
display_name = "Max Extra Time Pieces"
range_start = 0
range_end = 16
default = 16
class YarnCostMin(Range):
"""The minimum possible yarn needed to stitch a hat."""
display_name = "Minimum Yarn Cost"
range_start = 1
range_end = 12
default = 4
class YarnCostMax(Range):
"""The maximum possible yarn needed to stitch a hat."""
display_name = "Maximum Yarn Cost"
range_start = 1
range_end = 12
default = 8
class YarnAvailable(Range):
"""How much yarn is available to collect in the item pool."""
display_name = "Yarn Available"
range_start = 30
range_end = 75
default = 45
class MinExtraYarn(Range):
"""The minimum amount of extra yarn in the item pool.
There must be at least this much more yarn over the total amount of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool."""
display_name = "Max Extra Yarn"
range_start = 0
range_end = 15
default = 10
class MinPonCost(Range):
"""The minimum amount of Pons that any shop item can cost."""
display_name = "Minimum Shop Pon Cost"
range_start = 10
range_end = 800
default = 75
class MaxPonCost(Range):
"""The maximum amount of Pons that any shop item can cost."""
display_name = "Maximum Shop Pon Cost"
range_start = 10
range_end = 800
default = 300
class BadgeSellerMinItems(Range):
"""The smallest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Minimum Items"
range_start = 0
range_end = 10
default = 4
class BadgeSellerMaxItems(Range):
"""The largest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Maximum Items"
range_start = 0
range_end = 10
default = 8
class CTRWithSprint(Toggle):
"""If enabled, clearing Cheating the Race with just Sprint Hat can be in logic."""
display_name = "Cheating the Race with Sprint Hat"
default = 0
# DLC
class EnableDLC1(Toggle):
"""Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift.
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
@@ -279,151 +442,118 @@ class BaseballBat(Toggle):
default = 0
class ChapterCostIncrement(Range):
"""Lower values mean chapter costs increase slower. Higher values make the cost differences more steep."""
display_name = "Chapter Cost Increment"
range_start = 1
range_end = 8
default = 4
class ChapterCostMinDifference(Range):
"""The minimum difference between chapter costs."""
display_name = "Minimum Chapter Cost Difference"
range_start = 1
range_end = 8
default = 5
class LowestChapterCost(Range):
"""Value determining the lowest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Lowest Possible Chapter Cost"
range_start = 0
range_end = 10
default = 5
class HighestChapterCost(Range):
"""Value determining the highest possible cost for a chapter.
Chapter costs will, progressively, be calculated based on this value (except for the final chapter)."""
display_name = "Highest Possible Chapter Cost"
range_start = 15
range_end = 45
default = 25
class FinalChapterMinCost(Range):
"""Minimum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Minimum Time Piece Cost"
range_start = 0
range_end = 50
default = 30
class FinalChapterMaxCost(Range):
"""Maximum Time Pieces required to enter the final chapter. This is part of your goal."""
display_name = "Final Chapter Maximum Time Piece Cost"
range_start = 0
range_end = 50
default = 35
class MaxExtraTimePieces(Range):
"""Maximum amount of extra Time Pieces from the DLCs.
Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56."""
display_name = "Max Extra Time Pieces"
range_start = 0
range_end = 16
default = 16
# Death Wish
class EnableDeathWish(Toggle):
"""NOT IMPLEMENTED Shuffle Death Wish contracts into the game.
Each contract by default will have a single check granted upon completion.
"""Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion.
DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!"""
display_name = "Enable Death Wish"
default = 0
class DeathWishOnly(Toggle):
"""An alternative gameplay mode that allows you to exclusively play Death Wish in a seed.
This has the following effects:
- Death Wish is instantly unlocked from the start
- All hats and other progression items are instantly given to you
- Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start
- All chapters and their levels are unlocked, act shuffle is forced off
- Any checks other than Death Wish contracts are completely removed
- All Pons in the item pool are replaced with Health Pons or random cosmetics
- The EndGoal option is forced to complete Seal the Deal"""
display_name = "Death Wish Only"
default = 0
class DWShuffle(Toggle):
"""An alternative mode for Death Wish where each contract is unlocked one by one, in a random order.
Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence.
If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence.
Disabling candles is highly recommended."""
display_name = "Death Wish Shuffle"
default = 0
class DWShuffleCountMin(Range):
"""The minimum number of Death Wishes that can be in the Death Wish shuffle sequence.
The final result is clamped at the number of non-excluded Death Wishes."""
display_name = "Death Wish Shuffle Minimum Count"
range_start = 5
range_end = 38
default = 18
class DWShuffleCountMax(Range):
"""The maximum number of Death Wishes that can be in the Death Wish shuffle sequence.
The final result is clamped at the number of non-excluded Death Wishes."""
display_name = "Death Wish Shuffle Maximum Count"
range_start = 5
range_end = 38
default = 25
class DWEnableBonus(Toggle):
"""NOT IMPLEMENTED In Death Wish, allow the full completion of contracts to reward items."""
"""In Death Wish, allow the full completion of contracts to reward items.
WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS!
ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld!
Using Peace and Tranquility to auto-complete the bonuses will NOT count!"""
display_name = "Shuffle Death Wish Full Completions"
default = 0
class DWAutoCompleteBonuses(Toggle):
"""If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish.
This option will have no effect if bonus checks (DWEnableBonus) are turned on."""
display_name = "Auto Complete Bonus Stamps"
default = 1
class DWExcludeAnnoyingContracts(Toggle):
"""NOT IMPLEMENTED Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear."""
"""Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.
Excluded Death Wishes are automatically completed as soon as they are unlocked.
This option currently excludes the following contracts:
- Vault Codes in the Wind
- Boss Rush
- Camera Tourist
- The Mustache Gauntlet
- Rift Collapse: Deep Sea
- Cruisin' for a Bruisin'
- Seal the Deal (non-excluded if goal, but the checks are still excluded)"""
display_name = "Exclude Annoying Death Wish Contracts"
default = 1
class DWExcludeAnnoyingBonuses(Toggle):
"""NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions
from the pool. DANGER! DISABLE AT YOUR OWN RISK! THIS OPTION WHEN DISABLED CAN CREATE VERY DIFFICULT SEEDS!!!"""
"""If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool.
Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective.
This option currently excludes the following bonuses:
- So You're Back From Outer Space
- Encore! Encore!
- Snatcher's Hit List
- 10 Seconds until Self-Destruct
- Killing Two Birds
- Snatcher Coins in Battle of the Birds
- Zero Jumps
- Bird Sanctuary
- Wound-Up Windmill
- Snatcher Coins in Alpine Skyline
- Seal the Deal"""
display_name = "Exclude Annoying Death Wish Full Completions"
default = 1
# Yarn
class YarnCostMin(Range):
"""The minimum possible yarn needed to stitch each hat."""
display_name = "Minimum Yarn Cost"
range_start = 1
range_end = 12
default = 4
class DWExcludeCandles(Toggle):
"""If enabled, exclude all candle Death Wishes."""
display_name = "Exclude Candle Death Wishes"
default = 1
class YarnCostMax(Range):
"""The maximum possible yarn needed to stitch each hat."""
display_name = "Maximum Yarn Cost"
range_start = 1
range_end = 12
default = 8
class YarnAvailable(Range):
"""How much yarn is available to collect in the item pool."""
display_name = "Yarn Available"
range_start = 30
range_end = 75
default = 45
class MinPonCost(Range):
"""The minimum amount of Pons that any shop item can cost."""
display_name = "Minimum Shop Pon Cost"
range_start = 10
range_end = 800
default = 75
class MaxPonCost(Range):
"""The maximum amount of Pons that any shop item can cost."""
display_name = "Maximum Shop Pon Cost"
range_start = 10
range_end = 800
default = 400
class BadgeSellerMinItems(Range):
"""The smallest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Minimum Items"
class DWTimePieceRequirement(Range):
"""How many Time Pieces that will be required to unlock Death Wish."""
display_name = "Death Wish Time Piece Requirement"
range_start = 0
range_end = 10
default = 4
range_end = 35
default = 15
class BadgeSellerMaxItems(Range):
"""The largest amount of items that the Badge Seller can have for sale."""
display_name = "Badge Seller Maximum Items"
range_start = 0
range_end = 10
default = 8
# Traps
class TrapChance(Range):
"""The chance for any junk item in the pool to be replaced by a trap."""
display_name = "Trap Chance"
@@ -487,7 +617,18 @@ ahit_options: typing.Dict[str, type(Option)] = {
"ExcludeTour": ExcludeTour,
"ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal,
"EnableDeathWish": EnableDeathWish,
"EnableDeathWish": EnableDeathWish,
"DWShuffle": DWShuffle,
"DWShuffleCountMin": DWShuffleCountMin,
"DWShuffleCountMax": DWShuffleCountMax,
"DeathWishOnly": DeathWishOnly,
"DWEnableBonus": DWEnableBonus,
"DWAutoCompleteBonuses": DWAutoCompleteBonuses,
"DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts,
"DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses,
"DWExcludeCandles": DWExcludeCandles,
"DWTimePieceRequirement": DWTimePieceRequirement,
"EnableDLC2": EnableDLC2,
"BaseballBat": BaseballBat,
"MetroMinPonCost": MetroMinPonCost,
@@ -507,6 +648,7 @@ ahit_options: typing.Dict[str, type(Option)] = {
"YarnCostMin": YarnCostMin,
"YarnCostMax": YarnCostMax,
"YarnAvailable": YarnAvailable,
"MinExtraYarn": MinExtraYarn,
"MinPonCost": MinPonCost,
"MaxPonCost": MaxPonCost,
@@ -523,34 +665,39 @@ ahit_options: typing.Dict[str, type(Option)] = {
slot_data_options: typing.Dict[str, type(Option)] = {
"EndGoal": EndGoal,
"ActRandomizer": ActRandomizer,
"ShuffleAlpineZiplines": ShuffleAlpineZiplines,
"LogicDifficulty": LogicDifficulty,
"KnowledgeChecks": KnowledgeChecks,
"RandomizeHatOrder": RandomizeHatOrder,
"UmbrellaLogic": UmbrellaLogic,
"CompassBadgeMode": CompassBadgeMode,
"ShuffleStorybookPages": ShuffleStorybookPages,
"ShuffleActContracts": ShuffleActContracts,
"ShuffleSubconPaintings": ShuffleSubconPaintings,
"EndGoal": EndGoal,
"ActRandomizer": ActRandomizer,
"ShuffleAlpineZiplines": ShuffleAlpineZiplines,
"LogicDifficulty": LogicDifficulty,
"KnowledgeChecks": KnowledgeChecks,
"RandomizeHatOrder": RandomizeHatOrder,
"UmbrellaLogic": UmbrellaLogic,
"CompassBadgeMode": CompassBadgeMode,
"ShuffleStorybookPages": ShuffleStorybookPages,
"ShuffleActContracts": ShuffleActContracts,
"ShuffleSubconPaintings": ShuffleSubconPaintings,
"EnableDLC1": EnableDLC1,
"Tasksanity": Tasksanity,
"TasksanityTaskStep": TasksanityTaskStep,
"TasksanityCheckCount": TasksanityCheckCount,
"ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal,
"ExcludeTour": ExcludeTour,
"EnableDLC1": EnableDLC1,
"Tasksanity": Tasksanity,
"TasksanityTaskStep": TasksanityTaskStep,
"TasksanityCheckCount": TasksanityCheckCount,
"ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal,
"ExcludeTour": ExcludeTour,
"EnableDeathWish": EnableDeathWish,
"EnableDeathWish": EnableDeathWish,
"DWShuffle": DWShuffle,
"DeathWishOnly": DeathWishOnly,
"DWEnableBonus": DWEnableBonus,
"DWAutoCompleteBonuses": DWAutoCompleteBonuses,
"DWTimePieceRequirement": DWTimePieceRequirement,
"EnableDLC2": EnableDLC2,
"MetroMinPonCost": MetroMinPonCost,
"MetroMaxPonCost": MetroMaxPonCost,
"BaseballBat": BaseballBat,
"EnableDLC2": EnableDLC2,
"MetroMinPonCost": MetroMinPonCost,
"MetroMaxPonCost": MetroMaxPonCost,
"BaseballBat": BaseballBat,
"MinPonCost": MinPonCost,
"MaxPonCost": MaxPonCost,
"MinPonCost": MinPonCost,
"MaxPonCost": MaxPonCost,
"death_link": DeathLink,
"death_link": DeathLink,
}

View File

@@ -254,12 +254,16 @@ blacklisted_acts = {
# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts.
blacklisted_combos = {
"The Illness has Spread": ["Alpine Free Roam"],
"Rush Hour": ["Nyakuza Free Roam"],
"Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam"],
"The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam"],
"Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam"],
"Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!"],
"Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"],
"Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"],
"Time Rift - Curly Tail Trail": ["Nyakuza Free Roam"],
"Time Rift - The Twilight Bell": ["Nyakuza Free Roam"],
"Time Rift - Alpine Skyline": ["Nyakuza Free Roam"],
"Time Rift - Rumbi Factory": ["Alpine Free Roam"],
"Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam"],
}
@@ -271,6 +275,11 @@ def create_regions(world: World):
# ------------------------------------------- HUB -------------------------------------------------- #
menu = create_region(w, "Menu")
spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu)
# we only need the menu and the spaceship regions
if world.is_dw_only():
return
create_rift_connections(w, create_region(w, "Time Rift - Gallery"))
create_rift_connections(w, create_region(w, "Time Rift - The Lab"))
@@ -418,8 +427,8 @@ def create_rift_connections(world: World, region: Region):
i = 1
for name in rift_access_regions[region.name]:
act_region = world.multiworld.get_region(name, world.player)
entrance_name = "{name} Portal - Entrance {num}"
connect_regions(act_region, region, entrance_name.format(name=region.name, num=i), world.player)
entrance_name = f"{region.name} Portal - Entrance {i}"
connect_regions(act_region, region, entrance_name, world.player)
i += 1
@@ -427,7 +436,7 @@ def create_tasksanity_locations(world: World):
ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player)
id_start: int = get_tasksanity_start_id()
for i in range(world.multiworld.TasksanityCheckCount[world.player].value):
location = HatInTimeLocation(world.player, format("Tasksanity Check %i" % (i+1)), id_start+i, ship_shape)
location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape)
ship_shape.locations.append(location)
@@ -603,7 +612,7 @@ def randomize_act_entrances(world: World):
candidate: Region
if len(candidate_list) > 0:
candidate = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)]
candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)]
else:
# plando can still break certain rules, so acts may not always end up shuffled.
for c in region_list:
@@ -619,7 +628,7 @@ def randomize_act_entrances(world: World):
if region.name in rift_access_regions.keys():
rift_dict.setdefault(region.name, candidate)
world.update_chapter_act_info(region, candidate)
update_chapter_act_info(world, region, candidate)
continue
if region.name in rift_access_regions.keys():
@@ -634,14 +643,14 @@ def randomize_act_entrances(world: World):
entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player)
reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate)
world.update_chapter_act_info(region, candidate)
update_chapter_act_info(world, region, candidate)
for name in blacklisted_acts.values():
if not is_act_blacklisted(world, name):
continue
region: Region = world.multiworld.get_region(name, world.player)
world.update_chapter_act_info(region, region)
update_chapter_act_info(world, region, region)
set_rift_rules(world, rift_dict)
@@ -650,7 +659,7 @@ def connect_time_rift(world: World, time_rift: Region, exit_region: Region):
count: int = len(rift_access_regions[time_rift.name])
i: int = 1
while i <= count:
name = format("%s Portal - Entrance %i" % (time_rift.name, i))
name = f"{time_rift.name} Portal - Entrance {i}"
entrance: Entrance = world.multiworld.get_entrance(name, world.player)
reconnect_regions(entrance, entrance.parent_region, exit_region)
i += 1
@@ -686,6 +695,9 @@ def create_region(world: World, name: str) -> Region:
reg = Region(name, world.player, world.multiworld)
for (key, data) in location_table.items():
if world.is_dw_only():
break
if data.nyakuza_thug != "":
continue
@@ -710,11 +722,11 @@ def create_badge_seller(world: World) -> Region:
max_items: int = 0
if world.multiworld.BadgeSellerMaxItems[world.player].value > 0:
max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value,
max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value,
world.multiworld.BadgeSellerMaxItems[world.player].value)
if max_items <= 0:
world.badge_seller_count = 0
world.set_badge_seller_count(0)
return badge_seller
for (key, data) in shop_locations.items():
@@ -729,14 +741,15 @@ def create_badge_seller(world: World) -> Region:
if count >= max_items:
break
world.badge_seller_count = max_items
world.set_badge_seller_count(max_items)
return badge_seller
def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int):
def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance:
entrance = Entrance(player, entrancename, start_region)
start_region.exits.append(entrance)
entrance.connect(exit_region)
return entrance
# Takes an entrance, removes its old connections, and reconnects it between the two regions specified.
@@ -785,12 +798,29 @@ def get_act_original_chapter(world: World, act_name: str) -> Region:
return world.multiworld.get_region(act_chapters[act_name], world.player)
# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game
def update_chapter_act_info(world: World, original_region: Region, new_region: Region):
original_act_info = chapter_act_info[original_region.name]
new_act_info = chapter_act_info[new_region.name]
world.act_connections[original_act_info] = new_act_info
def get_shuffled_region(self, region: str) -> str:
ci: str = chapter_act_info[region]
for key, val in self.act_connections.items():
if val == ci:
for name in chapter_act_info.keys():
if chapter_act_info[name] == key:
return name
def create_thug_shops(world: World):
min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value
max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value
count: int = -1
step: int = 0
old_name: str = ""
thug_items = world.get_nyakuza_thug_items()
for key, data in shop_locations.items():
if data.nyakuza_thug == "":
@@ -800,14 +830,14 @@ def create_thug_shops(world: World):
continue
try:
if world.nyakuza_thug_items[data.nyakuza_thug] <= 0:
if thug_items[data.nyakuza_thug] <= 0:
continue
except KeyError:
pass
if count == -1:
count = world.multiworld.random.randint(min_items, max_items)
world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count)
count = world.random.randint(min_items, max_items)
thug_items.setdefault(data.nyakuza_thug, count)
if count <= 0:
continue
@@ -823,6 +853,8 @@ def create_thug_shops(world: World):
step = 0
count = -1
world.set_nyakuza_thug_items(thug_items)
def create_events(world: World) -> int:
count: int = 0
@@ -832,9 +864,10 @@ def create_events(world: World) -> int:
continue
event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world)
event.show_in_spoiler = False
if data.act_complete_event:
act_completion: str = format("Act Completion (%s)" % data.region)
act_completion: str = f"Act Completion ({data.region})"
event.access_rule = world.multiworld.get_location(act_completion, world.player).access_rule
count += 1

View File

@@ -37,8 +37,9 @@ def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool:
def get_hat_cost(world: World, hat: HatType) -> int:
cost: int = 0
costs = world.get_hat_yarn_costs()
for h in world.get_hat_craft_order():
cost += world.get_hat_yarn_costs().get(h)
cost += costs[h]
if h == hat:
break
@@ -120,7 +121,7 @@ def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bo
if "Free Roam" in entrance.connected_region.name:
return True
name: str = format("Act Completion (%s)" % entrance.connected_region.name)
name: str = f"Act Completion ({entrance.connected_region.name})"
return world.multiworld.get_location(name, world.player).access_rule(state)
@@ -153,6 +154,9 @@ def set_rules(world: World):
if world.multiworld.EndGoal[world.player].value == 2:
final_chapter = ChapterIndex.METRO
chapter_list.append(ChapterIndex.FINALE)
elif world.multiworld.EndGoal[world.player].value == 3:
final_chapter = None
chapter_list.append(ChapterIndex.FINALE)
if world.is_dlc1():
chapter_list.append(ChapterIndex.CRUISE)
@@ -161,7 +165,7 @@ def set_rules(world: World):
chapter_list.append(ChapterIndex.METRO)
chapter_list.remove(starting_chapter)
world.multiworld.random.shuffle(chapter_list)
world.random.shuffle(chapter_list)
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
index1: int = 69
@@ -180,7 +184,7 @@ def set_rules(world: World):
if lowest_index == 0:
pos = 0
else:
pos = world.multiworld.random.randint(0, lowest_index)
pos = world.random.randint(0, lowest_index)
chapter_list.insert(pos, ChapterIndex.ALPINE)
@@ -190,7 +194,7 @@ def set_rules(world: World):
if index >= len(chapter_list):
chapter_list.append(ChapterIndex.METRO)
else:
chapter_list.insert(world.multiworld.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO)
chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO)
lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value
highest_cost: int = world.multiworld.HighestChapterCost[world.player].value
@@ -206,10 +210,9 @@ def set_rules(world: World):
if min_range >= highest_cost:
min_range = highest_cost-1
value: int = world.multiworld.random.randint(min_range, min(highest_cost,
max(lowest_cost, last_cost + cost_increment)))
value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment)))
cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost))
cost = world.random.randint(value, min(value + cost_increment, highest_cost))
if loop_count >= 1:
if last_cost + min_difference > cost:
cost = last_cost + min_difference
@@ -219,9 +222,10 @@ def set_rules(world: World):
last_cost = cost
loop_count += 1
world.set_chapter_cost(final_chapter, world.multiworld.random.randint(
world.multiworld.FinalChapterMinCost[world.player].value,
world.multiworld.FinalChapterMaxCost[world.player].value))
if final_chapter is not None:
world.set_chapter_cost(final_chapter, world.random.randint(
world.multiworld.FinalChapterMinCost[world.player].value,
world.multiworld.FinalChapterMaxCost[world.player].value))
add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player),
lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA)))
@@ -277,7 +281,7 @@ def set_rules(world: World):
for hat in data.required_hats:
if hat is not HatType.NONE:
add_rule(location, lambda state, hat=hat: can_use_hat(state, world, hat))
add_rule(location, lambda state, h=hat: can_use_hat(state, world, h))
if data.hookshot:
add_rule(location, lambda state: can_use_hookshot(state, world))
@@ -294,6 +298,9 @@ def set_rules(world: World):
elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells)
add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))
for misc in data.misc_required:
add_rule(location, lambda state, item=misc: state.has(item, world.player))
if get_difficulty(world) >= 1:
world.multiworld.KnowledgeChecks[world.player].value = 1
@@ -316,14 +323,14 @@ def set_rules(world: World):
act_entrance: Entrance = world.multiworld.get_entrance(act, world.player)
access_rules.append(act_entrance.access_rule)
required_region = act_entrance.connected_region
name: str = format("%s: Connection %i" % (key, i))
name: str = f"{key}: Connection {i}"
new_entrance: Entrance = connect_regions(required_region, region, name, world.player)
entrances.append(new_entrance)
# Copy access rules from act completions
if "Free Roam" not in required_region.name:
rule: typing.Callable[[CollectionState], bool]
name = format("Act Completion (%s)" % required_region.name)
name = f"Act Completion ({required_region.name})"
rule = world.multiworld.get_location(name, world.player).access_rule
access_rules.append(rule)
@@ -558,9 +565,9 @@ def set_mafia_town_rules(world: World):
def set_subcon_rules(world: World):
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: state.can_reach("Toilet of Doom", "Region", world.player)
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
and (not painting_logic(world) or has_paintings(state, world, 1))
or state.can_reach("Your Contract has Expired", "Region", world.player))
or state.has("YCHE Access", world.player))
if world.multiworld.UmbrellaLogic[world.player].value > 0:
add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
@@ -583,6 +590,9 @@ def set_subcon_rules(world: World):
lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player))
if painting_logic(world):
add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player),
lambda state: state.has("Progressive Painting Unlock", world.player))
for key in contract_locations:
if key == "Snatcher's Contract - The Subcon Well":
continue
@@ -679,7 +689,7 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked
# See randomize_act_entrances in Regions.py
# Called BEFORE set_rules!
# Called before set_rules
def set_rift_rules(world: World, regions: typing.Dict[str, Region]):
# This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances.

View File

@@ -1,29 +1,40 @@
from BaseClasses import Item, ItemClassification, Region, LocationProgressType
from .Items import HatInTimeItem, item_table, item_frequencies, item_dlc_enabled, junk_weights,\
create_item, create_multiple_items, create_junk_items, relic_groups, act_contracts, alps_hooks, \
get_total_time_pieces
from .Regions import create_region, create_regions, connect_regions, randomize_act_entrances, chapter_act_info, \
create_events, chapter_regions, act_chapters
from .Locations import HatInTimeLocation, location_table, get_total_locations, contract_locations, is_location_valid, \
get_location_names, get_tasksanity_start_id
from .Types import HatDLC, HatType, ChapterIndex
from .Options import ahit_options, slot_data_options, adjust_options
from worlds.AutoWorld import World
from BaseClasses import Item, ItemClassification, LocationProgressType, Tutorial
from .Items import HatInTimeItem, item_table, create_item, relic_groups, act_contracts, create_itempool
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id
from .Rules import set_rules
import typing
from .Options import ahit_options, slot_data_options, adjust_options
from .Types import HatType, ChapterIndex
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
from .DeathWishRules import set_dw_rules
from worlds.AutoWorld import World, WebWorld
from typing import List, Dict, TextIO
hat_craft_order: typing.Dict[int, typing.List[HatType]] = {}
hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {}
chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {}
hat_craft_order: Dict[int, List[HatType]] = {}
hat_yarn_costs: Dict[int, Dict[HatType, int]] = {}
chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {}
excluded_dws: Dict[int, List[str]] = {}
excluded_bonuses: Dict[int, List[str]] = {}
dw_shuffle: Dict[int, List[str]] = {}
nyakuza_thug_items: Dict[int, Dict[str, int]] = {}
badge_seller_count: Dict[int, int] = {}
class AWebInTime(WebWorld):
theme = "partyTime"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide for setting up A Hat in Time to be played in Archipelago.",
"English",
"ahit_en.md",
"setup/en",
["CookieCat"]
)]
class HatInTimeWorld(World):
"""
A Hat in Time is a cute-as-heck 3D platformer featuring a little girl who stitches hats for wicked powers!
A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers!
Freely explore giant worlds and recover Time Pieces to travel to new heights!
"""
@@ -34,37 +45,50 @@ class HatInTimeWorld(World):
location_name_to_id = get_location_names()
option_definitions = ahit_options
act_connections: typing.Dict[str, str] = {}
nyakuza_thug_items: typing.Dict[str, int] = {}
shop_locs: typing.List[str] = []
act_connections: Dict[str, str] = {}
shop_locs: List[str] = []
item_name_groups = relic_groups
badge_seller_count: int = 0
def generate_early(self):
adjust_options(self)
if self.multiworld.StartWithCompassBadge[self.player].value > 0:
self.multiworld.push_precollected(self.create_item("Compass Badge"))
if self.is_dw_only():
return
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
start_chapter: int = self.multiworld.StartingChapter[self.player].value
if start_chapter == 4 or start_chapter == 3:
if self.multiworld.ActRandomizer[self.player].value == 0 \
or self.multiworld.VanillaAlpine[self.player].value > 0:
if self.multiworld.ActRandomizer[self.player].value == 0:
if start_chapter == 4:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0:
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
if self.multiworld.StartWithCompassBadge[self.player].value > 0:
self.multiworld.push_precollected(self.create_item("Compass Badge"))
def create_regions(self):
self.nyakuza_thug_items = {}
excluded_dws[self.player] = []
excluded_bonuses[self.player] = []
dw_shuffle[self.player] = []
nyakuza_thug_items[self.player] = {}
badge_seller_count[self.player] = 0
self.shop_locs = []
self.badge_seller_count = 0
self.topology_present = self.multiworld.ActRandomizer[self.player].value
create_regions(self)
if self.multiworld.EnableDeathWish[self.player].value > 0:
create_dw_regions(self)
if self.is_dw_only():
return
create_events(self)
# place default contract locations if contract shuffle is off so logic can still utilize them
if self.multiworld.ShuffleActContracts[self.player].value == 0:
for name in contract_locations.keys():
@@ -82,69 +106,13 @@ class HatInTimeWorld(World):
hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE,
HatType.DWELLER, HatType.TIME_STOP]
self.topology_present = self.multiworld.ActRandomizer[self.player].value
# Item Pool
itempool: typing.List[Item] = []
self.calculate_yarn_costs()
yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value)
for i in range(int(len(yarn_pool) * (0.01 * self.multiworld.YarnBalancePercent[self.player].value))):
yarn_pool[i].classification = ItemClassification.progression
itempool += yarn_pool
if self.multiworld.RandomizeHatOrder[self.player].value > 0:
self.multiworld.random.shuffle(hat_craft_order[self.player])
self.random.shuffle(hat_craft_order[self.player])
if self.multiworld.RandomizeHatOrder[self.player].value == 2:
hat_craft_order[self.player].remove(HatType.TIME_STOP)
hat_craft_order[self.player].append(HatType.TIME_STOP)
for name in item_table.keys():
if name == "Yarn":
continue
if not item_dlc_enabled(self, name):
continue
item_type: ItemClassification = item_table.get(name).classification
if item_type is ItemClassification.filler or item_type is ItemClassification.trap:
continue
if name in act_contracts.keys() and self.multiworld.ShuffleActContracts[self.player].value == 0:
continue
if name in alps_hooks.keys() and self.multiworld.ShuffleAlpineZiplines[self.player].value == 0:
continue
if name == "Progressive Painting Unlock" \
and self.multiworld.ShuffleSubconPaintings[self.player].value == 0:
continue
if self.multiworld.StartWithCompassBadge[self.player].value > 0 and name == "Compass Badge":
continue
if name == "Time Piece":
tp_count: int = 40
max_extra: int = 0
if self.is_dlc1():
max_extra += 6
if self.is_dlc2():
max_extra += 10
tp_count += min(max_extra, self.multiworld.MaxExtraTimePieces[self.player].value)
tp_list: typing.List[Item] = create_multiple_items(self, name, tp_count)
for i in range(int(len(tp_list) * (0.01 * self.multiworld.TimePieceBalancePercent[self.player].value))):
tp_list[i].classification = ItemClassification.progression
itempool += tp_list
continue
itempool += create_multiple_items(self, name, item_frequencies.get(name, 1))
create_events(self)
total_locations: int = get_total_locations(self)
itempool += create_junk_items(self, total_locations-len(itempool))
self.multiworld.itempool += itempool
self.multiworld.itempool += create_itempool(self)
def set_rules(self):
self.act_connections = {}
@@ -156,11 +124,37 @@ class HatInTimeWorld(World):
ChapterIndex.CRUISE: -1,
ChapterIndex.METRO: -1}
if self.is_dw_only():
# we already have all items if this is the case, no need for rules
self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression,
None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode",
self.player)
if self.multiworld.DWEnableBonus[self.player].value == 0:
for name in death_wishes:
if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2():
continue
if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle():
continue
full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player)
full_clear.address = None
full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player))
full_clear.show_in_spoiler = False
return
if self.multiworld.ActRandomizer[self.player].value > 0:
randomize_act_entrances(self)
set_rules(self)
if self.multiworld.EnableDeathWish[self.player].value > 0:
set_dw_rules(self)
def create_item(self, name: str) -> Item:
return create_item(self, name)
@@ -182,16 +176,39 @@ class HatInTimeWorld(World):
"Hat3": int(hat_craft_order[self.player][2]),
"Hat4": int(hat_craft_order[self.player][3]),
"Hat5": int(hat_craft_order[self.player][4]),
"BadgeSellerItemCount": self.badge_seller_count,
"BadgeSellerItemCount": badge_seller_count[self.player],
"SeedNumber": self.multiworld.seed} # For shop prices
if self.multiworld.ActRandomizer[self.player].value > 0:
for name in self.act_connections.keys():
slot_data[name] = self.act_connections[name]
if self.is_dlc2():
for name in self.nyakuza_thug_items.keys():
slot_data[name] = self.nyakuza_thug_items[name]
if self.is_dlc2() and not self.is_dw_only():
for name in nyakuza_thug_items[self.player].keys():
slot_data[name] = nyakuza_thug_items[self.player][name]
if self.is_dw():
i: int = 0
for name in excluded_dws[self.player]:
if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal":
continue
slot_data[f"excluded_dw{i}"] = dw_classes[name]
i += 1
i = 0
if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0:
for name in excluded_bonuses[self.player]:
if name in excluded_dws[self.player]:
continue
slot_data[f"excluded_bonus{i}"] = dw_classes[name]
i += 1
if self.multiworld.DWShuffle[self.player].value > 0:
shuffled_dws = self.get_dw_shuffle()
for i in range(len(shuffled_dws)):
slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]]
for option_name in slot_data_options:
option = getattr(self.multiworld, option_name)[self.player]
@@ -199,7 +216,10 @@ class HatInTimeWorld(World):
return slot_data
def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
if self.is_dw_only():
return
new_hint_data = {}
alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"]
metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"]
@@ -220,63 +240,28 @@ class HatInTimeWorld(World):
else:
continue
new_hint_data[location.address] = self.get_shuffled_region(region_name)
new_hint_data[location.address] = get_shuffled_region(self, region_name)
if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0:
ship_shape_region = self.get_shuffled_region("Ship Shape")
ship_shape_region = get_shuffled_region(self, "Ship Shape")
id_start: int = get_tasksanity_start_id()
for i in range(self.multiworld.TasksanityCheckCount[self.player].value):
new_hint_data[id_start+i] = ship_shape_region
hint_data[self.player] = new_hint_data
def write_spoiler_header(self, spoiler_handle: typing.TextIO):
def write_spoiler_header(self, spoiler_handle: TextIO):
for i in self.get_chapter_costs():
spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)]))
for hat in hat_craft_order[self.player]:
spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat]))
def calculate_yarn_costs(self):
mw = self.multiworld
p = self.player
min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value))
max_cost: int = 0
for i in range(5):
cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost))
hat_yarn_costs[self.player][HatType(i)] = cost
max_cost += cost
available_yarn = mw.YarnAvailable[p].value
if max_cost > available_yarn:
mw.YarnAvailable[p].value = max_cost
available_yarn = max_cost
# make sure we always have at least 8 extra
if max_cost + 8 > available_yarn:
mw.YarnAvailable[p].value += (max_cost + 8) - available_yarn
def set_chapter_cost(self, chapter: ChapterIndex, cost: int):
chapter_timepiece_costs[self.player][chapter] = cost
def get_chapter_cost(self, chapter: ChapterIndex) -> int:
return chapter_timepiece_costs[self.player].get(chapter)
# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game
def update_chapter_act_info(self, original_region: Region, new_region: Region):
original_act_info = chapter_act_info[original_region.name]
new_act_info = chapter_act_info[new_region.name]
self.act_connections[original_act_info] = new_act_info
def get_shuffled_region(self, region: str) -> str:
ci: str = chapter_act_info[region]
for key, val in self.act_connections.items():
if val == ci:
for name in chapter_act_info.keys():
if chapter_act_info[name] == key:
return name
return chapter_timepiece_costs[self.player][chapter]
def get_hat_craft_order(self):
return hat_craft_order[self.player]
@@ -295,3 +280,47 @@ class HatInTimeWorld(World):
def is_dw(self) -> bool:
return self.multiworld.EnableDeathWish[self.player].value > 0
def is_dw_only(self) -> bool:
return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0
def get_excluded_dws(self):
return excluded_dws[self.player]
def get_excluded_bonuses(self):
return excluded_bonuses[self.player]
def is_dw_excluded(self, name: str) -> bool:
# don't exclude Seal the Deal if it's our goal
if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \
and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]:
return False
if name in excluded_dws[self.player]:
return True
return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player]
def is_bonus_excluded(self, name: str) -> bool:
if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]:
return True
return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player]
def get_dw_shuffle(self):
return dw_shuffle[self.player]
def set_dw_shuffle(self, shuffle: List[str]):
dw_shuffle[self.player] = shuffle
def get_badge_seller_count(self) -> int:
return badge_seller_count[self.player]
def set_badge_seller_count(self, value: int):
badge_seller_count[self.player] = value
def get_nyakuza_thug_items(self):
return nyakuza_thug_items[self.player]
def set_nyakuza_thug_items(self, items: Dict[str, int]):
nyakuza_thug_items[self.player] = items