From 1daafff04b10195837666f6dedb23f262f328c0e Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 8 Sep 2023 14:27:28 -0400 Subject: [PATCH] 1.0 Preparations --- worlds/ahit/DeathWishLocations.py | 276 ++++++++++++++--- worlds/ahit/DeathWishRules.py | 491 ++++++++++++++++++++++++++++++ worlds/ahit/Items.py | 170 ++++++++--- worlds/ahit/Locations.py | 207 ++++++++++--- worlds/ahit/Options.py | 475 +++++++++++++++++++---------- worlds/ahit/Regions.py | 71 +++-- worlds/ahit/Rules.py | 44 +-- worlds/ahit/__init__.py | 301 +++++++++--------- 8 files changed, 1568 insertions(+), 467 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index 244c790ffb..e8e2e7941a 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -1,52 +1,240 @@ +from .Locations import HatInTimeLocation, death_wishes +from .Regions import connect_regions, create_region +from BaseClasses import Region, LocationProgressType +from worlds.generic.Rules import add_rule +from worlds.AutoWorld import World +from typing import List -death_wishes = [ - "Beat the Heat", - "So You're Back From Outer Space", - "Mafia's Jumps", - "Collect-a-thon", - "She Speedran from Outerspace", - "Vault Codes in the Wind", - "Encore! Encore!", - "Rift Collapse: Mafia of Cooks", +dw_prereqs = { + "So You're Back From Outer Space": ["Beat the Heat"], + "Snatcher's Hit List": ["Beat the Heat"], + "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"], + "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"], + "Collect-a-thon": ["So You're Back From Outer Space"], + "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"], + "Mafia's Jumps": ["She Speedran from Outer Space"], + "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"], + "Encore! Encore!": ["Collect-a-thon"], + + "Security Breach": ["Beat the Heat"], + "Rift Collapse: Dead Bird Studio": ["Security Breach"], + "The Great Big Hootenanny": ["Security Breach"], + "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"], + "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"], + "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"], + "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"], + "Zero Jumps": ["Rift Collapse: Dead Bird Studio"], + "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"], + + "Speedrun Well": ["Beat the Heat"], + "Rift Collapse: Sleepy Subcon": ["Speedrun Well"], + "Boss Rush": ["Speedrun Well"], + "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"], + "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"], + "Community Rift: Twilight Travels": ["Quality Time with Snatcher"], + "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"], + + "Bird Sanctuary": ["Beat the Heat"], + "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"], + "Wound-Up Windmill": ["Bird Sanctuary"], + "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"], + "Camera Tourist": ["Rift Collapse: Alpine Skyline"], + "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"], + "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"], + + "The Mustache Gauntlet": ["Wound-Up Windmill"], + "No More Bad Guys": ["The Mustache Gauntlet"], + "Seal the Deal": ["Encore! Encore!", "Killing Two Birds", + "Breaching the Contract", "No More Bad Guys"], + + "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio", + "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"], + + "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"], +} + +dw_candles = [ "Snatcher's Hit List", - "Snatcher Coins in Mafia Town" - - "Security Breach", - "10 Seconds until Self-Destruct", - "The Great Big Hootenanny", - "Killing Two Birds", - "Rift Collapse: Dead Bird Studio", - "Snatcher Coins in Battle of the Birds", "Zero Jumps", - - "Speedrun Well", - "Boss Rush", - "Quality Time with Snatcher", - "Breaching the Contract", - "Rift Collapse: Sleepy Subcon", - "Snatcher Coins in Subcon Forest", - - "Bird Sanctuary", - "Wound-Up Windmill", - "The Illness has Speedrun", - "Rift Collapse: Alpine Skyline", - "Snatcher Coins in Alpine Skyline", "Camera Tourist", - - "The Mustache Gauntlet", - "No More Bad Guys", - - "Seal the Deal", - "Rift Collapse: Deep Sea", - "Cruisin' for a Bruisin'", - - "Community Map: Rhythm Jump Studio", - "Community Map: Twilight Travels", - "Community Map: The Mountain Rift", + "Snatcher Coins in Mafia Town", + "Snatcher Coins in Battle of the Birds", + "Snatcher Coins in Subcon Forest", + "Snatcher Coins in Alpine Skyline", "Snatcher Coins in Nyakuza Metro", ] -dw_prereqs = { - "Snatcher's Hit List": ["Beat the Heat"], +annoying_dws = [ + "Vault Codes in the Wind", + "Boss Rush", + "Camera Tourist", + "The Mustache Gauntlet", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + "Seal the Deal", # Non-excluded if goal +] -} \ No newline at end of file +# includes the above as well +annoying_bonuses = [ + "So You're Back From Outer Space", + "Encore! Encore!", + "Snatcher's Hit List", + "10 Seconds until Self-Destruct", + "Killing Two Birds", + "Snatcher Coins in Battle of the Birds", + "Zero Jumps", + "Bird Sanctuary", + "Wound-Up Windmill", + "Snatcher Coins in Alpine Skyline", + "Seal the Deal", +] + +dw_classes = { + "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder", + "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace", + "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody", + "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy", + "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown", + "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX", + "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien", + "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien", + "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault", + "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown", + + "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards", + "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade", + "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds", + "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime", + "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX", + "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds", + "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses", + + "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell", + "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon", + "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush", + "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest", + "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX", + "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon", + + "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse", + "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps", + "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill", + "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness", + "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps", + "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1", + + "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle", + "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX", + + "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX", + "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise", + "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks", + + "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump", + "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels", + "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift", + + "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro", +} + + +def create_dw_regions(world: World): + if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + for name in annoying_dws: + world.get_excluded_dws().append(name) + + if world.multiworld.DWEnableBonus[world.player].value == 0 \ + or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + for name in death_wishes: + world.get_excluded_bonuses().append(name) + elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + for name in annoying_bonuses: + world.get_excluded_bonuses().append(name) + + if world.multiworld.DWExcludeCandles[world.player].value > 0: + for name in dw_candles: + if name in world.get_excluded_dws(): + continue + world.get_excluded_dws().append(name) + + spaceship = world.multiworld.get_region("Spaceship", world.player) + dw_map: Region = create_region(world, "Death Wish Map") + entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + + add_rule(entrance, lambda state: state.has("Time Piece", world.player, + world.multiworld.DWTimePieceRequirement[world.player].value)) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list: List[str] = [] + for name in death_wishes.keys(): + if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): + continue + + dw_list.append(name) + + world.random.shuffle(dw_list) + count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + dw_shuffle: List[str] = [] + total = min(len(dw_list), count) + for i in range(total): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.multiworld.EndGoal[world.player].value == 3: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.set_dw_shuffle(dw_shuffle) + prev_dw: Region + for i in range(len(dw_shuffle)): + name = dw_shuffle[i] + dw = create_region(world, name) + + if i == 0: + connect_regions(dw_map, dw, f"-> {name}", world.player) + else: + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + + loc_id = death_wishes[name] + main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + + if name in world.get_excluded_bonuses(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) + prev_dw = dw + else: + for key, loc_id in death_wishes.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + world.get_excluded_dws().append(key) + continue + + dw = create_region(world, key) + + if key == "Beat the Heat": + connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + elif key in dw_prereqs.keys(): + for name in dw_prereqs[key]: + parent = world.multiworld.get_region(name, world.player) + connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + + main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + + if key in world.get_excluded_bonuses(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(key): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) \ No newline at end of file diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index e69de29bb2..d40da5d90f 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,491 @@ +from worlds.AutoWorld import World, CollectionState +from .Locations import LocData, death_wishes, HatInTimeLocation +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, has_paintings +from .Types import HatType +from .DeathWishLocations import dw_prereqs, dw_candles +from .Items import HatInTimeItem +from BaseClasses import Entrance, Location, ItemClassification +from worlds.generic.Rules import add_rule +from typing import List, Callable + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "Beat the Heat": LocData(umbrella=True), + "So You're Back From Outer Space": LocData(hookshot=True), + "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), + "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), + + "Security Breach": LocData(hit_requirement=1), + "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), + + "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Boss Rush": LocData(hit_requirement=1, umbrella=True), + "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Bird Sanctuary": LocData(hookshot=True), + "Wound-Up Windmill": LocData(hookshot=True), + "The Illness has Speedrun": LocData(hookshot=True, required_hats=[HatType.SPRINT]), + "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Camera Tourist": LocData(misc_required=["Camera Badge"]), + + "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Rift Collapse - Deep Sea": LocData(hookshot=True, required_hats=[HatType.DWELLER]), +} + +# Includes main objective requirements +dw_bonus_requirements = { + # Some One-Hit Hero requirements need badge pins as well because of Hookshot + "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]), + + "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge"]), + "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), + + "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), + "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), + + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.SPRINT]), +} + +dw_stamp_costs = { + "So You're Back From Outer Space": 2, + "Collect-a-thon": 5, + "She Speedran from Outer Space": 8, + "Encore! Encore!": 10, + + "Security Breach": 4, + "The Great Big Hootenanny": 7, + "10 Seconds until Self-Destruct": 15, + "Killing Two Birds": 25, + "Snatcher Coins in Nyakuza Metro": 30, + + "Speedrun Well": 10, + "Boss Rush": 15, + "Quality Time with Snatcher": 20, + "Breaching the Contract": 40, + + "Bird Sanctuary": 15, + "Wound-Up Windmill": 30, + "The Illness has Speedrun": 35, + + "The Mustache Gauntlet": 35, + "No More Bad Guys": 50, + "Seal the Deal": 70, +} + + +def set_dw_rules(world: World): + if "Snatcher's Hit List" not in world.get_excluded_dws() \ + or "Camera Tourist" not in world.get_excluded_dws(): + create_enemy_events(world) + + dw_list: List[str] = [] + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list = world.get_dw_shuffle() + else: + for name in death_wishes.keys(): + dw_list.append(name) + + for name in dw_list: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + dw = world.multiworld.get_region(name, world.player) + temp_list: List[Location] = [] + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + temp_list.append(main_objective) + temp_list.append(full_clear) + + if world.multiworld.DWShuffle[world.player].value == 0: + if name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + + if world.multiworld.DWEnableBonus[world.player].value == 0: + # place nothing, but let the locations exist still, so we can use them for bonus stamp rules + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + full_clear.show_in_spoiler = False + + # Stamps are event locations + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + # Specific Rules + if name == "The Illness has Speedrun": + # killing the flowers without the umbrella is way too slow + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + elif name == "The Mustache Gauntlet": + # don't get burned bonus requires a way to kill fire crows without being burned + add_rule(full_clear, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE)) + elif name == "Vault Codes in the Wind": + add_rule(main_objective, lambda state: can_use_hat(state, world, HatType.TIME_STOP), "or") + + if name in dw_candles: + set_candle_dw_rules(name, world) + + main_rule: Callable[[CollectionState], bool] + + for i in range(len(temp_list)): + loc = temp_list[i] + data: LocData + + if loc.name == main_objective.name: + data = dw_requirements.get(name) + else: + data = dw_bonus_requirements.get(name) + + if data is None: + continue + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(loc, lambda state, paintings=data.paintings: state.has("Progressive Painting Unlock", + world.player, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(loc, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + main_rule = main_objective.access_rule + + if loc.name == main_objective.name: + add_rule(main_stamp, loc.access_rule) + elif loc.name == full_clear.name: + add_rule(loc, main_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + and not world.is_bonus_excluded(loc.name): + add_rule(bonus_stamps, loc.access_rule) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_shuffle = world.get_dw_shuffle() + for i in range(len(dw_shuffle)): + if i == 0: + continue + + name = dw_shuffle[i] + prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) + add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) + else: + for key, reqs in dw_prereqs.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + access_rules: List[Callable[[CollectionState], bool]] = [] + entrances: List[Entrance] = [] + + for parent in reqs: + entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player) + entrances.append(entrance) + + if not world.is_dw_excluded(parent): + access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player)) + + for entrance in entrances: + for rule in access_rules: + add_rule(entrance, rule) + + if world.multiworld.EndGoal[world.player].value == 3: + world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", + world.player) + + +def get_total_dw_stamps(state: CollectionState, world: World) -> int: + if world.multiworld.DWShuffle[world.player].value > 0: + return 999 # no stamp costs in death wish shuffle + + count: int = 0 + peace_and_tranquility: bool = world.multiworld.DWEnableBonus[world.player].value == 0 \ + and world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 + + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + if state.has(f"1 Stamp - {name}", world.player): + count += 1 + else: + continue + + # If bonus rewards and auto bonus completion is off, obtaining stamps via P&T is in logic + # Candles don't have P&T + if peace_and_tranquility and name not in dw_candles: + count += 2 + continue + + if state.has(f"2 Stamps - {name}", world.player): + count += 2 + elif name not in dw_candles: + # all non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + count += 1 + + return count + + +def set_candle_dw_rules(name: str, world: World): + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "Zero Jumps": + add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) + add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + and state.has("Train Rush Cleared", world.player) and can_use_hat(state, world, HatType.ICE)) + + elif name == "Snatcher's Hit List": + add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) + add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + + elif name == "Camera Tourist": + add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world)) + + elif name == "Snatcher Coins in Mafia Town": + add_rule(main_objective, lambda state: state.has("MT Access", world.player) + or state.has("HUMT Access", world.player)) + + add_rule(full_clear, lambda state: state.has("CTR Access", world.player) + or state.has("HUMT Access", world.player) + and (world.multiworld.UmbrellaLogic[world.player].value == 0 or state.has("Umbrella", world.player)) + or state.has("DWTM Access", world.player) + or state.has("TGV Access", world.player)) + + elif name == "Snatcher Coins in Battle of the Birds": + add_rule(main_objective, lambda state: state.has("PP Access", world.player) + or state.has("DBS Access", world.player) + or state.has("Train Rush Cleared", world.player)) + + add_rule(full_clear, lambda state: state.has("PP Access", world.player) + and state.has("DBS Access", world.player) + and state.has("Train Rush Cleared", world.player)) + + elif name == "Snatcher Coins in Subcon Forest": + add_rule(main_objective, lambda state: state.has("SF Access", world.player)) + + add_rule(main_objective, lambda state: has_paintings(state, world, 1) and (can_use_hookshot(state, world) + or can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + or has_paintings(state, world, 3)) + + add_rule(full_clear, lambda state: has_paintings(state, world, 3) and can_use_hookshot(state, world) + and (can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER))) + + elif name == "Snatcher Coins in Alpine Skyline": + add_rule(main_objective, lambda state: state.has("LC Access", world.player) + or state.has("WM Access", world.player)) + + add_rule(full_clear, lambda state: state.has("LC Access", world.player) + and state.has("WM Access", world.player)) + + elif name == "Snatcher Coins in Nyakuza Metro": + add_rule(main_objective, lambda state: state.has("Bluefin Tunnel Cleared", world.player) + or (state.has("Nyakuza Intro Cleared", world.player) + and (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)))) + + add_rule(full_clear, lambda state: state.has("Bluefin Tunnel Cleared", world.player) + and (state.has("Nyakuza Intro Cleared", world.player) + and (state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player)))) + + +def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: + total: int = 0 + + for name, hats in zero_jumps.items(): + if not state.has(f"{name} Cleared", world.player): + continue + + valid: bool = True + + for hat in hats: + if not can_use_hat(state, world, hat): + valid = False + break + + if valid: + total += 1 + + return total + + +def get_reachable_enemy_count(state: CollectionState, world: World) -> int: + count: int = 0 + for enemy in hit_list.keys(): + if enemy in bosses: + continue + + if state.has(enemy, world.player): + count += 1 + + return count + + +def can_reach_all_bosses(state: CollectionState, world: World) -> bool: + for boss in bosses: + if not state.has(boss, world.player): + return False + + return True + + +def create_enemy_events(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and not world.is_dlc1() \ + or world.multiworld.ExcludeTour[world.player].value > 0: + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(area, world.player) + event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region) + event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + + if enemy == "Toxic Flower": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + if area == "The Illness has Spread": + add_rule(event, lambda state: not zipline_logic(world) or + state.has("Zipline Unlock - The Birdhouse Path", world.player) + or state.has("Zipline Unlock - The Lava Cake Path", world.player) + or state.has("Zipline Unlock - The Windmill Path", world.player)) + + elif enemy == "Director": + if area == "Dead Bird Studio Basement": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + elif enemy == "Snatcher" or enemy == "Mustache Girl": + if area == "Boss Rush": + # need to be able to kill toilet + add_rule(event, lambda state: can_hit(state, world)) + + elif area == "The Finale" and enemy == "Mustache Girl": + add_rule(event, lambda state: can_use_hookshot(state, world) + and can_use_hat(state, world, HatType.DWELLER)) + + elif enemy == "Shock Squid" or enemy == "Ninja Cat": + if area == "Time Rift - Deep Sea": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + +# Zero Jumps completable levels, with required hats if any +zero_jumps = { + "Welcome to Mafia Town": [], + "Cheating the Race": [HatType.TIME_STOP], + "Picture Perfect": [], + "Train Rush": [HatType.ICE], + "Contractual Obligations": [], + "Your Contract has Expired": [], + "Mail Delivery Service": [], # rule for needing sprint is already on act completion +} + +# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them +hit_list = { + "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", + "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks", + "So You're Back From Outer Space"], + + "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell", + "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet", + "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"], + + "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"], + + "Rat": ["Down with the Mafia!", "Bluefin Tunnel"], + + "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea", + "Rift Collapse: Sleepy Subcon"], + + "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"], + + "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well", + "The Lava Cake", "The Windmill"], + + "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary", + "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"], + + "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"], + + "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"], + + "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"], + + "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet", + "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea", + "Rift Collapse: Alpine Skyline"], + + # Bosses + "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], + + "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Toilet": ["Toilet of Doom", "Boss Rush"], + + "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", + "Quality Time with Snatcher"], + + "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"], + + "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], +} + +bosses = [ + "Mafia Boss", + "Conductor", + "Toilet", + "Snatcher", + "Toxic Flower", + "Mustache Girl", +] diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py index deac2587b6..729350da1d 100644 --- a/worlds/ahit/Items.py +++ b/worlds/ahit/Items.py @@ -1,19 +1,121 @@ from BaseClasses import Item, ItemClassification from worlds.AutoWorld import World -from .Types import HatDLC -import typing +from .Types import HatDLC, HatType +from .Locations import get_total_locations +from .Rules import get_difficulty, is_player_knowledgeable +from typing import Optional, NamedTuple, List, Dict -class ItemData(typing.NamedTuple): - code: typing.Optional[int] +class ItemData(NamedTuple): + code: Optional[int] classification: ItemClassification - dlc_flags: typing.Optional[HatDLC] = HatDLC.none + dlc_flags: Optional[HatDLC] = HatDLC.none class HatInTimeItem(Item): game: str = "A Hat in Time" +def create_itempool(world: World) -> List[Item]: + itempool: List[Item] = [] + if not world.is_dw_only(): + calculate_yarn_costs(world) + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.multiworld.YarnAvailable[world.player].value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(world, name): + continue + + item_type: ItemClassification = item_table.get(name).classification + if get_difficulty(world) >= 1 or is_player_knowledgeable(world) \ + and (name == "Scooter Badge" or name == "No Bonk Badge"): + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin": + item_type = ItemClassification.progression + + if world.is_dw_only(): + if item_type is ItemClassification.progression \ + or item_type is ItemClassification.progression_skip_balancing: + continue + + # progression balance anything useful, since we have basically no progression in this mode + if item_type is ItemClassification.useful: + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + continue + + if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + continue + + if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if world.is_dlc1(): + max_extra += 6 + + if world.is_dlc2(): + max_extra += 10 + + tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) + + for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) + + total_locations: int = get_total_locations(world) + itempool += create_junk_items(world, total_locations - len(itempool)) + return itempool + + +def calculate_yarn_costs(world: World): + mw = world.multiworld + p = world.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + world.get_hat_yarn_costs()[HatType(i)] = cost + max_cost += cost + + available_yarn: int = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + if max_cost + mw.MinExtraYarn[p].value > available_yarn: + mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + + def item_dlc_enabled(world: World, name: str) -> bool: data = item_table[name] @@ -29,43 +131,38 @@ def item_dlc_enabled(world: World, name: str) -> bool: return False -def get_total_time_pieces(world: World) -> int: - count: int = 40 - if world.is_dlc1(): - count += 6 - - if world.is_dlc2(): - count += 10 - - return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) - - def create_item(world: World, name: str) -> Item: data = item_table[name] return HatInTimeItem(name, data.classification, data.code, world.player) -def create_multiple_items(world: World, name: str, count: int = 1) -> typing.List[Item]: +def create_multiple_items(world: World, name: str, count: int = 1, + item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + data = item_table[name] - itemlist: typing.List[Item] = [] + itemlist: List[Item] = [] for i in range(count): - itemlist += [HatInTimeItem(name, data.classification, data.code, world.player)] + itemlist += [HatInTimeItem(name, item_type, data.code, world.player)] return itemlist -def create_junk_items(world: World, count: int) -> typing.List[Item]: +def create_junk_items(world: World, count: int) -> List[Item]: trap_chance = world.multiworld.TrapChance[world.player].value - junk_pool: typing.List[Item] = [] - junk_list: typing.Dict[str, int] = {} - trap_list: typing.Dict[str, int] = {} + junk_pool: List[Item] = [] + junk_list: Dict[str, int] = {} + trap_list: Dict[str, int] = {} ic: ItemClassification for name in item_table.keys(): ic = item_table[name].classification if ic == ItemClassification.filler: + if world.is_dw_only() and "Pons" in name: + continue + junk_list[name] = junk_weights.get(name) + elif trap_chance > 0 and ic == ItemClassification.trap: if name == "Baby Trap": trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value @@ -75,12 +172,12 @@ def create_junk_items(world: World, count: int) -> typing.List[Item]: trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value for i in range(count): - if trap_chance > 0 and world.multiworld.random.randint(1, 100) <= trap_chance: + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: junk_pool += [world.create_item( - world.multiworld.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] else: junk_pool += [world.create_item( - world.multiworld.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] return junk_pool @@ -110,21 +207,22 @@ ahit_items = { "Hover Badge": ItemData(300026, ItemClassification.useful), "Hookshot Badge": ItemData(300027, ItemClassification.progression), "Item Magnet Badge": ItemData(300028, ItemClassification.useful), - "No Bonk Badge": ItemData(300029, ItemClassification.progression), + "No Bonk Badge": ItemData(300029, ItemClassification.useful), "Compass Badge": ItemData(300030, ItemClassification.useful), - "Scooter Badge": ItemData(300031, ItemClassification.progression), - "Badge Pin": ItemData(300043, ItemClassification.useful), + "Scooter Badge": ItemData(300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), # Other - # "Rift Token": ItemData(300032, ItemClassification.filler), - "Random Cosmetic": ItemData(300044, ItemClassification.filler), "Umbrella": ItemData(300033, ItemClassification.progression), + "Badge Pin": ItemData(300043, ItemClassification.useful), # Garbage items "25 Pons": ItemData(300034, ItemClassification.filler), "50 Pons": ItemData(300035, ItemClassification.filler), "100 Pons": ItemData(300036, ItemClassification.filler), "Health Pon": ItemData(300037, ItemClassification.filler), + "Random Cosmetic": ItemData(300044, ItemClassification.filler), # Traps "Baby Trap": ItemData(300039, ItemClassification.trap), @@ -144,10 +242,6 @@ ahit_items = { "Metro Ticket - Green": ItemData(300046, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Blue": ItemData(300047, ItemClassification.progression, HatDLC.dlc2), "Metro Ticket - Pink": ItemData(300048, ItemClassification.progression, HatDLC.dlc2), - - # Death Wish items - "One-Hit Hero Badge": ItemData(300038, ItemClassification.progression, HatDLC.death_wish), - "Camera Badge": ItemData(300042, ItemClassification.progression, HatDLC.death_wish), } act_contracts = { @@ -180,10 +274,10 @@ item_frequencies = { junk_weights = { "25 Pons": 50, - "50 Pons": 10, + "50 Pons": 25, + "100 Pons": 10, "Health Pon": 35, - "100 Pons": 5, - "Random Cosmetic": 25, + "Random Cosmetic": 35, } item_table = { diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 5e32240b30..8e63dad539 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -6,12 +6,13 @@ from .Options import TasksanityCheckCount class LocData(NamedTuple): - id: int - region: str + id: Optional[int] = 0 + region: Optional[str] = "" required_hats: Optional[List[HatType]] = [HatType.NONE] hookshot: Optional[bool] = False dlc_flags: Optional[HatDLC] = HatDLC.none paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] # For UmbrellaLogic setting umbrella: Optional[bool] = False # Umbrella required for this check @@ -29,12 +30,28 @@ class HatInTimeLocation(Location): def get_total_locations(world: World) -> int: total: int = 0 - for (name) in location_table.keys(): - if is_location_valid(world, name): - total += 1 + if not world.is_dw_only(): + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 - if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: - total += world.multiworld.TasksanityCheckCount[world.player].value + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + if world.is_dw(): + if world.multiworld.DWShuffle[world.player].value > 0: + total += len(world.get_dw_shuffle()) + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += len(world.get_dw_shuffle()) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += 37 + if world.is_dlc2(): + total += 1 return total @@ -58,15 +75,20 @@ def is_location_valid(world: World, location: str) -> bool: if not location_dlc_enabled(world, location): return False - if location in storybook_pages.keys() \ - and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + and location in storybook_pages.keys(): return False - if location in shop_locations and location not in world.shop_locs: + if location not in world.shop_locs and location in shop_locations: return False data = location_table.get(location) or event_locs.get(location) - if data.region == "Time Rift - Tour" and world.multiworld.ExcludeTour[world.player].value > 0: + if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + return False + + # No need for all those event items if we're not doing candles + if data.dlc_flags is HatDLC.death_wish and world.multiworld.DWExcludeCandles[world.player].value > 0 \ + and location in event_locs.keys(): return False return True @@ -76,7 +98,11 @@ def get_location_names() -> Dict[str, int]: names = {name: data.id for name, data in location_table.items()} id_start: int = get_tasksanity_start_id() for i in range(TasksanityCheckCount.range_end): - names.setdefault(format("Tasksanity Check %i") % (i+1), id_start+i) + names.setdefault(f"Tasksanity Check {i+1}", id_start+i) + + for (key, loc_id) in death_wishes.items(): + names.setdefault(f"{key} - Main Objective", loc_id) + names.setdefault(f"{key} - All Clear", loc_id+1) return names @@ -169,7 +195,7 @@ ahit_locations = { "Dead Bird Studio Basement - Cameras": LocData(305431, "Dead Bird Studio Basement", hookshot=True), "Dead Bird Studio Basement - Locked Room": LocData(305819, "Dead Bird Studio Basement", hookshot=True), - # 320000 range - Subcon Forest + # Subcon Forest "Contractual Obligations - Cherry Bomb Bone Cage": LocData(324761, "Contractual Obligations"), "Subcon Village - Tree Top Ice Cube": LocData(325078, "Subcon Forest Area"), "Subcon Village - Graveyard Ice Cube": LocData(325077, "Subcon Forest Area"), @@ -178,7 +204,8 @@ ahit_locations = { "Subcon Village - Snatcher Statue Chest": LocData(323730, "Subcon Forest Area", paintings=1), "Subcon Village - Stump Platform Chest": LocData(323729, "Subcon Forest Area"), "Subcon Forest - Giant Tree Climb": LocData(325470, "Subcon Forest Area"), - + + "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Gravestone": LocData(326296, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=1), @@ -189,30 +216,6 @@ ahit_locations = { "Subcon Forest - Swamp Treehouse": LocData(325468, "Subcon Forest Area", paintings=1), "Subcon Forest - Swamp Tree Chest": LocData(323728, "Subcon Forest Area", paintings=1), - "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), - "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), - - "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", - required_hats=[HatType.DWELLER], paintings=3), - - "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", - required_hats=[HatType.DWELLER], - hookshot=True, - paintings=3), - "Subcon Forest - Burning House": LocData(324710, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Tree Climb": LocData(325079, "Subcon Forest Area", paintings=2), "Subcon Forest - Burning Stump Chest": LocData(323731, "Subcon Forest Area", paintings=2), @@ -221,7 +224,6 @@ ahit_locations = { "Subcon Forest - Spider Bone Cage B": LocData(325080, "Subcon Forest Area", paintings=2), "Subcon Forest - Triple Spider Bounce": LocData(324765, "Subcon Forest Area", paintings=2), "Subcon Forest - Noose Treehouse": LocData(324856, "Subcon Forest Area", hookshot=True, paintings=2), - "Subcon Forest - Ice Cube Shack": LocData(324465, "Subcon Forest Area", paintings=1), "Subcon Forest - Long Tree Climb Chest": LocData(323734, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=2), @@ -234,6 +236,30 @@ ahit_locations = { "Subcon Forest - Magnet Badge Bush": LocData(325479, "Subcon Forest Area", required_hats=[HatType.BREWING], paintings=3), + + "Subcon Forest - Dweller Stump": LocData(324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(325472, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(325082, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Shack": LocData(324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(324766, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + hookshot=True, + paintings=3), "Subcon Well - Hookshot Badge Chest": LocData(324114, "The Subcon Well", hit_requirement=1, paintings=1), "Subcon Well - Above Chest": LocData(324612, "The Subcon Well", hit_requirement=1, paintings=1), @@ -245,7 +271,7 @@ ahit_locations = { "Queen Vanessa's Manor - Hall Chest": LocData(323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), "Queen Vanessa's Manor - Chandelier": LocData(325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), - # 330000 range - Alpine Skyline + # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(334856, "Goat Village"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(334855, "Goat Village"), "Alpine Skyline - Goat Refinery": LocData(333635, "Alpine Skyline Area"), @@ -320,7 +346,6 @@ ahit_locations = { } act_completions = { - # 310000 range - Act Completions "Act Completion (Time Rift - Gallery)": LocData(312758, "Time Rift - Gallery"), "Act Completion (Time Rift - The Lab)": LocData(312838, "Time Rift - The Lab"), @@ -479,13 +504,6 @@ storybook_pages = { "Rumbi Factory - Page: Last Area": LocData(345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), } -contract_locations = { - "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), - "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), - "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), - "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), -} - shop_locations = { "Badge Seller - Item 1": LocData(301003, "Badge Seller"), "Badge Seller - Item 2": LocData(301004, "Badge Seller"), @@ -623,6 +641,13 @@ shop_locations = { } +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(300203, "Subcon Forest Area"), +} + # Don't put any of the locations from peaks here, the rules for their entrances are set already zipline_unlocks = { "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", @@ -652,7 +677,9 @@ tihs_locations = [ event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town", act_complete_event=False), - "Subcon Forest Access": LocData(0, "Subcon Forest Area", act_complete_event=False), + "TOD Access": LocData(0, "Toilet of Doom", act_complete_event=False), + "YCHE Access": LocData(0, "Your Contract has Expired", act_complete_event=False), + "Birdhouse Cleared": LocData(0, "The Birdhouse"), "Lava Cake Cleared": LocData(0, "The Lava Cake"), "Windmill Cleared": LocData(0, "The Windmill"), @@ -670,6 +697,39 @@ event_locs = { "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2), "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2), "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2), + + + # -------------- Death Wish Candle Related --------------- # + + + # Snatcher Coins + "MT Access": LocData(0, "Mafia Town Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "DWTM Access": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "CTR Access": LocData(0, "Cheating the Race", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "TGV Access": LocData(0, "The Golden Vault", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "DBS Access": LocData(0, "Dead Bird Studio - Elevator Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "PP Access": LocData(0, "Picture Perfect", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "SF Access": LocData(0, "Subcon Forest Area", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + "LC Access": LocData(0, "The Lava Cake", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "WM Access": LocData(0, "The Windmill", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + # Camera Tourist + "Mafia Boss": LocData(0, "Down with the Mafia!", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Conductor": LocData(0, "Dead Bird Studio Basement", dlc_flags=HatDLC.death_wish), + "Snatcher": LocData(0, "Your Contract has Expired", act_complete_event=False, dlc_flags=HatDLC.death_wish), + "Evil Flower": LocData(0, "The Illness has Spread", act_complete_event=False, dlc_flags=HatDLC.death_wish), + + # Zero Jumps + "Welcome to Mafia Town Cleared": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + "Picture Perfect Cleared": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + "Contractual Obligations Cleared": LocData(0, "Contractual Obligations", dlc_flags=HatDLC.death_wish), + "Your Contract has Expired Cleared": LocData(0, "Your Contract has Expired", dlc_flags=HatDLC.death_wish), + "Mail Delivery Service Cleared": LocData(0, "Mail Delivery Service", dlc_flags=HatDLC.death_wish), + "Cheating the Race Cleared": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Train Rush Cleared": LocData(0, "Train Rush", dlc_flags=HatDLC.death_wish), } location_table = { @@ -679,3 +739,52 @@ location_table = { **contract_locations, **shop_locations, } + +# DO NOT ALTER THE ORDER OF THIS LIST +# This file is in here instead of DeathWishLocations.py to prevent circular import problems +death_wishes = { + "Beat the Heat": 350000, + "Snatcher's Hit List": 350002, + "So You're Back From Outer Space": 350004, + "Collect-a-thon": 350006, + "Rift Collapse: Mafia of Cooks": 350008, + "She Speedran from Outer Space": 350010, + "Mafia's Jumps": 350012, + "Vault Codes in the Wind": 350014, + "Encore! Encore!": 350016, + "Snatcher Coins in Mafia Town": 350018, + + "Security Breach": 350020, + "The Great Big Hootenanny": 350022, + "Rift Collapse: Dead Bird Studio": 350024, + "10 Seconds until Self-Destruct": 350026, + "Killing Two Birds": 350028, + "Snatcher Coins in Battle of the Birds": 350030, + "Zero Jumps": 350032, + + "Speedrun Well": 350034, + "Rift Collapse: Sleepy Subcon": 350036, + "Boss Rush": 350038, + "Quality Time with Snatcher": 350040, + "Breaching the Contract": 350042, + "Snatcher Coins in Subcon Forest": 350044, + + "Bird Sanctuary": 350046, + "Rift Collapse: Alpine Skyline": 350048, + "Wound-Up Windmill": 350050, + "The Illness has Speedrun": 350052, + "Snatcher Coins in Alpine Skyline": 350054, + "Camera Tourist": 350056, + + "The Mustache Gauntlet": 350058, + "No More Bad Guys": 350060, + + "Seal the Deal": 350062, + "Rift Collapse: Deep Sea": 350064, + "Cruisin' for a Bruisin'": 350066, + + "Community Rift: Rhythm Jump Studio": 350068, + "Community Rift: Twilight Travels": 350070, + "Community Rift: The Mountain Rift": 350072, + "Snatcher Coins in Nyakuza Metro": 350074, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 97cd838762..2356302913 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,7 +1,6 @@ import typing from worlds.AutoWorld import World from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict -from .Items import get_total_time_pieces def adjust_options(world: World): @@ -46,25 +45,59 @@ def adjust_options(world: World): if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: world.multiworld.EndGoal[world.player].value = 1 + # Don't allow Seal the Deal goal if Death Wish content is disabled + if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): + world.multiworld.EndGoal[world.player].value = 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + + if world.is_dw_only(): + world.multiworld.EndGoal[world.player].value = 3 + world.multiworld.ActRandomizer[world.player].value = 0 + world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 + world.multiworld.ShuffleSubconPaintings[world.player].value = 0 + world.multiworld.ShuffleStorybookPages[world.player].value = 0 + world.multiworld.ShuffleActContracts[world.player].value = 0 + world.multiworld.EnableDLC1[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = 0 + world.multiworld.KnowledgeChecks[world.player].value = 0 + world.multiworld.DWTimePieceRequirement[world.player].value = 0 + world.multiworld.progression_balancing[world.player].value = 0 + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + -# General class EndGoal(Choice): """The end goal required to beat the game. Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. - Requires DLC2 content to be enabled.""" + Requires DLC2 content to be enabled. + + Seal the Deal: Reach and complete the Seal the Deal death wish main objective. + Requires Death Wish content to be enabled.""" display_name = "End Goal" option_finale = 1 option_rush_hour = 2 + option_seal_the_deal = 3 default = 1 class ActRandomizer(Choice): """If enabled, shuffle the game's Acts between each other. Light will cause Time Rifts to only be shuffled amongst each other, - and Blue Time Rifts and Purple Time Rifts are shuffled separately.""" + and Blue Time Rifts and Purple Time Rifts to be shuffled separately.""" display_name = "Shuffle Acts" option_false = 0 option_light = 1 @@ -77,14 +110,9 @@ class ActPlando(OptionDict): display_name = "Act Plando" -class ShuffleAlpineZiplines(Toggle): - """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" - display_name = "Shuffle Alpine Ziplines" - default = 0 - - class FinaleShuffle(Toggle): """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" + display_name = "Finale Shuffle" default = 0 @@ -105,9 +133,13 @@ class KnowledgeChecks(Toggle): default = 0 -class RandomizeHatOrder(Toggle): - """Randomize the order that hats are stitched in.""" +class RandomizeHatOrder(Choice): + """Randomize the order that hats are stitched in. + Time Stop Last will force Time Stop to be the last hat in the sequence.""" display_name = "Randomize Hat Order" + option_false = 0 + option_true = 1 + option_time_stop_last = 2 default = 1 @@ -127,12 +159,6 @@ class TimePieceBalancePercent(Range): range_end = 100 -class UmbrellaLogic(Toggle): - """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" - display_name = "Umbrella Logic" - default = 0 - - class StartWithCompassBadge(Toggle): """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world (instead of just Relics). Recommended if you're not familiar with where item locations are.""" @@ -151,6 +177,12 @@ class CompassBadgeMode(Choice): default = 1 +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + class ShuffleStorybookPages(Toggle): """If enabled, each storybook page in the purple Time Rifts is an item check. The Compass Badge can track these down for you.""" @@ -164,6 +196,12 @@ class ShuffleActContracts(Toggle): default = 1 +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + class ShuffleSubconPaintings(Toggle): """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. These items are progressive, with the order of Village-Swamp-Courtyard.""" @@ -181,13 +219,138 @@ class StartingChapter(Choice): default = 1 +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 4 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch a hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch a hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 75 + default = 45 + + +class MinExtraYarn(Range): + """The minimum amount of extra yarn in the item pool. + There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, + there must be at least 50 yarn in the pool.""" + display_name = "Max Extra Yarn" + range_start = 0 + range_end = 15 + default = 10 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 300 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + class CTRWithSprint(Toggle): """If enabled, clearing Cheating the Race with just Sprint Hat can be in logic.""" display_name = "Cheating the Race with Sprint Hat" default = 0 -# DLC class EnableDLC1(Toggle): """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" @@ -279,151 +442,118 @@ class BaseballBat(Toggle): default = 0 -class ChapterCostIncrement(Range): - """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" - display_name = "Chapter Cost Increment" - range_start = 1 - range_end = 8 - default = 4 - - -class ChapterCostMinDifference(Range): - """The minimum difference between chapter costs.""" - display_name = "Minimum Chapter Cost Difference" - range_start = 1 - range_end = 8 - default = 5 - - -class LowestChapterCost(Range): - """Value determining the lowest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" - display_name = "Lowest Possible Chapter Cost" - range_start = 0 - range_end = 10 - default = 5 - - -class HighestChapterCost(Range): - """Value determining the highest possible cost for a chapter. - Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" - display_name = "Highest Possible Chapter Cost" - range_start = 15 - range_end = 45 - default = 25 - - -class FinalChapterMinCost(Range): - """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" - display_name = "Final Chapter Minimum Time Piece Cost" - range_start = 0 - range_end = 50 - default = 30 - - -class FinalChapterMaxCost(Range): - """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" - display_name = "Final Chapter Maximum Time Piece Cost" - range_start = 0 - range_end = 50 - default = 35 - - -class MaxExtraTimePieces(Range): - """Maximum amount of extra Time Pieces from the DLCs. - Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" - display_name = "Max Extra Time Pieces" - range_start = 0 - range_end = 16 - default = 16 - - -# Death Wish class EnableDeathWish(Toggle): - """NOT IMPLEMENTED Shuffle Death Wish contracts into the game. - Each contract by default will have a single check granted upon completion. + """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" display_name = "Enable Death Wish" default = 0 +class DeathWishOnly(Toggle): + """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed. + This has the following effects: + - Death Wish is instantly unlocked from the start + - All hats and other progression items are instantly given to you + - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start + - All chapters and their levels are unlocked, act shuffle is forced off + - Any checks other than Death Wish contracts are completely removed + - All Pons in the item pool are replaced with Health Pons or random cosmetics + - The EndGoal option is forced to complete Seal the Deal""" + display_name = "Death Wish Only" + default = 0 + + +class DWShuffle(Toggle): + """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order. + Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence. + If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. + Disabling candles is highly recommended.""" + display_name = "Death Wish Shuffle" + default = 0 + + +class DWShuffleCountMin(Range): + """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Minimum Count" + range_start = 5 + range_end = 38 + default = 18 + + +class DWShuffleCountMax(Range): + """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Maximum Count" + range_start = 5 + range_end = 38 + default = 25 + + class DWEnableBonus(Toggle): - """NOT IMPLEMENTED In Death Wish, allow the full completion of contracts to reward items.""" + """In Death Wish, allow the full completion of contracts to reward items. + WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! + ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! + Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" display_name = "Shuffle Death Wish Full Completions" default = 0 +class DWAutoCompleteBonuses(Toggle): + """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. + This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" + display_name = "Auto Complete Bonus Stamps" + default = 1 + + class DWExcludeAnnoyingContracts(Toggle): - """NOT IMPLEMENTED Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear.""" + """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. + Excluded Death Wishes are automatically completed as soon as they are unlocked. + This option currently excludes the following contracts: + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" display_name = "Exclude Annoying Death Wish Contracts" default = 1 class DWExcludeAnnoyingBonuses(Toggle): - """NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions - from the pool. DANGER! DISABLE AT YOUR OWN RISK! THIS OPTION WHEN DISABLED CAN CREATE VERY DIFFICULT SEEDS!!!""" + """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. + Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. + This option currently excludes the following bonuses: + - So You're Back From Outer Space + - Encore! Encore! + - Snatcher's Hit List + - 10 Seconds until Self-Destruct + - Killing Two Birds + - Snatcher Coins in Battle of the Birds + - Zero Jumps + - Bird Sanctuary + - Wound-Up Windmill + - Snatcher Coins in Alpine Skyline + - Seal the Deal""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 -# Yarn -class YarnCostMin(Range): - """The minimum possible yarn needed to stitch each hat.""" - display_name = "Minimum Yarn Cost" - range_start = 1 - range_end = 12 - default = 4 +class DWExcludeCandles(Toggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + default = 1 -class YarnCostMax(Range): - """The maximum possible yarn needed to stitch each hat.""" - display_name = "Maximum Yarn Cost" - range_start = 1 - range_end = 12 - default = 8 - - -class YarnAvailable(Range): - """How much yarn is available to collect in the item pool.""" - display_name = "Yarn Available" - range_start = 30 - range_end = 75 - default = 45 - - -class MinPonCost(Range): - """The minimum amount of Pons that any shop item can cost.""" - display_name = "Minimum Shop Pon Cost" - range_start = 10 - range_end = 800 - default = 75 - - -class MaxPonCost(Range): - """The maximum amount of Pons that any shop item can cost.""" - display_name = "Maximum Shop Pon Cost" - range_start = 10 - range_end = 800 - default = 400 - - -class BadgeSellerMinItems(Range): - """The smallest amount of items that the Badge Seller can have for sale.""" - display_name = "Badge Seller Minimum Items" +class DWTimePieceRequirement(Range): + """How many Time Pieces that will be required to unlock Death Wish.""" + display_name = "Death Wish Time Piece Requirement" range_start = 0 - range_end = 10 - default = 4 + range_end = 35 + default = 15 -class BadgeSellerMaxItems(Range): - """The largest amount of items that the Badge Seller can have for sale.""" - display_name = "Badge Seller Maximum Items" - range_start = 0 - range_end = 10 - default = 8 - - -# Traps class TrapChance(Range): """The chance for any junk item in the pool to be replaced by a trap.""" display_name = "Trap Chance" @@ -487,7 +617,18 @@ ahit_options: typing.Dict[str, type(Option)] = { "ExcludeTour": ExcludeTour, "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, - "EnableDeathWish": EnableDeathWish, + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DWShuffleCountMin": DWShuffleCountMin, + "DWShuffleCountMax": DWShuffleCountMax, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, + "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, + "DWExcludeCandles": DWExcludeCandles, + "DWTimePieceRequirement": DWTimePieceRequirement, + "EnableDLC2": EnableDLC2, "BaseballBat": BaseballBat, "MetroMinPonCost": MetroMinPonCost, @@ -507,6 +648,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "YarnCostMin": YarnCostMin, "YarnCostMax": YarnCostMax, "YarnAvailable": YarnAvailable, + "MinExtraYarn": MinExtraYarn, "MinPonCost": MinPonCost, "MaxPonCost": MaxPonCost, @@ -523,34 +665,39 @@ ahit_options: typing.Dict[str, type(Option)] = { slot_data_options: typing.Dict[str, type(Option)] = { - "EndGoal": EndGoal, - "ActRandomizer": ActRandomizer, - "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "LogicDifficulty": LogicDifficulty, - "KnowledgeChecks": KnowledgeChecks, - "RandomizeHatOrder": RandomizeHatOrder, - "UmbrellaLogic": UmbrellaLogic, - "CompassBadgeMode": CompassBadgeMode, - "ShuffleStorybookPages": ShuffleStorybookPages, - "ShuffleActContracts": ShuffleActContracts, - "ShuffleSubconPaintings": ShuffleSubconPaintings, + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "KnowledgeChecks": KnowledgeChecks, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, - "EnableDLC1": EnableDLC1, - "Tasksanity": Tasksanity, - "TasksanityTaskStep": TasksanityTaskStep, - "TasksanityCheckCount": TasksanityCheckCount, - "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, - "ExcludeTour": ExcludeTour, + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, - "EnableDeathWish": EnableDeathWish, + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWTimePieceRequirement": DWTimePieceRequirement, - "EnableDLC2": EnableDLC2, - "MetroMinPonCost": MetroMinPonCost, - "MetroMaxPonCost": MetroMaxPonCost, - "BaseballBat": BaseballBat, + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, - "MinPonCost": MinPonCost, - "MaxPonCost": MaxPonCost, + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, - "death_link": DeathLink, + "death_link": DeathLink, } diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index be13fa1f22..d7711cbdd6 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -254,12 +254,16 @@ blacklisted_acts = { # Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. blacklisted_combos = { - "The Illness has Spread": ["Alpine Free Roam"], - "Rush Hour": ["Nyakuza Free Roam"], - "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam"], + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!"], "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"], "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam"], "Time Rift - Rumbi Factory": ["Alpine Free Roam"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam"], } @@ -271,6 +275,11 @@ def create_regions(world: World): # ------------------------------------------- HUB -------------------------------------------------- # menu = create_region(w, "Menu") spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + + # we only need the menu and the spaceship regions + if world.is_dw_only(): + return + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) create_rift_connections(w, create_region(w, "Time Rift - The Lab")) @@ -418,8 +427,8 @@ def create_rift_connections(world: World, region: Region): i = 1 for name in rift_access_regions[region.name]: act_region = world.multiworld.get_region(name, world.player) - entrance_name = "{name} Portal - Entrance {num}" - connect_regions(act_region, region, entrance_name.format(name=region.name, num=i), world.player) + entrance_name = f"{region.name} Portal - Entrance {i}" + connect_regions(act_region, region, entrance_name, world.player) i += 1 @@ -427,7 +436,7 @@ def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) id_start: int = get_tasksanity_start_id() for i in range(world.multiworld.TasksanityCheckCount[world.player].value): - location = HatInTimeLocation(world.player, format("Tasksanity Check %i" % (i+1)), id_start+i, ship_shape) + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) @@ -603,7 +612,7 @@ def randomize_act_entrances(world: World): candidate: Region if len(candidate_list) > 0: - candidate = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] else: # plando can still break certain rules, so acts may not always end up shuffled. for c in region_list: @@ -619,7 +628,7 @@ def randomize_act_entrances(world: World): if region.name in rift_access_regions.keys(): rift_dict.setdefault(region.name, candidate) - world.update_chapter_act_info(region, candidate) + update_chapter_act_info(world, region, candidate) continue if region.name in rift_access_regions.keys(): @@ -634,14 +643,14 @@ def randomize_act_entrances(world: World): entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) - world.update_chapter_act_info(region, candidate) + update_chapter_act_info(world, region, candidate) for name in blacklisted_acts.values(): if not is_act_blacklisted(world, name): continue region: Region = world.multiworld.get_region(name, world.player) - world.update_chapter_act_info(region, region) + update_chapter_act_info(world, region, region) set_rift_rules(world, rift_dict) @@ -650,7 +659,7 @@ def connect_time_rift(world: World, time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 while i <= count: - name = format("%s Portal - Entrance %i" % (time_rift.name, i)) + name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance = world.multiworld.get_entrance(name, world.player) reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 @@ -686,6 +695,9 @@ def create_region(world: World, name: str) -> Region: reg = Region(name, world.player, world.multiworld) for (key, data) in location_table.items(): + if world.is_dw_only(): + break + if data.nyakuza_thug != "": continue @@ -710,11 +722,11 @@ def create_badge_seller(world: World) -> Region: max_items: int = 0 if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: - max_items = world.multiworld.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, world.multiworld.BadgeSellerMaxItems[world.player].value) if max_items <= 0: - world.badge_seller_count = 0 + world.set_badge_seller_count(0) return badge_seller for (key, data) in shop_locations.items(): @@ -729,14 +741,15 @@ def create_badge_seller(world: World) -> Region: if count >= max_items: break - world.badge_seller_count = max_items + world.set_badge_seller_count(max_items) return badge_seller -def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int): +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: entrance = Entrance(player, entrancename, start_region) start_region.exits.append(entrance) entrance.connect(exit_region) + return entrance # Takes an entrance, removes its old connections, and reconnects it between the two regions specified. @@ -785,12 +798,29 @@ def get_act_original_chapter(world: World, act_name: str) -> Region: return world.multiworld.get_region(act_chapters[act_name], world.player) +# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game +def update_chapter_act_info(world: World, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + world.act_connections[original_act_info] = new_act_info + + +def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name + + def create_thug_shops(world: World): min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value count: int = -1 step: int = 0 old_name: str = "" + thug_items = world.get_nyakuza_thug_items() for key, data in shop_locations.items(): if data.nyakuza_thug == "": @@ -800,14 +830,14 @@ def create_thug_shops(world: World): continue try: - if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: + if thug_items[data.nyakuza_thug] <= 0: continue except KeyError: pass if count == -1: - count = world.multiworld.random.randint(min_items, max_items) - world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) + count = world.random.randint(min_items, max_items) + thug_items.setdefault(data.nyakuza_thug, count) if count <= 0: continue @@ -823,6 +853,8 @@ def create_thug_shops(world: World): step = 0 count = -1 + world.set_nyakuza_thug_items(thug_items) + def create_events(world: World) -> int: count: int = 0 @@ -832,9 +864,10 @@ def create_events(world: World) -> int: continue event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + event.show_in_spoiler = False if data.act_complete_event: - act_completion: str = format("Act Completion (%s)" % data.region) + act_completion: str = f"Act Completion ({data.region})" event.access_rule = world.multiworld.get_location(act_completion, world.player).access_rule count += 1 diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7fc5bf93e7..4030a27e3c 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -37,8 +37,9 @@ def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: def get_hat_cost(world: World, hat: HatType) -> int: cost: int = 0 + costs = world.get_hat_yarn_costs() for h in world.get_hat_craft_order(): - cost += world.get_hat_yarn_costs().get(h) + cost += costs[h] if h == hat: break @@ -120,7 +121,7 @@ def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bo if "Free Roam" in entrance.connected_region.name: return True - name: str = format("Act Completion (%s)" % entrance.connected_region.name) + name: str = f"Act Completion ({entrance.connected_region.name})" return world.multiworld.get_location(name, world.player).access_rule(state) @@ -153,6 +154,9 @@ def set_rules(world: World): if world.multiworld.EndGoal[world.player].value == 2: final_chapter = ChapterIndex.METRO chapter_list.append(ChapterIndex.FINALE) + elif world.multiworld.EndGoal[world.player].value == 3: + final_chapter = None + chapter_list.append(ChapterIndex.FINALE) if world.is_dlc1(): chapter_list.append(ChapterIndex.CRUISE) @@ -161,7 +165,7 @@ def set_rules(world: World): chapter_list.append(ChapterIndex.METRO) chapter_list.remove(starting_chapter) - world.multiworld.random.shuffle(chapter_list) + world.random.shuffle(chapter_list) if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): index1: int = 69 @@ -180,7 +184,7 @@ def set_rules(world: World): if lowest_index == 0: pos = 0 else: - pos = world.multiworld.random.randint(0, lowest_index) + pos = world.random.randint(0, lowest_index) chapter_list.insert(pos, ChapterIndex.ALPINE) @@ -190,7 +194,7 @@ def set_rules(world: World): if index >= len(chapter_list): chapter_list.append(ChapterIndex.METRO) else: - chapter_list.insert(world.multiworld.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value highest_cost: int = world.multiworld.HighestChapterCost[world.player].value @@ -206,10 +210,9 @@ def set_rules(world: World): if min_range >= highest_cost: min_range = highest_cost-1 - value: int = world.multiworld.random.randint(min_range, min(highest_cost, - max(lowest_cost, last_cost + cost_increment))) + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) - cost = world.multiworld.random.randint(value, min(value + cost_increment, highest_cost)) + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) if loop_count >= 1: if last_cost + min_difference > cost: cost = last_cost + min_difference @@ -219,9 +222,10 @@ def set_rules(world: World): last_cost = cost loop_count += 1 - world.set_chapter_cost(final_chapter, world.multiworld.random.randint( - world.multiworld.FinalChapterMinCost[world.player].value, - world.multiworld.FinalChapterMaxCost[world.player].value)) + if final_chapter is not None: + world.set_chapter_cost(final_chapter, world.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) @@ -277,7 +281,7 @@ def set_rules(world: World): for hat in data.required_hats: if hat is not HatType.NONE: - add_rule(location, lambda state, hat=hat: can_use_hat(state, world, hat)) + add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) if data.hookshot: add_rule(location, lambda state: can_use_hookshot(state, world)) @@ -294,6 +298,9 @@ def set_rules(world: World): elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + for misc in data.misc_required: + add_rule(location, lambda state, item=misc: state.has(item, world.player)) + if get_difficulty(world) >= 1: world.multiworld.KnowledgeChecks[world.player].value = 1 @@ -316,14 +323,14 @@ def set_rules(world: World): act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) access_rules.append(act_entrance.access_rule) required_region = act_entrance.connected_region - name: str = format("%s: Connection %i" % (key, i)) + name: str = f"{key}: Connection {i}" new_entrance: Entrance = connect_regions(required_region, region, name, world.player) entrances.append(new_entrance) # Copy access rules from act completions if "Free Roam" not in required_region.name: rule: typing.Callable[[CollectionState], bool] - name = format("Act Completion (%s)" % required_region.name) + name = f"Act Completion ({required_region.name})" rule = world.multiworld.get_location(name, world.player).access_rule access_rules.append(rule) @@ -558,9 +565,9 @@ def set_mafia_town_rules(world: World): def set_subcon_rules(world: World): set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: state.can_reach("Toilet of Doom", "Region", world.player) + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) and (not painting_logic(world) or has_paintings(state, world, 1)) - or state.can_reach("Your Contract has Expired", "Region", world.player)) + or state.has("YCHE Access", world.player)) if world.multiworld.UmbrellaLogic[world.player].value > 0: add_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), @@ -583,6 +590,9 @@ def set_subcon_rules(world: World): lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) if painting_logic(world): + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: state.has("Progressive Painting Unlock", world.player)) + for key in contract_locations: if key == "Snatcher's Contract - The Subcon Well": continue @@ -679,7 +689,7 @@ def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked # See randomize_act_entrances in Regions.py -# Called BEFORE set_rules! +# Called before set_rules def set_rift_rules(world: World, regions: typing.Dict[str, Region]): # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 0f6b4aff23..61870c4e65 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,29 +1,40 @@ -from BaseClasses import Item, ItemClassification, Region, LocationProgressType - -from .Items import HatInTimeItem, item_table, item_frequencies, item_dlc_enabled, junk_weights,\ - create_item, create_multiple_items, create_junk_items, relic_groups, act_contracts, alps_hooks, \ - get_total_time_pieces - -from .Regions import create_region, create_regions, connect_regions, randomize_act_entrances, chapter_act_info, \ - create_events, chapter_regions, act_chapters - -from .Locations import HatInTimeLocation, location_table, get_total_locations, contract_locations, is_location_valid, \ - get_location_names, get_tasksanity_start_id - -from .Types import HatDLC, HatType, ChapterIndex -from .Options import ahit_options, slot_data_options, adjust_options -from worlds.AutoWorld import World +from BaseClasses import Item, ItemClassification, LocationProgressType, Tutorial +from .Items import HatInTimeItem, item_table, create_item, relic_groups, act_contracts, create_itempool +from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id from .Rules import set_rules -import typing +from .Options import ahit_options, slot_data_options, adjust_options +from .Types import HatType, ChapterIndex +from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes +from .DeathWishRules import set_dw_rules +from worlds.AutoWorld import World, WebWorld +from typing import List, Dict, TextIO -hat_craft_order: typing.Dict[int, typing.List[HatType]] = {} -hat_yarn_costs: typing.Dict[int, typing.Dict[HatType, int]] = {} -chapter_timepiece_costs: typing.Dict[int, typing.Dict[ChapterIndex, int]] = {} +hat_craft_order: Dict[int, List[HatType]] = {} +hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} +chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} +excluded_dws: Dict[int, List[str]] = {} +excluded_bonuses: Dict[int, List[str]] = {} +dw_shuffle: Dict[int, List[str]] = {} +nyakuza_thug_items: Dict[int, Dict[str, int]] = {} +badge_seller_count: Dict[int, int] = {} + + +class AWebInTime(WebWorld): + theme = "partyTime" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide for setting up A Hat in Time to be played in Archipelago.", + "English", + "ahit_en.md", + "setup/en", + ["CookieCat"] + )] class HatInTimeWorld(World): """ - A Hat in Time is a cute-as-heck 3D platformer featuring a little girl who stitches hats for wicked powers! + A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers! Freely explore giant worlds and recover Time Pieces to travel to new heights! """ @@ -34,37 +45,50 @@ class HatInTimeWorld(World): location_name_to_id = get_location_names() option_definitions = ahit_options - act_connections: typing.Dict[str, str] = {} - nyakuza_thug_items: typing.Dict[str, int] = {} - shop_locs: typing.List[str] = [] + act_connections: Dict[str, str] = {} + shop_locs: List[str] = [] item_name_groups = relic_groups - badge_seller_count: int = 0 def generate_early(self): adjust_options(self) + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + if self.is_dw_only(): + return + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock start_chapter: int = self.multiworld.StartingChapter[self.player].value if start_chapter == 4 or start_chapter == 3: - if self.multiworld.ActRandomizer[self.player].value == 0 \ - or self.multiworld.VanillaAlpine[self.player].value > 0: + if self.multiworld.ActRandomizer[self.player].value == 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) - if self.multiworld.StartWithCompassBadge[self.player].value > 0: - self.multiworld.push_precollected(self.create_item("Compass Badge")) - def create_regions(self): - self.nyakuza_thug_items = {} + excluded_dws[self.player] = [] + excluded_bonuses[self.player] = [] + dw_shuffle[self.player] = [] + nyakuza_thug_items[self.player] = {} + badge_seller_count[self.player] = 0 self.shop_locs = [] - self.badge_seller_count = 0 + self.topology_present = self.multiworld.ActRandomizer[self.player].value + create_regions(self) + if self.multiworld.EnableDeathWish[self.player].value > 0: + create_dw_regions(self) + + if self.is_dw_only(): + return + + create_events(self) + # place default contract locations if contract shuffle is off so logic can still utilize them if self.multiworld.ShuffleActContracts[self.player].value == 0: for name in contract_locations.keys(): @@ -82,69 +106,13 @@ class HatInTimeWorld(World): hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, HatType.DWELLER, HatType.TIME_STOP] - self.topology_present = self.multiworld.ActRandomizer[self.player].value - - # Item Pool - itempool: typing.List[Item] = [] - self.calculate_yarn_costs() - yarn_pool: typing.List[Item] = create_multiple_items(self, "Yarn", self.multiworld.YarnAvailable[self.player].value) - - for i in range(int(len(yarn_pool) * (0.01 * self.multiworld.YarnBalancePercent[self.player].value))): - yarn_pool[i].classification = ItemClassification.progression - - itempool += yarn_pool - if self.multiworld.RandomizeHatOrder[self.player].value > 0: - self.multiworld.random.shuffle(hat_craft_order[self.player]) + self.random.shuffle(hat_craft_order[self.player]) + if self.multiworld.RandomizeHatOrder[self.player].value == 2: + hat_craft_order[self.player].remove(HatType.TIME_STOP) + hat_craft_order[self.player].append(HatType.TIME_STOP) - for name in item_table.keys(): - if name == "Yarn": - continue - - if not item_dlc_enabled(self, name): - continue - - item_type: ItemClassification = item_table.get(name).classification - if item_type is ItemClassification.filler or item_type is ItemClassification.trap: - continue - - if name in act_contracts.keys() and self.multiworld.ShuffleActContracts[self.player].value == 0: - continue - - if name in alps_hooks.keys() and self.multiworld.ShuffleAlpineZiplines[self.player].value == 0: - continue - - if name == "Progressive Painting Unlock" \ - and self.multiworld.ShuffleSubconPaintings[self.player].value == 0: - continue - - if self.multiworld.StartWithCompassBadge[self.player].value > 0 and name == "Compass Badge": - continue - - if name == "Time Piece": - tp_count: int = 40 - max_extra: int = 0 - if self.is_dlc1(): - max_extra += 6 - - if self.is_dlc2(): - max_extra += 10 - - tp_count += min(max_extra, self.multiworld.MaxExtraTimePieces[self.player].value) - tp_list: typing.List[Item] = create_multiple_items(self, name, tp_count) - - for i in range(int(len(tp_list) * (0.01 * self.multiworld.TimePieceBalancePercent[self.player].value))): - tp_list[i].classification = ItemClassification.progression - - itempool += tp_list - continue - - itempool += create_multiple_items(self, name, item_frequencies.get(name, 1)) - - create_events(self) - total_locations: int = get_total_locations(self) - itempool += create_junk_items(self, total_locations-len(itempool)) - self.multiworld.itempool += itempool + self.multiworld.itempool += create_itempool(self) def set_rules(self): self.act_connections = {} @@ -156,11 +124,37 @@ class HatInTimeWorld(World): ChapterIndex.CRUISE: -1, ChapterIndex.METRO: -1} + if self.is_dw_only(): + # we already have all items if this is the case, no need for rules + self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, + None, self.player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", + self.player) + + if self.multiworld.DWEnableBonus[self.player].value == 0: + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): + continue + + if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + continue + + full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player)) + full_clear.show_in_spoiler = False + + return + if self.multiworld.ActRandomizer[self.player].value > 0: randomize_act_entrances(self) set_rules(self) + if self.multiworld.EnableDeathWish[self.player].value > 0: + set_dw_rules(self) + def create_item(self, name: str) -> Item: return create_item(self, name) @@ -182,16 +176,39 @@ class HatInTimeWorld(World): "Hat3": int(hat_craft_order[self.player][2]), "Hat4": int(hat_craft_order[self.player][3]), "Hat5": int(hat_craft_order[self.player][4]), - "BadgeSellerItemCount": self.badge_seller_count, + "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": self.multiworld.seed} # For shop prices if self.multiworld.ActRandomizer[self.player].value > 0: for name in self.act_connections.keys(): slot_data[name] = self.act_connections[name] - if self.is_dlc2(): - for name in self.nyakuza_thug_items.keys(): - slot_data[name] = self.nyakuza_thug_items[name] + if self.is_dlc2() and not self.is_dw_only(): + for name in nyakuza_thug_items[self.player].keys(): + slot_data[name] = nyakuza_thug_items[self.player][name] + + if self.is_dw(): + i: int = 0 + for name in excluded_dws[self.player]: + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + continue + + slot_data[f"excluded_dw{i}"] = dw_classes[name] + i += 1 + + i = 0 + if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + for name in excluded_bonuses[self.player]: + if name in excluded_dws[self.player]: + continue + + slot_data[f"excluded_bonus{i}"] = dw_classes[name] + i += 1 + + if self.multiworld.DWShuffle[self.player].value > 0: + shuffled_dws = self.get_dw_shuffle() + for i in range(len(shuffled_dws)): + slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] for option_name in slot_data_options: option = getattr(self.multiworld, option_name)[self.player] @@ -199,7 +216,10 @@ class HatInTimeWorld(World): return slot_data - def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only(): + return + new_hint_data = {} alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] @@ -220,63 +240,28 @@ class HatInTimeWorld(World): else: continue - new_hint_data[location.address] = self.get_shuffled_region(region_name) + new_hint_data[location.address] = get_shuffled_region(self, region_name) if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: - ship_shape_region = self.get_shuffled_region("Ship Shape") + ship_shape_region = get_shuffled_region(self, "Ship Shape") id_start: int = get_tasksanity_start_id() for i in range(self.multiworld.TasksanityCheckCount[self.player].value): new_hint_data[id_start+i] = ship_shape_region hint_data[self.player] = new_hint_data - def write_spoiler_header(self, spoiler_handle: typing.TextIO): + def write_spoiler_header(self, spoiler_handle: TextIO): for i in self.get_chapter_costs(): spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) for hat in hat_craft_order[self.player]: spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) - def calculate_yarn_costs(self): - mw = self.multiworld - p = self.player - min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) - max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) - - max_cost: int = 0 - for i in range(5): - cost = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) - hat_yarn_costs[self.player][HatType(i)] = cost - max_cost += cost - - available_yarn = mw.YarnAvailable[p].value - if max_cost > available_yarn: - mw.YarnAvailable[p].value = max_cost - available_yarn = max_cost - - # make sure we always have at least 8 extra - if max_cost + 8 > available_yarn: - mw.YarnAvailable[p].value += (max_cost + 8) - available_yarn - def set_chapter_cost(self, chapter: ChapterIndex, cost: int): chapter_timepiece_costs[self.player][chapter] = cost def get_chapter_cost(self, chapter: ChapterIndex) -> int: - return chapter_timepiece_costs[self.player].get(chapter) - - # Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game - def update_chapter_act_info(self, original_region: Region, new_region: Region): - original_act_info = chapter_act_info[original_region.name] - new_act_info = chapter_act_info[new_region.name] - self.act_connections[original_act_info] = new_act_info - - def get_shuffled_region(self, region: str) -> str: - ci: str = chapter_act_info[region] - for key, val in self.act_connections.items(): - if val == ci: - for name in chapter_act_info.keys(): - if chapter_act_info[name] == key: - return name + return chapter_timepiece_costs[self.player][chapter] def get_hat_craft_order(self): return hat_craft_order[self.player] @@ -295,3 +280,47 @@ class HatInTimeWorld(World): def is_dw(self) -> bool: return self.multiworld.EnableDeathWish[self.player].value > 0 + + def is_dw_only(self) -> bool: + return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + + def get_excluded_dws(self): + return excluded_dws[self.player] + + def get_excluded_bonuses(self): + return excluded_bonuses[self.player] + + def is_dw_excluded(self, name: str) -> bool: + # don't exclude Seal the Deal if it's our goal + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + return False + + if name in excluded_dws[self.player]: + return True + + return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + + def is_bonus_excluded(self, name: str) -> bool: + if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + return True + + return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + + def get_dw_shuffle(self): + return dw_shuffle[self.player] + + def set_dw_shuffle(self, shuffle: List[str]): + dw_shuffle[self.player] = shuffle + + def get_badge_seller_count(self) -> int: + return badge_seller_count[self.player] + + def set_badge_seller_count(self, value: int): + badge_seller_count[self.player] = value + + def get_nyakuza_thug_items(self): + return nyakuza_thug_items[self.player] + + def set_nyakuza_thug_items(self, items: Dict[str, int]): + nyakuza_thug_items[self.player] = items