diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index b34e6bb4..713113e6 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -206,7 +206,7 @@ ahit_locations = { "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"), "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"), "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"), - "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Behind Boss Firewall"), "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"), "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"), @@ -233,7 +233,7 @@ ahit_locations = { "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", required_hats=[HatType.DWELLER], paintings=2), - "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Boss Arena"), "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_type=HitType.dweller_bell, paintings=1), @@ -411,7 +411,7 @@ act_completions = { "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", required_hats=[HatType.SPRINT]), - "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired - Post Fight", hit_type=HitType.umbrella), "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), @@ -976,7 +976,6 @@ event_locs = { **snatcher_coins, "HUMT Access": LocData(0, "Heating Up Mafia Town"), "TOD Access": LocData(0, "Toilet of Doom"), - "YCHE Access": LocData(0, "Your Contract has Expired"), "AFR Access": LocData(0, "Alpine Free Roam"), "TIHS Access": LocData(0, "The Illness has Spread"), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 31edf1d0..857c04f1 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -347,7 +347,7 @@ def create_regions(world: "HatInTimeWorld"): sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) - create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + sf_finale = create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) @@ -386,11 +386,24 @@ def create_regions(world: "HatInTimeWorld"): create_rift_connections(world, create_region(world, "Time Rift - Bazaar")) sf_area: Region = create_region(world, "Subcon Forest Area") + sf_behind_boss_firewall: Region = create_region(world, "Subcon Forest Behind Boss Firewall") + sf_boss_arena: Region = create_region(world, "Subcon Forest Boss Arena") + sf_area.connect(sf_behind_boss_firewall, "SF Area -> SF Behind Boss Firewall") + sf_behind_boss_firewall.connect(sf_boss_arena, "SF Behind Boss Firewall -> SF Boss Arena") sf_act1.connect(sf_area, "Subcon Forest Entrance CO") sf_act2.connect(sf_area, "Subcon Forest Entrance SW") sf_act3.connect(sf_area, "Subcon Forest Entrance TOD") sf_act4.connect(sf_area, "Subcon Forest Entrance QVM") sf_act5.connect(sf_area, "Subcon Forest Entrance MDS") + # YCHE puts the player directly in the boss arena, with no access to the rest of Subcon Forest by default. + sf_finale.connect(sf_boss_arena, "Subcon Forest Entrance YCHE") + # To support the Snatcher Hover expert logic for Act Completion (Your Contract has Expired), the act completion has + # to go in a separate region because the Snatcher Hover gives direct access to the Act Completion, but does not + # give access to the act itself. + sf_finale_post_fight: Region = create_region(world, "Your Contract has Expired - Post Fight") + # This connection must never have any rules placed on it because they will not be inherited when setting up act + # connections, only the rules for the entrances to the act and the rules for the Act Completion are inherited. + sf_finale.connect(sf_finale_post_fight, "YCHE -> YCHE - Post Fight") create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon")) create_rift_connections(world, create_region(world, "Time Rift - Pipe")) @@ -947,6 +960,16 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: return name +def get_region_shuffled_to(world: "HatInTimeWorld", region: str) -> str: + if world.options.ActRandomizer: + original_ci: str = chapter_act_info[region] + shuffled_ci = world.act_connections[original_ci] + return next(act_name for act_name, ci in chapter_act_info.items() + if ci == shuffled_ci) + else: + return region + + def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int: count = 0 region = world.multiworld.get_region(region_name, world.player) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 6753b8eb..2ca0628a 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -481,9 +481,8 @@ def set_hard_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: has_paintings(state, world, 3)) - # Cherry bridge over boss arena gap (painting still expected) - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + # Cherry bridge over boss arena gap + set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"), lambda state: True) set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), lambda state: has_paintings(state, world, 2, True)) @@ -566,27 +565,61 @@ def set_expert_rules(world: "HatInTimeWorld"): lambda state: True) # Expert: Cherry Hovering - subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player) - yche = world.multiworld.get_region("Your Contract has Expired", world.player) - entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE") + # Skipping the boss firewall is possible with a Cherry Hover. + set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"), + lambda state: has_paintings(state, world, 1, True)) + # The boss arena gap can be crossed in reverse with a Cherry Hover. + subcon_boss_arena = world.get_region("Subcon Forest Boss Arena") + subcon_behind_boss_firewall = world.get_region("Subcon Forest Behind Boss Firewall") + subcon_boss_arena.connect(subcon_behind_boss_firewall, "SF Boss Arena -> SF Behind Boss Firewall") - if world.options.NoPaintingSkips: - add_rule(entrance, lambda state: has_paintings(state, world, 1)) + subcon_area = world.get_region("Subcon Forest Area") + + # The boss firewall can be skipped in reverse with a Cherry Hover, but it is not possible to remove the boss + # firewall from reverse because the paintings to burn to remove the firewall are on the other side of the firewall. + # Therefore, a painting skip is required. The paintings could be burned by already having access to + # "Subcon Forest Area" through another entrance, but making a new connection to "Subcon Forest Area" in that case + # would be pointless. + if not world.options.NoPaintingSkips: + # The import cannot be done at the module-level because it would cause a circular import. + from .Regions import get_region_shuffled_to + + subcon_behind_boss_firewall.connect(subcon_area, "SF Behind Boss Firewall -> SF Area") + + # Because the Your Contract has Expired entrance can now reach "Subcon Forest Area", it needs to be connected to + # each of the Subcon Forest Time Rift entrances, like the other Subcon Forest Acts. + yche = world.get_region("Your Contract has Expired") + + def connect_to_shuffled_act_at(original_act_name): + region_name = get_region_shuffled_to(world, original_act_name) + return yche.connect(world.get_region(region_name), f"{original_act_name} Portal - Entrance YCHE") + + # Rules copied from `Rules.set_rift_rules()` with painting logic removed because painting skips must be + # available. + entrance = connect_to_shuffled_act_at("Time Rift - Pipe") + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, world.get_entrance("Subcon Forest - Act 2").connected_region, entrance) + + entrance = connect_to_shuffled_act_at("Time Rift - Village") + add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, world.get_entrance("Subcon Forest - Act 4").connected_region, entrance) + + entrance = connect_to_shuffled_act_at("Time Rift - Sleepy Subcon") + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), lambda state: can_use_hookshot(state, world) and can_hit(state, world) and has_paintings(state, world, 1, True)) # Set painting rules only. Skipping paintings is determined in has_paintings - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: has_paintings(state, world, 1, True)) set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), lambda state: has_paintings(state, world, 3, True)) # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him - subcon_area.connect(yche, "Snatcher Hover") - set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), - lambda state: True) + yche_post_fight = world.get_region("Your Contract has Expired - Post Fight") + subcon_area.connect(yche_post_fight, "Snatcher Hover") + # Cherry Hover from YCHE also works, so there are no requirements for the Act Completion. + set_rule(world.get_location("Act Completion (Your Contract has Expired)"), lambda state: True) if world.is_dlc2(): # Expert: clear Rush Hour with nothing @@ -681,12 +714,18 @@ def set_subcon_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.DWELLER)) - # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way - set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), - lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) - and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + # You can't skip over the boss arena wall without cherry hover. + set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"), + lambda state: has_paintings(state, world, 1, False)) - # The painting wall can't be skipped without cherry hover, which is Expert + # The hookpoints to cross the boss arena gap are only present in Toilet of Doom. + set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"), + lambda state: state.has("TOD Access", world.player) + and can_use_hookshot(state, world)) + + # The Act Completion is in the Toilet of Doom region, so the same rules as passing the boss firewall and crossing + # the boss arena gap are required. "TOD Access" is implied from the region so does not need to be included in the + # rule. set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), lambda state: can_use_hookshot(state, world) and can_hit(state, world) and has_paintings(state, world, 1, False))